Directus CMS Setup

Agency uses Directus 11 as its headless CMS. The stack is fully scripted: one command provisions every collection and seeds content in English, French, and Spanish. You do not click through the Directus admin to create data model.

One-command setup

From the repository root:

cp .env.example .env
docker compose up -d --build

That's the whole setup. Here's what the init sequence does automatically:

StepServiceWhat happens
1database + cachePostgreSQL 16 + Redis 7 boot and become healthy
2directusImage is built from directus/Dockerfile, which vendors @directus-labs/seo-plugin into the extensions folder. Admin is auto-created from DIRECTUS_ADMIN_EMAIL / DIRECTUS_ADMIN_PASSWORD.
3directus-init (one-shot)Runs bootstrap.mjs → every collection, field, relation, translation child, language (en/fr/es), and Public-role permission is created from code. Then runs seed.mjs → every collection filled with EN/FR/ES content.
4directus-init exitsDirectus keeps running. You log in at http://localhost:8055 and the CMS is fully populated.

Environment variables

.env (copied from .env.example):

DIRECTUS_ADMIN_EMAIL=admin@example.com
DIRECTUS_ADMIN_PASSWORD=change-me
POSTGRES_USER=directus
POSTGRES_PASSWORD=change-me
POSTGRES_DB=directus
DIRECTUS_KEY=replace-with-a-random-uuid
DIRECTUS_SECRET=replace-with-a-random-uuid

Change admin + DB credentials before deploying.

Re-running bootstrap

Both scripts are idempotent: safe to re-run any time. On re-run they print [skip] ... already exists for existing collections/fields and move on.

# Container path (uses the init sidecar):
docker compose run --rm directus-init

# Host path (if you have Node installed):
cd directus/scripts
npm install
DIRECTUS_URL=http://localhost:8055 \
  DIRECTUS_ADMIN_EMAIL=admin@example.com \
  DIRECTUS_ADMIN_PASSWORD=change-me \
  npm run setup

What gets created

18 user collections, each with its *_translations child and a public read permission:

  • Pages group (yellow): pages, page_sections, seo_landing_pages, legal_pages, docs_pages
  • Blog group (pink): blog_posts, blog_categories, team_members
  • Marketing group (lavender): services, pricing_plans, testimonials, faqs, case_studies, portfolio_projects
  • Navigation group (cyan): navigation, footer_groups, footer_links
  • System: form_submissions (public-create only), languages (en/fr/es)
  • Top-level singleton: site_settings

Every collection with an seo field uses the SEO Plugin interface from @directus-labs/seo-plugin: structured editor with live Search Preview, no raw JSON.

How translations work

Two different systems, don't confuse them:

SystemPurposeDo you touch it?
languages user collection + *_translations childrenPer-row content translations (blog title in EN/FR/ES)Yes: edit inside each item under the "Translations" tab
directus_translations at /admin/settings/translationsTranslation strings for UI labels via $t: referencesNo: leave empty, it's unrelated to content

Next steps