yesid.dev
A bilingual CMS portfolio that edits in Directus and serves visitors from generated edge-ready content.
yesid.dev is built for the same outcome I would sell to a client: edit content in Directus, regenerate the TypeScript cache, and ship pages that do not call the CMS for every visitor.
The result is a bilingual site that stays easy to update without paying a runtime tax on every page view. Content has one source of truth, the site has fast edge delivery, and future projects can inherit the same brand system instead of starting from zero.
Quality is part of the identity here. This is a proudly Quebec-built product, bilingual by default, with English and French treated as first-class surfaces instead of a translation pass at the end.
The Montreal voice matters, but so do the details: mobile-first layout, accessible controls, light and dark themes, and prefers-reduced-motion behavior. The point is not just that the site looks like me. It has to behave like I care.
The content pipeline is intentionally boring: Directus stores copy and media on Neon, export-fallbacks.ts pulls that state into a generated TypeScript cache, and SvelteKit serves the result through Vercel. The important part is the boundary: editors get a real CMS, visitors get edge-ready files.
flowchart LR
directus["Directus CMS"] --> export["export-fallbacks.ts"]
export --> cache["generated TypeScript cache"]
cache --> svelte["SvelteKit"]
svelte --> vercel["Vercel edge"]- `Directus` is the source of truth for copy, media, project sections, and metrics.
- The project row owns hero media, light variants, and the `featured` toggle for the home proof reel.
- Generated files under `apps/web/src/lib/content` are a build cache, not hand-authored content.
- Published visitors hit `SvelteKit` output and `Vercel` edge delivery with `0` CMS calls per visit.
- Dev CMS and prod CMS stay separated so content work can be reviewed before promotion.
- The integrity lock catches generated cache drift before a push can hide it.
The hero media model is explicit because gallery order is not a contract. Image 1 is the desktop/default card image. Image 2 is optional and only means mobile or secondary when the CMS field is filled. Extra gallery images stay in the case study body.
type ProjectHeroMedia = {
image: string;
imageLight?: string;
imageSecondary?: string;
imageSecondaryLight?: string;
featured: boolean;
};
// image 1 drives desktop/default cards.
// image 2 is optional and drives mobile/secondary split cards.Assets and content move through the same repeatable path. Upload media to Directus, update the dev CMS row, then regenerate the fallback cache that the web app imports.
export OP_SERVICE_ACCOUNT_TOKEN="$(grep ^OP_TOKEN= .env | cut -d= -f2-)"
op run --env-file=apps/cms/.env -- env PUBLIC_DIRECTUS_URL=https://cms.dev.yesid.dev bun apps/cms/scripts/migrate-assets.ts
op run --env-file=apps/cms/.env -- env PUBLIC_DIRECTUS_URL=https://cms.dev.yesid.dev bun apps/cms/scripts/content-projects-yesid.ts --apply
op run --env-file=apps/cms/.env -- env PUBLIC_DIRECTUS_URL=https://cms.dev.yesid.dev bun apps/cms/scripts/export-fallbacks.ts --module=projectsThe regen command can also run alone when the CMS already has the correct state and only the generated cache needs to be refreshed.
export OP_SERVICE_ACCOUNT_TOKEN="$(grep ^OP_TOKEN= .env | cut -d= -f2-)"
op run --env-file=apps/cms/.env -- env PUBLIC_DIRECTUS_URL=https://cms.dev.yesid.dev bun apps/cms/scripts/export-fallbacks.ts --module=projectsThe generated module is the contract the web app consumes. The app imports typed content, not live CMS responses, so page rendering stays predictable and cheap.
type GeneratedContentCache = {
source: 'Directus CMS';
file: 'apps/web/src/lib/content/projects.ts';
contract: 'Project[]';
runtimeCmsCallsPerVisit: 0;
};
export const projects = [...] satisfies Project[];The quality gate is intentionally boring too. A content change still has to pass type checking, CMS script tests, web unit tests, and the integrity lock before it is ready for the operator push.
cd apps/web && bunx svelte-check --tsconfig ./tsconfig.json
cd apps/cms && bun test
cd apps/web && bunx vitest runThe README collapsible is kept last on purpose. Public repos can feed it automatically; private client repos can keep the technical proof inside the CMS without exposing source code.
Case study for a bilingual SvelteKit portfolio powered by Directus CMS, generated TypeScript caches, and Vercel edge delivery with zero CMS calls per visit.