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:
| Step | Service | What happens |
|---|---|---|
| 1 | database + cache | PostgreSQL 16 + Redis 7 boot and become healthy |
| 2 | directus | Image 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. |
| 3 | directus-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. |
| 4 | directus-init exits | Directus 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:
| System | Purpose | Do you touch it? |
|---|---|---|
languages user collection + *_translations children | Per-row content translations (blog title in EN/FR/ES) | Yes: edit inside each item under the "Translations" tab |
directus_translations at /admin/settings/translations | Translation strings for UI labels via $t: references | No: leave empty, it's unrelated to content |
Next steps
- Frontend Setup: connect Next.js via GraphQL.
- Content Management: edit pages through the Directus admin.
- Hosting & Deployment: ship to production.