Operational reference for inspecting and rebuilding the Strapi-backed JSON cache files under .cache/ (primary) and .cache/strapi-fallback/ (stale-data mirror).
Caching, GraphQL transport, and webhook handling are owned by @datum-cloud/strapi-revalidate. The shared client and cache manager are constructed once in src/libs/strapi/_runtime.ts and reused by every Strapi fetcher and route in this codebase.
| Variable | Purpose |
|---|---|
STRAPI_URL |
Strapi GraphQL origin (defaults exist in code; set in production). |
STRAPI_TOKEN |
Bearer token sent on every Strapi request as Authorization: Bearer …. |
STRAPI_WEBHOOK_SECRET |
Shared secret. Used by the inbound webhook and the cache admin API (different headers, see below). |
STRAPI_CACHE_TTL |
Primary cache TTL in seconds (default 2592000 = 30 days). |
STRAPI_TIMEOUT |
Per-request timeout in seconds (default 3). |
STRAPI_DEBUG |
"true" to log cache hit/miss and GraphQL retry decisions (default off). |
⚠️ STRAPI_CACHE_ENABLEDwas removed when the cache layer moved to the package — caching is always on. The env var is ignored.
| Endpoint family | Header expected |
|---|---|
POST /api/webhooks/strapi-content |
Authorization: Bearer <secret> (or X-Strapi-Signature / strapi-webhook-secret) |
GET /api/cache, GET /api/cache/:name, POST /api/cache/strapi |
X-Webhook-Secret: <secret> |
Both check the same STRAPI_WEBHOOK_SECRET value byte-for-byte. The split exists because the inbound webhook is verified by the package handler (which accepts the Strapi-conventional headers above), while the admin endpoints are still verified by the in-repo src/libs/cacheApiAuth.ts.
Strapi Cloud configuration: the webhook entry in Strapi must be configured to send Authorization: Bearer <STRAPI_WEBHOOK_SECRET> — the legacy X-Webhook-Secret header is no longer accepted by the inbound webhook handler.
Returns a JSON overview of filesystem cache entries (size, TTL expiry when present), grouped loosely by origin.
Authentication: Same as below — header X-Webhook-Secret: <secret>.
Implementation: src/pages/api/cache/index.ts, src/libs/cacheViewer.ts.
Example:
curl -sS "${ORIGIN}/api/cache" -H "X-Webhook-Secret: ${STRAPI_WEBHOOK_SECRET}"Response is pretty-printed JSON with top-level entries and bySource (strapi, strapi-fallback, etc.).
Returns the full parsed JSON for a single cache key (for example strapi-authors, strapi-card-members, article-my-slug).
Authentication: Header X-Webhook-Secret: <secret> (same as other cache APIs).
Implementation: src/pages/api/cache/[name].ts, src/libs/cacheViewer.ts, src/libs/cacheApiAuth.ts.
Query parameters:
| Parameter | Values | Default | Meaning |
|---|---|---|---|
source |
main, fallback, auto |
auto |
Main .cache/, fallback, or both |
Examples:
curl -sS "${ORIGIN}/api/cache/strapi-authors" \
-H "X-Webhook-Secret: ${STRAPI_WEBHOOK_SECRET}"
curl -sS "${ORIGIN}/api/cache/article-my-post?source=fallback" \
-H "X-Webhook-Secret: ${STRAPI_WEBHOOK_SECRET}"Success response (200):
{
"success": true,
"key": "strapi-authors",
"source": "strapi",
"expiresAt": "May 28, 2026, 3:45 PM",
"expired": false,
"size": "12.4 KB",
"data": []
}HTTP status codes:
| Status | When |
|---|---|
| 200 | Entry found and JSON parsed |
| 400 | Invalid cache name or invalid source query |
| 401 | Missing or wrong X-Webhook-Secret |
| 404 | No .json file for that key in the searched location(s) |
| 405 | Method other than GET |
| 500 | File exists but JSON is corrupt |
Notes:
- Reads raw filesystem files. Expired TTL entries are returned with
"expired": trueand are not deleted (unlikeCache.get()used at runtime). - Cache names must match safe key rules (alphanumeric start; letters, digits,
.,_,-only; no path segments).
Rebuilds cache entries using the same Strapi GraphQL helpers as the site (fetchStrapiArticles, fetchStrapiAuthors, getStrapiTeamMembers, getStrapiCardMembers, fetchStrapiArticleBySlug). All helpers route through the shared package-backed cache manager in src/libs/strapi/_runtime.ts, so a successful refetch transparently mirrors to the fallback at the same key.
Implementation: src/pages/api/cache/strapi.ts, src/libs/strapi/regenerateCache.ts, src/libs/strapi/_runtime.ts.
| Header | Value |
|---|---|
X-Webhook-Secret |
Exact value of STRAPI_WEBHOOK_SECRET |
401 Unauthorized — secret missing, wrong length, mismatch, or STRAPI_WEBHOOK_SECRET not configured on the server.
JSON is read only when Content-Type includes application/json. If you send JSON without that header, the body is ignored.
Use when you want idempotent warmup: populate Strapi-derived keys only if the corresponding .cache/<key>.json file does not exist (same behavior as before named force regeneration existed).
Request: POST /api/cache/strapi
Body: omit entirely, omit JSON, or JSON without a names property (see edge cases).
Behavior (summary):
strapi-articles— if missing →fetchStrapiArticles()strapi-authors— if missing →fetchStrapiAuthors()strapi-roadmaps— if missing →fetchStrapiRoadmaps()strapi-team-members— if missing →getStrapiTeamMembers()(derived from cached/fetched authors + article list semantics in code paths)strapi-article-{slug}— for each article in the list, if missing →fetchStrapiArticleBySlug(slug)strapi-author-slug-{slug}— for eligible authors with articles, if missing →fetchStrapiAuthorBySlug(slug)
Important: strapi-roadmaps and strapi-author-slug-* participate in fill-missing but are not valid targets for named force regeneration (force list is fixed below + per-article keys only).
Request: POST /api/cache/strapi
Headers:
Content-Type: application/jsonX-Webhook-Secret: <secret>
Body:
{
"names": [
"strapi-articles",
"strapi-authors",
"strapi-team-members",
"strapi-card-members",
"strapi-article-your-blog-post-slug"
]
}For each supplied name:
- The primary cache entry
{name}under.cache/is deleted (.json+.expires+.tagssidecar viaCacheManager.delete). - The matching fetch helper runs to repopulate. On success it writes both the primary entry and a fallback mirror at the same key under
.cache/strapi-fallback/(the package'sCacheManager.sethandles both writes atomically — see fallback note below).
De-duplication: Duplicate strings (after trim) are processed once. Non-string entries in names are ignored (validation can still fail if no valid strings remain).
| Pattern | Meaning |
|---|---|
strapi-articles |
Article list slice used across the site; calls fetchStrapiArticles() |
strapi-authors |
Full author list; calls fetchStrapiAuthors() |
strapi-team-members |
Derived list (isTeam); calls getStrapiTeamMembers() |
strapi-card-members |
Derived list (isCard); calls getStrapiCardMembers() |
strapi-article-{slug} |
Single article payload; {slug} is the blog slug. Calls fetchStrapiArticleBySlug(slug) |
The literal key strapi-article-{slug} is not a real key — substitute your post slug (for example strapi-article-announcing-datum-platform).
Slug validation (for article keys):
- Prefix must be
strapi-article-. - The slug segment must be non-empty (trimmed).
- Must not contain
/or\, and must not be.or...
Team and card caches are rebuilt from fetchStrapiAuthors() (after their own TTL file is cleared). If you need those lists to reflect author schema changes (isTeam / isCard), regenerate strapi-authors in the same or an earlier request. Depending on CMS shape, strapi-articles author flags may matter too — regenerate both when in doubt.
POST /api/webhooks/strapi-content invalidates primary cache entries by tag (articles, authors, roadmaps, plus per-slug tags like article:<slug>). Fallback entries are intentionally preserved — they exist to keep the site serving stale data when Strapi is unreachable, and clearing them defeats that purpose.
Named force regeneration also deletes only the primary entry (CacheManager.delete does not touch the fallback). On the next refetch, the package's CacheManager.set writes both the primary entry and a fallback mirror at the same key.
- On a successful Strapi response, the fallback mirror is overwritten with fresh data.
- If Strapi is unreachable during a refetch, the fetcher (e.g.
fetchStrapiArticleBySlug) reads the existing fallback entry viacache.getFallback(...)so the request still returns content.
Operators who need to drop stale fallback data must delete the file(s) manually — for example rm .cache/strapi-fallback/strapi-article-<slug>.json. Neither the webhook nor the admin force-regen endpoint touches the fallback directory.
Legacy fallback files: the pre-package layout stored fallbacks under different keys (e.g.
.cache/strapi-fallback/articles.jsonmirrored.cache/strapi-articles.json). After the migration to@datum-cloud/strapi-revalidate, fallback keys match the primary key. Old files atarticles.json,article-<slug>.json,roadmaps.jsonetc. are orphaned but harmless; delete them at your leisure.
Responses are Content-Type: application/json.
success is always true in the happy path handler (individual Strapi failures are reported under details.errors).
{
"success": true,
"message": "Strapi cache regeneration completed",
"details": {
"regenerated": ["strapi-articles"],
"skipped": ["strapi-authors", "strapi-roadmaps"],
"errors": [],
"regeneratedCount": 1,
"skippedCount": 2
}
}(regenerated / skipped entries are cache key names matching .cache/<key>.json filenames without the extension.)
HTTP 200. success is false when one or more names failed refetch (details.errors non-empty).
{
"success": true,
"message": "Strapi cache force regeneration completed",
"details": {
"mode": "force",
"regenerated": ["strapi-articles", "strapi-article-acme-announcement"],
"skipped": [],
"errors": [],
"regeneratedCount": 2,
"skippedCount": 0
}
}details.skipped is currently unused for force regeneration (reserved / always empty arrays).
Partial failure example:
{
"success": false,
"message": "Strapi cache force regeneration completed",
"details": {
"mode": "force",
"regenerated": ["strapi-articles"],
"skipped": [],
"errors": ["strapi-article-missing-post: Article not found or Strapi unavailable"],
"regeneratedCount": 1,
"skippedCount": 0
}
}| Status | When |
|---|---|
| 400 | Invalid JSON body; names present but not an array; validation failed (unknown key, invalid article slug rules, empty names array after trimming). |
| 401 | Webhook secret check failed. |
| 405 | Non-POST (middleware may still normalize). |
| 500 | Unhandled exception in the handler. |
400 validation bodies look like:
{
"success": false,
"error": "Validation failed",
"details": ["strapi-roadmaps: Unknown cache name \"strapi-roadmaps\". Expected ..."]
}Fill missing (no JSON body):
curl -sS -X POST "${ORIGIN}/api/cache/strapi" \
-H "X-Webhook-Secret: ${STRAPI_WEBHOOK_SECRET}"Force fixed keys + one article:
curl -sS -X POST "${ORIGIN}/api/cache/strapi" \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: ${STRAPI_WEBHOOK_SECRET}" \
-d '{
"names": [
"strapi-articles",
"strapi-authors",
"strapi-team-members",
"strapi-card-members",
"strapi-article-my-post-slug"
]
}'If you invoke this logic from tooling or scripts inside this codebase:
| Export | Module |
|---|---|
regenerateStrapiCacheIfMissing |
@libs/strapi/regenerateCache |
forceRegenerateStrapiCache |
@libs/strapi/regenerateCache |
validateStrapiForceRegenerateRequest |
@libs/strapi/regenerateCache |
validateStrapiForceRegenerateName |
@libs/strapi/regenerateCache |
STRAPI_FORCE_REGENERATE_KEYS |
@libs/strapi/regenerateCache |
ARTICLE_CACHE_PREFIX |
@libs/strapi/regenerateCache |
| Route | Role |
|---|---|
POST /api/webhooks/strapi-content |
Strapi CMS webhook: invalidates primary cache by tag (fallback preserved by design) and re-warms the affected fetchers via onRevalidate |
GET /api/cache |
Inspect cache filenames and TTL metadata |
GET /api/cache/:name |
Full JSON for one cache key (source query: main, fallback, auto) |