Tutorials webb that aggregates content from multiple sources into a single bilingual (EN/SV) site with tag-based navigation.
- umsme — markdown and auto-generated screens pulled from the
umsmeapp repo'stutorial/directory. Screens are produced by the app's Storybook (tutorial/screenshot.jsinside umsme) and must stay there. - local — tutorials authored directly in this repo under
content/. - gdrive — tutorials authored in publicly-shared Google Docs. The sync downloads each doc's markdown export, decodes its inline base64 images into
sources/gdrive/tutorial/screens/, flattens layout-grid tables, and strips trailing version-history sections. SeeAUTHORING-GDRIVE.mdfor the author-facing format spec. - github — tutorials authored as a directory inside a public GitHub repo, each with a main markdown file (default
README.md) plus sibling image files. The sync clones the repo, copies the referenced images intosources/github/tutorial/screens/, and rewrites their links. Each tutorial names its ownrepo/dir/ref/files, so tutorials can live in different directories of one repo or across several repos. SeeAUTHORING-GITHUB.mdfor the author-facing format spec.
npm install
npm run sync # populates gitignored sources/ (umsme shallow-clone + gdrive markdown export)
npm run build # writes dist/
npm run serve # serves dist/ at http://localhost:8000Or run all three in order:
npm run devnpm run sync runs both source-specific syncs in order. You can also run them individually:
npm run sync:umsme— shallow + sparse checkout of the umsme repo intosources/umsme/, limited to thetutorial/directory. The synced commit SHA is recorded insources/umsme/.synced-sha.- Re-running updates to the latest
master. scripts/sync-umsme.sh --ref <branch|tag|sha>syncs a specific ref instead.
- Re-running updates to the latest
npm run sync:gdrive— fetches each{ source: "gdrive" }tutorial intutorials.data.yamlfrom Google Docs' markdown export, decodes inline base64 images, cleans the markdown (heading quirks, table flattening, trailing version-history removal), and writes tosources/gdrive/tutorial/. Per-doc sha256 hashes are recorded insources/gdrive/.synced-sha. Each gdrive tutorial entry carries adocs: { en?, sv? }map of Google Doc IDs — single-language tutorials are allowed.npm run sync:github— clones each repo referenced by a{ source: "github" }tutorial intutorials.data.yaml, reads the main markdown file, copies its sibling images intosources/github/tutorial/screens/(named<slug>__<file>), rewrites the image links to../screens/..., and writes tosources/github/tutorial/. The checked-out commit SHA per<repo>@<ref>is recorded insources/github/.synced-sha. Each github entry carriesrepo, optionaldir/ref, and an optionalfiles: { en?, sv? }map of main-file names per language — single-language tutorials are allowed. SeeAUTHORING-GITHUB.md.
sources/ is gitignored — upstream content never enters this repo's history.
Swedish is the default language and lives at the site root: /, /app/, /app/installApp/. English is mirrored one level deeper under /en/: /en/, /en/app/, /en/app/installApp/. The language switcher on every page links to the paired URL in the other language, so each tutorial is deep-linkable in either language.
The default language is set by DEFAULT_LANG in tutorials.config.js.
- Create
content/en/<slug>.mdandcontent/sv/<slug>.md. - Reference screens (if any) as
../screens/<name>.png, with the actual file atcontent/screens/<name>.png. - Add an entry under the tag in
tutorials.data.yaml:tutorials: <tag>: <slug>: {source: local} # or a bare `<slug>:` if the tag's default_source is local
npm run build.
tutorials.data.yaml holds the tag labels and the tutorials as plain data
(no code), as a hierarchy tutorials: <tag>: <slug>: {…}. Each tag opens with
default_source, which applies to every slug under it that doesn't set its own
source. The order of slugs under a tag is the display order in that tag's
listing. DEFAULT_LANG
and SOURCES stay in tutorials.config.js, which reads and validates the YAML
and flattens it into the TUTORIALS list the build and sync scripts consume.
A pull request that touches only tutorials.data.yaml needs no code
review: the file is deliberately left unowned in .github/CODEOWNERS, so with
the branch protection below the PR's author can merge it as soon as the
"Check build" status is green. A PR touching anything else needs the
maintainer's approval. No bot account or token is involved.
This is safe because the file is validated strictly before anything uses it
(scripts/data-schema.js, run by tutorials.config.js and as the first CI
step via node scripts/validate-data.js): slug keys are [A-Za-z0-9-] and
unique, tag keys must exist under tags, repo must be an
https://github.com/<owner>/<repo> URL,
dir/files can't escape the repo, docs are Google Doc ids, and unknown
fields are rejected. On top of that, the production build runs in a throw-away
container (see Deployment), so even a schema gap can't reach the server.
Branch protection for main (repository settings, one-time):
- Require a pull request before merging, required approvals: 0
- Require review from Code Owners — on
- Require status checks to pass:
check_build(anddeploy_scripts) - Settings → General → Allow auto-merge — on, so authors can queue a data-only PR to merge when CI finishes
build.js # build pipeline
template.html # page shell
site.css # styles
tutorials.data.yaml # tag labels + tutorials by tag/slug (content data; mergeable without review)
tutorials.config.js # DEFAULT_LANG, SOURCES; loads, validates + flattens tutorials.data.yaml
scripts/data-schema.js # strict schema for tutorials.data.yaml
scripts/validate-data.js # CLI wrapper for the schema check (CI runs it)
scripts/sync-umsme.sh # umsme source loader
scripts/sync-gdrive.js # gdrive source loader
scripts/sync-github.js # github source loader
content/ # locally-authored tutorials
deploy/ # server-side auto-deploy (systemd timer + docker); installed by hand
sources/ # gitignored — populated by sync scripts
dist/ # gitignored — build output
The production site at tutorial.uppsalamakerspace.se rebuilds hourly via a
server-side systemd timer. The timer runs a small host script that clones
main, builds it inside a locked-down, throw-away Docker container, and rsyncs
the result into the webroot — nothing from this repo executes on the host
itself. The host script lives in deploy/ but is installed by root by hand and
is not updated automatically. See deploy/README.md
for the threat model, setup, migration and operator commands.