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

TierWhereWhen to edit
Runtime (CMS)Directus collections at http://localhost:8055Any visible copy that should be editable without a deploy
Build-time fallbacksfrontend/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.

CollectionSingleton?Renders inNotes
site_settingsYesHeader logo, footer contact, JSON-LD, default SEOEdit once per site
pagesNoEach routed page (/, /about, custom slugs)Has child page_sections (O2M)
page_sectionsNoStacked sections inside a pagetype field decides which React component renders it
navigationNoHeader navSelf-referencing parent for hierarchical menus
footer_groupsNoFooter column headings
footer_linksNoIndividual footer linksM2O → footer_groups
servicesNoServices section + /services pageHas icon, features JSON, seo
pricing_plansNoPricing sectionFields: price_monthly, price_yearly, currency
faqsNoFAQ sectioncategory for filtering
testimonialsNoTestimonials sectionrating, client info
case_studiesNo/case-studies/[slug]metrics JSON, full seo
blog_categoriesNoBlog filter chips
team_membersNoUsed as blog post author
blog_postsNo/blog, /blog/[slug]M2O author + category
legal_pagesNo/legal/[slug] (terms, privacy, etc.)WYSIWYG body
docs_pagesNo/docs/[slug] (help center)WYSIWYG body
seo_landing_pagesNoIndustry-specific SEO pageschallenges / solutions / metrics JSON
portfolio_projectsNoPortfolio showcasetags, result, external url
form_submissionsNo(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).

  1. Click ContentPages.
  2. Click the row whose slug is home (or whichever page you want to edit).
  3. Edit fields on the parent (e.g., slug, status, seo).
  4. Scroll to the Translations section: pick a locale tab (EN / FR / ES) and edit title, description, etc. for that locale.
  5. 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.
  6. 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 *_translations collection holding title, description, body, etc.: one row per language.

To add a translation for an existing item

  1. Open the parent row in Directus admin.
  2. In the Translations field block, click the language tab (e.g., Français).
  3. If empty, Directus auto-creates the row when you save. Fill in the locale-specific fields.
  4. Save.

To add a new language

The shipped locales are en, fr, es (all LTR). To add a fourth:

  1. Frontend: add the code to frontend/src/config/i18n.ts (locales array, localeNames map).
  2. Messages: create frontend/src/messages/<code>.json mirroring en.json.
  3. Directus: open SettingsData Modellanguages, add a row with the new code (e.g., de).
  4. Re-bootstrap permissions isn't required; languages is just a lookup table.
  5. 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):

  1. Bootstrap: open directus/scripts/bootstrap.mjs, find the services_translations block, add:
    await ensureField("services_translations", {
      field: "subtitle",
      type: "string",
      meta: { interface: "input", width: "full" },
      schema: { is_nullable: true },
    });
    
  2. Re-run bootstrap (idempotent: it skips existing fields):
    docker compose run --rm directus-init
    
  3. TypeScript: add subtitle?: string; to ServicesTranslation in frontend/src/types/directus.ts.
  4. Fetcher: add subtitle to the GraphQL query in frontend/src/lib/directus.ts.
  5. Component: render service.subtitle in frontend/src/components/sections/ServicesSection.tsx.
  6. Edit a service row in Directus admin → fill subtitle for each locale → confirm it renders.

Header & footer

Header nav and footer are CMS-driven, not config-driven.

  • Header nav: edit the navigation collection. Use parent for nested menus. The Header.tsx component pulls them by sort order.
  • Footer link groups: edit footer_groups (column headings) and footer_links (individual links, M2O → footer_groups). The Footer.tsx component groups and renders them.
  • Footer contact / legal blurb: edit the site_settings singleton.

Both honor translations.

Blog posts

  1. Author: make sure your author exists in team_members (edit via Directus admin if needed).
  2. Category: pick or add one in blog_categories.
  3. Post: go to blog_postsCreate item:
    • Set slug, status: "published", link to author and category.
    • In Translations: write the title, excerpt, body (WYSIWYG) per locale.
    • Set seo JSON if you want page-specific metadata.
  4. Save. The post appears at /blog/<slug> for each locale and in the /blog listing.

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:

  1. Open a collection in admin.
  2. Click the menu → Import / ExportImport.
  3. Upload a CSV matching the collection's schema (column names = field names).
  4. 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:

  1. Check SettingsFlows in Directus admin: make sure the revalidate flow is enabled.
  2. Check the frontend logs for /api/revalidate requests.
  3. Confirm REVALIDATE_URL is reachable from the Directus container (host.docker.internal:3000 for local Docker; the production frontend URL otherwise).
  4. 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:

  1. Open SettingsAccess ControlPublic in Directus admin.
  2. Click the collection → set Read to No Access.
  3. 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:

  1. Edit a row in Directus → save.
  2. Switch to the frontend tab → refresh.
  3. If the change isn't visible, check the cache invalidation flow above.

Related guides