Nuxt Content Setup
SaaS4Builders uses Nuxt Content v3 to manage all editorial content — documentation pages, blog posts, changelogs, pricing, and legal pages. Content is stored as Markdown and YAML files in the repository, organized by language, and validated at build time through Zod schemas defined in content.config.ts.
This approach keeps your marketing and editorial content version-controlled alongside your code, while allowing non-developers to edit content through Nuxt Studio.
Content Directory Structure
All content lives under frontend/content/, organized by locale:
frontend/content/
├── en/ # English
│ ├── 0.index.yml # Home page
│ ├── 1.docs/ # Documentation articles
│ │ ├── 1.getting-started.md
│ │ └── ...
│ ├── 2.pricing.yml # Pricing page
│ ├── 3.blog.yml # Blog index
│ ├── 3.blog/ # Blog posts
│ │ ├── my-first-post.md
│ │ └── ...
│ ├── 4.changelog.yml # Changelog index
│ ├── 4.changelog/ # Version entries
│ │ ├── v1.0.0.md
│ │ └── ...
│ └── 5.terms.md # Terms of service
├── fr/ # French (same structure)
├── es/ # Spanish
└── it/ # Italian
The numeric prefixes (0., 1., 2., etc.) control the ordering in navigation — Nuxt Content uses them for sorting and strips them from the URL slug.
Four locales are supported out of the box: English (en), French (fr), Spanish (es), and Italian (it). Each locale has an identical directory structure, ensuring every page exists in all supported languages.
Collection Definitions
Collections are defined in frontend/content.config.ts. The configuration dynamically generates collections for each supported language using a factory function:
import { defineCollection, z } from '@nuxt/content'
import { locales as availableLanguages } from './i18n/locales.config'
const createCollectionsForLanguage = (lang: string) => ({
[`index_${lang}`]: defineCollection({
source: `${lang}/0.index.yml`,
type: 'page',
schema: indexPageSchema,
}),
[`docs_${lang}`]: defineCollection({
source: `${lang}/1.docs/**/*`,
type: 'page',
schema: z.object({
links: z.array(createLinkSchema()).optional(),
}),
}),
[`pricing_${lang}`]: defineCollection({
source: `${lang}/2.pricing.yml`,
type: 'page',
schema: pricingPageSchema,
}),
[`blog_${lang}`]: defineCollection({
source: `${lang}/3.blog.yml`,
type: 'page',
}),
[`posts_${lang}`]: defineCollection({
source: `${lang}/3.blog/**/*`,
type: 'page',
schema: postSchema,
}),
[`changelog_${lang}`]: defineCollection({
source: `${lang}/4.changelog.yml`,
type: 'page',
}),
[`versions_${lang}`]: defineCollection({
source: `${lang}/4.changelog/**/*`,
type: 'page',
schema: versionSchema,
}),
[`terms_${lang}`]: defineCollection({
source: `${lang}/5.terms.md`,
type: 'page',
}),
})
const supportedLanguages = availableLanguages.map((lang) => lang.code)
export const collections = supportedLanguages.reduce((acc, lang) => {
return { ...acc, ...createCollectionsForLanguage(lang) }
}, {})
This generates 8 collections per language (32 total for 4 locales), each with appropriate source patterns and schemas.
Content Types
Home Page (index)
The home page is a YAML file (0.index.yml) with a rich schema covering:
| Section | Description |
|---|---|
hero | Hero section with CTA links and optional image |
workflows | Feature workflow cards with images |
sections | Feature sections with icon-based items |
features | Feature grid with title, description, icon |
testimonials | Customer quotes with avatars |
cta | Call-to-action with links |
Documentation (docs)
Documentation pages are Markdown files under 1.docs/. They support an optional links array in frontmatter for navigation buttons:
---
title: 'Getting Started'
description: 'Set up your development environment'
links:
- label: 'View on GitHub'
to: 'https://github.com/your-org/your-repo'
icon: 'i-simple-icons-github'
target: '_blank'
---
Your documentation content here...
Pricing (pricing)
The pricing page is a YAML file (2.pricing.yml) that contains editorial content — social proof and FAQ. The actual plans data (names, prices, features) comes from the backend API (the catalog), not from the content file. This separation keeps pricing in sync with Stripe and avoids duplication between content and billing logic.
title: A plan for every need
description: Our plans are designed to meet the requirements of both beginners and players.
seo:
title: Pricing
description: Choose the plan that's right for you.
logos:
title: Trusted by teams worldwide
items:
- name: TechFlow
quote: Scaled from 3 to 40 team members with CraftDesk
- name: Bright Agency
quote: Our go-to tool for client project management
faq:
title: Frequently Asked Questions
description: Everything you need to know about pricing.
items:
- label: Is there a free plan?
content: 'Yes, the Starter plan is free forever. No credit card required.'
defaultOpen: true
- label: Can I switch plans at any time?
content: 'Yes. Upgrades take effect immediately with prorated billing.'
- label: Do you offer annual billing?
content: 'Yes. Annual plans save approximately 17% compared to monthly billing.'
The schema reflects this split — it only defines logos and faq, not plans:
const pricingPageSchema = z.object({
logos: z.object({
title: z.string().nonempty(),
icons: z.array(z.string()).optional(),
items: z.array(
z.object({
name: z.string().nonempty(),
quote: z.string().nonempty(),
})
).optional(),
}),
faq: createBaseSchema().extend({
items: z.array(
z.object({
label: z.string().nonempty(),
content: z.string().nonempty(),
defaultOpen: z.boolean().optional(),
})
),
}),
})
Blog Posts (posts)
Blog posts are Markdown files with image, author, date, and badge frontmatter:
---
title: 'Announcing SaaS4Builders v1.0'
description: 'Our first stable release is here'
image:
src: '/images/blog/v1-launch.png'
authors:
- name: 'John Doe'
to: 'https://twitter.com/johndoe'
avatar:
src: '/images/avatars/john.jpg'
date: 2026-03-15
badge:
label: 'Release'
---
Blog post content in Markdown...
Changelog Versions (versions)
Version entries document release notes:
---
title: 'v1.2.0'
description: 'Multi-currency support and performance improvements'
date: 2026-03-20
image: '/images/changelog/v1.2.png'
---
## What's New
- Multi-currency support for subscriptions
- 40% faster dashboard loading
- Bug fixes for team invitations
Terms of Service (terms)
A simple Markdown page with standard title/description frontmatter. No additional schema required.
Reusable Schemas
The content configuration defines several reusable Zod schemas used across collections:
Image Schema
const createImageSchema = () =>
z.object({
src: z.string().nonempty().editor({ input: 'media' }),
alt: z.string().optional(),
loading: z.enum(['lazy', 'eager']).optional(),
srcset: z.string().optional(),
})
The .editor({ input: 'media' }) annotation tells Nuxt Studio to render a media picker for this field instead of a plain text input.
Link Schema
const createLinkSchema = () =>
z.object({
label: z.string().nonempty(),
to: z.string().nonempty(),
icon: z.string().optional().editor({ input: 'icon' }),
size: z.enum(['xs', 'sm', 'md', 'lg', 'xl']).optional(),
trailing: z.boolean().optional(),
target: z.string().optional(),
color: z.enum(['primary', 'secondary', 'neutral', 'error', 'warning', 'success', 'info']).optional(),
variant: z.enum(['solid', 'outline', 'subtle', 'soft', 'ghost', 'link']).optional(),
})
Links support all Nuxt UI button variants and colors, making CTA buttons fully configurable from content files.
Feature Item Schema
const createFeatureItemSchema = () =>
z.object({
title: z.string().nonempty(),
description: z.string().nonempty(),
icon: z.string().nonempty().editor({ input: 'icon' }),
})
Storage and Build
Nuxt Content v3 uses SQLite as its storage backend. During development, content is stored at frontend/.data/contents.sqlite. During the build process, the content is parsed, validated against schemas, and dumped into the SQLite database for fast querying at runtime.
queryCollection, useContent, etc.). This means changes to content files require a rebuild (or hot-reload in development mode).Adding Content
Adding a Blog Post
- Create a Markdown file under the appropriate locale directory:
frontend/content/en/3.blog/my-new-post.md
- Add the required frontmatter:
---
title: 'My New Post'
description: 'A brief description for SEO'
image:
src: '/images/blog/my-post.png'
authors:
- name: 'Your Name'
to: 'https://your-website.com'
avatar:
src: '/images/avatars/you.jpg'
date: 2026-04-01
badge:
label: 'Tutorial'
---
- Write your content using Markdown with MDC syntax (see below).
- Repeat for other locales (
fr/,es/,it/) to maintain full i18n coverage.
Adding a Documentation Page
- Create the file in
frontend/content/{locale}/1.docs/:
frontend/content/en/1.docs/3.my-new-section/1.my-page.md
- Add frontmatter with optional navigation links:
---
title: 'My Page Title'
description: 'What this page covers'
links:
- label: 'Source Code'
to: 'https://github.com/...'
icon: 'i-simple-icons-github'
target: '_blank'
---
Adding a Changelog Entry
- Create a Markdown file in
frontend/content/{locale}/4.changelog/:
frontend/content/en/4.changelog/v1.3.0.md
- Add version frontmatter:
---
title: 'v1.3.0'
description: 'Summary of changes'
date: 2026-04-15
---
MDC Syntax
Nuxt Content supports MDC (Markdown Components) syntax for richer content. Common patterns used in this project:
Callouts
::callout{type="warning"}
This is an important warning.
::
::callout{type="info"}
Helpful information here.
::
Code Groups
::code-group
\`\`\`bash [npm]
npm install @nuxtjs/content
\`\`\`
\`\`\`bash [pnpm]
pnpm add @nuxtjs/content
\`\`\`
::
Prose Components
Standard Markdown elements (headings, lists, tables, code blocks) are rendered through Nuxt Content's prose components, which are styled consistently with your UI theme.
Adding a New Locale
To add a fifth locale (e.g., German de):
- Add the locale to
frontend/i18n/locales.config.ts. - Create the content directory:
frontend/content/de/. - Copy the structure from an existing locale and translate the content.
- The collection factory in
content.config.tsautomatically generates collections for the new locale — no code changes needed.
What's Next
- Nuxt Studio Integration — Visual editing with Nuxt Studio and the Sanctum auth bridge.
- Frontend i18n — How locale switching works across the application.
- Overview — The full internationalization architecture.
Web Push
VAPID key setup, service worker registration, push subscription lifecycle, and the full backend-to-browser delivery pipeline for Web Push notifications.
Nuxt Studio Integration
Visual content editing with Nuxt Studio, secured by a custom Sanctum-based auth bridge that reuses your existing platform admin permissions.