Skip to content

Fully static build pipeline MVP - #839

Merged
ajolipa merged 13 commits into
developfrom
rb/static-image-support
Oct 28, 2026
Merged

ajolipa merged 13 commits into
developfrom
rb/static-image-support

Conversation

@ajolipa

@ajolipa ajolipa commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

In this PR

This PR revamps the STATIC_BUILD=true build mode content loaders to pull from static JSON files sitting at /api/... rather than making HTTP requests to a live API. It also adds a static-build script that triggers a data export from FairData, downloads and writes the created files to the local filesystem, and then triggers the Astro build process in static mode as well as calling FairImage to trigger level 0 IIIF asset generation, and writes the output to a provided R2 bucket with provided credentials. For detailed (LLM-generated, but proofread by me) documentation of the static build script, see the new docs/static-build-pipeline.md file.

To keep the git tree from getting too twisted up this PR doesn't include the static maps piece, which is currently sitting in a separate branch; once both are merged, the static build script should be updated so that it also calls the static maps script as part of its workflow.

Some notes and questions

  • For this piece, it's just assumed that one way or another the necessary parameters and environment variables will be passed to the script; this can be done on a dev's local machine currently, or managed by a GH action or other tool like pstudio that ends up calling the script. The existing FDS site registry is a good starting point for organizing the necessary per-project parameters.
  • To keep things simple, I made the assumption with these changes that static builds will always be built using this pipeline; the Astro content loaders no longer support pulling data at build time directly from the live API. If we want to keep the option to build static site assets without first exporting the FairData project to JSON, then I guess we'd need a separate flag to distinguish between those modes, but that felt unnecessarily complex.
  • I punted for now on dealing with the galleries content loader; it still makes a live call to whatever link is provided in the config file. Ideally it should instead read that data from the correct path in the /api JSON files, but that felt out of scope for this MVP given how few of the sites use the gallery feature for now... but should be flagged as something to come back to in the next batch of improvements.

jamiefolsom and others added 8 commits October 3, 2026 02:26
With STATIC_BUILD set, the Astro config attaches no adapter, so a static build emits plain files and no serverless function. The three preview routes, which only make sense with a server, are prerendered with no paths in static mode by a small integration. Nothing changes when STATIC_BUILD is unset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbHx5mB9KZKURcTMDh8B9e
Astro cannot build server islands without an adapter, and a static site has no server to render them later. With STATIC_BUILD set, a small plugin drops the server:defer directive from each component as it is loaded, before Astro compiles it, so every island renders into the page at build time. Server-rendered sites keep deferring. The plugin works at load time because Astro's own component plugin compiles in a pre-ordered transform that runs before any integration's transform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbHx5mB9KZKURcTMDh8B9e
@netlify

netlify Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for padp-staging ready!

Name Link
🔨 Latest commit ef14d3b
🔍 Latest deploy log https://app.netlify.com/projects/padp-staging/deploys/6ae24bbeec1b212d10c6574a
😎 Deploy Preview https://deploy-preview-839--padp-staging.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for pss-scavenger-hunt ready!

Name Link
🔨 Latest commit ef14d3b
🔍 Latest deploy log https://app.netlify.com/projects/pss-scavenger-hunt/deploys/6ae24bbe31bb626a409ba9cc
😎 Deploy Preview https://deploy-preview-839--pss-scavenger-hunt.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for marronage-staging ready!

Name Link
🔨 Latest commit ef14d3b
🔍 Latest deploy log https://app.netlify.com/projects/marronage-staging/deploys/6ae24bbe48355ecd3fbd59e3
😎 Deploy Preview https://deploy-preview-839--marronage-staging.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for relnet-staging ready!

Name Link
🔨 Latest commit ef14d3b
🔍 Latest deploy log https://app.netlify.com/projects/relnet-staging/deploys/6ae24bbe6d72dd3ac62f692d
😎 Deploy Preview https://deploy-preview-839--relnet-staging.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@ajolipa
ajolipa requested a review from blms October 13, 2026 02:30
iiif-cloud's create_static_assets endpoint now rejects requests without a
storage credential (no more ENV fallback) and always uses its own
server-side-configured storage endpoint/region, so this:
- renames the request body key sent to iiif-cloud from r2 to storage, and
  drops the now-ignored endpoint sub-key from that request
- renames R2_* env vars to STATIC_STORAGE_*, and config.r2 to config.storage
- renames staticBuild.r2.mjs to staticBuild.storage.mjs (still used for this
  script's own upload of the built site, which still needs its own endpoint)
- updates docs/static-build-pipeline.md to match, including removing the
  stale claim that iiif-cloud falls back to its own credentials, and noting
  that the storage endpoint/region can no longer be set per-request

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@blms blms left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great so far!

I think at this point for the first iteration we'll need to just run npm run static-build by hand from dev machines for each site. Not ideal but we are pretty much out of time on static builds. We can revisit once we get through some more client-facing frontend work on this site and see what time remains.

To keep the git tree from getting too twisted up this PR doesn't include the static maps piece, which is currently sitting in a separate branch; once both are merged, the static build script should be updated so that it also calls the static maps script as part of its workflow.

Oops sorry, just merged that! Once you merge develop in, a plan could be something like:

  • Have the script run npm run build:maps -- --export-dir . --out .static-maps in parallel w/ the image step and the Astro build. It reads places from api/.
    • Write the maps to their own folder, not dist/, because the Astro build clears dist/ when it starts.
  • In the upload step, also upload that folder to <destination>/_fds/maps/, if it isn't empty.
  • Add to the docs that this needs the pmtiles command (and git) .
    • And that each map layer needs a static.url in the site config: /_fds/maps/style.json for the basemap, or a path in /_fds/maps/overlays/ for overlays (for example /_fds/maps/overlays/historic/{z}/{x}/{y}.png). See docs/static-deploy.md for a table.

Comment thread src/loaders/coreData/helpers.ts Outdated
}
try {
const relatedRecords = JSON.parse(fs.readFileSync(`${DATA_BASE_PATH}/${indexName}/${uuid}/${model}.json`, 'utf-8'))[model];
record.relatedRecords[model] = filterRelated(relatedRecords)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Media and taxonomies have no content collections, so I think they'll need to be stored in full. filterRelated keeps only uuid and the relationship fields, which leaves content_url undefined (so image URLs come out as /iiif/3/image/undefined/...) and taxonomy labels empty. Could we add an exception here for media_contents and taxonomies to skip filterRelated?

Comment thread src/loaders/coreData/helpers.ts Outdated

// We need to modify the media contents to point at local records
// This process should be improved!
record.relatedRecords['media_contents'] = _.map(record.relatedRecords['media_contents'], (media: any) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The services and static search read media from relatedRecords.mediaContents, not media_contents, so this line, as well as line 52 and line 57 (when they are grabbing keys from the loop iteration) will need to be updated with the camel-case key.

Comment thread src/loaders/geometry/index.ts Outdated

for (let page = 1; page <= pages; page += 1) {
logger.info(`Fetching page ${page}...`);
const listRecords = fs.existsSync('api/places/index.json') ? JSON.parse(fs.readFileSync(`api/places/index.json`, 'utf-8')).places : [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this loader also runs for non-static builds when PRELOAD_MAP=true, so that'll be trying to hit the file system here. Those builds have no api/ directory, so those sites would lose their map geometry. Could it read api/ only when STATIC_BUILD, and use the API as before otherwise?

Comment thread scripts/staticBuild.storage.mjs Outdated
* @param {Object} config - see staticBuild.config.mjs#resolveConfig
* @param {string} sourceDir - local directory to upload (e.g. "dist")
*/
export const uploadDirectory = async (config, sourceDir) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the static maps integration: we'll need to let the same upload put a folder under a sub-path of destination, e.g. uploadDirectory(config, mapsDir, '_fds/maps'). Something like this:

Suggested change
export const uploadDirectory = async (config, sourceDir) => {
export const uploadDirectory = async (config, sourceDir, keyPrefix = '') => {

And then on line 33:

const key = path.posix.join(config.destination, keyPrefix, ...relativePath.split(path.sep));

Comment thread docs/static-build-pipeline.md Outdated
| Variable | Required | Description |
|---|---|---|
| `IIIF_CLOUD_URL` | Yes | Base URL of the iiif-cloud instance, e.g. `https://iiif.example.com`. |
| `IIIF_CLOUD_API_KEY` | Yes | API key for an admin iiif-cloud user account (required to call `create_static_assets`). |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIIF_CLOUD_API_KEY doesn't need to be admin anymore right?

];

const DEFAULT_POLL_INTERVAL_SECONDS = 10;
const DEFAULT_POLL_MAX_ATTEMPTS = 360; // 1 hour at the default interval

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One other thought here, similar to the fairdata question about polling, should we go for a similar reset-on-progress approach here? Or maybe for MVP we just set the polling attempts really high for big projects locally? I imagine 1 hour won't be enough for larger projects but not sure.

@ajolipa
ajolipa requested a review from blms October 23, 2026 05:23

@blms blms left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking great! Just a couple more issues with the camel case media contents thing. Once those are fixed feel free to merge.

Comment thread src/loaders/coreData/helpers.ts Resolved
Comment thread src/loaders/coreData/helpers.ts Resolved
ajolipa and others added 2 commits October 28, 2026 15:23
Co-authored-by: Ben Silverman <ben.lm.silverman@gmail.com>
Co-authored-by: Ben Silverman <ben.lm.silverman@gmail.com>
@ajolipa ajolipa added the next release Issues in the next release label Oct 28, 2026
@ajolipa
ajolipa merged commit 6f07da5 into develop Oct 28, 2026
2 checks passed
@ajolipa
ajolipa deleted the rb/static-image-support branch October 28, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

next release Issues in the next release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants