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

  1. New Website Recipe
    1. Who runs what
    2. Standing warning: which database, which environment
    3. 0. Inputs
    4. 1. Look up the collection
    5. 2. Instance file and site note
    6. 3. First publish
    7. 4. Create and configure the repository
    8. 6. The site’s own values
    9. 7. Record the site in inventory-app
    10. 8. Discovery check
    11. 9. List the site on the organization site
    12. Steps you may find in older notes
    13. 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:

0. Inputs

Before starting, have on hand:

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:

  1. Stage the package on a data-packages branch: the same command as step 2, with --publish added.

    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
    
  2. Commit packages/<slug>-data/ on that branch.
  3. In data-packages, with npm login done, 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
    
  4. 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:

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):

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.