yesid.dev: explaining how the pieces fit
My professional website in three languages, with an editable content system, project stories, and an interactive guide to how software fits together.
My professional website in English, French, and Spanish, built to explain my work, show the projects behind it, and give people a place to start a conversation.
A site for the questions people actually have#
Someone looking for help with a website or a reporting problem should not have to arrive with a list of software tools. They may know that information is being entered twice, that a report takes too much work, or that their website no longer reflects what they do. The technical choices come after understanding that situation.
yesid.dev is where I bring those questions together with my work as a freelance digital solutions developer in Montreal. It contains services, projects, writing, a page that explains the tools, and ways to contact me. Building it also meant deciding how I would keep those explanations current.
This is my own professional website. I own the decisions about what it says, how its parts connect, and what I am ready to put my name on.
Several ways into the work#
The service pages organize the work into databases and SQL, pipelines and automation, dashboards and analytics, and websites and e-commerce. Each gives someone a starting point. A visitor does not need to know which category their problem belongs to before getting in touch.
Projects provide the more concrete view: what was built, which pieces were involved, and what can actually be shown. The blog leaves room for the reasoning behind a decision. Keeping those formats separate helps a project story stay focused, while a longer explanation can take the space it needs.
The tools page has a different job. It explains the parts of software in ordinary terms: the interface people use, the logic behind it, the information it remembers, and the place it runs. Its interactive blueprints let visitors start with a type of project and see one possible arrangement of those parts.
These are examples to explore. A diagram is a useful opening for a discussion, but a real recommendation still depends on the requirements.
What I built, and what it takes to keep it current#
The central choice was to separate writing from page layout. Project stories, service descriptions, and translations have a place in Directus, the content editor. The SvelteKit website turns that structured material into the pages people read. I can correct an explanation without rebuilding its layout by hand, while the same display rules keep the pages connected.
The site works in English, French, and Spanish. That includes practical details such as form labels and error messages, not just three versions of a headline. The contact implementation also registers what a visitor has typed so those values can be carried through a language change. It is a specific feature for that transition, not permanent storage of an unfinished message.
The transit-inspired design gives the site a recognisable language, but the interface still has to do ordinary work. A long explanation needs navigation. An expandable card needs to let someone select text without closing it. A contact form needs to distinguish an empty field, a request in progress, and a failed send. These choices are where the visual idea meets the person using it.
Publishing has a deliberate step. Saving a correction in the editor does not immediately change the public site: the content must be exported, the new site version built, and that version deployed. This suits the editorial pace of a professional website. It also makes me responsible for knowing which version is being shown when a correction has not appeared.
AI tools helped with implementation and review. I directed the architecture and examined the result, including cases where a page looked correct but still used text stored in the wrong place. Owning this project means following those connections, rather than treating the screen alone as proof that the work is complete.
That is the project as a whole: a place to explain my work, backed by an editing and publishing process I have to maintain. The sections below examine particular decisions. Each can be read on its own, whether the interest is content, languages, interface behaviour, or operations.
The site looks like one product, but the work is divided into four domains: the public website, the editing system, the code that moves and checks content, and the shared design resources. Knowing which domain owns a change helps me avoid fixing a problem in the wrong place.
The web application, built with SvelteKit, owns the visitor's experience. It lays out a project, resolves its language, renders its content, connects navigation, and manages interactions such as opening a section. Directus is the editing system, backed by a PostgreSQL database on Neon and run on Railway. It holds structured records for projects, services, articles, translations, and shared content.
Between them is tooling written for the project. Fetchers read CMS records, transformations turn those records into the shapes the website expects, validators check them, and exporters generate the content modules used by the site. These steps have different jobs. A database relationship that is convenient for editing does not need to become a complicated object every page has to interpret.
A shared package holds the content types, runtime schemas, and pure helpers used on both sides. For example, the CMS exporter and the web application can agree on what a project section contains without maintaining separate interpretations of that structure.
The fourth domain comes from yesid.dev-design. The website consumes versioned, locally vendored design packages for interface primitives, tokens, motion, language routing, analytics, and search metadata. This site applies those resources to its own content and layouts. It is not the whole design system, and a product-specific page does not automatically belong in that shared system.
Consider three small changes. Correcting a service description belongs in the CMS. Changing the behaviour of a project card belongs in the web application. Changing a shared motion rule belongs in the design resources and then needs adoption by the consumer. Keeping those routes separate makes the origin of a change easier to follow.
The description of a service changes. A project needs a more precise explanation. A translation needs correcting. I wanted those changes to have a home in an editing system while preserving a consistent page layout.
A project has an identity, a title, a short line, an introduction, ordered sections, related services, technologies, tags, links, and presentation status. In Directus, some of these are related records. The export transforms them into a simpler object for the website. Section order comes from the stored sort values; links to technologies and tags are resolved through their relationships.
The shared section schema is small:
export const ProjectSectionSchema = z.object({
title: LocalizedStringSchema,
content: LocalizedBlockEditorDocSchema,
});It says that a section contains a translated title and a translated rich-content document. It does not specify an orange border, a two-column layout, or an animation. Those decisions belong to the interface.
This is useful when the same project needs a short introduction for a visitor browsing the portfolio and a detailed explanation for someone evaluating the work. The one-line field serves the card. The introduction establishes the problem. The ordered sections can explain a data model, an interface decision, or an operating constraint without putting all of that on the card.
The model also separates the project's presentation status from repository visibility. A record mapped to a public project page does not change permissions on its source repository. A private-repository label and a live-site link are distinct pieces of information.
For a concrete editing example, imagine correcting one sentence in a project's French introduction. The intended change is in that French content document. Its project identity, Spanish version, related services, and page component do not need to change just because the sentence did. That is the practical value of the structure: a small editorial change has a specific place to live.
The body of a section uses BlockEditorDoc, the JSON document produced by the CMS block editor. It contains a timestamp, a version, and an ordered blocks array. Each block has an identity, a type, and data appropriate to that type.
The supported types cover headings, paragraphs, nested lists, code, quotations, images, and separators. A paragraph carries text. A code block carries plain code text. An image carries a file reference and a caption. The renderer walks the array and sends each type to the corresponding component.
This makes a worked explanation possible without treating everything as a paragraph. A section can begin with a question, show a short source excerpt, explain it in ordinary language, and finish with an image. The order is part of the content. The rendering rules are shared.
Expandable cards are a separate layer. They wrap a project's section; they are not an invented “collapsible” block inside the document. The project page resolves the section's title and body for the chosen language, then places that document inside CollapsibleSection. The card supplies the trigger and open state. BlockRenderer supplies the content.
That distinction matters to readers. Someone can follow the problem and result without reading every implementation detail. Someone interested in the publishing pipeline can open a complete explanation of that pipeline. Each section needs enough context to make sense on its own, rather than requiring the reader to reconstruct an argument from scattered fragments.
The page also distinguishes image-gallery sections from article sections and builds navigation around them. On a wide screen, the section navigation, article, and project summary can sit beside one another. At a narrow width, the reading order is stacked and a compact navigation control helps move between sections.
Content helpers extract text and headings from the same documents. They can calculate a reading estimate using the rule ceil(words / 200), with a minimum of one minute. For illustration, 1,200 words gives six minutes; 1,201 rounds up to seven. That is a planning estimate from text length, not a measurement of how quickly a person understands code or a diagram.
The model has limits worth respecting. A new kind of content needs a defined shape and a renderer; it does not appear just because an editor pastes unfamiliar markup. The current model has no video block. Image captions and alternative text also need more separation than the current contract gives them.
A saved edit is not yet a new public page. For the normal live-content publishing path, the CMS data must be read, transformed, checked, written into generated content modules, built into the website, and deployed.
The web package runs the content export as a prebuild step. The exporter gathers the full set of required content, including projects, services, articles, navigation, and page content. Different fetchers understand their own collection's relationships. They produce objects that the website can consume without asking Directus to reconstruct those relationships on every page visit.
The project fetcher, for example, converts section translations into language maps and sorts repeated content. It then parses the resulting project array through the shared schema. A field being present in the database is not enough if its exported shape is incompatible with the application.
Before deriving output files, the export registry checks that the complete required dataset is present. It also prepares the media variants used by the export. The output is a set of TypeScript modules and a manifest recording file hashes and whether the data came from a live read or a cache.
The website build consumes those modules. SvelteKit prepares the routes that can be prerendered, and Vercel serves the resulting deployment. Some server and interactive work still exists. Preparing the site's text ahead of time does not eliminate image requests, contact submissions, or every external connection.
Here is a useful publishing example. I correct a French project paragraph and save it in Directus. The editor now shows the correction, but the current public deployment still contains its previous exported version. A successful export and build prepare the new version. Deploying that version makes it available to visitors. Looking only at the editor, or only at a successful file-generation step, would answer the wrong question about publication.
This arrangement fits a site whose project stories and service descriptions change at an editorial pace. It accepts a publishing step in exchange for serving prepared content. It would need reconsideration for information that must appear instantly after every write, such as a live operational feed. The site's choice is tied to its content, not a rule I would apply to every project.
There are several caches in this system, and they solve different problems. The export cache is a local snapshot of a previous CMS read. Generated modules are the content inputs committed or produced for a build. Build caching can reuse completed work. Delivery caching helps serve a deployed response. Calling all of them “the cache” would hide the question that matters: which version is being used, and why?
The export deliberately treats local work differently from a trusted deployment that has opted into fresh CMS content. Locally, a failed CMS read can fall back to the saved snapshot. If there is no usable snapshot, the exporter can leave existing modules untouched and report that it emitted nothing. This lets interface work continue, but the output must not be mistaken for a fresh CMS export.
For a trusted live export, a non-live outcome is a failure. The decision is expressed directly:
export function decideExit(outcome: RunOutcome, policy: FallbackPolicy): number {
return policy === 'fail' && outcome.source !== 'live' ? 1 : 0;
}The number is an exit status: zero permits the command to continue successfully; one reports failure. Under the strict policy, live content gives zero, cached content gives one, and no emitted content gives one. Under the local soft policy, the command can finish with zero while clearly reporting its fallback source.
Imagine the CMS is unavailable while I am changing a card's spacing. Using yesterday's content locally can still be useful. Now imagine I have just corrected a public service description and expect the deployment to contain it. Quietly substituting yesterday's text would defeat that purpose. The second case needs a failed build that I can investigate, not a reassuring result with old content.
An explicit skip setting is another deliberate route: CI and ordinary previews can build from committed modules without contacting the CMS. The distinction is visible in the configuration, rather than inferred from whether a command happened to finish.
The exporter also has a timeout around fetching, so a stalled read has a failure path. Its complete-data check prevents knowingly emitting a selected subset as a complete export. File emission is still a sequence of writes, not a database transaction across every file. If writing is interrupted, the output needs verification and regeneration before use. “Complete export” describes the required dataset; it should not be stretched into an unsupported guarantee about every possible disk failure.
English, French, and Spanish are working languages for me. On the website, they affect more than the opening headline: navigation, project explanations, contact labels, validation messages, and related pages also need language-aware content.
English uses the unprefixed address. French pages use /fr and Spanish pages use /es. In the content contract, translated values use the keys en, fr, and es. A project section therefore has a title map and a body-document map, rather than three unrelated copies of the entire project.
The schema requires English as a base. French and Spanish are optional in the shared shape, allowing translations to be filled over time. A valid object is therefore not proof that every language version is ready. Shape validation and editorial completeness are different checks.
For ordinary translated strings, a blank required English value is rejected. The export reads the translation rows, keeps the recognised values, and forms the language map. Rich content has its own document schema. A translation is not interchangeable with an arbitrary string where the renderer expects an ordered set of blocks.
Consider one section with a title and a body in all three languages. That is six editorial values to keep coherent: three titles and three bodies. Correcting a fact in the French body does not update the other two by itself. The identity and order can stay shared, but the meaning still has to be reviewed across the three versions.
I want the versions to read naturally for their audiences. Québec French, Colombian Spanish, and Canadian English do not always use the same sentence structure or the same familiar terms. The important invariant is the underlying account: the same feature, responsibility, limitation, and example. Translating an identifier inside a source excerpt, on the other hand, would change the code rather than adapt the explanation.
This gives me a more useful question than “does the language button exist?” I can ask whether someone can read the project, understand an error, follow the related link, and contact me in their chosen language. The content model supports that work, but it does not complete the editorial work on its own.
Imagine someone starts a message in Spanish, then changes the interface to French. The form labels should change. The words the person wrote should remain theirs.
A language change also changes the page address. In this implementation, the page subtree is recreated when that address changes, so values held only inside the old component would disappear with it. The contact form explicitly registers the three values that need to cross that boundary:
const name = persisted<string>('contact-name', '');
const email = persisted<string>('contact-email', '');
const message = persisted<string>('contact-message', '');The stable identifiers matter. “contact-message” still means the same field whether the visible label is in English, French, or Spanish. A translated label would be a poor identifier because changing language would change the key needed to find the value.
Before a recognised language navigation, the handoff mechanism captures registered values together with reading position and focus information. It places a versioned record in session storage. On the new page, the registered controls can recover their values; focus and scroll are restored after the page has rendered, and the stored handoff is cleared. This is a transfer for a navigation, not a promise of permanent message drafts.
The helper also seeds values before the new control paints. That avoids briefly showing a default value and then replacing it. For an expandable section, the same idea avoids opening a card for a moment before restoring its closed state.
The implementation distinguishes a successful form submission from the translated sentences used to describe it. It can preserve the fact that submission succeeded and rebuild the confirmation in the new language. Preserving the old translated sentence would leave part of the interface in the previous language.
This is a specific mechanism for registered values, not a claim that every route is seamless. A French blueprint's contact link also needs the correct French destination. Choosing that destination and keeping the visitor's message are separate responsibilities. Both matter to the person using the site.
The site borrows from transit signage, route diagrams, technical drawings, and terminal windows. Orange and yellow recur as points of reference. Section markers and the destination board carry that interest through the pages. The reference is personal, but the components still have ordinary jobs: explain a relationship, identify an action, or make a long page easier to navigate.
Shared design tokens give those decisions names. A component can use a role such as primary colour, border, or motion duration rather than inventing another unrelated value. The website also has local styling and layout decisions. Reusing primitives does not mean every page becomes the same composition.
The consumer package records where its shared resources come from. Vendored packages keep a reviewed copy available to the website, and an integrity check can compare that copy with its recorded adoption. That creates a boundary between developing the shared system and adopting it into this product. An edit made directly to a copied package is not automatically an upstream improvement.
The expandable card illustrates how appearance and interaction meet. Its header is a real button, with the expansion state supplied through the underlying collapsible primitive. Pointer users can also click a non-interactive part of the card. The handler deliberately leaves links, inputs, and buttons alone, avoids toggling a parent when a nested card owns the click, and ignores a click ending a text selection.
Those exceptions have a reader-facing purpose. Copying a sentence should not close the section. Following a link should follow the link. Clicking the header should toggle once, even though the surrounding card also handles clicks.
Light and dark themes, responsive layouts, reduced-motion rules, and quiet-mode behaviour add more contexts in which the same content has to remain usable. The visual idea gives the site its character. The shared parts and interaction rules help keep that character from getting in the reader's way.
The contact page offers a short form, a direct email link, and a link to book an introductory call. It also provides context about location, languages, and the kinds of problems I work on.
The form itself has a sequence. It first checks the name, email, and message. An empty value is different from an email value that fails the format check. The error messages come from translated content and refer to the corresponding field. Only a valid form reaches the sending step.
While a request is running, the implementation prevents another submission through the same handler and disables the send control. The button exposes its busy state and changes its visible wording. The visitor should not have to guess whether the first click did anything.
The request is sent to Web3Forms. If that service reports failure, or the request throws, the form records an error and clears its sending state. It returns before playing the success sequence. The confirmation is based on the service response, not on a separate inspection of the destination inbox.
There is also an earlier boundary: the moment between the prepared HTML appearing and the browser attaching its JavaScript handlers. The button remains disabled until the handler is ready. The source explains the reason for that guard: without it, a native form submission could reload the page and put entered values into the address's query string. This is a concrete case where loading behaviour, privacy, and interface feedback intersect.
Desktop and mobile versions can render different form layouts. Their element identifiers include the layout so labels and error descriptions target the correct control. The handoff identifier remains independent of that visual layout, because “the message field” is still the same conceptual value.
Consider a slow connection. The visitor sends once, sees the pending indication, then receives an error. The useful outcome is a form that explains what happened and permits recovery. Animating a success message on a timer would be easier to draw, but it would not describe the request's outcome. The form's sequence is part of what makes the contact page a working feature.
I find these topics easier to reason about when they are connected to a particular action. For the contact form, labels need to identify fields. A field with an error uses aria-invalid and refers to its error text with aria-describedby. A submission in progress exposes aria-busy. These attributes complement visible feedback rather than replacing it.
For a long article, the section heading needs to remain a keyboard-operable control. The section navigation needs targets that correspond to the content. Expanding a card, selecting text inside it, and following a link are different actions, even when they share the same rectangle on screen.
The code examples have their own rendering boundary. The block stores source as plain text. The interface can receive highlighted HTML prepared on the server; when that is unavailable, it has a plain escaped rendering path. Copying the example uses the code body. This keeps the explanation readable without requiring the browser to treat the example as executable page markup.
Ordinary rich text is a different case because the editor can store inline HTML marks. A valid BlockEditorDoc describes structure; it does not by itself establish a complete HTML-sanitisation policy. The editor's permissions, accepted markup, and render path need to be treated as their own boundary. I would not use a schema check as a substitute for that analysis.
Similarly, a browser-side email check is useful feedback, but it cannot be the authority for protecting an external submission service. A build credential belongs in the build environment, not in a public article or browser bundle. And keeping a preview out of search results does not make it private: noindex is an indexing instruction, not access control.
These are concrete responsibilities I can inspect and improve. They describe implementation choices, not an accessibility certification or a blanket assertion that a website is secure.
Different checks answer different questions. Type checking can catch a mismatch between what a component expects and what it receives. Runtime schemas can reject malformed CMS data. Unit tests can exercise a transformation or a failure-policy branch. Browser tests can exercise a page as an interaction.
The shared schemas also include compile-time comparisons between their inferred types and the handwritten interfaces. That helps make a model change visible on both sides of the CMS-to-web boundary. Otherwise, a field could be added to one interpretation and disappear in another.
The export tests cover cases such as an untrusted preview attempting a live export, a timeout, a missing mirrored asset, and a non-live result under strict policy. The expandable-card tests ask whether a header toggles once, whether an interactive child keeps its own action, and whether the semantic button retains its expansion state. These are useful questions because they are tied to specific mistakes.
The web workflow checks types and unit tests, generated-content integrity, vendored design integrity, product tokens, the shared package, the build, and the client payload. It also has a browser-test lane. The CMS workflow has its own validation and operational jobs. Their presence is evidence of how the work is organised; a particular release still needs its own results.
Generated-content integrity is especially easy to overstate. The manifest records SHA-256 values for the emitted files. If a file changes without a corresponding manifest update, verification can detect the mismatch. It also records whether the export used live data or a cache.
A hash does not judge the truth of a paragraph. If someone changed both a file and its recorded hash, matching them would not independently prove that the content came from the CMS. The check is valuable for detecting drift, but the publishing process and review still carry the source-of-truth responsibility.
That is why I prefer a specific statement such as “this test exercises a failed export” over a large test count presented as a quality score. Counts do not explain which failure matters, what was checked, or what a reader can rely on.
When a page is wrong, I need to identify which layer is wrong before editing it. The CMS record may contain old wording. The exported module may not include the newest edit. The website may have built successfully without that export. Or the public address may still be serving a different deployment. These are different situations with different remedies.
For example, suppose a corrected title appears in Directus but not on the public page. I would first establish which language record was edited, which content source the export reported, and which build and deployment the page belongs to. Rewriting the page component at that point could conceal a publishing problem rather than solve it.
Schema operations deserve a separate path from ordinary editorial changes. The repository contains CMS snapshots and tooling to compare the intended configuration with a target environment. Applying a schema change can affect fields, relationships, or existing data. The workflow therefore separates validation and read-only comparisons from explicit operational actions. Changing a paragraph is not the same operation as changing the structure that stores it.
Development and production also have different purposes. A development environment is useful for inspecting a change before it reaches the public site. A production operation needs the right target, the intended scope, and evidence of what happened. A successful local demonstration should not quietly become a claim about production.
The shared design boundary adds another maintenance question. If a problem is in a primitive used across products, fixing only the consumer copy can create drift. If the problem is specific to this site's project layout, moving it into the shared package may spread a product assumption elsewhere. The location of the fix is part of the design.
This is the operating side of owning the website: being able to explain where content comes from, where a change belongs, what the checks establish, and what must happen before visitors see a new version.
AI tools contributed to implementation and review. In my published account of this project, I describe Claude as the main implementation tool and Codex as a reviewer that also sometimes helped implement changes. I directed the architecture, maintained the project context, and reviewed what came back.
One example was the content boundary. A sentence written directly inside a page component can look identical to a sentence coming from the CMS. I found shared copy still in the frontend when the chosen model said it should be editable in the CMS. Looking at the page was not enough to see that difference; I had to follow the data through the files.
That example connects the domains described here. The visible result belonged to the website, the intended source belonged to the editor, and the missing connection belonged to the transformation and export path. Calling the page “finished” because it looked correct would have missed the actual requirement.
The short code excerpts are useful for the same reason. The section schema shows what an editor-controlled section can contain. The export decision shows when fallback content must fail a build. The registered contact values show what the interface intends to carry across a language change. Each excerpt supports a limited explanation that can be checked against the surrounding implementation.
I do not need to claim that I typed every line to take responsibility for the result. I do need to understand the decisions I am presenting, examine changes rather than accept them by appearance, and recognise where further investigation is needed. Generated code, third-party packages, shared components, and my own decisions all contribute to the product; my review has to account for their interaction.
How a trilingual portfolio connects SvelteKit, Directus and a reviewed publishing pipeline, with structured content and contact journeys that preserve a draft.
Let's build somethingthat moves.
Database, pipeline, dashboard, website: wherever it's stuck, that's where we start.