Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Arnaud Durand — portfolio

Site FR/EN « Papier d’atelier », étapes 5A–5B terminées, contrôle final du 14 septembre 2026. Six projets, sept événements et trois récits par langue. Destination prévue : GitHub Pages. Site : https://wesper-dev.github.io/ (français : /fr/).
Portfolio FR/EN « Papier d’atelier », publié sur GitHub Pages : https://wesper-dev.github.io/ (français : /fr/). Six projets, sept événements et trois récits par langue. Le workflow contrôle les pull requests et déploie les versions de main après un build réussi.

## Démarrer et vérifier

Expand Down Expand Up @@ -31,7 +31,7 @@ Seul **`dist/client/`** est un artefact statique public. `dist/server/` et les a

## Indexation

Le build par défaut est un aperçu : `noindex, nofollow` et `Disallow: /`. Préparer un export indexable après validation du résultat :
Le build par défaut est un aperçu : `noindex, nofollow` et `Disallow: /`. Pour construire localement le même export indexable que celui publié par le workflow :

```sh
SITE_PUBLIC=1 npm run build
Expand All @@ -42,12 +42,12 @@ Cette commande construit uniquement en local ; elle ne publie rien. Les canonica

## Ajouter ou modifier du contenu

- `content/projects.json` : six fiches bilingues, rôle, statut, dépôt, tags et éventuel récit. Les aperçus d’accueil consomment ces fiches.
- `content/projects.json` : six fiches bilingues, rôle, statut, dépôt et éventuel récit. Les tags sont stockés dans chaque langue (`fr.tags`, `en.tags`) ; conserver les noms techniques et traduire les libellés éditoriaux. Les aperçus d’accueil consomment ces fiches.
- `content/hackathons.json` : événements, date connue, contexte/contribution/résultat FR/EN. `project` mène au récit local ; `link` est une destination publique facultative et explicitement libellée. Ne pas ajouter une source privée comme lien de navigation par défaut.
- `content/stories/{en,fr}/` : récits Markdown. Sous-ensemble pris en charge : titre `#`, sections `##`, paragraphes, listes de liens HTTPS et code en ligne. Aucun HTML brut. Modifier les deux langues ensemble.
- `content/home.ts` : présentation, parcours, engagements et libellés. `content/indexes.json` : introductions des index.
- `components/` : présentation ; les quatre composants de page partagent navigation, contact et pied de page via `site-shell.tsx`.
- `lib/metadata.ts` : métadonnées par route. `public/og.jpg` : carte typographique générale ; le récit Droit de Retard utilise sa propre capture comme image de partage.
- `lib/metadata.ts` : métadonnées par route. `public/og.jpg` : carte typographique générale ; `public/images/droit-de-retard-social.jpg` est la carte 1200 × 630 du récit Droit de Retard. La capture complète reste affichée dans la page.
- `public/fonts/` : polices WOFF2 auto-hébergées, sources et licences SIL OFL.

Modèle de fiche projet : identifiant stable, nom, date réellement connue (ou null), dépôt, statut, contribution personnelle, résumé FR/EN, tags, crédits et liens publics. Garder la preuve et son niveau de vérification dans le dossier privé de travail, puis copier uniquement les faits sélectionnés. Ne pas transformer une hypothèse en fait.
Expand Down Expand Up @@ -103,6 +103,12 @@ Ajouter un objet à `content/hackathons.json` sans modifier les composants, en r
}
```

Pour les projets, partir d’une fiche existante dans `content/projects.json` : conserver `role`, `status` et `summary` dans les deux langues, créditer l’origine collective et utiliser `year: null` lorsque l’année est inconnue. Les dates ne doivent pas être déduites d’un intitulé ou inventées.
Pour les projets, partir d’une fiche existante dans `content/projects.json` : conserver `role`, `status`, `summary` et `tags` dans les deux langues, créditer l’origine collective et utiliser `year: null` lorsque l’année est inconnue. Les dates ne doivent pas être déduites d’un intitulé ou inventées.

Pour une nouvelle image : conserver l’original hors de l’export, dimensionner pour l’usage réel, choisir JPEG/WebP pour une photographie ou capture adaptée et vérifier visuellement la compression. La carte générale de partage reste en 1200 × 630 et sous 300 ko ; reporter tout changement de nom/dimensions dans `lib/metadata.ts`. Renseigner l’alternative textuelle et le crédit avant de publier.

## HTTPS et en-têtes de sécurité

HTTPS est activé sur GitHub Pages. Au relevé du 14 septembre 2026, les réponses du site ne portent pas les en-têtes HTTP `Strict-Transport-Security`, `Content-Security-Policy`, `X-Content-Type-Options`, `Referrer-Policy` ni `X-Frame-Options` ; `Access-Control-Allow-Origin: *` est émis par la plateforme. Ces en-têtes de réponse ne sont pas configurables depuis les fichiers de ce dépôt : un contrôle complet demanderait un proxy/CDN configurable ou un autre hébergement. Voir [HTTPS sur Pages](https://docs.github.com/en/pages/getting-started-with-github-pages/securing-your-github-pages-site-with-https) et la [demande de prise en charge des en-têtes](https://github.com/orgs/community/discussions/54257).

Le contenu est statique et public, sans authentification, formulaire de saisie ni script tiers ; CORS ne donne ici accès qu’aux fichiers déjà publics. Cette architecture réduit l’exposition, sans constituer une garantie générale de sécurité. Aucun changement d’hébergement n’est prévu pour ce seul motif. Certaines protections, dont une partie de CSP, peuvent aussi être définies en HTML avec leurs limites ; une [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy) devra être conçue et testée avec les scripts du site avant ajout. Cette note documente les limites observées, elle ne remplace pas la maintenance des dépendances.
2 changes: 1 addition & 1 deletion components/project-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import projects from '../content/projects.json';
import indexes from '../content/indexes.json';
import type { Locale } from '../content/home';
import { SiteShell, pagePath, Arrow } from './site-shell';
export default function ProjectIndex({locale}:{locale:Locale}) {const t=indexes[locale].projects;return <SiteShell locale={locale} path="projects/"><section className="page-intro"><p className="eyebrow">{locale==='fr'?'Carnet de réalisations':'A collection of work'}</p><h1>{t.title}<span>.</span></h1><p className="intro">{t.intro}</p></section><div className="project-catalog">{projects.map((p,i)=><article className="catalog-entry" key={p.id}><div className="catalog-heading"><span className="index">{String(i+1).padStart(2,'0')} /</span><div><p className="eyebrow">{p.tags.join(' · ')}{p.year?' · '+p.year:''}</p><h2>{p.story?<a href={pagePath(locale,'projects/'+p.story+'/')}>{p.name}</a>:p.name}</h2></div></div><div className="catalog-copy"><p>{p[locale].summary}</p><p className="contribution">{p[locale].role}</p><p className="project-status">{p[locale].status}</p><div className="link-row">{p.story&&<a className="text-link" href={pagePath(locale,'projects/'+p.story+'/')}>{t.story} <Arrow/></a>}<a className="text-link" href={p.repo}>{t.code} <Arrow/></a>{p.upstream&&<a href={p.upstream}>{locale==='fr'?'Dépôt d’origine':'Upstream repository'} <Arrow/></a>}</div></div></article>)}</div></SiteShell>}
export default function ProjectIndex({locale}:{locale:Locale}) {const t=indexes[locale].projects;return <SiteShell locale={locale} path="projects/"><section className="page-intro"><p className="eyebrow">{locale==='fr'?'Carnet de réalisations':'A collection of work'}</p><h1>{t.title}<span>.</span></h1><p className="intro">{t.intro}</p></section><div className="project-catalog">{projects.map((p,i)=><article className="catalog-entry" key={p.id}><div className="catalog-heading"><span className="index">{String(i+1).padStart(2,'0')} /</span><div><p className="eyebrow">{p[locale].tags.join(' · ')}{p.year?' · '+p.year:''}</p><h2>{p.story?<a href={pagePath(locale,'projects/'+p.story+'/')}>{p.name}</a>:p.name}</h2></div></div><div className="catalog-copy"><p>{p[locale].summary}</p><p className="contribution">{p[locale].role}</p><p className="project-status">{p[locale].status}</p><div className="link-row">{p.story&&<a className="text-link" href={pagePath(locale,'projects/'+p.story+'/')}>{t.story} <Arrow/></a>}<a className="text-link" href={p.repo}>{t.code} <Arrow/></a>{p.upstream&&<a href={p.upstream}>{locale==='fr'?'Dépôt d’origine':'Upstream repository'} <Arrow/></a>}</div></div></article>)}</div></SiteShell>}
2 changes: 1 addition & 1 deletion components/project-story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import {SiteShell,pagePath,Arrow} from './site-shell';
function inline(text:string) {return text.split(/(\[[^\]]+\]\(https?:\/\/[^)]+\)|`[^`]+`)/g).map((part,i)=>{const link=part.match(/^\[([^\]]+)\]\((https?:\/\/[^)]+)\)$/);return link?<a key={i} href={link[2]}>{link[1]}</a>:part.startsWith('`')?<code key={i}>{part.slice(1,-1)}</code>:<Fragment key={i}>{part}</Fragment>})}
export default function ProjectStory({locale,id}:{locale:Locale;id:StoryId}) {
const [heading,lead,...blocks]=stories[locale][id].trim().split(/\n\s*\n/);const p=projects.find(p=>p.id===id)!;const fr=locale==='fr';
return <SiteShell locale={locale} path={'projects/'+id+'/'}><article className="story"><header className="page-intro"><a className="back-link" href={pagePath(locale,'projects/')}>{fr?'← Tous les projets':'← All projects'}</a><p className="eyebrow">{p.tags.join(' · ')} · {p.year}</p><h1>{heading.replace(/^# /,'')}</h1><p className="intro">{lead}</p></header><div className="story-layout"><aside className="story-aside"><p className="eyebrow">{fr?'Ma contribution':'My contribution'}</p><p>{p[locale].role}</p><p className="project-status">{p[locale].status}</p><a className="text-link" href={p.repo}>GitHub <Arrow/></a></aside><div className="prose">{blocks.map((block,i)=>block.startsWith('## ')?<h2 key={i}>{block.slice(3)}</h2>:block.startsWith('- ')?<ul key={i}>{block.split('\n').map((line,j)=><li key={j}>{inline(line.slice(2))}</li>)}</ul>:<p key={i}>{inline(block.replace(/\n/g,' '))}</p>)}</div></div>{id==='droit-de-retard'&&<figure className="story-figure"><a href="/images/droit-de-retard.png"><img src="/images/droit-de-retard.png" width="1440" height="1180" loading="lazy" alt={fr?'Interface de démonstration Droit de Retard : document de vol et trace de l’analyse.':'Droit de Retard demo interface: flight document and analysis trace.'}/></a><figcaption>{fr?'Interface de démonstration du dépôt Droit de Retard.':'Demo interface from the Droit de Retard repository.'} <a href="/images/droit-de-retard.png">{fr?'Ouvrir l’image en taille réelle':'Open full-size image'} ↗</a> · <a href="https://github.com/Wesper-Dev/droit-de-retard/blob/main/docs/images/interface.png">{fr?'Source et crédit':'Source and credit'}</a></figcaption></figure>}<nav className="story-next" aria-label={fr?'Autres récits':'More stories'}>{projects.filter(p=>p.story&&p.id!==id).map(p=><a key={p.id} href={pagePath(locale,'projects/'+p.id+'/')}>{p.name} <Arrow/></a>)}</nav></article></SiteShell>;
return <SiteShell locale={locale} path={'projects/'+id+'/'}><article className="story"><header className="page-intro"><a className="back-link" href={pagePath(locale,'projects/')}>{fr?'← Tous les projets':'← All projects'}</a><p className="eyebrow">{p[locale].tags.join(' · ')} · {p.year}</p><h1>{heading.replace(/^# /,'')}</h1><p className="intro">{lead}</p></header><div className="story-layout"><aside className="story-aside"><p className="eyebrow">{fr?'Ma contribution':'My contribution'}</p><p>{p[locale].role}</p><p className="project-status">{p[locale].status}</p><a className="text-link" href={p.repo}>GitHub <Arrow/></a></aside><div className="prose">{blocks.map((block,i)=>block.startsWith('## ')?<h2 key={i}>{block.slice(3)}</h2>:block.startsWith('- ')?<ul key={i}>{block.split('\n').map((line,j)=><li key={j}>{inline(line.slice(2))}</li>)}</ul>:<p key={i}>{inline(block.replace(/\n/g,' '))}</p>)}</div></div>{id==='droit-de-retard'&&<figure className="story-figure"><a href="/images/droit-de-retard.png"><img src="/images/droit-de-retard.png" width="1440" height="1180" loading="lazy" alt={fr?'Interface de démonstration Droit de Retard : document de vol et trace de l’analyse.':'Droit de Retard demo interface: flight document and analysis trace.'}/></a><figcaption>{fr?'Interface de démonstration du dépôt Droit de Retard.':'Demo interface from the Droit de Retard repository.'} <a href="/images/droit-de-retard.png">{fr?'Ouvrir l’image en taille réelle':'Open full-size image'} ↗</a> · <a href="https://github.com/Wesper-Dev/droit-de-retard/blob/main/docs/images/interface.png">{fr?'Source et crédit':'Source and credit'}</a></figcaption></figure>}<nav className="story-next" aria-label={fr?'Autres récits':'More stories'}>{projects.filter(p=>p.story&&p.id!==id).map(p=><a key={p.id} href={pagePath(locale,'projects/'+p.id+'/')}>{p.name} <Arrow/></a>)}</nav></article></SiteShell>;
}
4 changes: 2 additions & 2 deletions content/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const home = {
role: 'AI Platform / Applied AI Engineer', availability: 'Looking for a permanent role after 16 October 2026',
selected: 'A few things I’ve built', selectedNote: 'Ideas explored in teams, then taken a little further.',
code: 'Explore the repository', screenshot: 'Droit de Retard · demo interface from the public repository',
projects: projects.filter(p=>p.story).map(p=>({name:p.name,category:p.tags.join(' · '),text:p.en.summary,role:p.en.role,url:p.repo,id:p.id})),
projects: projects.filter(p=>p.story).map(p=>({name:p.name,category:p.en.tags.join(' · '),text:p.en.summary,role:p.en.role,url:p.repo,id:p.id})),
foundation:'And underneath, a systems foundation.',
foundationText:'Projects from École 42 Paris: processes, concurrency and the small details that make software work.',
systems:[{name:'Minishell',text:'A shell in C · pipes, redirections & processes'},{name:'Inception',text:'Docker infrastructure · NGINX, WordPress & MariaDB'},{name:'Philosophers',text:'Concurrency in C · threads & mutexes'}],
Expand All @@ -27,7 +27,7 @@ export const home = {
role:'AI Platform / Applied AI Engineer', availability:'À la recherche d’un CDI après le 16 octobre 2026',
selected:'Quelques projets en chemin',selectedNote:'Des idées explorées en équipe, puis prolongées.',
code:'Explorer le dépôt', screenshot:'Droit de Retard · interface de démonstration du dépôt public',
projects: projects.filter(p=>p.story).map(p=>({name:p.name,category:p.tags.join(' · '),text:p.fr.summary,role:p.fr.role,url:p.repo,id:p.id})),
projects: projects.filter(p=>p.story).map(p=>({name:p.name,category:p.fr.tags.join(' · '),text:p.fr.summary,role:p.fr.role,url:p.repo,id:p.id})),
foundation:'Et sous le capot, un socle systèmes.',foundationText:'Des projets d’École 42 Paris : les processus, la concurrence et les détails qui font fonctionner un logiciel.',
systems:[{name:'Minishell',text:'Un shell en C · pipes, redirections et processus'},{name:'Inception',text:'Infrastructure Docker · NGINX, WordPress et MariaDB'},{name:'Philosophers',text:'Concurrence en C · threads et mutex'}],
hackTitle:'Des rencontres. Des problèmes nouveaux.',hackIntro:'Les hackathons me permettent de rencontrer des gens et d’explorer des terrains inconnus. Certaines idées deviennent des projets au long cours ; d’autres restent des expériences.',hackMore:'Quelques explorations en équipe',
Expand Down
Loading