docs(deployment): add DeployHQ static-output publishing guide#276
docs(deployment): add DeployHQ static-output publishing guide#276facundofarias wants to merge 5 commits into
Conversation
Adds docs/deployment/deployhq.md covering how to publish Instatic's published/current static output to an S3/R2/Spaces bucket or a web server over SSH/SFTP with DeployHQ, and registers it in the deployment index. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc33b04568
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Instatic runs the editor and content engine on your own server, but everything a | ||
| visitor sees is plain static HTML and CSS written to `published/current`. That | ||
| output is a perfect fit for a separate, fast public host: keep the CMS private | ||
| (behind auth, on a small box) and serve the published site from a CDN-backed | ||
| bucket or a hardened web server. |
There was a problem hiding this comment.
Proxy dynamic-hole requests or restrict this export
For any page containing a request-dependent node, the exported HTML is only a shell: server/publish/holeRuntime.ts fetches /_instatic/hole/<nodeId> from the page's origin. Under the documented separate-origin setup that request goes to the bucket or static web server, where no Instatic hole handler exists, and the swallowed failure leaves placeholder content permanently visible. Either limit this workflow to hole-free sites or document a proxy from the public origin to the CMS's hole endpoint, including the required cookie behavior.
AGENTS.md reference: AGENTS.md:L53-L53
Useful? React with 👍 / 👎.
| git clone git@github.com:you/your-site-published.git deploy && cd deploy | ||
|
|
||
| # each publish | ||
| rsync -a --delete /path/to/instatic/published/current/ ./ |
There was a problem hiding this comment.
Include locally hosted uploads in the deploy artifact
When a site uses the normal local media storage, this command copies only published/current, but server/publish/mediaPrefetch.ts deliberately preserves media URLs such as /uploads/abc-hero.png; the publish slot itself contains HTML plus generated CSS/runtime assets, not those upload files. The separate host will therefore return 404s for local images, variants, fonts, and public plugin assets unless the procedure also exports the required public upload objects or configures /uploads/ to reach their existing origin.
Useful? React with 👍 / 👎.
| **Bucket + CDN (recommended for public sites).** Configure a | ||
| [static hosting](https://www.deployhq.com/features/static-hosting?utm_source=instatic-docs&utm_medium=referral&utm_campaign=instatic-integration&utm_content=static-hosting-target) | ||
| destination pointing at your S3, R2, or Spaces bucket. DeployHQ uploads only the | ||
| changed files, so publishes stay fast even on a large site. Front it with a CDN and | ||
| point your public domain at the CDN. |
There was a problem hiding this comment.
Configure clean-URL routing for bucket targets
On bucket/CDN targets that perform exact object-key lookup, extensionless public routes will not resolve: server/publish/staticArtefact.ts writes /about as about.html and /posts/hello as posts/hello.html, while visitors still request the extensionless paths. This recommended bucket setup does not add a rewrite, so the homepage may work while ordinary pages return the bucket's 404; document CDN/worker rewrites from extensionless paths to .html objects or change the exported layout to directory indexes.
AGENTS.md reference: AGENTS.md:L53-L53
Useful? React with 👍 / 👎.
- Note dynamic "hole" nodes fetch /_instatic/hole/* from the page origin (holeRuntime.ts); gate the pattern to hole-free sites or document a proxy - Ship referenced /uploads media in the deploy artifact, since the publish slot excludes local media (mediaPrefetch.ts) - Add clean-URL routing: lead with the web-server target (try_files .html); document the required CDN rewrite for bucket targets (staticArtefact.ts) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review — all three P1s were spot on. I verified each against source and addressed them in
Happy to keep iterating. @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d48ff02c50
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| git clone git@github.com:you/your-site-published.git deploy && cd deploy | ||
|
|
||
| # each publish | ||
| rsync -a --delete "$UPLOADS_DIR/published/current/" ./ # pages, CSS, runtime, 404.html |
There was a problem hiding this comment.
Preserve the deploy repository during the destructive sync
After the one-time clone, ./ contains the repository's .git directory, but the publish slot does not. The local rsync --help defines --delete as “delete extraneous files from dest dirs,” so this command deletes .git before the following git add, making the documented workflow fail on its first publish. Sync through a protected staging directory or explicitly exclude/protect repository metadata.
Useful? React with 👍 / 👎.
|
|
||
| # each publish | ||
| rsync -a --delete "$UPLOADS_DIR/published/current/" ./ # pages, CSS, runtime, 404.html | ||
| rsync -a "$UPLOADS_DIR/uploads/" ./uploads/ # media referenced as /uploads/... |
There was a problem hiding this comment.
Copy local media from the actual uploads root
With the standard local-disk adapter, media is written directly to join(UPLOADS_DIR, storagePath) and /uploads/<storagePath> is served by stripping /uploads before resolving against UPLOADS_DIR (src/core/plugins/mediaStorageRegistry.ts and server/router.ts). Therefore $UPLOADS_DIR/uploads/ normally does not exist, and this command copies none of the images, fonts, or variants that the deployed pages reference. The artifact must source those public paths from the UPLOADS_DIR root, excluding non-public trees such as published, or use the documented proxy alternative.
AGENTS.md reference: AGENTS.md:L292-L292
Useful? React with 👍 / 👎.
| If your site uses external object storage for media and has no dynamic holes, both | ||
| caveats fall away. |
There was a problem hiding this comment.
Account for signed-redirect object storage
External object storage does not always eliminate the cross-origin caveat: an adapter using servingMode: 'signed-redirect' stores /_instatic/media/<adapterId>/<storagePath> URLs, whose redirects are generated by tryServeMediaRedirect in server/router.ts. On the separate static origin described here, those media URLs still 404 unless /_instatic/media/* is proxied to the CMS; only public-url adapters can fall away as stated.
AGENTS.md reference: AGENTS.md:L292-L292
Useful? React with 👍 / 👎.
- Exclude .git from the rsync --delete pass so the deploy repo survives - Stop copying media into the artifact; route /uploads/* (and /_instatic/media/* for signed-redirect adapters) to the CMS instead — /uploads resolves to UPLOADS_DIR root, and copying risks non-public trees - Narrow the "external storage removes the caveat" note to public-url adapters only (router.ts, mediaStorageRegistry.ts) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catches on the re-review — the fixes I pushed for the first round introduced these, and they're all correct. Addressed in
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49ca40ff88
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| The `published/current` slot is self-contained HTML, CSS, and runtime assets (plus a | ||
| `404.html`). Two things it references still resolve **against the CMS origin**, so | ||
| serving the slot from a different origin needs a plan for them: |
There was a problem hiding this comment.
Route all CMS-backed public endpoints
When a published site contains a CMS-native form or an infinite loop—and even for the holes acknowledged here—the Step 3/4 configurations still proxy only media. src/modules/base/forms/formRuntimeJs.ts calls /_instatic/form/{challenge,submit}, server/publish/loopRuntime.ts calls /_instatic/loop/*, and server/publish/moduleJsBundle.ts emits /_instatic/module-js/*; these requests therefore hit the static host and 404, preventing forms from submitting and load-more behavior from working. Expand the compatibility list and proxy/CDN rules to cover every CMS-backed public route, including the hole routes.
AGENTS.md reference: AGENTS.md:L53-L53
Useful? React with 👍 / 👎.
| # each publish | ||
| rsync -a --delete --exclude='.git' "$UPLOADS_DIR/published/current/" ./ | ||
| git add -A | ||
| git commit -m "Publish $(date -u +%FT%TZ)" || echo "nothing changed" |
There was a problem hiding this comment.
Let real commit failures stop deployment
When git commit fails because of missing identity configuration, signing, a hook, or an I/O error—not merely because there are no changes—this || echo converts the failure into success and proceeds to push the previous commit. In an automated post-publish job, that can exit successfully while leaving the old site deployed. Check whether the index is unchanged before committing, and allow actual commit failures to abort the job.
Useful? React with 👍 / 👎.
- Proxy the full public /_instatic/* runtime surface (forms, loops, module JS, holes, runtime/css/assets), not just media; block /admin/* and the /_instatic/mcp editor bridge from the public origin - Lead with the fully-static best case (zero proxying) and note dynamic features erode the static/private split - Replace `git commit || echo` with set -euo pipefail + a staged-diff check so real commit failures abort instead of redeploying the previous release Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Both round-3 findings were correct — fixed in
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 775e7603cc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - **The CMS host and the public host are different origins.** `PUBLIC_ORIGIN` on the | ||
| CMS configures the origins its CSRF check trusts (where you reach the admin) — it is | ||
| not the public URL of the static site. After the first publish, open the deployed |
There was a problem hiding this comment.
Add the static site to PUBLIC_ORIGIN
When the deployed site contains a form, proxying /_instatic/form/* is insufficient: handlePublicFormRequest rejects the POST unless its browser Origin is accepted by originAllowed, and the browser preserves the static site's origin through the reverse proxy. Following this note and configuring only the private admin/CMS origin therefore makes every proxied form challenge and submission return 403; instruct operators to include both the CMS/admin origin and the deployed public origin in the comma-separated PUBLIC_ORIGIN value.
AGENTS.md reference: AGENTS.md:L292-L292
Useful? React with 👍 / 👎.
| fi | ||
| ``` | ||
|
|
||
| The slot includes a `404.html` (the Netlify/GitHub-Pages convention) so hosts that |
There was a problem hiding this comment.
Make the 404 artifact requirement explicit
When a site has no enabled notFound template, publishSite deliberately skips writing 404.html, so the slot does not always contain this file. The nginx error_page example and the bucket/CDN fallback later in this guide both assume it exists, leaving such installations with a missing fallback or the host's default error response; make this statement conditional or require operators to create and publish a not-found template first.
AGENTS.md reference: AGENTS.md:L292-L292
Useful? React with 👍 / 👎.
- Note that proxying /_instatic/form/* also requires adding the deployed public origin to the CMS PUBLIC_ORIGIN, or form challenge/submit 403 - Make every 404.html reference conditional on a published not-found template (publishSite skips 404.html when none is enabled) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Both round-4 findings correct — fixed in the latest commit:
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9d335c84a4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| If a publish ships broken markup, use | ||
| [one-click rollback](https://www.deployhq.com/features/one-click-rollback?utm_source=instatic-docs&utm_medium=referral&utm_campaign=instatic-integration&utm_content=rollback-target) | ||
| to redeploy the previous release. Because deployments are atomic, rollback is an | ||
| instant pointer swap, not a re-upload. |
There was a problem hiding this comment.
Limit rollback claims to fully static exports
When the exported site uses holes or other CMS-backed runtime content, rolling DeployHQ back only restores the old files; it does not restore the CMS's published snapshot or publishVersion. Consequently, the old shell's hole requests carry an obsolete ?v= and handleHoleRequest replaces them with stale sentinels, while module-JS and forms still resolve against the latest CMS snapshot. Presenting rollback as a complete recovery path can therefore leave a rolled-back dynamic site blank or inconsistent; document that it is safe only for fully static output, or require a coordinated CMS rollback.
AGENTS.md reference: AGENTS.md:L340-L340
Useful? React with 👍 / 👎.
| - `/` and paths ending `/` → append `index.html` | ||
| - any other extensionless path → append `.html` |
There was a problem hiding this comment.
Route non-root trailing-slash URLs to the baked HTML file
For a page such as about, the publisher writes about.html, while the live router accepts both /about and /about/ by trimming trailing slashes. This rewrite sends /about/ to about/index.html, which is never produced, so valid trailing-slash page and data-row URLs 404 on the bucket; the nginx try_files example has the same gap. Redirect non-root trailing-slash requests to the canonical path or strip the slash before appending .html.
AGENTS.md reference: AGENTS.md:L340-L340
Useful? React with 👍 / 👎.
| The `published/current` slot is self-contained HTML, CSS, and runtime assets (plus a | ||
| `404.html`), but exported pages call **CMS-backed endpoints at runtime**. Served from |
There was a problem hiding this comment.
Make the introductory 404 artifact claim conditional
On sites without an enabled not-found template, publishSite does not write 404.html, so the opening compatibility statement still incorrectly promises that every publish slot contains one. This contradicts the corrected Step 1 guidance and can lead operators to configure a missing fallback; qualify this occurrence as conditional too.
AGENTS.md reference: AGENTS.md:L340-L340
Useful? React with 👍 / 👎.
Add a deployment guide for publishing Instatic's static output with DeployHQ.
Instatic writes clean static HTML/CSS to
published/current. This addsdocs/deployment/deployhq.md, a guide for serving that output from a separatepublic host — an S3/R2/Spaces bucket behind a CDN, or your own web server over
SSH/SFTP — while the CMS stays private. It complements
vps.md(which runs the CMSserver) rather than replacing it, and it slots into the existing
docs/deployment/format (TL;DR table → steps → Runtime notes → Troubleshooting →Related).
It also registers the guide in the deployment index (
README.md→ Docs Inventory).The guide is honest about the one seam DeployHQ needs: it deploys from Git, so
there's a small sync step to get
published/currentinto a deploy repo. No claimsare made about Instatic internals beyond the documented runtime contract.
Disclosure: I work on DeployHQ. Happy to adjust scope, wording, or placement — or
drop it entirely if it's not a fit for the docs.