The boilerplate includes a powerful, multi-locale CMS for managing pages, blog posts (with categories, tags, and pagination), and media. Content is stored in Supabase with JSONB columns for localized content, cached with Next.js unstable_cache, and rendered with shortcode support.

CMS Pages

CMS pages list with locale badges and actions

CMS Page Editor

Page editor with WYSIWYG and SEO settings

Media Library

Media library with drag-and-drop upload

File Structure

lib/cms/
├── queries.ts           # CRUD operations & cached queries
├── storage.ts           # Supabase Storage for media
├── types.ts             # TypeScript interfaces
└── index.ts             # Re-exports

lib/shortcodes/
├── parser.ts            # Shortcode parsing engine
└── index.ts

components/
├── shortcodes/
│   └── index.tsx        # Shortcode React components
├── html-renderer.tsx    # Content renderer with shortcode support
└── admin/
    └── media-library.tsx # Admin media library component

app/
├── [locale]/(admin)/admin-dashboard/cms/
│   ├── page.tsx         # CMS overview
│   ├── pages/           # Pages management
│   │   ├── page.tsx     # List pages
│   │   ├── new/         # Create page
│   │   └── [pageId]/    # Edit page
│   ├── blocks/          # Reusable content blocks
│   └── media/           # Media library
└── api/admin/cms/
    ├── pages/route.ts   # Pages API
    ├── blocks/route.ts  # Blocks API
    └── media/route.ts   # Media upload/delete API

Pages & Blog

CMS pages and blog posts use the same data structure. Blog posts are simply pages with a blog/ prefix in their slug. All content supports multi-locale with per-locale publishing status.

Page Data Structure

Each CMS page stores its title, content, excerpt, and SEO metadata as JSONB fields keyed by locale (e.g., {"fr-FR": "...", "fr-CH": "...", "en-US": "...", "en-CA": "..."}). This means all translations for a page live in a single row, making queries efficient and avoiding join overhead.

Database Schema

CMS pages are stored in the cms_pages table with columns for slug, status (published/draft), locale-keyed JSONB content, SEO metadata, and timestamps. CMS blocks use a similar structure in cms_blocks for reusable content snippets. Both tables have RLS policies restricting admin-only write access while allowing public read access for published content.

Fetching Content

CMS content is fetched through cached query functions in lib/cms/queries.ts. Pages are fetched by slug and cached for 24 hours using Next.js unstable_cache with tag-based revalidation. When content is updated through the admin CMS, the cache is automatically invalidated.

Rendering CMS Content

CMS pages are rendered through the dynamic route at app/[locale]/(frontend)/[slug]/page.tsx. The page component fetches the CMS content by slug, extracts the locale-specific fields (title, content, SEO metadata), and renders the HTML through the HtmlRenderer component. The renderer processes shortcodes, sanitizes the HTML, and applies consistent styling.

Blog Posts

Blog posts are CMS pages with a blog/ prefix in the slug:

Slug URL Type
about /[locale]/about Standard page
blog/getting-started /[locale]/blog/getting-started Blog post
blog/announcement /[locale]/blog/announcement Blog post

Blog Post Features

Feature Description
Featured Image Header image with hover zoom on cards, full-width on detail page
Custom Excerpt Per-locale excerpt (max 300 chars) with auto-generated fallback
Reading Time Automatically calculated from content length
Publication Date Shown on cards and detail page
Last Updated Displayed if different from publication date
OpenGraph Article metadata for social sharing (BlogPosting JSON-LD)
Categories Color-coded categories with multi-locale names. One category per post. Filter bar on listing page (?category=slug)
Tags Multi-locale tags (many per post). Displayed as badges on cards and detail page. Filter via ?tag=slug
Pagination 9 posts per page with page numbers, prev/next navigation. URL-based (?page=2), preserves active filters

Advanced SEO Controls

Each page has per-locale SEO settings:

Field Description
SEO Title Custom meta title (overrides page title)
Meta Description Custom description for search results
Noindex Hide page from search engines (robots: noindex)
Nofollow Prevent search engines following links (robots: nofollow)
Canonical URL Custom canonical per locale (prevents duplicate content)
OG Image Custom social share image (1200x630px recommended)

Visual indicators in the admin show indexing status: green = indexed, amber = noindex.

Pages API

Endpoint Method Description
/api/admin/cms/pages GET List all pages (admin)
/api/admin/cms/pages?id=xxx GET Get page by ID
/api/admin/cms/pages?slug=xxx GET Get page by slug
/api/admin/cms/pages POST Create new page
/api/admin/cms/pages PATCH Update page (full edit)
/api/admin/cms/pages PATCH Toggle published per locale via { action: 'toggle_published', id, locale }
/api/admin/cms/pages?id=xxx DELETE Delete page

Categories & Tags API

Endpoint Method Description
/api/admin/cms/categories GET List all blog categories (multi-locale)
/api/admin/cms/categories POST Create category (multi-locale name + slug + color)
/api/admin/cms/categories/[categoryId] PATCH Update a category
/api/admin/cms/categories/[categoryId] DELETE Delete a category
/api/admin/cms/tags GET List all blog tags (multi-locale)
/api/admin/cms/tags POST Create tag
/api/admin/cms/tags/[tagId] PATCH Update a tag
/api/admin/cms/tags/[tagId] DELETE Delete a tag
Reading multi-locale JSONB — always use the localized helpers

Every read of multi-locale JSONB fields (title, content, excerpt, published, seo_*) MUST go through the helpers in @/lib/i18n/localized: getLocalizedValue, getLocalizedStringValue, getLocalizedBooleanValue, and isLocalizedPublished. Older rows may still carry legacy language-only keys (fr, en) instead of full BCP-47 keys (fr-FR, en-US, en-CA, fr-CH); the helpers tolerate both forms. Reading JSONB with hardcoded keys (e.g. page.title['fr-FR'] or page.title.en) will silently render empty content for legacy rows. Admin write validators normalize incoming payloads back to BCP-47 so future inserts stay clean.

Cache Revalidation

On-demand cache revalidation is triggered when CMS content is updated through the admin dashboard. The system uses tag-based invalidation, so updating a specific page only clears that page's cache without affecting other cached content. CMS pages use 24-hour revalidation, billing data uses 1-hour revalidation, and home/pricing pages use ISR with 1-hour intervals.

Media Library

The Media Library uses a single Supabase Storage bucket (media, hardcoded in lib/cms/storage.ts and app/api/admin/cms/media/route.ts) with public reads and admin-only writes (gated by profiles.is_admin = true). Uploads are MIME-validated, capped at 10 MB, and filename-sanitized. The admin dashboard exposes a drag-and-drop UI at /admin-dashboard/cms/media with folder organization (uploads, images, documents, videos) and one-click URL copy.

See Media Library for the full API surface, bucket policy details, and security validation rules.

WYSIWYG Editor

The CMS includes a TipTap-based rich text editor with a comprehensive formatting toolbar. Located in components/admin/wysiwyg-editor.tsx.

Formatting Features

Category Features
Text Formatting Bold, italic, underline, strikethrough, inline code
Headings H1, H2, H3 with keyboard shortcuts
Alignment Left, center, right, justify
Lists Bullet lists, ordered lists
Blocks Blockquote, code block, horizontal rule
Media Links with URL input, images via media picker
History Undo/redo support

Shortcode Insertion

The "Insert" dropdown menu provides quick insertion of shortcodes:

  • YouTube/Vimeo video embeds
  • Callout boxes (info, warning, success, error)
  • CTA buttons with variants
  • Accordions for collapsible content
  • Cards with icons
  • Highlighted text
  • Dividers and spacers

Shortcodes

Shortcodes embed interactive React components inside CMS content using a WordPress-like syntax (e.g. [youtube id="abc"], [callout type="info"]…[/callout]). The registry lives in lib/shortcodes/parser.ts (SHORTCODE_DEFINITIONS) and the rendering switch in components/shortcodes/index.tsx (ShortcodeRenderer). Built-in shortcodes cover media embeds (YouTube, Vimeo), callouts, buttons, layout helpers (divider, spacer), and content blocks (accordion, card, highlight).

See Shortcodes for the complete attribute reference, syntax rules, and the steps to register a new shortcode.