New Website Recipe
The one ordered procedure for standing up a new gallery or exhibition website: from choosing the legacy collection to a live, deployed, discovered site. Each step below names the command, the directory it runs from, who runs it, and what proves it worked.
This recipe covers gallery and exhibition sites only — the
scope of epic
#1735,
which built the tools this recipe uses:
#1733
(the lookup command, step 1 below) and
#1734
(the parameterised dxa-gallery/dxa-exhibition exporters, steps 2–3
below). A standalone site (a whole virtual museum, like islamicart or
baroqueart) is not created by this recipe — it needs its own forked
exporter (see
scripts/exporters/README.md,
“A standalone site”).
Table of Contents
- New Website Recipe
- Who runs what
- Standing warning: which database, which environment
- 0. Inputs
- 1. Look up the collection
- 2. Instance file and site note
- 3. First publish
- 4. Create and configure the repository
- 6. The site’s own values
- 7. Record the site in inventory-app
- 8. Discovery check
- 9. List the site on the organization site
- Steps you may find in older notes
- First end-to-end run
Who runs what
| Step | Who | Why |
|---|---|---|
| 1. Look up the collection | any developer | reads the staging/app database, no special access |
| 2. Instance file and site note | any developer | an ordinary pull request |
| 3. First publish | the operator holding the npm session | npm publish needs an authenticated npmjs login with 2FA |
| 4. Create and configure the repository | an operator with gh admin rights on the org |
repository administration is not a permission GITHUB_TOKEN can ever hold |
| 5. Texts | any developer | scripts/site-i18n only ever issues SELECTs against the legacy database |
| 6. Catalogue, sheet and theme | any developer | the site’s own pull request |
| 7. Record the site in inventory-app | any developer | an ordinary pull request |
| 8. Discovery check | any developer | a read-only dry run |
| 9. List the site on the organization site | any developer | reads GitHub, then an ordinary pull request |
Standing warning: which database, which environment
Two different tools in this recipe read from two different places, and neither is the staging database by default:
scripts/exporters/*’s own.envpoints at productionDB_*credentials. Steps 2–3 below only avoid that because they are run through theexporterservice in the repo-rootcompose.yml, which forcesDB_*tostaging-mysqland wins over the exporter’s own.env— seescripts/exporters/README.md. Running an exporter directly on the host instead reads whatever its.envpoints at.scripts/site-i18n’s.envpoints at the production legacy database, over the VPN, and step 5 below genuinely connects to it — there is no staging copy of the legacy database for this tool. This is safe by construction: every statementscripts/site-i18nissues against the legacy database is aSELECT, confirmed inscripts/site-i18n/README.md.
0. Inputs
Before starting, have on hand:
- the legacy gallery or exhibition — its legacy id, slug, or exact English title;
- the site slug — kebab-case, also the repository name
(
museumwithnofrontiers/<slug>) and the data package (@museumwnf/<slug>-data); - the class —
galleryorexhibition(standalonesites are not created by this recipe, see above; aminisite, one of legacy’s virtual exhibitions, followsminisite-template’s README, whose steps 0 and 2 arescripts/minisites/legacy-minisite.mjsand whose step 3 is steps 7 to 9 here); - the namespace — one lowercase word, no hyphens (
carpets,waterInIslam) — the four DXA sites’ own choices are examples:carpets,amulets,colours(nottheUseOfColoursInArt),waterInIslam; - the display title, e.g.
"Carpets".
1. Look up the collection
Resolve the legacy selector to the inventory-app collection UUID, inside
the app or staging-app container (from the inventory-app checkout):
docker compose exec staging-app php artisan importer:find-collection gallery <legacy id|slug|exact English title> --json
# or: exhibition, in place of gallery
If the answer is “there are no commands defined in the importer namespace”,
the container runs a built image (inventory-app:staging) that predates the
command. Either rebuild that image first or run the command with the
checkout mounted over it, which is what the first end-to-end run did:
docker compose run --rm --no-deps -T -v "${PWD}:/var/www/app" staging-app php artisan importer:find-collection gallery <legacy id|slug|exact English title> --json
The command’s signature and output fields are in
app/Console/Commands/FindCollection.php.
Proof: exactly one row is returned, its type matches the class from
step 0, and its id is the collection_id step 2 needs.
2. Instance file and site note
Two new files, scripts/exporters/instances/<slug>.json and
scripts/exporters/instances/<slug>.md, in one pull request on
inventory-app. The field shapes (kind, slug, name, collection_id,
package_name) and what the site note holds are in
scripts/exporters/instances/README.md.
kind is "gallery" or "exhibition" and picks whether dxa-gallery or
dxa-exhibition reads the file.
Proof, from the inventory-app repo root, against staging:
docker compose --profile jobs run --rm exporter dxa-gallery --instance <slug> --force
# or: dxa-exhibition, in place of dxa-gallery
writes scripts/exporters/dxa-gallery/output/<slug>/ (or
scripts/exporters/dxa-exhibition/output/<slug>/) without error. See
scripts/exporters/README.md
for validating the counts before the first publish.
3. First publish
Every package’s source lives in data-packages, which publishes each version from its release workflow. That workflow cannot create a package, so a new site’s first version is published by hand, by the operator, who logs in to npm for it. The full steps are under “A new package” in data-packages’ README; in short, with data-packages cloned next to inventory-app:
-
Stage the package on a data-packages branch: the same command as step 2, with
--publishadded.git -C ../data-packages switch -c feature/<slug>-data docker compose --profile jobs run --rm exporter dxa-gallery --instance <slug> --force --publish --base-url https://inventory.metanull.eu # or: dxa-exhibition, in place of dxa-gallery - Commit
packages/<slug>-data/on that branch. -
In data-packages, with
npm logindone, publish the first version and set the package’s trusted publisher:node scripts/publish.mjs --first <slug>-data npm trust github @museumwnf/<slug>-data --repository museumwithnofrontiers/data-packages --file release.yml --allow-publish - Open the pull request and merge it. Every later version is published by the release workflow.
Proof:
npm view @museumwnf/<slug>-data version
answers with a version (registry lag is real — wait a few minutes before concluding the publish did not happen; never re-publish on the strength of one check).
4. Create and configure the repository
Two paths end in the same site; pick one.
With the tool. From a viewer-workflows checkout (or its container —
same pattern as propagate.mjs), by an operator authenticated with gh and
holding admin rights on the museumwithnofrontiers org (this tool never
stores a token). --class picks the template: gallery-template or
exhibition-template. A gallery or an exhibition also needs its own colours,
in a small JSON file, one entry per __PALETTE_<NAME>__ placeholder of the
template’s theme/tokens.css — a gallery’s five from
.legacy-code/dxa-client/src/sites/<code>/_variables.scss, an exhibition’s
six from its legacy compiled stylesheet (the comment above the placeholders
says how). The family’s theme (@museumwnf/viewer-layout/dxa/gallery.css or
exhibition.css) reads them for every surface, text and border:
{ "THEME_DARK": "#504819", "THEME_MEDIUM_DARK": "#6b612b",
"THEME_MEDIUM": "#7e743e", "THEME_LIGHT": "#91864d", "BACKGROUND_COLOR": "#fffff0" }
export GH_TOKEN=$(gh auth token)
docker run --rm -it \
-e GH_TOKEN \
-v "$PWD:/w" \
-w /w node:lts-alpine sh -c "apk add --no-cache git github-cli >/dev/null && \
node tools/new-website.mjs --slug <slug> --class gallery|exhibition --namespace <ns> --title '<Site name>' --palette <slug>.json"
Add --dry-run first to preview without touching anything; it also checks
the palette file against the template. The full flag reference is in
viewer-workflows’
MAINTENANCE.md
(“Creating a website”). The tool requires the data package published on
npmjs first (step 3) — it refuses to run otherwise, unless --settings-only.
The tool creates the repository from the class’s template, switches on Pages
(source: GitHub Actions), the main-requires-pr ruleset, classic branch
protection with the four required checks, allow-auto-merge,
delete-branch-on-merge, CodeQL default setup and Dependabot security
fixes/alerts, then opens and auto-merges the first PR (the placeholders and
the palette replaced, @museumwnf/<slug>-data@latest installed).
By hand. Anyone with a GitHub account: “Use this template” on
gallery-template
or exhibition-template,
then that template’s own scripts/setup-repo.sh (or .ps1) and its
README’s “Admin” steps: the same settings, the placeholders and the palette
replaced by hand, the dataset installed. The template’s install guard
refuses to install while a placeholder or a palette colour is left.
Proof:
- the tool’s own console summary (
[Repository],[Palette],[Pull request]lines), or the setup script’s report; - the first PR reaches
MERGEDonce its checks pass (auto-merge armed, unless--no-mergewas passed). A gallery’s or an exhibition’s smoke test keeps a few curatorial picks asTODO(dataset)on purpose, so that PR’sci / Teststays red until the picks (step 6) are pushed onto it; -
gh api repos/museumwithnofrontiers/<slug> --jq .template_repository.full_nameprints
museumwithnofrontiers/gallery-templateormuseumwithnofrontiers/exhibition-template; -
gh run list -R museumwithnofrontiers/<slug> --workflow=deploy.yml --branch main --limit 1is
completed success; https://museumwithnofrontiers.github.io/<slug>/answers, in its own colours.## 5. Texts
From scripts/site-i18n (inventory-app checkout), any developer, reading
the production legacy database over the VPN (see the standing warning
above):
docker run --rm --network host -v "${PWD}:/app" -v site-i18n-node-modules:/app/node_modules -w /app node:22-alpine npm run extract -- <legacy selector> --layout website --namespace <ns> --force
--layout website writes exactly this one site’s own texts (en.json plus
one file per legacy language that has at least one of the site’s own
texts) into output/<slug>/locales/. Reuse the same legacy selector step 0
identified — the lookup command in step 1 already resolved it — rather
than running a separate npm run list search, for consistency (documented
in
scripts/site-i18n/README.md).
Copy output/<slug>/locales/ into the site repository’s own locales/
and open the texts PR there. One file needs a merge rather than a copy:
the scaffold’s locales/en.json already exists and holds the placeholder
entry <ns>.credits.body that the credits page reads. Replace its value
with the extracted credits and add the other extracted entries. If the legacy database holds this site’s texts
in English only, en.json is the only file the extraction writes, and
that is not an error.
Proof: ci / Texts (blocking) on that PR validates the copied keys
against the viewer-i18n dictionary; once merged and deployed, the About
page shows the extracted text for a gallery (an exhibition’s About page
is its about theme, from the data package) and the Credits page shows it
for both.
6. The site’s own values
The site’s own work, on the repository step 4 created. Its pages are the
family’s (@museumwnf/viewer-layout/dxa); what is its own is the source
chips and the Explore-partner notice in src/dataset.config.js, the
curatorial picks of tests/smoke.test.js, and the theme beyond the palette —
each marked TODO(dataset) in the file, and described in the family
template’s README (steps 4 to 7). Not repeated here.
Proof: the site’s own tests (npm run test) and build pass.
7. Record the site in inventory-app
Add the new site as a submodule under .new-architecture/, in the same
form as the existing entries — check
.gitmodules
first: the .new-architecture/* entries carry no shallow = true line,
unlike the .legacy-code/* and .legacy-database/* ones.
git submodule add https://github.com/museumwithnofrontiers/<slug> .new-architecture/<slug>
Open this as its own pull request on inventory-app.
Proof: the diff shows the new .gitmodules stanza and the gitlink; the
“Record the site commits in this repository” step of
scripts/import-tool/README.md
now walks over this site the next time .new-architecture pointers are
bumped after a re-import.
8. Discovery check
From the viewer-workflows checkout used in step 4 (or a fresh one):
node tools/propagate.mjs --dry-run --expect <slug>-data@<published version> --repo museumwithnofrontiers/<slug>
The flags are documented in propagate.mjs’s own header and in
MAINTENANCE.md
(“Step 3, with the tool”). --repo restricts the run to this one site;
drop it to also see the site appear in the full discovered list — a site is
discovered from its template_repository link, not from any file listing
sites by name.
Run it in the node:lts-alpine container with GH_TOKEN, as
MAINTENANCE.md describes, from a real clone: a git worktree cannot be
mounted, because its .git is a pointer file and gh auth setup-git
refuses it. Run on the host, the same command may answer that the version
is “not on the registry” while npm pack serves it; that is the host-side
registry lag, not a missing publish, and the container does not see it.
Proof: the dry-run’s report includes this site.
9. List the site on the organization site
https://museumwithnofrontiers.github.io lists every website, from a
sites.json that
scripts/org-site/list-sites.mjs
generates. The new site is found the way step 8 found it, and takes its
title and kind from the instance file of step 2. Regenerate the list into the
.new-architecture/museumwithnofrontiers.github.io submodule, then commit
sites.json there on a branch and open a pull request on that repository:
docker run --rm -e GH_TOKEN=$(gh auth token) -v "${PWD}:/w" -w /w node:lts-alpine sh -c "apk add --no-cache github-cli >/dev/null && node scripts/org-site/list-sites.mjs"
Proof: once that pull request merges, the site appears on https://museumwithnofrontiers.github.io under its group.
Steps you may find in older notes
Two steps that used to be part of this recipe are not steps any more. Per the epic #1735 decomposition comment (2026-09-21):
- Adding the new site to
dependents.json. That file no longer exists. Since the move to themuseumwithnofrontiersorg, both propagation (step 8 above) andpackage-ci.yml’s downstream build discover a website from thetemplate_repositorylink GitHub itself records at creation (step 4) — which is also why step 4’s proof checks that link directly. - Adding a Dependabot entry in
inventory-appfor the new site. A website is its own repository, with its owndependabot.ymlinherited from its template.inventory-app’s own hand-maintained, CI-enforceddependabot.ymlonly listsinventory-app’s own Node projects; its exporter and viewer entries are already globbed patterns, so a new site adds nothing there.
First end-to-end run
This recipe was first run end to end on coins-medals, the first site of
M5 wave 1, on 2026-09-21
(story #1930;
the run’s proofs are its comments). Every step ran as written. Three
observations were folded into the steps above rather than kept here: the
built staging image that predates the lookup command (step 1), the
scaffold’s placeholder locale entries that the texts PR must keep until
step 6 (step 5), and the two container traps of the discovery check
(step 8). The tool’s closing hint about dependents.json and a Dependabot
entry, the two retired steps listed above, was corrected in
viewer-workflows#29.