Portfolio of Aurélien Bertrand, Data Engineer based in Bordeaux, France. A static site in French and English, driven entirely by data files: a profile page, a projects page with one overview per project, and tech notes (optional).
- Data-driven: profile, experience, education, certifications, volunteering, labels and themes all live in JSON files. No content is hard-coded.
- Validated at build time: every file is checked against a schema (zod). A malformed date, an invalid color or a broken translation blocks deployment with a precise error.
- Projects and notes: every project has an overview page; notes (billets) can be linked to a project, are written in Markdown in both languages and come with an RSS feed.
- Bilingual: French at
/, English at/en/, on the same page. Translation files hold only the text; everything else comes from the French source. - Light and dark: follows the device setting, with a toggle to switch.
- Seasonal skins: one season is always active (spring, summer, autumn, winter), with holidays layered on top (Halloween, Christmas, New Year, Fête de la musique), each with a day and a night version. See THEMES.md.
- Lightweight: HTML and CSS generated at build time, a single small browser script, self-hosted fonts, no third-party calls.
- Accessible: keyboard navigation, screen-reader labels, checked contrast in both themes, animations disabled when the device asks for reduced motion, readable without JavaScript.
- Mobile-first, as much as desktop.
| Purpose | Choice |
|---|---|
| Generator | Astro 5, fully static output |
| Language | TypeScript (strict) |
| Data | JSON and Markdown, validated with zod (Astro content collections) |
| Styling | Plain CSS: design tokens, frosted glass, View Transitions |
| Fonts | Plus Jakarta Sans, JetBrains Mono (@fontsource, self-hosted) |
| Hosting | GitHub Pages, deployed with GitHub Actions |
Requirements: Node.js 22 and npm.
npm install
npm run dev # dev server: http://localhost:4321
npm run build # builds dist/ and validates all data
npm run preview # serves dist/ to check the final outputnpm run build doubles as the test suite: if it passes, the content is valid and the site
is ready to ship.
Everything on the page comes from these files; the code never needs to change.
| File | Content |
|---|---|
src/data/profil.json |
Name, photo, job, location, spoken languages, email, links, "About" |
src/data/site.json |
Title and description, accent color, section order and titles, every UI label |
src/data/experiences.json |
Experience: one company, one or more positions |
src/data/formations.json |
Education |
src/data/certifications.json |
Certifications |
src/data/benevolat.json |
Volunteering |
src/content/projets/*.md |
Projects: one file per project (tile and overview page) |
src/content/articles/<name>/ |
Notes: one folder per note, fr.md, optional en.md, images |
src/data/en/*.json |
English translation (text only) |
src/data/themes.json |
Seasonal and holiday skins, see THEMES.md |
To add an entry, add an object with a unique id to the relevant array. Display order is
computed automatically, from most recent to oldest.
site.json settings
-
affichage: what is shown, set separately forlocal(npm run dev) anddeploye(npm run build, i.e. the live site andnpm run preview). Each block has the same keys:"profil": { "experience": true, "formation": true, "certifications": true, "benevolat": true }, "projets": { "aLaUne": true, "page": true, "fiches": "auto" }, "billets": false, "madeBy": true
profil: the sections of the profile page.projets.aLaUne: the "Featured projects" section of the profile page.projets.page: the/projets/page, its navigation link and every link to it ("All projects", breadcrumb, "Back to Projects").projets.fiches: the/projets/<name>/overview pages and every link to them ("View details")."auto"(the default) followsprojets.page. Without overviews, a project linked from a note points to the Projects page instead, or to nothing.billets: the notes section (navigation link, pages, RSS feed). Notes are still validated at build time either way.madeBy: the unlisted/madeby/page (see below).
With only the profile left, the navigation bar disappears.
-
sections: array order is page order;titrechanges a section's heading. An empty section disappears on its own. -
hauteurSectionRepliee: above this height (in pixels), a section collapses behind a "Show all" button. -
projetsALaUne: how many project tiles the home page shows (the first ones byordre); the Projects page shows them all. -
couleurs.accentClair/couleurs.accentSombre: button and link color in light and dark themes (#RRGGBB), used when no skin is active. -
textes: every label on the site ("Contact me", "See more"…).
profil.json settings
photo: put the image inpublic/(e.g.public/img/photo.jpg) and set"photo": "img/photo.jpg". Leave it empty to show initials instead.statut: availability badge;tonisok(green),soon(orange) ornone(grey). An emptystatut.textehides the badge.langues: one flag per language next to the location, withnomandniveaushown in a tooltip.codeisfr,enores(flags drawn insrc/components/Drapeau.astro).liens.cv: putpublic/cv.pdfin place and set"cv": "cv.pdf". An empty link hides its button.liens.buymeacoffee: your Buy Me a Coffee page, used by the "Made by" badge. Empty: the badge code shows aTON-PSEUDOplaceholder.
Dates, logos and projects
Dates: "YYYY-MM" format (e.g. "2024-03"), or "YYYY" alone for education. For an
ongoing position or commitment, use "fin": null: the page shows "Present" and the duration
("2 yrs 8 mos") updates itself.
Logos: optional logo field, a path inside public/ (e.g. "logos/acme.png") or a URL.
Square images work best (400 × 400). Without a logo, initials are shown.
Projects: a src/content/projets/<name>.md file creates a tile on the /projets/ page
(and on the home page if it is among the first projetsALaUne) and a /projets/<name>/
overview page. Keep it to the big picture; the details go in notes.
---
titre: Terminus
resume: One or two sentences.
date: 2026-09 # project start
statut: en-ligne # en-ligne | en-cours | archive
role: Side project
lien: https://… # live project (optional)
depot: https://github.com/… # source code (optional)
stack: [HTML, CSS]
ordre: 1 # optional: forces the order (lowest first)
couleur: "#E8962E" # optional: badge color
---
## Quelle était la situation au départ ?
## Qu'est-ce que ça fait ?
## Comment je l'ai construit, et pourquoi ?
## Comment ça tourne sans moi ?
## Qu'est-ce que j'en ai tiré ?The body is plain Markdown, and each ## heading starts its own tile on the page. Every
overview follows the same five questions, in this order:
- Quelle était la situation au départ ? The context, the problem, for whom, the constraints (budget, data, privacy…).
- Qu'est-ce que ça fait ? What the product does, ideally its three key features.
- Comment je l'ai construit, et pourquoi ? Architecture and two or three decisions,
with the options that were set aside (
###subheadings). - Comment ça tourne sans moi ? Automation, tests, deployment, monitoring, cost.
- Qu'est-ce que j'en ai tiré ? What worked, what I would do differently, what's next. For a project still in progress: Où j'en suis ?
Skip a question rather than leave it empty. Keep a page to a two- or three-minute read:
details belong in a note. Put a non-breaking space before ?.
Notes (billets)
One folder per note in src/content/articles/; the folder name is the address
(/articles/<name>/, /en/articles/<name>/). Images go in the folder, next to the text, and
are referenced as .
fr.md holds the full header and the French text:
---
titre: Forecasting powder…
resume: One or two sentences.
date: 2026-10-21 # publication date (YYYY-MM-DD)
miseAJour: 2026-11-02 # optional: last significant update
projet: bluebird # optional: file name of the related project
sujets: [Data, Météo] # topics: in the RSS feed only, not shown for now
brouillon: true # optional: shown with `npm run dev`, never published
---
Text…en.md (optional) holds only the translation: titre, resume, optionally sujets, and
the English text. Without it, the English page shows the French text with a notice. An
unknown project, an unknown field in en.md or an en.md without fr.md fails the build.
The notes section only appears when affichage → billets is true in site.json and at
least one note is published. Drafts count as published with npm run dev only: set
affichage.local.billets to true to preview them.
English translation
Files in src/data/en/ contain only the text to translate: everything else (dates,
logos, links…) comes from the French file with the same name.
- Entries are matched by
id(experience, education, certifications, volunteering,site.jsonsections). - Lists without an
id(positions within a company, profile languages) follow the French order; plain text lists (skills) are replaced as a whole. - A missing field stays in French; an unknown field or
idfails the build. - Project pages are written in French only; the surrounding interface is translated.
- Notes are translated with an
en.mdnext tofr.md(see above).
/madeby/ is an unlisted page (no link leads to it, noindex) that hands out the
badge to put on other sites: a preview of every variant (themes auto, clair, sombre,
accent, with or without the Buy Me a Coffee button), the matching HTML code, and two copy
buttons: the HTML (no JavaScript needed on the target site) or a standalone SVG image (for
READMEs and anywhere HTML is not allowed). The badge is defined once in src/lib/badge.ts;
the coffee logo is the official one, public/logos/buymeacoffee.svg.
src/
├── data/ # content and settings (JSON); data/en/: translations
├── content/projets/ # one Markdown file per project
├── content/articles/ # one folder per note (fr.md, en.md, images)
├── content.config.ts # data schemas
├── lib/ # settings per language, translations, dates, links, decor
├── layouts/Base.astro # <head>, pre-render scripts, header and footer
├── vues/ # pages shared by both languages: profile, projects, notes
├── pages/ # routes: /, /projets/…, /articles/… (and /en/…), 404
├── components/
│ ├── sections/ # one home page section per file
│ └── habillages/ # decor for the seasonal and holiday skins
├── scripts/ui.ts # all browser-side JavaScript
└── styles/
├── tokens.css # design tokens (light / dark)
├── global.css # layout and components
└── habillages/ # one file per skin
The codebase (file names, identifiers, comments) is written in French; see AGENTS.md for conventions.
| Branch | Role | Write access |
|---|---|---|
dev |
Development | Direct push |
main |
Production, what is live | Pull requests from dev only |
gh-pages |
Built site (dist/), served by GitHub Pages |
Deployment workflow only |
- Work happens on
dev; CI builds the site on every push. - A
dev→mainpull request must pass two checks: Build (the site builds) and Source dev (the PR comes fromdev). - On merge,
deploy.ymlbuildsmainand publishesdist/togh-pages. The site is live one to two minutes later.
Repository protection
Rules are defined as GitHub rulesets (Settings → Rules → Rulesets):
- main: pull request required, Build and Source dev checks required, no force push or deletion. Nobody can bypass them, admins included.
- gh-pages: writes restricted to the "gh-pages deploy (GitHub Actions)" deploy key
(Settings → Deploy keys), whose private key is the workflow's
GH_PAGES_DEPLOY_KEYsecret. No force push or deletion.
GitHub Pages serves the gh-pages branch, / folder (Settings → Pages). To redeploy
without changes: Actions → Deploy to gh-pages → Run workflow.
| Document | Audience |
|---|---|
| README.md | Project overview and content editing |
| THEMES.md | Light / dark appearance and skins: calendar, settings, adding a theme |
| AGENTS.md | Code conventions, for contributors and coding agents |
Aurélien Bertrand (oreLINK) · Data Engineer at Betclic Group, Bordeaux LinkedIn · GitHub