Where each piece of site content lives in Directus, the rules that decide what is public, and how an edit reaches yesid.dev.
| Reference | Value |
|---|---|
| Subject | Content model, publication rules, translations and the editing path; for editors and developers mapping a page to its CMS record |
| Verified source | apps/cms/directus/snapshot and collections on main at 950ba889; checked 2026-11-07. Image publishing, the Article Block Editor fields, project one-liner rules and relation list labels re-verified on main at 0461e045 (#677, #678, #690, #693), checked 2026-11-10 |
| Support state | Content and publication contracts checked against main 950ba889. Metric displays show values and labels; optional CMS source/date fields are not displayed. Live schema/grants require their own readback |
flowchart LR
E["Edit EN, FR, ES"] --> S["Status published, Save"]
S --> P["Trigger: Publish site<br/>or merge to main"]
P --> B["Vercel production build<br/>then delayed CMS feedback"]
B --> C["Check yesid.dev"]
There is one CMS, cms.yesid.dev, for yesid.dev. Saving CMS content does not itself start a build. A production build uses live CMS content, whether triggered by Publish site or a merge to main. A merge after a CMS content write therefore publishes that content when the production build succeeds; the code diff does not bound the content published. PR previews render the committed fixture.
| Site area | Directus collections |
|---|---|
| Home sections | Singletons block_hero, block_manifesto, block_proof_reel, block_services_grid, block_about_intro, block_cta, block_closer |
| About, Contact, Blog and Projects pages | Singletons block_about_content, block_contact_content, block_blog_page_content, block_projects_page_content; contact_channels, about_languages |
| Projects | projects with translations, projects_sections, projects_impact_metrics, projects_services |
| Services | services with translations, services_sections, services_deliverables |
| Blog | blog_posts with blog_posts_translations (one post, one child row per language), blog_posts_tags, tags |
| Tech stack | tech_stack with project and service junctions |
| Site-wide | site_meta, site_labels (UI strings), site_pages (which pages exist), nav_links, route_seo, legal_pages, error_pages, morph_shapes, illustrations, languages |
| Media registry | directus_files, asset_records, asset_versions, asset_usages, asset_releases |
| Publishing | build_queue, shown as Publish site at the bottom of the sidebar, hosts the Publish site flow and a how-to notice |
Every translatable collection has a matching *_translations collection keyed by language. The sidebar folders follow the site and are navigation only, not content: Home, Services, Projects, Blog, About, Contact (home_page to contact_page), then Legal pages, Tech stack and tags (tech_stack_page, with tags), Site shell (site_shell: page registry, navigation links, site labels, error pages), Brand assets, Site settings (site_config: identity, route SEO, languages) and Publish site last.
block_* singletons are single objects: /items/<block> returns and accepts an object, not an array. Scripts use readSingleton and asSingletonRow.
| Rule | Enforced by |
|---|---|
Blog posts and CMS projects export only with editorial status published; exported Project status public/wip/private is a separate availability control |
Exporter status filters |
| Required page singletons must be published, or the production build fails | Exporter |
Services also honor a visible flag in listings and detail pages |
Service fetcher and web consumers |
| A Blog language is public only when its row exists, Show on site is on, and it has a title plus article text (external posts: a title and the post URL). Each language stands alone; a post can be live without Spanish | Blog fetcher |
| Projects and Services: switching English off hides the whole item; switching French or Spanish off makes that language fall back to English | Project and Service fetchers |
A project one_liner feeds its share card and description. Each language's one-liner must fit the card subtitle at 18px or larger using supported characters, or the production build fails naming the project, locale and field. Newlines become spaces |
validate-project-lines.ts, project-copy.ts |
Concrete site pages need their published site_pages entry; details inherit the listing entry and entity/locale checks |
Web page registry |
| Retire content by archiving, not deleting: archived rows keep their relations and can be republished | Editorial practice |
| Anonymous API reads follow per-collection Public policies, separate from site publication | Directus policies |
The Translation reminder flow runs when a content item is set to published: it logs the item and sends the editor an in-app notification to check the French and Spanish tabs. It does not block saving or publishing and makes no requests.
Field groups, labels and ordering in the editor live in the snapshot. Each form follows its page top to bottom; buttons, links and screen-reader text sit in an Interface text group, and exported fields the site does not read sit in Not shown on the site. Projects, Services and Blog posts share one group shape; listing page text starts with Heading and intro. Saved views, language bookmarks (English, Français, Español for Blog) and default list columns live in directus/collections/presets.json; the Insights dashboards Start here, Publishing and recent changes, Services, projects and tech stack, and Blog posts live in dashboards.json and panels.json. Shared, untranslated settings are marked in each form. Changing a bookmark or view never publishes anything. Project, service and tech-stack relation lists show the related item's name (#690).
The Blog body uses the Article Block Editor interface (see Images below). Its snapshot field (blog_posts_translations/body.json) enables headings, nested lists, code, images, paragraphs, checklists, quotes, underline and tables; leave it empty for external posts. Table cells keep bold, italic, underline, inline code, line breaks and http, https, mailto or tel links; the site strips anything else. Do not use the checklist tool: the site has no checklist block, so a checklist in a published language fails the production build. Use a nested list instead.
Each post can pick a topic icon in its svg_illustration field, a relation to the illustrations collection. An illustration id is a short kebab-case topic name such as database, and it must match one of the 21 icon file names in apps/web/src/lib/assets/blog-icons/: the site renders no icon for an unknown id. Leave it empty and the site picks one of four topics for the post's category, the same one every build.
Upload images to Directus and reference them by file. The next production build copies each referenced PNG, JPEG, WebP, GIF or AVIF to the site; no code change is needed, and visitors never load images from the CMS. A file must be in a delivery folder (about, blog, brand, illustrations, og, projects, services) or unfiled. New SVGs and committed mirrors follow the reviewed procedure on the Operations and recovery page. Fixture previews show a FIXTURE IMAGE PLACEHOLDER for a new upload.
Project descriptions, project sections, blog bodies and legal bodies use the Article Block Editor in the snapshot. Below the editor, each image has numbered controls: Layout (Normal, Row for consecutive images sharing a row, Beside next block before a paragraph, table or list), Light version (upload or pick the light capture) and Show both themes (show both captures together instead of swapping with the theme). Without a picked light version, a dark file named example.dark.png pairs with a unique example.light.png at build time. Portrait captures size from their stored dimensions.