Content Editing
Almost all marketing content in SaaSForge Agency lives in Directus: a headless CMS that ships in docker-compose.yml and boots into a fully scaffolded schema. You edit pages, sections, services, blog posts, FAQs, navigation, footer, and legal text in the admin UI at http://localhost:8055.
This guide is the editor's handbook: every collection, the translations workflow (EN / FR / ES), and how edits propagate to the live site.
Two-tier content model
| Tier | Where | When to edit |
|---|---|---|
| Runtime (CMS) | Directus collections at http://localhost:8055 | Any visible copy that should be editable without a deploy |
| Build-time fallbacks | frontend/src/config/ui/pages.ts + frontend/src/config/brand.ts | "Directus is offline" copy and developer-side defaults |
Most edits go in Directus. The fallbacks exist so the site renders gracefully if the CMS is empty or down.
The 18 Directus collections
Each content collection has a translations child (*_translations) keyed to languages_code: see Translations workflow below.
| Collection | Singleton? | Renders in | Notes |
|---|---|---|---|
site_settings | Yes | Header logo, footer contact, JSON-LD, default SEO | Edit once per site |
pages | No | Each routed page (/, /about, custom slugs) | Has child page_sections (O2M) |
page_sections | No | Stacked sections inside a page | type field decides which React component renders it |
navigation | No | Header nav | Self-referencing parent for hierarchical menus |
footer_groups | No | Footer column headings | |
footer_links | No | Individual footer links | M2O → footer_groups |
services | No | Services section + /services page | Has icon, features JSON, seo |
pricing_plans | No | Pricing section | Fields: price_monthly, price_yearly, currency |
faqs | No | FAQ section | category for filtering |
testimonials | No | Testimonials section | rating, client info |
case_studies | No | /case-studies/[slug] | metrics JSON, full seo |
blog_categories | No | Blog filter chips | |
team_members | No | Used as blog post author | |
blog_posts | No | /blog, /blog/[slug] | M2O author + category |
legal_pages | No | /legal/[slug] (terms, privacy, etc.) | WYSIWYG body |
docs_pages | No | /docs/[slug] (help center) | WYSIWYG body |
seo_landing_pages | No | Industry-specific SEO pages | challenges / solutions / metrics JSON |
portfolio_projects | No | Portfolio showcase | tags, result, external url |
form_submissions | No | (write-only target for contact form) | Public can create; only admins read |
Editing your first page
Open http://localhost:8055, log in with the admin credentials from .env (DIRECTUS_ADMIN_EMAIL / DIRECTUS_ADMIN_PASSWORD).
- Click Content → Pages.
- Click the row whose
slugishome(or whichever page you want to edit). - Edit fields on the parent (e.g.,
slug,status,seo). - Scroll to the Translations section: pick a locale tab (EN / FR / ES) and edit
title,description, etc. for that locale. - Scroll to the Page Sections section: these are the stacked blocks rendered on the page. Drag to reorder, click into one to edit its
type,variant,payload. - Click Save.
Within ~1 second the Next.js frontend cache is invalidated (via the bootstrap-installed Directus Flow) and the page rebuilds on next request.
Translations workflow (EN / FR / ES)
This is the unique part of Agency. Every translatable collection has:
- A parent row holding non-translatable fields (
slug,sort,status,icon,seo). - One or more child rows in the matching
*_translationscollection holdingtitle,description,body, etc.: one row per language.
To add a translation for an existing item
- Open the parent row in Directus admin.
- In the Translations field block, click the language tab (e.g., Français).
- If empty, Directus auto-creates the row when you save. Fill in the locale-specific fields.
- Save.
To add a new language
The shipped locales are en, fr, es (all LTR). To add a fourth:
- Frontend: add the code to
frontend/src/config/i18n.ts(localesarray,localeNamesmap). - Messages: create
frontend/src/messages/<code>.jsonmirroringen.json. - Directus: open Settings → Data Model → languages, add a row with the new code (e.g.,
de). - Re-bootstrap permissions isn't required;
languagesis just a lookup table. - Edit existing rows in Directus admin to add translations for the new locale.
The *_translations rows still link to languages by code: no schema change needed.
Adding a new translatable field
Say you want a subtitle field on services (translatable):
- Bootstrap: open
directus/scripts/bootstrap.mjs, find theservices_translationsblock, add:await ensureField("services_translations", { field: "subtitle", type: "string", meta: { interface: "input", width: "full" }, schema: { is_nullable: true }, }); - Re-run bootstrap (idempotent: it skips existing fields):
docker compose run --rm directus-init - TypeScript: add
subtitle?: string;toServicesTranslationinfrontend/src/types/directus.ts. - Fetcher: add
subtitleto the GraphQL query infrontend/src/lib/directus.ts. - Component: render
service.subtitleinfrontend/src/components/sections/ServicesSection.tsx. - Edit a service row in Directus admin → fill
subtitlefor each locale → confirm it renders.
Header & footer
Header nav and footer are CMS-driven, not config-driven.
- Header nav: edit the
navigationcollection. Useparentfor nested menus. TheHeader.tsxcomponent pulls them by sort order. - Footer link groups: edit
footer_groups(column headings) andfooter_links(individual links, M2O →footer_groups). TheFooter.tsxcomponent groups and renders them. - Footer contact / legal blurb: edit the
site_settingssingleton.
Both honor translations.
Blog posts
- Author: make sure your author exists in
team_members(edit via Directus admin if needed). - Category: pick or add one in
blog_categories. - Post: go to
blog_posts→ Create item:- Set
slug,status: "published", link toauthorandcategory. - In Translations: write the
title,excerpt,body(WYSIWYG) per locale. - Set
seoJSON if you want page-specific metadata.
- Set
- Save. The post appears at
/blog/<slug>for each locale and in the/bloglisting.
Legal pages
/legal/terms, /legal/privacy, etc. are stored as legal_pages rows with a WYSIWYG content body per locale. Add a row, set its slug, write the per-locale content. The route /legal/[slug] picks it up automatically.
For a legal review: edit each translation tab; export via the Directus API if your legal team needs a Word/PDF copy.
Help / docs pages
Same pattern as legal: docs_pages collection, WYSIWYG body per locale, served at /docs/[slug]. Use this for end-user help articles. (For developer docs, use .mdx files in this very boilerlykit-sync folder.)
Bulk content import
Directus supports CSV import per collection:
- Open a collection in admin.
- Click the ⋮ menu → Import / Export → Import.
- Upload a CSV matching the collection's schema (column names = field names).
- Map the columns, click import.
For translations: import the parent rows first (with their primary keys), then import the *_translations rows referencing those keys + languages_code.
For programmatic bulk imports, hit the Directus REST API directly (see Working with Directus).
Cache invalidation flow
Every write to a content collection triggers a Directus Flow that POSTs to ${REVALIDATE_URL}/api/revalidate with the REVALIDATE_SECRET. Next.js receives the call and revalidates the affected ISR cache tags.
If your edit doesn't show up:
- Check Settings → Flows in Directus admin: make sure the revalidate flow is enabled.
- Check the frontend logs for
/api/revalidaterequests. - Confirm
REVALIDATE_URLis reachable from the Directus container (host.docker.internal:3000for local Docker; the production frontend URL otherwise). - As a fallback, hard-refresh the browser (cache busted client-side).
Permissions: what the Public role can read
bootstrap.mjs grants the Public role read access on every content collection so the frontend can fetch without a token. Write access is restricted to authenticated admin users.
The exception: form_submissions allows Public create (so the contact form works) but not read.
To restrict a collection to logged-in users:
- Open Settings → Access Control → Public in Directus admin.
- Click the collection → set Read to No Access.
- Decide how the frontend will authenticate (e.g., a static token via env var, or a per-user login flow: outside the Agency template's defaults).
After editing
Edits are live within ~1 second (cache invalidation is automatic). For a stale-render check:
- Edit a row in Directus → save.
- Switch to the frontend tab → refresh.
- If the change isn't visible, check the cache invalidation flow above.
Related guides
- UI Components: the React component catalog
- UI Customization: editing components, adding sections
- Working with Directus: adding fields, collections, permissions
- Theming: colors, fonts, dark mode