Content — pages + blogs

Shopify Storefront Page / Article shapes verbatim so storefront code ports 1:1. PUBLISHED only — drafts and deleted rows 404 (lib status filter AND the anon RLS policy). Stored HTML is server-sanitized on write — safe to render raw (dangerouslySetInnerHTML).

Path convention (src/lib/content/paths.ts, shared with menus + redirects): /pages/<handle>, /blogs/<handle>, /blogs/<blog>/<post>. Handle renames create URL redirects automatically — wire redirects.md into your 404 handler and old content URLs keep working.

Policy pages every store carries (privacy-policy, terms-of-service, refund-policy, shipping-policy) are seeded as drafts — they 404 here until the merchant publishes them.

Content is admin-authored; the shared dev tenant seeds no published pages/blogs, so the executable blocks prove the 404 contract and the happy-path shapes are pinned by tests/store/content-store.test.ts and tests/contract/sdk-content.contract.test.ts (which create fixtures with admin auth).

GET /api/store/pages/:handle — one page

  • Purpose: render a static page (/pages/<handle>).
  • Auth: anon (x-client-id).
  • Request: path handle only.
  • Response:
{
  "page": {
    "id": "cpage_<hex>",
    "handle": "about-us",
    "title": "About us",
    "body": "<h2>…</h2>",              // sanitized HTML — render raw
    "bodySummary": "…",                 // stripped, ~160-char word-boundary cut
    "seo": {
      "title": "About us",              // fallback: title
      "description": "…"                // fallback: bodySummary
    },
    "publishedAt": "ISO-8601",
    "createdAt": "ISO-8601",
    "updatedAt": "ISO-8601"
  }
}
  • Errors: 404 not_found — unknown handle, draft, or deleted.
  • SDK: content.getPage(client, handle)
  • Components: static page template; footer policy links.
  • Settings: page status (draft/published) in admin → Content → Pages.
# Unknown handle → the clean 404 contract (drafts behave identically).
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/pages/doc-no-such-page-$RUN" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404
curl -s "$BASE/api/store/pages/doc-no-such-page-$RUN" -H "x-client-id: $CLIENT_ID" \
  | grep -q '"code":"not_found"'

GET /api/store/blogs/:handle/posts — blog listing

  • Purpose: the blog index (/blogs/<handle>), paginated.
  • Auth: anon (x-client-id).
  • Request: query {limit? (≤100, default 20), offset?}. Published only, published_at desc.
  • Response:
{
  "blog": { "handle": "news", "title": "News" },
  "articles": [ /* Article[] — the exact shape below */ ],
  "count": 12,
  "offset": 0,
  "limit": 20
}
  • Errors: 404 not_found (unknown blog handle) · 400 validation_failed (limit/offset out of range).
  • SDK: content.listBlogPosts(client, blogHandle, query?)
  • Components: blog index template.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/blogs/doc-no-such-blog-$RUN/posts" -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404

GET /api/store/blogs/:handle/posts/:postHandle — one article

  • Purpose: render one post (/blogs/<blog>/<post>).
  • Auth: anon (x-client-id).
  • Response — Shopify Article verbatim:
{
  "article": {
    "id": "bpost_<hex>",
    "handle": "hello-world",
    "title": "Hello world",
    "contentHtml": "<p>…</p>",         // sanitized HTML — render raw
    "content": "…",                     // plain-text version
    "excerpt": "…",                     // author-provided; falls back to truncated content
    "image": { "url": "https://…", "altText": null },  // null when no hero image
    "tags": ["news"],
    "author": { "name": "Maria" },     // null when unset
    "publishedAt": "ISO-8601",
    "seo": { "title": "…", "description": "…" },  // fallbacks: title / excerpt
    "blog": { "handle": "news", "title": "News" }
  }
}
  • Errors: 404 not_found — unknown blog OR unknown/draft post.
  • SDK: content.getBlogPost(client, blogHandle, postHandle)
  • Components: article template; article cards reuse the listing shape.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
  "$BASE/api/store/blogs/doc-no-such-blog-$RUN/posts/doc-no-post-$RUN" \
  -H "x-client-id: $CLIENT_ID")
test "$STATUS" = 404