From aaeb5a9d4c391299e754da386a05b3103bb0a25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Mon, 24 Aug 2026 16:02:25 +0200 Subject: [PATCH] docs: document Global Config content pinning Written by an AI agent; not yet human-reviewed. --- docs/cold-page-request.md | 38 ++++++++++------- .../1.getting-started/3.configuration.md | 1 + .../content/3.concepts/1.architecture.md | 8 +++- playground/content/4.deployment/1.vercel.md | 41 ++++++++++++++++++- 4 files changed, 70 insertions(+), 18 deletions(-) diff --git a/docs/cold-page-request.md b/docs/cold-page-request.md index 6da3639..b75ca41 100644 --- a/docs/cold-page-request.md +++ b/docs/cold-page-request.md @@ -8,6 +8,7 @@ sequenceDiagram participant Edge as Edge ISR participant SSR as Lambda SSR participant ContentRoute as /api/content/** + participant Config as Vercel Global Config participant Refs as Shared ref cache (content:refs) participant GH as GitHub participant Content as shared content @@ -17,20 +18,26 @@ sequenceDiagram SSR->>ContentRoute: $fetch (navigation) ContentRoute->>Content: getProdContent() - Content->>Refs: resolveContentSha(targetBranch, contentDir) - alt cache hit (within 60s TTL) - Refs-->>Content: cached content sha - else cache miss - Refs->>GH: commits?sha=&path= - GH-->>Refs: latest content sha - Refs-->>Content: content sha + Content->>Config: read contentSha (when connected) + alt pin set + Config-->>Content: pinned content sha + else no pin or read failed + Config-->>Content: undefined + Content->>Refs: resolveContentSha(targetBranch, contentDir) + alt cache hit (within 60s TTL) + Refs-->>Content: cached content sha + else cache miss + Refs->>GH: commits?sha=&path= + GH-->>Refs: latest content sha + Refs-->>Content: content sha + end end Content->>Content: rebuild if content sha advanced Content->>GH: init partial (~36 files, at ) ContentRoute-->>SSR: nav tree SSR->>ContentRoute: $fetch (page) - ContentRoute->>Content: getProdContent() (same sha → no rebuild) + ContentRoute->>Content: getProdContent() (recheck pin; same sha → no rebuild) Content->>GH: fetch + parse 1 page (at ) ContentRoute-->>SSR: parsed page @@ -38,9 +45,11 @@ sequenceDiagram Edge-->>Browser: HTML (cached for next visitor) ``` -**Cost:** one shared-cache lookup for the latest commit touching the content directory + the -instance builds its index from GitHub once per content revision, then one page parse. All reads -are pinned to the immutable ``. Code-only commits do not rebuild the content instance. +**Cost:** with a Global Config pin, a config lookup selects the content SHA. Without a pin, a +shared-cache lookup selects the latest commit touching the content directory. The instance builds +its index from GitHub once per content revision, then parses one page. All reads are pinned to the +immutable ``. Without a Global Config pin, code-only commits do not rebuild the content +instance. The ref cache is shared across *instances*, so GitHub is hit once per 60s TTL window rather than once per cold start. It is **not** shared across regions — Vercel's @@ -57,9 +66,10 @@ would turn an expired token into a site-wide outage for the window rather than o failed request. **On a content push**, `server/api/revalidate.post.ts` forces a fresh `resolveContentSha()` lookup, -which writes the latest content SHA into the same shared ref cache before fanning out ISR -purges for the affected pages, so a freshly-purged page's next render already -sees the new SHA instead of waiting out the 60s TTL. +which writes the latest branch content SHA into the same shared ref cache before fanning out ISR +purges for the affected pages. Without a Global Config pin, a freshly-purged page's next render sees +the new SHA instead of waiting out the 60-second TTL. With a pin, the next render stays on the pinned +SHA, while the refreshed branch pointer is ready if the pin is removed. Parsed manifests and bodies live under a parser-version + content-SHA namespace. Vercel Runtime Cache persists across deployments within an environment, so unrelated deployments can reuse diff --git a/playground/content/1.getting-started/3.configuration.md b/playground/content/1.getting-started/3.configuration.md index 2a5397a..69f9d25 100644 --- a/playground/content/1.getting-started/3.configuration.md +++ b/playground/content/1.getting-started/3.configuration.md @@ -146,6 +146,7 @@ export default defineAppConfig({ | Variable | Purpose | | --- | --- | | `GITHUB_TOKEN` | GitHub content reads and GraphQL history/RSS. Required in production. | +| `GLOBAL_CONFIG` | Connection string for the optional [`contentSha` production pin](/deployment/vercel#pin-production-content). Vercel creates it when you connect a Global Config store to the project. | | `WEBHOOK_SECRET` | HMAC secret for the GitHub push webhook (`/api/revalidate`). | | `VERCEL_BYPASS_TOKEN` | ISR purge on revalidation. Needed at **build** time too. | | `NUXT_OG_IMAGE_SECRET` | OG image signing. | diff --git a/playground/content/3.concepts/1.architecture.md b/playground/content/3.concepts/1.architecture.md index f757f10..8317c90 100644 --- a/playground/content/3.concepts/1.architecture.md +++ b/playground/content/3.concepts/1.architecture.md @@ -21,14 +21,18 @@ In development, none of this applies: content is read straight from your working ## Pinned to a commit -Production doesn't read "whatever is on `main` right now." On each request, the server resolves the latest commit **touching the content directory** on the production branch (a shared, 60-second-TTL cache keeps this to about one GitHub call per minute) and pins every read to that immutable SHA. +Production doesn't read "whatever is on `main` right now." On each server render, comark-docs first checks a connected Vercel Global Config store for a `contentSha` value. When the value exists, every production content read is pinned to that commit. You can use this override to hold production on a reviewed version or roll content back without changing the production branch. See [Pin production content](/deployment/vercel#pin-production-content) for setup and cache timing. + +Without a `contentSha` value, the server resolves the latest commit **touching the content directory** on the production branch. A shared, 60-second-TTL cache keeps this to about one GitHub call per minute. If Global Config is unavailable, comark-docs also falls back to this branch-based resolution. + +The Global Config pin applies only to the production Vercel environment. Preview deployments continue to follow their target branch, and local development reads from your working tree. Pinning buys two things: - **Consistency** — a request never mixes files from two commits, even mid-push. - **Cacheability** — content at a SHA can never change, so parsed pages are cached hard. -Code-only commits don't move the content SHA, so they don't invalidate anything. +When no Global Config pin is active, code-only commits don't move the content SHA, so they don't invalidate anything. ## Cache tiers diff --git a/playground/content/4.deployment/1.vercel.md b/playground/content/4.deployment/1.vercel.md index 34aec5e..f1021ed 100644 --- a/playground/content/4.deployment/1.vercel.md +++ b/playground/content/4.deployment/1.vercel.md @@ -1,6 +1,6 @@ --- title: Vercel -description: Deploy your docs on Vercel with instant content updates, ISR caching, and builds that skip content-only pushes. +description: Deploy your docs on Vercel with content pinning, ISR caching, instant updates, and builds that skip content-only pushes. --- The layer is built for [Vercel](https://vercel.com): rendered pages are cached at the edge with [ISR](https://vercel.com/docs/incremental-static-regeneration), parsed content persists in the Runtime Cache across deployments, and a GitHub webhook purges pages the moment content changes. @@ -20,6 +20,7 @@ In the project's **Settings → Environment Variables**: | Variable | Purpose | | --- | --- | | `GITHUB_TOKEN` | Reads content from GitHub and queries the GraphQL API for page history and RSS. A fine-grained token with read access to the repository contents is enough. | +| `GLOBAL_CONFIG` | Optional connection string for pinning production content to a specific commit. Vercel creates it when you connect a Global Config store. | | `WEBHOOK_SECRET` | Shared secret that signs the push webhook. Generate a random string. | | `VERCEL_BYPASS_TOKEN` | Lets the revalidation endpoint purge ISR pages. Must be available at **build** time — it's baked into the deployment's ISR configuration, so a runtime-only value leaves purging broken. | | `NUXT_OG_IMAGE_SECRET` | Signs OG image URLs. Generate a random string. | @@ -60,6 +61,40 @@ Adjust the path if your content directory differs. Both setups behave the same; :: +## Pin production content + +By default, production follows the latest commit that changed your content directory. An optional Vercel Global Config value named `contentSha` lets you hold production on a reviewed commit or roll content back without moving the production branch. The pin affects production only; preview deployments and local development don't read it. + +::steps{level="3"} + +### Connect a Global Config store + +In your Vercel project, open **Global Config**, then create a project store or connect an existing store. Vercel creates a `GLOBAL_CONFIG` environment variable containing the store's connection string. See [Vercel's Global Config setup guide](https://vercel.com/docs/global-config/get-started) for the dashboard workflow. + +If you connect the store after the current production deployment was built, redeploy once so its server functions receive `GLOBAL_CONFIG`. Later item updates don't require a redeploy. + +### Add the content SHA + +Copy the full commit SHA for the content version you want to serve. In the store's **Items** editor, add `contentSha` as a JSON string, then select **Save Items**: + +```json [Global Config items] +{ + "contentSha": "0123456789abcdef0123456789abcdef01234567" +} +``` + +New server renders now read content from that commit. Pages already cached by ISR update as they expire, which takes up to the configured [`comarkDocs.isr`](/getting-started/configuration#nuxtconfigts-comarkdocs-options) duration (300 seconds by default). + +### Remove or move the pin + +Replace `contentSha` with another full commit SHA to move the pin. Delete the item to resume following the latest content commit on the production branch. Neither change requires a redeploy, but the same ISR expiration window applies. + +:: + +::warning +`contentSha` must resolve to a commit in the configured GitHub repository. An invalid or inaccessible value makes production content reads fail. A missing key, disconnected store, or Global Config read error safely falls back to the production branch. +:: + ## ISR behavior The layer generates ISR route rules for every top-level content section, the landing page, previews, and the machine-readable routes (`/raw/**`, `/llms.txt`, `/rss.xml`). Pages expire after 300 seconds by default, or immediately when the webhook purges them. @@ -68,7 +103,9 @@ Tune or disable this with [`comarkDocs.isr`](/getting-started/configuration#nuxt ## Rolling back content -Content follows the head of the production branch, not the deployment — so rolling back a *deployment* in Vercel does not roll back *content*. Instead, roll back content with git: +Content follows the head of the production branch unless a `contentSha` pin is active, so rolling back a *deployment* in Vercel does not roll back *content*. For a temporary rollback that leaves git history unchanged, [pin production to an older content commit](#pin-production-content). + +For a permanent rollback on the production branch, revert the content commit with git: ```bash [Terminal] git revert