Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions docs/cold-page-request.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,30 +18,38 @@ 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=<branch>&path=<contentDir>
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=<branch>&path=<contentDir>
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 <content-sha>)
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 <content-sha>)
ContentRoute-->>SSR: parsed page

SSR-->>Edge: HTML
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 `<content-sha>`. 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 `<content-sha>`. 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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions playground/content/1.getting-started/3.configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
8 changes: 6 additions & 2 deletions playground/content/3.concepts/1.architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 39 additions & 2 deletions playground/content/4.deployment/1.vercel.md
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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. |
Expand Down Expand Up @@ -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.
Expand All @@ -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 <bad-commit>
Expand Down
Loading