From 10b3d992fe22a364e8adb49928458be1e9eddc86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Bertrand?= Date: Wed, 28 Oct 2026 22:44:05 +0100 Subject: [PATCH] feat: add new pages and components for articles and projects - Added a new page for individual articles (Billet.astro) with metadata and content rendering. - Created a list page for articles (Billets.astro) that displays articles grouped by year. - Introduced a new unlisted page (MadeBy.astro) for showcasing a badge with options for HTML and SVG. - Updated the project page (Projet.astro) to include related articles and improved navigation. - Added a new projects overview page (Projets.astro) that groups projects by status and displays recent articles. - Enhanced the homepage (Accueil.astro) to filter sections based on display settings. - Introduced CSS variables for light and dark themes in tokens.css. --- AGENTS.md | 29 ++- README.md | 113 +++++++++-- astro.config.mjs | 3 + public/logos/buymeacoffee.svg | 32 ++++ src/components/BilletCard.astro | 45 +++++ src/components/Icons.astro | 2 + src/components/ProjetCard.astro | 52 ++--- src/components/SectionHead.astro | 13 +- src/components/sections/Projets.astro | 34 ++-- src/content.config.ts | 39 +++- src/content/articles/.gitkeep | 0 .../articles/terminus-sous-le-capot/fr.md | 37 ++++ src/content/projets/bluebird.md | 36 ++-- src/content/projets/frais-reels.md | 14 +- src/content/projets/morseau.md | 20 +- src/content/projets/pottoka.md | 20 +- src/content/projets/poudreuse.md | 28 +-- src/content/projets/terminus.md | 41 ++-- src/data/en/site.json | 57 +++++- src/data/profil.json | 3 +- src/data/site.json | 83 ++++++-- src/layouts/Base.astro | 43 +++-- src/lib/badge.ts | 150 +++++++++++++++ src/lib/billets.ts | 119 ++++++++++++ src/lib/dates.ts | 13 ++ src/lib/projets.ts | 27 ++- src/lib/reglages.ts | 84 +++++++-- src/lib/tuiles.ts | 40 ++++ src/pages/[outil].astro | 10 + src/pages/articles/[...chemin].astro | 16 ++ src/pages/articles/[flux].xml.ts | 6 + src/pages/en/[outil].astro | 9 + src/pages/en/articles/[...chemin].astro | 15 ++ src/pages/en/articles/[flux].xml.ts | 6 + src/pages/en/projets/[...chemin].astro | 16 ++ src/pages/en/projets/[slug].astro | 9 - src/pages/projets/[...chemin].astro | 17 ++ src/pages/projets/[slug].astro | 9 - src/scripts/ui.ts | 54 +++++- src/styles/global.css | 178 +++++++++++++++--- src/styles/tokens.css | 7 + src/vues/Accueil.astro | 16 +- src/vues/Billet.astro | 81 ++++++++ src/vues/Billets.astro | 44 +++++ src/vues/MadeBy.astro | 93 +++++++++ src/vues/Projet.astro | 102 +++++----- src/vues/Projets.astro | 62 ++++++ 47 files changed, 1636 insertions(+), 291 deletions(-) create mode 100644 public/logos/buymeacoffee.svg create mode 100644 src/components/BilletCard.astro create mode 100644 src/content/articles/.gitkeep create mode 100644 src/content/articles/terminus-sous-le-capot/fr.md create mode 100644 src/lib/badge.ts create mode 100644 src/lib/billets.ts create mode 100644 src/lib/tuiles.ts create mode 100644 src/pages/[outil].astro create mode 100644 src/pages/articles/[...chemin].astro create mode 100644 src/pages/articles/[flux].xml.ts create mode 100644 src/pages/en/[outil].astro create mode 100644 src/pages/en/articles/[...chemin].astro create mode 100644 src/pages/en/articles/[flux].xml.ts create mode 100644 src/pages/en/projets/[...chemin].astro delete mode 100644 src/pages/en/projets/[slug].astro create mode 100644 src/pages/projets/[...chemin].astro delete mode 100644 src/pages/projets/[slug].astro create mode 100644 src/vues/Billet.astro create mode 100644 src/vues/Billets.astro create mode 100644 src/vues/MadeBy.astro create mode 100644 src/vues/Projets.astro diff --git a/AGENTS.md b/AGENTS.md index d0bdc3d..9e8201b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,8 +6,11 @@ fonctionnement du site pour son propriétaire est dans [README.md](README.md) ; ## Le projet -Portfolio personnel statique (Astro 5, TypeScript strict), une seule page qui défile, en -français (`/`, par défaut) et en anglais (`/en/`). Aucun framework client, aucune +Portfolio personnel statique (Astro 5, TypeScript strict), en français (`/`, par défaut) et +en anglais (`/en/`), en trois rubriques : **Profil** (l'accueil, façon CV, avec les projets à la +une), **Projets** (tous les projets, une fiche par projet) et **Billets** (billets tech, liés ou +non à un projet). Ce qui s'affiche se règle dans `site.json → affichage`, à part pour +`npm run dev` (`local`) et pour le site publié (`deploye`) ; lire `reglages(langue).affichage`. Aucun framework client, aucune dépendance d'exécution : du HTML/CSS généré au build, plus un seul script navigateur (`src/scripts/ui.ts`). Déployé sur GitHub Pages. @@ -31,18 +34,22 @@ ordinateur **et** à ~390 px de large (pas de défilement horizontal). |---|---| | `src/data/*.json` | Tout le contenu et les réglages (profil, expériences, formations, certifications, bénévolat, textes, thèmes). Source de vérité, en français. | | `src/data/en/*.json` | Traduction anglaise : **uniquement les champs à traduire** (voir « Langues »). | -| `src/content/projets/*.md` | Un projet par fichier (en français seulement). | -| `src/content.config.ts` | Schémas des listes de `src/data/` et des projets. | +| `src/content/projets/*.md` | Un projet par fichier (en français seulement) : sa fiche. | +| `src/content/articles//` | Un billet par dossier : `fr.md` (en-tête complet), `en.md` (traduction, facultatif), images. | +| `src/content.config.ts` | Schémas des listes de `src/data/`, des projets et des billets. | | `src/lib/reglages.ts` | Schémas de `site.json`, `profil.json`, `themes.json` ; `reglages(langue)` → `{ site, profil, t }`. | | `src/lib/i18n.ts` | Langues, `langueDe(Astro.url)`, pose des traductions sur le français. | | `src/lib/contenu.ts` | `liste(nom, langue)` : une liste de `src/data/` traduite. | -| `src/lib/dates.ts` | Dates « AAAA-MM », périodes et durées, en fr et en en. | +| `src/lib/dates.ts` | Dates « AAAA-MM » et « AAAA-MM-JJ », périodes et durées, en fr et en en. | +| `src/lib/badge.ts` | Badge « Made by ore_link » : son CSS, son HTML et sa version SVG, par thème, avec ou sans café. | +| `src/lib/tuiles.ts` | Plugin rehype : sur les fiches projet, une tuile de verre par intertitre `##` (les cinq questions, voir README). | +| `src/lib/billets.ts` | `billets(langue)` : billets publiés et traduits ; pages et flux RSS. Vide si la rubrique est coupée. | | `src/lib/paths.ts` | `url(chemin, langue)` pour les liens internes, `asset(chemin)` pour `public/`. | -| `src/vues/` | Pages communes aux deux langues (accueil, projet). | -| `src/pages/` | Routes seulement : `/`, `/en/`, `/projets/[slug]/`, `/en/projets/[slug]/`, 404. Elles rendent une vue. | +| `src/vues/` | Pages communes aux deux langues (accueil, projets, fiche, billets, billet, made by). | +| `src/pages/` | Routes seulement : `/`, `/projets/[...chemin]/` (page Projets et fiches), `/articles/[...chemin]/` (liste et billets), `/articles/rss.xml`, `/madeby/` (`[outil].astro`, non répertoriée), leurs versions `/en/…`, 404. Elles rendent une vue. | | `src/components/sections/` | Une section de l'accueil par fichier. | -| `src/layouts/Base.astro` | ``, scripts d'avant-rendu (thème, habillage), haut de page, pied de page. | -| `src/scripts/ui.ts` | Tout le JS navigateur : thème, menu des langues, replis « Voir plus », bulles, liens externes, feux d'artifice du Nouvel An. | +| `src/layouts/Base.astro` | ``, scripts d'avant-rendu (thème, habillage), haut de page (rubriques), pied de page. | +| `src/scripts/ui.ts` | Tout le JS navigateur : thème, menu des langues, replis « Voir plus », bulles, filtre des billets, liens externes, feux d'artifice du Nouvel An. | | `src/styles/tokens.css` | Jetons de design (couleurs clair/sombre, verre, rayons). | | `src/styles/global.css` | Mise en page et composants. | | `src/styles/habillages/`, `src/components/habillages/` | Habillages (saisons, fêtes) : un CSS et un composant chacun, `commun.css`, `Etoiles` et `Flocons` partagés. | @@ -83,6 +90,10 @@ ordinateur **et** à ~390 px de large (pas de défilement horizontal). position, les listes de textes (compétences) sont remplacées en entier. Un champ absent reste en français ; un champ ou un `id` inconnu **fait échouer le build** (c'est voulu). - Les pages de projets ne sont pas traduites (seule l'interface autour l'est). +- Les billets le sont : `en.md` à côté de `fr.md`, avec seulement `titre`, `resume`, `sujets` + (facultatif) et le texte. Sans `en.md`, la page anglaise montre le français avec un bandeau. + Un champ inconnu dans `en.md`, un `en.md` sans `fr.md` ou un `projet` inconnu fait échouer le + build. Dates des billets en « AAAA-MM-JJ » ; `brouillon: true` ne sort qu'avec `npm run dev`. ## Thèmes diff --git a/README.md b/README.md index d620a22..e1ec70c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ # oreLINK · Portfolio **Portfolio of Aurélien Bertrand, Data Engineer based in Bordeaux, France.** -A single-page static site in French and English, driven entirely by data files. +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). [**orelink.github.io**](https://orelink.github.io) · [English version](https://orelink.github.io/en/) @@ -22,6 +23,8 @@ A single-page static site in French and English, driven entirely by data files. 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. @@ -72,7 +75,8 @@ Everything on the page comes from these files; the code never needs to change. | `src/data/formations.json` | Education | | `src/data/certifications.json` | Certifications | | `src/data/benevolat.json` | Volunteering | -| `src/content/projets/*.md` | Projects: one file per project (home page tile and detail page) | +| `src/content/projets/*.md` | Projects: one file per project (tile and overview page) | +| `src/content/articles//` | 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](THEMES.md) | @@ -82,10 +86,34 @@ computed automatically, from most recent to oldest.
site.json settings -- `sections`: array order is page order. `"afficher": false` hides a section; `titre` - changes its heading. An empty section disappears on its own. +- `affichage`: what is shown, set separately for `local` (`npm run dev`) and `deploye` + (`npm run build`, i.e. the live site and `npm run preview`). Each block has the same keys: + + ```json + "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//` overview pages and every link to them + ("View details"). `"auto"` (the default) follows `projets.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; `titre` changes 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 by `ordre`); + 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"…). @@ -103,6 +131,8 @@ computed automatically, from most recent to oldest. tooltip. `code` is `fr`, `en` or `es` (flags drawn in `src/components/Drapeau.astro`). - `liens.cv`: put `public/cv.pdf` in 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 a `TON-PSEUDO` placeholder.
@@ -116,8 +146,9 @@ ongoing position or commitment, use `"fin": null`: the page shows "Present" and **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/.md` file creates a tile on the home page **and** -a `/projets//` "How I built it" page. +**Projects**: a `src/content/projets/.md` file creates a tile on the `/projets/` page +(and on the home page if it is among the first `projetsALaUne`) **and** a `/projets//` +overview page. Keep it to the big picture; the details go in notes. ```yaml --- @@ -133,11 +164,60 @@ ordre: 1 # optional: forces the order (lowest first) couleur: "#E8962E" # optional: badge color --- -## The need -… +## 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; `##` headings build the page's table of contents. +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: + +1. **Quelle était la situation au départ ?** The context, the problem, for whom, the + constraints (budget, data, privacy…). +2. **Qu'est-ce que ça fait ?** What the product does, ideally its three key features. +3. **Comment je l'ai construit, et pourquoi ?** Architecture and two or three decisions, + with the options that were set aside (`###` subheadings). +4. **Comment ça tourne sans moi ?** Automation, tests, deployment, monitoring, cost. +5. **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//`, `/en/articles//`). Images go in the folder, next to the text, and +are referenced as `![Description](./chart.png)`. + +`fr.md` holds the full header and the French text: + +```yaml +--- +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.
@@ -153,20 +233,31 @@ logos, links…) comes from the French file with the same name. order; plain text lists (skills) are replaced as a whole. - A missing field stays in French; an unknown field or `id` fails the build. - Project pages are written in French only; the surrounding interface is translated. +- Notes are translated with an `en.md` next to `fr.md` (see above). +## "Made by ore_link" badge + +`/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`. + ## Architecture ``` 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 # , pre-render scripts, header and footer -├── vues/ # home and project page, shared by both languages -├── pages/ # routes: /, /en/, /projets/…, /en/projets/…, 404 +├── 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 diff --git a/astro.config.mjs b/astro.config.mjs index 4cca05e..bee55ca 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,5 +1,6 @@ // @ts-check import { defineConfig } from 'astro/config'; +import { tuilesParQuestion } from './src/lib/tuiles.ts'; // Déploiement GitHub Pages (repo orelink.github.io → page « utilisateur », base '/'). // Pour un repo « projet » (ex. mon-portfolio), passer base à '/mon-portfolio'. @@ -10,5 +11,7 @@ export default defineConfig({ markdown: { // Coloration des blocs de code par variables CSS : elle suit les jetons du thème. shikiConfig: { theme: 'css-variables' }, + // Fiches projet : une tuile par question (src/lib/tuiles.ts). + rehypePlugins: [tuilesParQuestion], }, }); diff --git a/public/logos/buymeacoffee.svg b/public/logos/buymeacoffee.svg new file mode 100644 index 0000000..04c00a3 --- /dev/null +++ b/public/logos/buymeacoffee.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/BilletCard.astro b/src/components/BilletCard.astro new file mode 100644 index 0000000..572da55 --- /dev/null +++ b/src/components/BilletCard.astro @@ -0,0 +1,45 @@ +--- +// Tuile d'un billet : date, temps de lecture, titre, résumé et projet lié. Toute la tuile +// mène au billet ; la pastille du projet, au-dessus, mène à sa fiche (ou à la page Projets, selon +// l'affichage). `compact` : sans le résumé. +import { getEntry } from 'astro:content'; +import { jourLisible } from '../lib/dates'; +import { url } from '../lib/paths'; +import { lienProjet } from '../lib/projets'; +import { reglages } from '../lib/reglages'; +import { langueDe } from '../lib/i18n'; +import type { Billet } from '../lib/billets'; + +interface Props { + billet: Billet; + compact?: boolean; + /** false sur la fiche d'un projet : le projet lié est celui de la page. */ + avecProjet?: boolean; +} +const { billet: b, compact = false, avecProjet = true } = Astro.props; +const langue = langueDe(Astro.url); +const { t } = reglages(langue); +const projet = avecProjet && b.projet ? await getEntry('projets', b.projet) : undefined; +const versProjet = projet && lienProjet(projet.id, langue); +const Pastille = versProjet ? 'a' : 'span'; // sans fiche ni page Projets, le projet n'est pas un lien +--- + +
+ +

{b.titre}

+ {!compact &&

{b.resume}

} + { + projet && ( +
+ + + {t.projetLie} : {projet.data.titre} + +
+ ) + } +
diff --git a/src/components/Icons.astro b/src/components/Icons.astro index 75e0d95..2c134b9 100644 --- a/src/components/Icons.astro +++ b/src/components/Icons.astro @@ -22,5 +22,7 @@ + + diff --git a/src/components/ProjetCard.astro b/src/components/ProjetCard.astro index 68d7aa8..015ba97 100644 --- a/src/components/ProjetCard.astro +++ b/src/components/ProjetCard.astro @@ -1,10 +1,11 @@ --- -// Tuile d'un projet. Toute la tuile mène à la page « Comment je l'ai réalisé » ; les deux petits -// boutons ouvrent le projet en ligne et son code. +// Tuile d'un projet. Toute la tuile mène à sa fiche (si les fiches sont affichées) ; les deux petits +// boutons ouvrent le projet en ligne et son code. Le nombre de billets liés s'affiche quand il y en a. import type { CollectionEntry } from 'astro:content'; import Icon from './Icon.astro'; import Tags from './Tags.astro'; import { statuts } from '../lib/projets'; +import { billetsDuProjet } from '../lib/billets'; import { url } from '../lib/paths'; import { reglages } from '../lib/reglages'; import { langueDe } from '../lib/i18n'; @@ -15,8 +16,10 @@ interface Props { const { projet } = Astro.props; const p = projet.data; const langue = langueDe(Astro.url); -const { t } = reglages(langue); +const { t, affichage } = reglages(langue); const statut = statuts(t)[p.statut]; +const nbBillets = (await billetsDuProjet(projet.id, langue)).length; +const avecPied = affichage.fichesProjets || nbBillets > 0 || p.lien || p.depot; ---
@@ -27,24 +30,27 @@ const statut = statuts(t)[p.statut];

{p.resume}

- + { + avecPied && ( +
+ {affichage.fichesProjets && ( + + {t.voirLaFiche} : {p.titre} + + + )} + {nbBillets > 0 && {nbBillets} {nbBillets > 1 ? t.billetsPluriel : t.billet}} + {p.lien && ( + + {t.voirLeProjet} {p.titre} + + )} + {p.depot && ( + + {t.codeSource} : {p.titre} + + )} +
+ ) + }
diff --git a/src/components/SectionHead.astro b/src/components/SectionHead.astro index b48f8bb..032f128 100644 --- a/src/components/SectionHead.astro +++ b/src/components/SectionHead.astro @@ -1,14 +1,19 @@ --- -// Titre de section, posé sur le fond (hors tuile), avec le nombre d'éléments. +// Titre de section, posé sur le fond (hors tuile), avec le nombre d'éléments et, au besoin, un lien +// vers la page qui les montre tous. +import Icon from './Icon.astro'; + interface Props { id: string; titre: string; - nombre: number; + nombre?: number; + lien?: { href: string; label: string }; } -const { id, titre, nombre } = Astro.props; +const { id, titre, nombre, lien } = Astro.props; ---

{titre}

- {nombre} + {nombre !== undefined && {nombre}} + {lien && {lien.label} }
diff --git a/src/components/sections/Projets.astro b/src/components/sections/Projets.astro index 185e76e..d4222d1 100644 --- a/src/components/sections/Projets.astro +++ b/src/components/sections/Projets.astro @@ -1,31 +1,39 @@ --- -// Projets : une tuile par projet, en grille sur grand écran. +// Projets à la une : les premières tuiles selon `ordre` (projetsALaUne dans site.json), et un lien +// vers la page Projets qui les montre tous, si elle est affichée. import { getCollection } from 'astro:content'; import SectionHead from '../SectionHead.astro'; -import Repli from '../Repli.astro'; import ProjetCard from '../ProjetCard.astro'; import { trierProjets } from '../../lib/projets'; +import { url } from '../../lib/paths'; +import { reglages } from '../../lib/reglages'; +import { langueDe } from '../../lib/i18n'; interface Props { titre: string; } const { titre } = Astro.props; -const projets = trierProjets(await getCollection('projets')); +const langue = langueDe(Astro.url); +const { site, t, affichage } = reglages(langue); +const tous = trierProjets(await getCollection('projets')); +const projets = tous.slice(0, site.projetsALaUne); --- { projets.length > 0 && (
- - -
    - {projets.map((p) => ( -
  • - -
  • - ))} -
-
+ +
    + {projets.map((p) => ( +
  • + +
  • + ))} +
) } diff --git a/src/content.config.ts b/src/content.config.ts index 28ec053..1fbe503 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,4 +1,4 @@ -import { defineCollection, z } from 'astro:content'; +import { defineCollection, reference, z } from 'astro:content'; import { file, glob } from 'astro/loaders'; // Toutes les données sont validées au build : une date mal écrite ou un champ manquant fait @@ -103,4 +103,39 @@ const projets = defineCollection({ }), }); -export const collections = { experiences, formations, certifications, benevolat, projets }; +// ── Billets : un dossier par billet, src/content/articles// ──────────────────────────── +// fr.md porte l'en-tête complet et le texte ; en.md (facultatif) seulement le titre, le résumé +// (et les sujets) traduits, plus le texte. Les images se rangent dans le dossier, à côté du texte. +// Publiés à /articles// et /en/articles//. +// YAML lit 2026-10-21 (sans guillemets) comme une date : on la remet en texte « AAAA-MM-JJ ». +const jour = z.preprocess( + (v) => (v instanceof Date ? v.toISOString().slice(0, 10) : v), + z.string().regex(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/, 'Format attendu : AAAA-MM-JJ (ex. 2026-10-21)'), +); +const dossier = ({ entry }: { entry: string }) => entry.split('/')[0]; + +const billets = defineCollection({ + loader: glob({ pattern: '*/fr.md', base: './src/content/articles', generateId: dossier }), + schema: z.object({ + titre: z.string(), + resume: z.string(), + date: jour, // publication + miseAJour: jour.optional(), + projet: reference('projets').optional(), // nom du fichier du projet (ex. bluebird) + sujets: z.array(z.string()).default([]), + brouillon: z.boolean().default(false), // visible avec `npm run dev`, absent du site publié + }), +}); + +const billetsEn = defineCollection({ + loader: glob({ pattern: '*/en.md', base: './src/content/articles', generateId: dossier }), + schema: z + .object({ + titre: z.string(), + resume: z.string(), + sujets: z.array(z.string()).optional(), // remplace la liste française en entier + }) + .strict(), // un champ inconnu fait échouer le build, comme dans src/data/en/ +}); + +export const collections = { experiences, formations, certifications, benevolat, projets, billets, billetsEn }; diff --git a/src/content/articles/.gitkeep b/src/content/articles/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/content/articles/terminus-sous-le-capot/fr.md b/src/content/articles/terminus-sous-le-capot/fr.md new file mode 100644 index 0000000..fbe158b --- /dev/null +++ b/src/content/articles/terminus-sous-le-capot/fr.md @@ -0,0 +1,37 @@ +--- +titre: "Terminus, sous le capot" +resume: Les choix techniques derrière Terminus. Calcul à rebours, temps réel ciblé, hors ligne, et ce que j'ai écarté. +date: 2026-10-28 +projet: terminus +sujets: [Open data, Mobilité] +brouillon: true # matière mise de côté depuis la fiche projet ; à relire avant publication +--- + +## Architecture + +- **Aucun framework, aucun build** : HTML, CSS et JavaScript en modules ES natifs, hébergés sur GitHub Pages. +- **Les API publiques sont appelées depuis le navigateur** : SIRI-Lite de Bordeaux Métropole pour le temps réel, la Géoplateforme IGN pour le géocodage, la marche à pied et le fond de carte. +- **Les données du réseau sont générées à l'avance** depuis le GTFS de TBM (arrêts, graphes, tracés, horaires du soir), un fichier par mode, chargé à la demande. +- **Le calcul est une fonction pure**, testée avec `node:test`, sans navigateur. + +## Itinéraire à rebours, jusqu'à deux correspondances + +Le moteur part de l'arrivée et remonte le temps. Les correspondances à pied entre stations proches sont **précalculées à la génération** : aucun appel réseau à l'exécution pour elles. + +## Le temps réel, seulement là où il sert + +Le flux GTFS-RT de tout le réseau pèse 2,7 Mo : trop lourd toutes les 30 secondes sur un téléphone. Terminus n'interroge que les quais du trajet affiché, à la montée et à la descente de chaque tronçon, soit environ 100 Ko par calcul. + +## Ce que j'ai essayé, puis écarté + +J'ai prototypé un calcul en ordre « Connection Scan ». Mêmes résultats sur 1 500 trajets, mais 10 à 60 % **plus lent** en JavaScript avec mon modèle d'objets. Je l'ai documenté et laissé de côté. + +## Hors ligne dès la deuxième visite + +Un service worker garde les pages, les styles et les données générées sur le téléphone. GitHub Pages ne les met en cache que 10 minutes. + +## Industrialisation + +- Les données sont **régénérées chaque lundi** par une GitHub Action : branche dédiée, pull request, fusion automatique si les tests passent, mise en ligne. +- La branche `main` n'accepte que des pull requests dont les tests passent. +- Un design system documenté (jetons, verre, mouvement, accessibilité), avec des thèmes de saison imposés par leurs dates. Ce portfolio en reprend le design. diff --git a/src/content/projets/bluebird.md b/src/content/projets/bluebird.md index 2e16734..afd0668 100644 --- a/src/content/projets/bluebird.md +++ b/src/content/projets/bluebird.md @@ -11,13 +11,25 @@ ordre: 2 couleur: "#2F7FE0" --- -## Le besoin +## Quelle était la situation au départ ? -Un *bluebird day*, pour les skieurs, c'est le premier jour de grand soleil après une tempête de neige. Bluebird aide à les trouver : pour chaque station d'un massif, il donne la probabilité qu'il neige ce matin, ce midi, cet après-midi, ce soir, cette nuit et demain, et la probabilité de trouver de la poudreuse sur les pistes ou hors-piste. +Un *bluebird day*, pour les skieurs, c'est le premier jour de grand soleil après une tempête de neige. -Les stations sont classées par probabilité dans des tuiles, à la manière d'une page de paris sportifs. +## Qu'est-ce que ça fait ? -## Architecture : un pipeline en couches +Bluebird aide à les trouver : pour chaque station d'un massif, il donne la probabilité qu'il neige ce matin, ce midi, cet après-midi, ce soir, cette nuit et demain, et la probabilité de trouver de la poudreuse sur les pistes ou hors-piste. + +Les stations sont classées par probabilité dans des tuiles, à la manière d'une page de paris sportifs. Trois indicateurs : + +- `snowfall_chance` : va-t-il neiger sur ce créneau ? +- `onpiste_powder_chance` : la neige fraîche couvrira-t-elle les pistes après le damage ? +- `offpiste_powder_chance` : trouvera-t-on de la poudreuse vierge hors-piste ? + +En fin de saison, un **Rewind** fait le bilan à partir des prévisions archivées : la station la plus enneigée, la plus longue chute de neige, les pistes et les hors-pistes les mieux servis. Le premier, *Rewind 25/26*, couvre la saison du 1er décembre 2025 au 1er mai 2026. + +## Comment je l'ai construit, et pourquoi ? + +Les indicateurs sont calculés à partir de **prévisions d'ensemble** : plusieurs scénarios météo, d'où une vraie probabilité plutôt qu'un oui ou non. Tout part de fichiers de configuration YAML (massifs, stations, indicateurs, tuiles, sources). Le pipeline Python suit quatre couches : @@ -30,22 +42,10 @@ Tout part de fichiers de configuration YAML (massifs, stations, indicateurs, tui Le site (Svelte et Vite) embarque la configuration au build et ne lit que les JSON de la couche Diamond. -## Les indicateurs - -- `snowfall_chance` : va-t-il neiger sur ce créneau ? -- `onpiste_powder_chance` : la neige fraîche couvrira-t-elle les pistes après le damage ? -- `offpiste_powder_chance` : trouvera-t-on de la poudreuse vierge hors-piste ? - -Ils sont calculés à partir de **prévisions d'ensemble** : plusieurs scénarios météo, d'où une vraie probabilité plutôt qu'un oui ou non. - -## Automatisation +## Comment ça tourne sans moi ? Une GitHub Action relance le pipeline **toutes les 6 heures**, avec un job par source. Le pied de page du site montre l'état de chaque source, transformation, indicateur et tuile après le dernier rafraîchissement : si une source tombe, on le voit. -## Rewind - -En fin de saison, un **Rewind** fait le bilan à partir des prévisions archivées : la station la plus enneigée, la plus longue chute de neige, les pistes et les hors-pistes les mieux servis. Le premier, *Rewind 25/26*, couvre la saison du 1er décembre 2025 au 1er mai 2026. - -## Ce que j'en retiens +## Qu'est-ce que j'en ai tiré ? Le découpage en couches rend chaque étape testable et rejouable. Ajouter une station ou un indicateur se fait dans la configuration, pas dans le code. diff --git a/src/content/projets/frais-reels.md b/src/content/projets/frais-reels.md index a7ae6e2..7575c7e 100644 --- a/src/content/projets/frais-reels.md +++ b/src/content/projets/frais-reels.md @@ -11,28 +11,30 @@ ordre: 6 couleur: "#1F8A5B" --- -## Le besoin +## Quelle était la situation au départ ? -Chaque année, les salariés peuvent déduire leurs frais réels au lieu de l'abattement forfaitaire de 10 %. Le calcul est fastidieux et la question reste : *est-ce que ça vaut le coup ?* Le simulateur y répond pour les revenus 2025 (déclaration 2026). +Chaque année, les salariés peuvent déduire leurs frais réels au lieu de l'abattement forfaitaire de 10 %. Le calcul est fastidieux et la question reste : *est-ce que ça vaut le coup ?* -## Le parcours +## Qu'est-ce que ça fait ? + +Le simulateur y répond pour les revenus 2025 (déclaration 2026), en quatre temps : 1. **Accueil** : les dates limites de déclaration. 2. **Liste des pièces** à avoir sous la main, en cinq catégories. 3. **Simulateur en cinq étapes**, une question à la fois. 4. **Synthèse** : trois scénarios comparés, et un **PDF complet** à télécharger. -## Architecture +## Comment je l'ai construit, et pourquoi ? - **Un fichier central pour la fiscalité** (`config/fiscalite.js`) : tous les barèmes de l'année au même endroit, faciles à mettre à jour l'an prochain. - **Des références légales** (BOFiP) associées à chaque règle. - **Un moteur de calcul fait de fonctions pures**, sans effet de bord, séparé de l'interface. - **Un état global** géré avec `useReducer`. -## Confidentialité +### Confidentialité Aucun backend, aucun cookie, aucun `localStorage` : tout le calcul a lieu dans le navigateur. La mesure d'audience (GoatCounter) ne compte que les visites, sans cookie ni donnée personnelle, et sans aucun lien avec les montants saisis. -## Déploiement +## Comment ça tourne sans moi ? GitHub Actions vérifie le build sur chaque pull request et publie sur GitHub Pages à chaque push sur `main`. diff --git a/src/content/projets/morseau.md b/src/content/projets/morseau.md index 62b6c79..fa5c2e3 100644 --- a/src/content/projets/morseau.md +++ b/src/content/projets/morseau.md @@ -11,11 +11,13 @@ ordre: 5 couleur: "#1F4E8C" --- -## L'idée +## Qu'est-ce que ça fait ? -Un site statique inspiré de Duolingo pour apprendre le morse. Chaque caractère se lit dans les éclats de la lampe d'un timonier. Le site est prévu en quatre sections : un répertoire, puis deviner un caractère, un mot, un message. La première est en ligne. +Un site statique inspiré de Duolingo pour apprendre le morse. Chaque caractère se lit dans les éclats de la lampe d'un timonier. -## Le principe : séparer la logique morse de son affichage +## Comment je l'ai construit, et pourquoi ? + +### Séparer la logique morse de son affichage Le même moteur servira aux quatre sections, donc il ne dépend ni de Svelte ni du DOM : @@ -23,13 +25,11 @@ Le même moteur servira aux quatre sections, donc il ne dépend ni de Svelte ni - `morse-engine.ts` : transforme un texte en **ligne de temps d'évènements de lampe** (allumée, éteinte) ; - `transmission.ts` : joue cette ligne de temps et expose l'état de la lampe à l'interface. -## Deux façons d'étendre le site - -### Ajouter une animation +### Étendre le site : ajouter une animation Une animation est un composant qui reçoit deux propriétés, `lit` (la lampe est-elle allumée ?) et `playing`. On l'enregistre dans un registre, et elle entre dans le tirage au hasard sans autre changement. -### Ajouter une langue +### Étendre le site : ajouter une langue Le morse « français » recouvre deux choses distinctes : les textes de l'interface **et** les caractères enseignés, car les lettres accentuées ont leurs propres codes. Les deux sont donc séparés : @@ -38,6 +38,10 @@ Le morse « français » recouvre deux choses distinctes : les textes de l'inter La base internationale (A à Z, chiffres, ponctuation, signaux de service) n'est jamais dupliquée. -## Déploiement +## Comment ça tourne sans moi ? Une GitHub Action construit et publie le site sur GitHub Pages à chaque push sur `main`. + +## Où j'en suis ? + +Le site est prévu en quatre sections : un répertoire, puis deviner un caractère, un mot, un message. La première est en ligne. diff --git a/src/content/projets/pottoka.md b/src/content/projets/pottoka.md index 2cf4008..b795992 100644 --- a/src/content/projets/pottoka.md +++ b/src/content/projets/pottoka.md @@ -11,18 +11,20 @@ ordre: 4 couleur: "#D23A4B" --- -## Le besoin +## Quelle était la situation au départ ? -Peu d'outils permettent d'apprendre le basque depuis le français. Pottoka propose des leçons courtes, des exercices et des cours de lecture, dans le sens **français → basque**. +Peu d'outils permettent d'apprendre le basque depuis le français. -## Principes +## Qu'est-ce que ça fait ? + +Pottoka propose des leçons courtes, des exercices et des cours de lecture, dans le sens **français → basque**. + +## Comment je l'ai construit, et pourquoi ? - **Aucun backend, aucun compte** : rien n'est enregistré, chaque session repart de zéro. - **Le contenu est séparé du code** : vocabulaire, exercices et leçons sont en JSON et en Markdown, modifiables sans toucher au JavaScript. - **Une seule mise en page** qui s'adapte du téléphone à l'ordinateur, avec une barre latérale à partir de 900 px. -## Architecture - ```text src/ ├─ data/ contenu : arbre des chapitres, leçons, cours, groupes de vocabulaire @@ -36,10 +38,10 @@ Le moteur (`engine/`) fait le travail : - il corrige les réponses en normalisant la casse, les accents et la ponctuation ; - il **génère les nombres basques** de façon algorithmique, car le basque compte en base 20 (vigésimal). -## Une difficulté : la justesse du contenu +## Qu'est-ce que j'en ai tiré ? -Le contenu basque livré est un **contenu de départ à faire valider** par une personne bascophone. Les points à vérifier sont signalés dans les cours. Pour un outil d'apprentissage, la qualité du contenu compte autant que le code. +Séparer le contenu du moteur permet d'ajouter des leçons sans risque de casser l'application, et de faire relire le contenu par quelqu'un qui ne code pas. -## Ce que j'en retiens +### Une difficulté : la justesse du contenu -Séparer le contenu du moteur permet d'ajouter des leçons sans risque de casser l'application, et de faire relire le contenu par quelqu'un qui ne code pas. +Le contenu basque livré est un **contenu de départ à faire valider** par une personne bascophone. Les points à vérifier sont signalés dans les cours. Pour un outil d'apprentissage, la qualité du contenu compte autant que le code. diff --git a/src/content/projets/poudreuse.md b/src/content/projets/poudreuse.md index 52d53cf..ee98f3c 100644 --- a/src/content/projets/poudreuse.md +++ b/src/content/projets/poudreuse.md @@ -10,11 +10,20 @@ ordre: 3 couleur: "#4BA3C7" --- -## L'idée +## Qu'est-ce que ça fait ? Construire sa station de ski (pistes, remontées mécaniques, bâtiments) sur une montagne crédible. Chaque partie se joue sur un **massif généré procéduralement** : la même graine redonne toujours exactement le même domaine, et l'adresse se partage (`?size=grand&seed=4217`). -## Générer une montagne crédible +Le domaine vit : + +- **Le temps** : jour, nuit et saisons. +- **Le climat et la météo** : températures, vent, nuages, brouillard, précipitations, tempêtes. +- **Le manteau neigeux** : enneigement, risque d'avalanche, déclenchements et écoulement. +- **Les remontées** : un moteur générique (débrayable, pince fixe, pulsé, va-et-vient), avec pylônes automatiques et prise en compte du vent. + +## Comment je l'ai construit, et pourquoi ? + +### Générer une montagne crédible La génération est faite de **données pures, sans Three.js**, exécutées dans un Web Worker pour ne pas bloquer l'affichage : @@ -23,19 +32,14 @@ La génération est faite de **données pures, sans Three.js**, exécutées dans 3. l'hydrologie : remplissage des cuvettes, écoulement, lacs et torrents ; 4. les forêts étagées, une aire plate pour la station et des altitudes réalistes. -Des tests vérifient le déterminisme, les altitudes, les pentes, le drainage, les lacs et les rivières. +### Organisation du code -## Faire vivre le domaine - -- **Le temps** : jour, nuit et saisons. -- **Le climat et la météo** : températures, vent, nuages, brouillard, précipitations, tempêtes. -- **Le manteau neigeux** : enneigement, risque d'avalanche, déclenchements et écoulement. -- **Les remontées** : un moteur générique (débrayable, pince fixe, pulsé, va-et-vient), avec pylônes automatiques et prise en compte du vent. +Les paramètres sont rangés par thème (monde, terrain, neige, climat, remontées…), séparés de la simulation (`sim/`), de la génération (`gen/`) et du rendu (`render/`). On peut ainsi régler le jeu sans toucher au moteur. -## Organisation du code +## Comment ça tourne sans moi ? -Les paramètres sont rangés par thème (monde, terrain, neige, climat, remontées…), séparés de la simulation (`sim/`), de la génération (`gen/`) et du rendu (`render/`). On peut ainsi régler le jeu sans toucher au moteur. +Des tests vérifient le déterminisme de la génération, les altitudes, les pentes, le drainage, les lacs et les rivières. -## Et ensuite +## Où j'en suis ? Le projet est en cours et pas encore en ligne. Viendront ensuite la gestion des skieurs et la publication sur GitHub Pages. diff --git a/src/content/projets/terminus.md b/src/content/projets/terminus.md index 461f915..1c42efc 100644 --- a/src/content/projets/terminus.md +++ b/src/content/projets/terminus.md @@ -11,41 +11,32 @@ ordre: 1 couleur: "#E8962E" --- -## Le besoin +## Avant -Le soir, en ville, une seule question compte : *à quelle heure faut-il partir pour attraper le dernier tram ?* Les applications de transport répondent à « comment aller de A à B maintenant », pas à « jusqu'à quand puis-je rester ». Terminus calcule l'itinéraire **à rebours**, depuis la destination, et affiche un compte à rebours. Il est pensé pour le téléphone, à une main, dehors, la nuit. +### Quelle était la situation au départ ? -## Architecture +Le soir, en ville, une seule question compte : *à quelle heure faut-il partir pour attraper le dernier tram ?* Les applications de transport répondent à « comment aller de A à B maintenant », pas à « jusqu'à quand puis-je rester ». -- **Aucun framework, aucun build** : HTML, CSS et JavaScript en modules ES natifs, hébergés sur GitHub Pages. -- **Les API publiques sont appelées depuis le navigateur** : SIRI-Lite de Bordeaux Métropole pour le temps réel, la Géoplateforme IGN pour le géocodage, la marche à pied et le fond de carte. -- **Les données du réseau sont générées à l'avance** depuis le GTFS de TBM (arrêts, graphes, tracés, horaires du soir), un fichier par mode, chargé à la demande. -- **Le calcul est une fonction pure**, testée avec `node:test`, sans navigateur. +Il fallait donc un outil qui réponde à cette question-là, sur le réseau de Bordeaux Métropole, et qui se consulte sur le téléphone, d'une main, dehors, la nuit. -## Les choix qui ont compté +## Pendant -### Itinéraire à rebours, jusqu'à deux correspondances +### Qu'est-ce que j'ai construit ? -Le moteur part de l'arrivée et remonte le temps. Les correspondances à pied entre stations proches sont **précalculées à la génération** : aucun appel réseau à l'exécution pour elles. +Terminus part de la destination et remonte le temps : il trouve le dernier trajet possible et affiche un compte à rebours jusqu'au moment de partir. -### Le temps réel, seulement là où il sert +- **Le dernier départ possible**, jusqu'à deux correspondances, et par où passer. +- **Les horaires en temps réel**, sur le trajet affiché. +- **Hors ligne** dès la deuxième visite. -Le flux GTFS-RT de tout le réseau pèse 2,7 Mo : trop lourd toutes les 30 secondes sur un téléphone. Terminus n'interroge que les quais du trajet affiché, à la montée et à la descente de chaque tronçon, soit environ 100 Ko par calcul. +Le choix qui a tout guidé : **aucun serveur**. Les données du réseau (TBM) sont préparées à l'avance, et le téléphone ne demande en direct que ce qui sert au trajet affiché : environ 100 Ko par calcul, au lieu de 2,7 Mo pour tout le réseau. -### Ce que j'ai essayé, puis écarté +## Après -J'ai prototypé un calcul en ordre « Connection Scan ». Mêmes résultats sur 1 500 trajets, mais 10 à 60 % **plus lent** en JavaScript avec mon modèle d'objets. Je l'ai documenté et laissé de côté. +### Qu'est-ce que ça a changé ? -### Hors ligne dès la deuxième visite +**Il tourne tout seul.** Chaque lundi, les données du réseau sont régénérées, testées puis mises en ligne automatiquement. Une mise à jour qui casserait un calcul n'est jamais publiée. -Un service worker garde les pages, les styles et les données générées sur le téléphone. GitHub Pages ne les met en cache que 10 minutes. +**Ce que j'en retiens.** Un site statique peut aller très loin, avec calcul d'itinéraire, temps réel et cartes, sans serveur. La vraie difficulté n'est pas le code mais **la donnée** : la fiabilité des horaires, les cas limites après minuit et la vérification des résultats. -## Industrialisation - -- Les données sont **régénérées chaque lundi** par une GitHub Action : branche dédiée, pull request, fusion automatique si les tests passent, mise en ligne. -- La branche `main` n'accepte que des pull requests dont les tests passent. -- Un design system documenté (jetons, verre, mouvement, accessibilité), avec des thèmes de saison imposés par leurs dates. Ce portfolio en reprend le design. - -## Ce que j'en retiens - -Un site statique peut aller très loin : calcul d'itinéraire, temps réel et cartes, sans serveur. La difficulté n'est pas le code mais **la donnée** : la fiabilité des horaires, les cas limites après minuit et la vérification des résultats. +**Et au-delà de Terminus**, le design system créé pour lui (jetons, verre, mouvement, accessibilité) sert aujourd'hui à ce portfolio. diff --git a/src/data/en/site.json b/src/data/en/site.json index a234c86..65e1048 100644 --- a/src/data/en/site.json +++ b/src/data/en/site.json @@ -6,7 +6,7 @@ "sections": [ { "id": "experience", "titre": "Experience" }, - { "id": "projets", "titre": "Projects" }, + { "id": "projets", "titre": "Featured projects" }, { "id": "formation", "titre": "Education" }, { "id": "certifications", "titre": "Certifications" }, { "id": "benevolat", "titre": "Volunteering" } @@ -24,18 +24,14 @@ "expireeEn": "Expired", "identifiant": "Credential ID", "afficherCertification": "Show credential", - "commentJeLaiFait": "How I built it", - "commentJeLaiRealise": "How I built it", + "voirLaFiche": "View details", + "ficheProjet": "Project overview", "voirLeProjet": "View project", "codeSource": "Source code", - "retour": "Back to portfolio", "statut": "Status", "commenceEn": "Started", "role": "Role", "technologies": "Technologies", - "sommaire": "Contents", - "precedent": "Previous", - "suivant": "Next", "statuts": { "en-ligne": "Live", "en-cours": "In progress", @@ -51,7 +47,50 @@ "chez": "at", "competences": "Skills", "liensExternes": "External links", - "autresProjets": "Other projects", - "changerDeLangue": "Change language" + "retourAuxProjets": "Back to Projects", + "retourAuxBillets": "Back to Notes", + "changerDeLangue": "Change language", + "navigation": "Main navigation", + "navProfil": "Profile", + "navProjets": "Projects", + "navBillets": "Notes", + "filAriane": "Breadcrumb", + "projetsTitre": "Projects", + "projetsIntro": "What I build on the side: an everyday need, something that bugs me, or an idea to try out. Each project has an overview, and sometimes notes that go behind the scenes.", + "groupesProjets": { + "en-ligne": "Live", + "en-cours": "In progress", + "archive": "Archived" + }, + "tousLesProjets": "All projects", + "billetsTitre": "Notes", + "billetsIntro": "Tech notes, mostly about data: what I learn while building my projects, and what the numbers do (or don't) say about the news.", + "billet": "note", + "billetsPluriel": "notes", + "derniersBillets": "Latest notes", + "tousLesBillets": "All notes", + "billetsSurCeProjet": "Notes on this project", + "projetLie": "Related project", + "minDeLecture": "min read", + "misAJourLe": "Updated", + "sAbonner": "Subscribe (RSS)", + "fluxRss": "Notes RSS feed", + "lireAussiEn": "Lire en français", + "enFrancais": "In French", + "nonTraduit": "This note is only available in French.", + "madeByIntro": "The badge for my other sites: pick a theme, then copy the HTML code, or the SVG image where HTML isn't allowed.", + "apercu": "Preview", + "surSiteClair": "On a light site", + "surSiteSombre": "On a dark site", + "badgeThemes": { "auto": "Auto", "clair": "Light", "sombre": "Dark", "accent": "Accent" }, + "avecCafe": "with coffee", + "sansCafe": "without coffee", + "leCode": "The code", + "badgeTheme": "Badge theme", + "badgeCafe": "Buy Me a Coffee button", + "copierCode": "Copy code", + "copierSvg": "Copy SVG", + "copie": "Copied", + "madeByNote": "Auto follows the visitor's device theme: black on a light device, white on a dark one. The SVG image embeds the coffee logo, so it shows up anywhere, even where HTML is not allowed." } } diff --git a/src/data/profil.json b/src/data/profil.json index 2dce5b7..40a1cb3 100644 --- a/src/data/profil.json +++ b/src/data/profil.json @@ -23,7 +23,8 @@ "liens": { "linkedin": "https://www.linkedin.com/in/aurelienbertrand/", "github": "https://github.com/oreLINK", - "cv": "" + "cv": "", + "buymeacoffee": "" }, "aPropos": "Chez Betclic, je fais parler la donnée : modélisation, transformations dbt et SQL, produits data et tableaux de bord pour les équipes métiers, avec un coup de main de l'IA générative. À côté, je code beaucoup pour moi : un besoin du quotidien, un blocage qui m'agace ou juste une idée à tester, et ça devient un projet. J'aime tenter des choses, me planter, trouver la solution… et recommencer." } diff --git a/src/data/site.json b/src/data/site.json index 4e60c46..3168dba 100644 --- a/src/data/site.json +++ b/src/data/site.json @@ -15,16 +15,33 @@ "accentSombre": "#9DB1FF" }, + "affichage": { + "local": { + "profil": { "experience": true, "formation": true, "certifications": true, "benevolat": true }, + "projets": { "aLaUne": true, "page": true, "fiches": "auto" }, + "billets": true, + "madeBy": true + }, + "deploye": { + "profil": { "experience": true, "formation": true, "certifications": true, "benevolat": true }, + "projets": { "aLaUne": false, "page": false, "fiches": "auto" }, + "billets": false, + "madeBy": true + } + }, + "sections": [ - { "id": "experience", "titre": "Expérience", "afficher": true }, - { "id": "projets", "titre": "Projets", "afficher": false }, - { "id": "formation", "titre": "Formation", "afficher": true }, - { "id": "certifications", "titre": "Certifications", "afficher": true }, - { "id": "benevolat", "titre": "Bénévolat", "afficher": true } + { "id": "experience", "titre": "Expérience" }, + { "id": "projets", "titre": "Projets à la une" }, + { "id": "formation", "titre": "Formation" }, + { "id": "certifications", "titre": "Certifications" }, + { "id": "benevolat", "titre": "Bénévolat" } ], "hauteurSectionRepliee": 500, + "projetsALaUne": 3, + "textes": { "meContacter": "Me contacter", "aPropos": "À propos", @@ -37,18 +54,14 @@ "expireeEn": "Expirée en", "identifiant": "Identifiant", "afficherCertification": "Afficher la certification", - "commentJeLaiFait": "Comment je l'ai fait", - "commentJeLaiRealise": "Comment je l'ai réalisé", + "voirLaFiche": "Voir la fiche", + "ficheProjet": "Fiche projet", "voirLeProjet": "Voir le projet", "codeSource": "Code source", - "retour": "Retour au portfolio", "statut": "Statut", "commenceEn": "Commencé en", "role": "Rôle", "technologies": "Technologies", - "sommaire": "Sommaire", - "precedent": "Précédent", - "suivant": "Suivant", "statuts": { "en-ligne": "En ligne", "en-cours": "En cours", @@ -64,8 +77,52 @@ "chez": "chez", "competences": "Compétences", "liensExternes": "Liens externes", - "autresProjets": "Autres projets", - "changerDeLangue": "Changer de langue" + "retourAuxProjets": "Retour aux Projets", + "retourAuxBillets": "Retour aux Billets", + "changerDeLangue": "Changer de langue", + "navigation": "Navigation principale", + "navProfil": "Profil", + "navProjets": "Projets", + "navBillets": "Billets", + "filAriane": "Fil d'Ariane", + "projetsTitre": "Projets", + "projetsIntro": "Ce que je construis à côté : un besoin du quotidien, un blocage qui m'agace ou une idée à tester. Chaque projet a sa fiche, et parfois des billets qui racontent les coulisses.", + "groupesProjets": { + "en-ligne": "En ligne", + "en-cours": "En cours", + "archive": "Archivés" + }, + "tousLesProjets": "Tous les projets", + "billetsTitre": "Billets", + "billetsIntro": "Des billets tech, surtout sur la data : ce que j'apprends en construisant mes projets, et ce que les chiffres disent (ou pas) de l'actualité.", + "billet": "billet", + "billetsPluriel": "billets", + "derniersBillets": "Derniers billets", + "tousLesBillets": "Tous les billets", + "billetsSurCeProjet": "Billets sur ce projet", + "projetLie": "Projet lié", + "minDeLecture": "min de lecture", + "misAJourLe": "Mis à jour le", + "sAbonner": "S'abonner (RSS)", + "fluxRss": "Flux RSS des billets", + "lireAussiEn": "Read in English", + "enFrancais": "En français", + "nonTraduit": "Ce billet n'existe qu'en français.", + "madeByTitre": "Made by", + "madeByIntro": "Le badge à poser sur mes autres sites : choisir un thème, puis copier le code HTML, ou l'image SVG là où le HTML n'est pas accepté.", + "apercu": "Aperçu", + "surSiteClair": "Sur un site clair", + "surSiteSombre": "Sur un site sombre", + "badgeThemes": { "auto": "Auto", "clair": "Clair", "sombre": "Sombre", "accent": "Accent" }, + "avecCafe": "avec café", + "sansCafe": "sans café", + "leCode": "Le code", + "badgeTheme": "Thème du badge", + "badgeCafe": "Bouton Buy Me a Coffee", + "copierCode": "Copier le code", + "copierSvg": "Copier le SVG", + "copie": "Copié", + "madeByNote": "Auto suit le thème de l'appareil du visiteur : noir sur un appareil en clair, blanc en sombre. L'image SVG intègre le logo du café : elle s'affiche partout, même là où le HTML est refusé." }, "piedDePage": { diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 2c23ecc..9edc3e7 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -14,17 +14,32 @@ import NouvelAn from '../components/habillages/NouvelAn.astro'; import FeteMusique from '../components/habillages/FeteMusique.astro'; import { url, asset } from '../lib/paths'; import { reglages, themes } from '../lib/reglages'; +import { billets } from '../lib/billets'; import { LANGUES, cheminSansLangue, langueDe, type Langue } from '../lib/i18n'; interface Props { title?: string; description?: string; - /** Lien de retour affiché en haut à gauche (pages de projet). */ - back?: { href: string; label: string }; + /** Rubrique de la page, marquée dans la navigation du haut. */ + rubrique?: 'profil' | 'projets' | 'billets'; + /** Page non répertoriée : les moteurs de recherche ne l'indexent pas. */ + noindex?: boolean; } const langue = langueDe(Astro.url); -const { site, profil, t } = reglages(langue); -const { title = site.meta.titre, description = site.meta.description, back } = Astro.props; +const { site, profil, t, affichage } = reglages(langue); +const { title = site.meta.titre, description = site.meta.description, rubrique, noindex = false } = Astro.props; +// Navigation entre les rubriques affichées (site.json → affichage) ; « Billets » seulement s'il y en a +// de publiés. Avec une seule rubrique, pas de navigation. +const avecBillets = (await billets(langue)).length > 0; +const rubriques = [ + { id: 'profil', label: t.navProfil, href: url('', langue) }, + ...(affichage.pageProjets ? [{ id: 'projets', label: t.navProjets, href: url('projets/', langue) }] : []), + ...(avecBillets ? [{ id: 'billets', label: t.navBillets, href: url('articles/', langue) }] : []), +]; +const flux = url('articles/rss.xml', langue); +// Sur le profil, le grand logo tient lieu de logo du haut : d'une page à l'autre, l'un se change en +// l'autre (view-transition-name « logo », global.css). +const logoEnHaut = !(rubrique === 'profil' && profil.pseudo); const canonical = new URL(Astro.url.pathname, Astro.site); // La même page dans chaque langue (sélecteur de langue et liens hreflang). const chemin = cheminSansLangue(Astro.url); @@ -47,6 +62,7 @@ const accents = `--accent-light: ${site.couleurs.accentClair}; --accent-dark: ${ {title} + {noindex && } {versions.map((v) => )} @@ -59,11 +75,15 @@ const accents = `--accent-light: ${site.couleurs.accentClair}; --accent-dark: ${ + {avecBillets && } {/* Avant le premier rendu : thème choisi pendant la visite, pour éviter un flash de la mauvaise - couleur. Sans choix, le thème est celui du système (voir ui.ts). */} + couleur. Sans choix, le thème est celui du système (voir ui.ts). Arrivée par une transition + de page : le profil s'affiche sans attendre l'apparition au défilement, pour que le logo + soit visible au bout de son déplacement. */}