Skip to content

docs(deployment): add DeployHQ static-output publishing guide#276

Open
facundofarias wants to merge 5 commits into
CoreBunch:mainfrom
facundofarias:docs/deployhq-static-publishing
Open

docs(deployment): add DeployHQ static-output publishing guide#276
facundofarias wants to merge 5 commits into
CoreBunch:mainfrom
facundofarias:docs/deployhq-static-publishing

Conversation

@facundofarias

Copy link
Copy Markdown

Add a deployment guide for publishing Instatic's static output with DeployHQ.

Instatic writes clean static HTML/CSS to published/current. This adds
docs/deployment/deployhq.md, a guide for serving that output from a separate
public 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 CMS
server) 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/current into a deploy repo. No claims
are 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.

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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread docs/deployment/deployhq.md Outdated
Comment on lines +3 to +7
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread docs/deployment/deployhq.md Outdated
git clone git@github.com:you/your-site-published.git deploy && cd deploy

# each publish
rsync -a --delete /path/to/instatic/published/current/ ./

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread docs/deployment/deployhq.md Outdated
Comment on lines +83 to +87
**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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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>
@facundofarias

Copy link
Copy Markdown
Author

Thanks for the review — all three P1s were spot on. I verified each against source and addressed them in d48ff02c:

  1. Dynamic holes (server/publish/holeRuntime.ts) — added a "Site compatibility — read first" section that gates this workflow to hole-free sites, or documents proxying /_instatic/hole/* (and hole-runtime.js) from the public origin back to the CMS with the request cookies forwarded. Also surfaced in Runtime notes + Troubleshooting.
  2. Local uploads (server/publish/mediaPrefetch.ts) — Step 1 now ships the referenced /uploads/ media alongside published/current in the deploy artifact, with a "route /uploads/* to the CMS instead" alternative.
  3. Clean-URL routing (server/publish/staticArtefact.ts) — reordered to lead with the web-server target (try_files $uri $uri.html, native resolution), and documented the required CDN rewrite (index.html / append .html / 404.html fallback) for bucket targets. Noted the exporter already writes 404.html.

Happy to keep iterating. @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread docs/deployment/deployhq.md Outdated
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread docs/deployment/deployhq.md Outdated

# each publish
rsync -a --delete "$UPLOADS_DIR/published/current/" ./ # pages, CSS, runtime, 404.html
rsync -a "$UPLOADS_DIR/uploads/" ./uploads/ # media referenced as /uploads/...

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread docs/deployment/deployhq.md Outdated
Comment on lines +33 to +34
If your site uses external object storage for media and has no dynamic holes, both
caveats fall away.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@facundofarias

Copy link
Copy Markdown
Author

Good catches on the re-review — the fixes I pushed for the first round introduced these, and they're all correct. Addressed in 49ca40ff:

  1. rsync --delete deleting .git — added --exclude='.git' so the sync no longer wipes the deploy repo's metadata.
  2. Uploads source path — verified /uploads/<path> resolves against UPLOADS_DIR root (server/router.ts strips /uploads), and copying the tree risks shipping non-public objects. Dropped media-copying entirely: the artifact is now just the publish slot, and /uploads/* (plus /_instatic/media/* for signed-redirect adapters) is routed to the CMS at the public origin.
  3. signed-redirect object storage (P2) — narrowed "external storage removes the caveat" to public-url adapters only, and documented routing /_instatic/media/* to the CMS otherwise.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread docs/deployment/deployhq.md Outdated
Comment on lines +17 to +19
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread docs/deployment/deployhq.md Outdated
# 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@facundofarias

Copy link
Copy Markdown
Author

Both round-3 findings were correct — fixed in 775e7603:

  1. Route all CMS-backed public endpoints — the config only proxied media. Generalized it to the full public /_instatic/* runtime surface (forms /_instatic/form/*, loops /_instatic/loop/*, module JS /_instatic/module-js/*, holes, and shared /_instatic/runtime|css|assets/*) plus /uploads/*. I explicitly block /admin/* and the /_instatic/mcp editor bridge on the public origin so the admin surface isn't exposed. Site-compatibility now leads with the fully-static best case (zero proxying) and is candid that dynamic features erode the static/private split.
  2. Commit failures masking deploys — replaced git commit || echo with set -euo pipefail and a git diff --cached --quiet check, so "no changes" skips cleanly but a real commit failure aborts instead of redeploying the previous release.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread docs/deployment/deployhq.md Outdated
Comment on lines +185 to +187
- **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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment thread docs/deployment/deployhq.md Outdated
fi
```

The slot includes a `404.html` (the Netlify/GitHub-Pages convention) so hosts that

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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>
@facundofarias

Copy link
Copy Markdown
Author

Both round-4 findings correct — fixed in the latest commit:

  1. Proxied forms need the public origin in PUBLIC_ORIGINhandlePublicFormRequest checks the browser Origin (which stays the static site's through the reverse proxy), so routing /_instatic/form/* alone 403s. The guide now instructs adding the deployed public origin to the CMS's comma-separated PUBLIC_ORIGIN alongside the admin origin, with a matching troubleshooting row.
  2. 404.html isn't guaranteedpublishSite skips it without an enabled not-found template. Every 404.html reference (Step 1, nginx error_page, bucket fallback) is now conditional on publishing one.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +177 to +180
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +161 to +162
- `/` and paths ending `/` → append `index.html`
- any other extensionless path → append `.html`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +17 to +18
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant