Compare commits

..

9 commits

Author SHA1 Message Date
76bd2f9fb3 Intro enrichie et polices optimisées en WOFF2
Reprend le texte de /a-propos dans l'intro des 3 hubs /code : stack,
rapport au logiciel libre, biais dans le code, enseignement. Convertit
les polices Raleway en WOFF2 (52 Ko vs 180 Ko en TTF), ne charge que
le SemiBold utilisé par le h1.
2026-02-23 01:25:51 +01:00
0ac8a33c4c Style épuré pour /code : Raleway, espacements, cards simplifiées
Ajoute la police Raleway (charte Tiqa) pour le h1. Supprime la
section compétences, simplifie les cards d'expérience en séparateurs,
retire les encadrés glassmorphism des sections Valeurs et Communauté,
allège les liens en ligne. Plus d'espacement vertical entre sections.
2026-02-23 01:15:59 +01:00
a7362730ba Sitemap hreflang corrigé pour URLs traduites et routes dynamiques
Remplace l'option i18n du plugin sitemap (qui supposait des chemins identiques entre langues) par un serialize custom basé sur getAlternateUrls(). Ajoute le support des routes dynamiques (albums photo, blog posts) via des patterns regex, et l'entrée statique /photo/blog. Toutes les pages traduites ont désormais des hreflang fr/en/ar + x-default corrects dans le sitemap et le HTML.
2026-02-23 01:04:02 +01:00
758b48521e Nettoyage projets : suppression DNS.Surf, Email.ML, GoBuild
Seuls Débats et DisMoi sont featured sur les hubs /code. Supprime
la vignette "Featured" du ProjectCard. Le hub filtre désormais sur
featured au lieu de prendre les 3 premiers.
2026-02-23 00:47:09 +01:00
6fb2f8e4d3 Fusion intro et titre pro pour les pages hub /code
Fusionne la double intro (sous-titre + bloc encadré) en un seul
paragraphe. Remplace le titre "Code" par "Artisan du logiciel" (FR),
"Software Craftsman" (EN), "حِرَفيّ البرمجيات" (AR).
2026-02-23 00:40:52 +01:00
89ab849050 Liens soulignés sur fond sombre pour meilleure lisibilité
Remplace text-indigo-600 par text-purple-200 avec soulignement
discret, cohérent avec le thème violet du site.
2026-02-23 00:33:00 +01:00
d21bf6f9c0 Recommandations featured sélectionnées pour le hub /code
Ajout du champ featured au schéma des recommandations. Les pages hub
affichent uniquement les recommandations marquées featured au lieu
des 3 plus récentes.
2026-02-23 00:19:56 +01:00
f4b71d387f Layout masonry avec ordre de lecture naturel pour les recommandations
Remplace CSS columns (ordre vertical) par deux colonnes flex avec
distribution en zigzag : les plus récentes en haut, lecture
gauche-droite ligne par ligne, hauteur naturelle des cartes préservée.
2026-02-23 00:14:18 +01:00
53c9f5ffb4 mise à jour des avatars de recommandations 2026-02-23 00:08:12 +01:00
36 changed files with 290 additions and 290 deletions

View file

@ -2,6 +2,7 @@ import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind"; import tailwind from "@astrojs/tailwind";
import { getAlternateUrls } from "./src/utils/page-translations.ts";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@ -10,13 +11,21 @@ export default defineConfig({
integrations: [ integrations: [
tailwind(), tailwind(),
sitemap({ sitemap({
i18n: { serialize(item) {
defaultLocale: "fr", const url = new URL(item.url);
locales: { const pathname = decodeURIComponent(url.pathname).replace(/\/$/, "") || "/";
fr: "fr", const alternates = getAlternateUrls(pathname);
en: "en", if (alternates) {
ar: "ar", const fullUrl = (path) =>
}, path === "/" ? `${url.origin}/` : `${url.origin}${path}/`;
item.links = [
{ lang: "fr", url: fullUrl(alternates.fr) },
{ lang: "en", url: fullUrl(alternates.en) },
{ lang: "ar", url: fullUrl(alternates.ar) },
{ lang: "x-default", url: fullUrl(alternates.fr) },
];
}
return item;
}, },
}), }),
], ],

View file

@ -2,7 +2,13 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Add Your Custom CSS Here */ @font-face {
font-family: 'Raleway';
src: url('../fonts/Raleway-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
.prose img { .prose img {
border-radius: 30px; border-radius: 30px;

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -7,7 +7,7 @@ interface Props {
const { href, external = false, class: className = '' } = Astro.props; const { href, external = false, class: className = '' } = Astro.props;
const baseClasses = 'text-indigo-600 dark:text-indigo-400 hover:underline'; const baseClasses = 'text-purple-200 underline decoration-purple-200/40 hover:decoration-white hover:text-white transition-colors';
const classes = `${baseClasses} ${className}`.trim(); const classes = `${baseClasses} ${className}`.trim();
const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer' } : {}; const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer' } : {};

View file

@ -17,15 +17,10 @@ const { title, description, technologies, url, github, featured = false } = Astr
? "bg-white/[0.12] border-purple-300/20 hover:bg-white/[0.18] hover:border-purple-300/30" ? "bg-white/[0.12] border-purple-300/20 hover:bg-white/[0.18] hover:border-purple-300/30"
: "bg-white/[0.06] border-white/[0.1] hover:bg-white/[0.12] hover:border-white/[0.2]", : "bg-white/[0.06] border-white/[0.1] hover:bg-white/[0.12] hover:border-white/[0.2]",
]}> ]}>
<div class="flex items-start justify-between mb-2"> <div class="mb-2">
<h3 class="text-lg font-bold text-white"> <h3 class="text-lg font-bold text-white">
{title} {title}
</h3> </h3>
{featured && (
<span class="text-[10px] font-semibold px-2 py-0.5 rounded-full bg-purple-400/20 text-purple-200 border border-purple-300/20">
Featured
</span>
)}
</div> </div>
<p class="text-sm text-white/60 leading-relaxed flex-1 mb-4"> <p class="text-sm text-white/60 leading-relaxed flex-1 mb-4">

View file

@ -79,6 +79,7 @@ const recommendationsCollection = defineCollection({
avatar: z.string().optional(), avatar: z.string().optional(),
url: z.string().url().optional(), url: z.string().url().optional(),
date: z.date(), date: z.date(),
featured: z.boolean().default(false),
relationship: z.string().optional(), relationship: z.string().optional(),
lang: z.enum(['fr', 'en']).default('fr'), lang: z.enum(['fr', 'en']).default('fr'),
}), }),

View file

@ -1,11 +0,0 @@
---
title: "DNS.Surf"
description: "أداة استعلام DNS عالمية. استعلام خوادم DNS من مناطق مختلفة."
date: 2023-01-01
category: "dev"
technologies: ["JavaScript"]
url: "https://dns.surf"
lang: "ar"
---
أداة عبر الإنترنت للتحقق من استعلام DNS لنطاق من مناطق مختلفة حول العالم. مفيدة لتشخيص مشاكل انتشار DNS.

View file

@ -1,11 +0,0 @@
---
title: "DNS.Surf"
description: "Worldwide DNS resolution tool. Query DNS servers from different regions."
date: 2023-01-01
category: "dev"
technologies: ["JavaScript"]
url: "https://dns.surf"
lang: "en"
---
Online tool to check DNS resolution of a domain from different regions worldwide. Useful for diagnosing DNS propagation issues.

View file

@ -1,11 +0,0 @@
---
title: "DNS.Surf"
description: "Outil de résolution DNS mondiale. Interroge les serveurs DNS de différentes régions."
date: 2023-01-01
category: "dev"
technologies: ["JavaScript"]
url: "https://dns.surf"
lang: "fr"
---
Outil en ligne permettant de vérifier la résolution DNS d'un domaine depuis différentes régions du monde. Utile pour diagnostiquer les problèmes de propagation DNS.

View file

@ -1,11 +0,0 @@
---
title: "Email.ML"
description: "خدمة بريد إلكتروني مؤقت بتصميم بسيط."
date: 2023-01-01
category: "dev"
technologies: ["JavaScript"]
url: "https://email.ml"
lang: "ar"
---
خدمة بريد إلكتروني مؤقت بتصميم أنيق. استقبال رسائل على عنوان يُمكن التخلّص منه، بدون تسجيل.

View file

@ -1,11 +0,0 @@
---
title: "Email.ML"
description: "Minimalist temporary email service."
date: 2023-01-01
category: "dev"
technologies: ["JavaScript"]
url: "https://email.ml"
lang: "en"
---
Temporary email service with a clean design. Receive emails on a disposable address, no signup required.

View file

@ -1,11 +0,0 @@
---
title: "Email.ML"
description: "Service d'email temporaire minimaliste."
date: 2023-01-01
category: "dev"
technologies: ["JavaScript"]
url: "https://email.ml"
lang: "fr"
---
Service d'email temporaire au design épuré. Permet de recevoir des emails sur une adresse jetable, sans inscription.

View file

@ -1,11 +0,0 @@
---
title: "GoBuild"
description: "SaaS لنمذجة المباني للمهنيين في قطاع البناء."
date: 2020-06-01
category: "dev"
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
url: "https://www.gobuild.fr"
lang: "ar"
---
تطبيق SaaS يتيح لمهنيي البناء نمذجة وتقدير مشاريعهم. طُوّر بصفتي مديرًا تقنيًا من 2020 إلى 2022.

View file

@ -1,11 +0,0 @@
---
title: "GoBuild"
description: "SaaS building modeling platform for construction professionals."
date: 2020-06-01
category: "dev"
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
url: "https://www.gobuild.fr"
lang: "en"
---
SaaS application enabling construction professionals to model and estimate their building projects. Developed as CTO from 2020 to 2022.

View file

@ -1,11 +0,0 @@
---
title: "GoBuild"
description: "SaaS de modélisation de bâtiments pour les professionnels du BTP."
date: 2020-06-01
category: "dev"
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
url: "https://www.gobuild.fr"
lang: "fr"
---
Application SaaS permettant aux professionnels du bâtiment de modéliser et chiffrer leurs projets de construction. Développé en tant que CTO de 2020 à 2022.

View file

@ -6,7 +6,7 @@ category: "dev"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"] technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"]
url: "https://mon-entreprise.urssaf.fr/" url: "https://mon-entreprise.urssaf.fr/"
github: "https://github.com/betagouv/mon-entreprise" github: "https://github.com/betagouv/mon-entreprise"
featured: true featured: false
lang: "ar" lang: "ar"
--- ---

View file

@ -6,7 +6,7 @@ category: "dev"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"] technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"]
url: "https://mon-entreprise.urssaf.fr/" url: "https://mon-entreprise.urssaf.fr/"
github: "https://github.com/betagouv/mon-entreprise" github: "https://github.com/betagouv/mon-entreprise"
featured: true featured: false
lang: "en" lang: "en"
--- ---

View file

@ -6,7 +6,7 @@ category: "dev"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"] technologies: ["TypeScript", "React.js", "Publicodes", "Node.js"]
url: "https://mon-entreprise.urssaf.fr/" url: "https://mon-entreprise.urssaf.fr/"
github: "https://github.com/betagouv/mon-entreprise" github: "https://github.com/betagouv/mon-entreprise"
featured: true featured: false
lang: "fr" lang: "fr"
--- ---

View file

@ -2,6 +2,7 @@
author: "Daniel Gall" author: "Daniel Gall"
authorRole: "Consultant" authorRole: "Consultant"
company: "Taos Conseil" company: "Taos Conseil"
avatar: daniel-gall.jpg
url: https://www.linkedin.com/in/daniel-g-385524 url: https://www.linkedin.com/in/daniel-g-385524
date: 2011-12-07 date: 2011-12-07
lang: "fr" lang: "fr"

View file

@ -2,8 +2,10 @@
author: "Grégoire Lacoste" author: "Grégoire Lacoste"
authorRole: "Chief Product Officer" authorRole: "Chief Product Officer"
company: "CertifiCall" company: "CertifiCall"
avatar: gregoire-lacoste.jpg
url: https://www.linkedin.com/in/gregoirelacoste url: https://www.linkedin.com/in/gregoirelacoste
date: 2020-12-08 date: 2020-12-08
featured: true
lang: "fr" lang: "fr"
--- ---
J'ai eu la chance de travailler avec Jalil sur plusieurs projets d'applications react/node ou php, son expérience, sa vision claire et sa pédagogie a toute épreuve en font un partenaire incontournable pour un projet réussi J'ai eu la chance de travailler avec Jalil sur plusieurs projets d'applications react/node ou php, son expérience, sa vision claire et sa pédagogie a toute épreuve en font un partenaire incontournable pour un projet réussi

View file

@ -1,7 +1,8 @@
--- ---
author: "Guillaume Gendrillon" author: "Guillaume Gendrillon"
authorRole: "Lead designer Information Voyageur et signalétique" authorRole: "Lead designer Information Voyageur et signalétique"
company: "RATPgroup" company: "RATP"
avatar: guillaume-gendrillon.webp
url: https://www.linkedin.com/in/guillaumegendrillon url: https://www.linkedin.com/in/guillaumegendrillon
date: 2011-12-05 date: 2011-12-05
lang: "fr" lang: "fr"

View file

@ -5,6 +5,7 @@ company: "DisMoi SAS"
avatar: john-samson.png avatar: john-samson.png
url: https://www.malt.fr/profile/jalilarfaoui url: https://www.malt.fr/profile/jalilarfaoui
date: 2022-06-17 date: 2022-06-17
featured: true
lang: "fr" lang: "fr"
--- ---
Jalil a su trouver la bonne architecture à notre projet à 5 pattes, il est réactif en cas d'urgence, et s'engage au-delà de son rôle de développeur, très apprécié par les autres membres de l'équipe. Jalil a su trouver la bonne architecture à notre projet à 5 pattes, il est réactif en cas d'urgence, et s'engage au-delà de son rôle de développeur, très apprécié par les autres membres de l'équipe.

View file

@ -5,6 +5,7 @@ company: "SNCF Connect & Tech"
avatar: maxime-boudier.jpg avatar: maxime-boudier.jpg
url: https://www.linkedin.com/in/maximeboudier url: https://www.linkedin.com/in/maximeboudier
date: 2020-12-12 date: 2020-12-12
featured: true
lang: "fr" lang: "fr"
--- ---
Une des personnes avec qui j'ai préféré travailler. En plus d'être passionné, très bon techniquement et j'en passe.. Jalil est une personne qu'on apprécie pour ses qualités humaines. J'ai beaucoup appris de toi Jalil, sur plusieurs plans, j'espère que nos chemin se re-croiseront. Une des personnes avec qui j'ai préféré travailler. En plus d'être passionné, très bon techniquement et j'en passe.. Jalil est une personne qu'on apprécie pour ses qualités humaines. J'ai beaucoup appris de toi Jalil, sur plusieurs plans, j'espère que nos chemin se re-croiseront.

View file

@ -2,6 +2,7 @@
author: "Olivier Cornudet" author: "Olivier Cornudet"
authorRole: "Consultant manager" authorRole: "Consultant manager"
company: "e-THEMIS" company: "e-THEMIS"
avatar: olivier-cornudet.jpg
url: https://www.linkedin.com/in/olivier-cornudet-9a398738 url: https://www.linkedin.com/in/olivier-cornudet-9a398738
date: 2015-02-11 date: 2015-02-11
lang: "fr" lang: "fr"

View file

@ -4,6 +4,7 @@ authorRole: "Expert Vue.js | Nuxt"
avatar: thomas-kientz.jpg avatar: thomas-kientz.jpg
url: https://www.malt.fr/profile/jalilarfaoui url: https://www.malt.fr/profile/jalilarfaoui
date: 2022-06-17 date: 2022-06-17
featured: true
lang: "fr" lang: "fr"
--- ---
Jalil est un développeur et mentor hors pair. Le développement logiciel est pour lui un art dont il adore partager sa passion. Je consulte Jalil régulièrement pour avoir son regard expérimenté tant le choix d'une nouvelle techno que pour des reviews de code. C'est un véritable atout à avoir dans son équipe, je le recommande fortement. Jalil est un développeur et mentor hors pair. Le développement logiciel est pour lui un art dont il adore partager sa passion. Je consulte Jalil régulièrement pour avoir son regard expérimenté tant le choix d'une nouvelle techno que pour des reviews de code. C'est un véritable atout à avoir dans son équipe, je le recommande fortement.

View file

@ -2,6 +2,7 @@
author: "Vadim Toropoff" author: "Vadim Toropoff"
authorRole: "Dirigeant" authorRole: "Dirigeant"
company: "Event Finder" company: "Event Finder"
avatar: vadim-toropoff.jpg
url: https://www.malt.fr/profile/jalilarfaoui url: https://www.malt.fr/profile/jalilarfaoui
date: 2015-10-08 date: 2015-10-08
lang: "fr" lang: "fr"

View file

@ -18,16 +18,12 @@ const experiences = (await getCollection("experiences"))
const recentExperiences = experiences.slice(0, 4); const recentExperiences = experiences.slice(0, 4);
const projects = (await getCollection("projects")) const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev") .filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => { .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
return b.data.date.getTime() - a.data.date.getTime();
})
.slice(0, 3);
const recommendations = (await getCollection("recommendations")) const recommendations = (await getCollection("recommendations"))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()) .filter((r) => r.data.featured)
.slice(0, 3); .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const recommendationTexts = recommendations.map((rec) => ({ const recommendationTexts = recommendations.map((rec) => ({
...rec, ...rec,
@ -50,16 +46,18 @@ function formatMonth(dateStr: string) {
> >
<section dir="rtl" lang="ar" class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0"> <section dir="rtl" lang="ar" class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0">
<div class="mb-10"> <div class="mb-10">
<h1 class="text-4xl sm:text-5xl font-bold text-white">برمجة</h1> <h1 class="text-4xl sm:text-5xl font-semibold text-white font-display tracking-wide">حِرَفيّ البرمجيات</h1>
<p class="mt-4 text-lg text-white/65 leading-relaxed max-w-2xl"> <div class="mt-6 space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
أكثر من 20 سنة في بناء البرمجيات. Craftsmanship، TDD، DDD — وهاجس التحيّزات التي نضعها في الكود دون أن ندري. <p>
TDD، Clean Code، Domain-Driven Design: هذه طريقتي في بناء البرمجيات. أرافق الفرق كمطوّر أول، أو قائد تقني، أو مدرب تقني. أدواتي: TypeScript/JavaScript، وكذلك PHP وElixir.
</p>
<p>
ما يميّزني ربّما: أهتمّ بجودة الكود بقدر اهتمامي بما ينتجه. أفضّل البرمجيات الحرّة والأدوات التي تلبّي احتياجات حقيقية. أتساءل أيضًا عن التحيّزات التي ندرجها في الكود، والتي تُديم علاقات اجتماعية تستحقّ المساءلة.
</p>
<p>
أُدرّس البرمجة في <Link href="https://www.univ-jfc.fr/" external>جامعة شامبوليون</Link> وأنشّط مجتمع <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters Albi</Link> منذ 2018.
</p> </p>
</div> </div>
<div class="facet-card rounded-2xl bg-white/[0.06] border border-white/[0.1] p-6 mb-10">
<p class="text-white/70 leading-relaxed">
مطوّر مستقل مقيم في <strong class="text-white">ألبي، فرنسا</strong>، أرافق الفرق كمطوّر أول، أو قائد تقني، أو مدرب تقني. أفضّل البرمجيات الحرّة والأدوات التي تلبي احتياجات حقيقية.
</p>
</div> </div>
<div class="mb-12"> <div class="mb-12">

View file

@ -5,6 +5,9 @@ import RecommendationCard from "../../../components/code/RecommendationCard.astr
const recommendations = (await getCollection("recommendations")) const recommendations = (await getCollection("recommendations"))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const leftCol = recommendations.filter((_, i) => i % 2 === 0);
const rightCol = recommendations.filter((_, i) => i % 2 === 1);
--- ---
<Layout <Layout
@ -24,9 +27,10 @@ const recommendations = (await getCollection("recommendations"))
<p class="mt-3 text-white/60 text-lg">ما يقوله الأشخاص الذين عملت معهم.</p> <p class="mt-3 text-white/60 text-lg">ما يقوله الأشخاص الذين عملت معهم.</p>
</div> </div>
<div class="columns-1 sm:columns-2 gap-5 space-y-5"> <div class="hidden sm:grid grid-cols-2 gap-5 items-start">
{recommendations.map((rec) => ( {[leftCol, rightCol].map((col) => (
<div class="break-inside-avoid"> <div class="space-y-5">
{col.map((rec) => (
<RecommendationCard <RecommendationCard
author={rec.data.author} author={rec.data.author}
authorRole={rec.data.authorRole} authorRole={rec.data.authorRole}
@ -37,8 +41,23 @@ const recommendations = (await getCollection("recommendations"))
url={rec.data.url} url={rec.data.url}
lang={rec.data.lang} lang={rec.data.lang}
/> />
))}
</div> </div>
))} ))}
</div> </div>
<div class="sm:hidden space-y-5">
{recommendations.map((rec) => (
<RecommendationCard
author={rec.data.author}
authorRole={rec.data.authorRole}
company={rec.data.company}
text={rec.body || ''}
date={rec.data.date}
avatar={rec.data.avatar}
url={rec.data.url}
lang={rec.data.lang}
/>
))}
</div>
</section> </section>
</Layout> </Layout>

View file

@ -5,9 +5,7 @@ import Layout from "../../layouts/main.astro";
import Link from "../../components/Link.astro"; import Link from "../../components/Link.astro";
import FeaturedRecommendation from "../../components/code/FeaturedRecommendation.astro"; import FeaturedRecommendation from "../../components/code/FeaturedRecommendation.astro";
import ProjectCard from "../../components/code/ProjectCard.astro"; import ProjectCard from "../../components/code/ProjectCard.astro";
import SkillBadge from "../../components/code/SkillBadge.astro";
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png"; import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
import skillsData from "../../data/skills.json";
const locale = "fr"; const locale = "fr";
@ -18,24 +16,18 @@ const experiences = (await getCollection("experiences"))
const recentExperiences = experiences.slice(0, 4); const recentExperiences = experiences.slice(0, 4);
const projects = (await getCollection("projects")) const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev") .filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => { .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
return b.data.date.getTime() - a.data.date.getTime();
})
.slice(0, 3);
const recommendations = (await getCollection("recommendations")) const recommendations = (await getCollection("recommendations"))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()) .filter((r) => r.data.featured)
.slice(0, 3); .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const recommendationTexts = recommendations.map((rec) => ({ const recommendationTexts = recommendations.map((rec) => ({
...rec, ...rec,
text: rec.body || '', text: rec.body || '',
})); }));
const topSkills = skillsData.categories.slice(0, 3);
function formatMonth(dateStr: string) { function formatMonth(dateStr: string) {
const [year, month] = dateStr.split('-'); const [year, month] = dateStr.split('-');
return new Date(parseInt(year), parseInt(month) - 1) return new Date(parseInt(year), parseInt(month) - 1)
@ -48,43 +40,45 @@ function formatMonth(dateStr: string) {
facet="code" facet="code"
description="Parcours professionnel de Jalil Arfaoui : développeur freelance spécialisé en Software Craftsmanship, TDD, DDD. TypeScript, PHP, Elixir." description="Parcours professionnel de Jalil Arfaoui : développeur freelance spécialisé en Software Craftsmanship, TDD, DDD. TypeScript, PHP, Elixir."
> >
<section class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-3xl mx-auto my-16 px-7 lg:px-0">
<div class="mb-10"> <div class="mb-16">
<h1 class="text-4xl sm:text-5xl font-bold text-white">Code</h1> <h1 class="text-4xl sm:text-5xl font-semibold text-white font-display tracking-wide">Artisan du logiciel</h1>
<p class="mt-4 text-lg text-white/65 leading-relaxed max-w-2xl"> <div class="mt-6 space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
Plus de 20 ans à construire du logiciel. Craftsmanship, TDD, DDD — et une obsession pour les biais qu'on met dans le code sans le savoir. <p>
TDD, Clean Code, Domain-Driven Design : c'est ma façon de construire du logiciel. J'accompagne les équipes comme développeur senior, tech lead ou coach technique. Ma stack : TypeScript/JavaScript, mais aussi PHP et Elixir.
</p>
<p>
Ce qui me distingue peut-être : je m'intéresse autant à la qualité du code qu'à ce qu'il produit. Je privilégie le logiciel libre et les outils qui répondent à de vrais besoins. Je m'interroge aussi sur les biais que nous inscrivons dans le code, et qui perpétuent des rapports sociaux qu'on doit questionner.
</p>
<p>
J'enseigne la programmation à <Link href="https://www.univ-jfc.fr/" external>l'université Champollion</Link> et j'anime les <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters d'Albi</Link> depuis 2018.
</p> </p>
</div> </div>
<div class="facet-card rounded-2xl bg-white/[0.06] border border-white/[0.1] p-6 mb-10">
<p class="text-white/70 leading-relaxed">
Développeur freelance basé à <strong class="text-white">Albi</strong>, j'accompagne les équipes comme développeur senior, tech lead ou coach technique. Je privilégie le logiciel libre et les outils qui répondent à de vrais besoins.
</p>
</div> </div>
<div class="mb-12"> <div class="mb-16">
<div class="flex items-center justify-between mb-5"> <div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white ">Parcours</h2> <h2 class="text-2xl font-bold text-white ">Parcours</h2>
<a href="/code/parcours" class="text-sm text-purple-200 hover:text-white transition-colors">Voir tout &rarr;</a> <a href="/code/parcours" class="text-sm text-purple-200 hover:text-white transition-colors">Voir tout &rarr;</a>
</div> </div>
<div class="space-y-3"> <div class="divide-y divide-white/[0.08]">
{recentExperiences.map((exp) => { {recentExperiences.map((exp) => {
const isOngoing = !exp.data.endDate; const isOngoing = !exp.data.endDate;
const start = formatMonth(exp.data.startDate); const start = formatMonth(exp.data.startDate);
const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent'; const end = exp.data.endDate ? formatMonth(exp.data.endDate) : 'Présent';
return ( return (
<div class="facet-card rounded-xl bg-white/[0.06] border border-white/[0.1] p-4 hover:bg-white/[0.1] transition-colors"> <div class="py-4 first:pt-0 last:pb-0">
<div class="flex items-start justify-between gap-4"> <div class="flex items-start justify-between gap-4">
<div class="min-w-0"> <div class="min-w-0">
<p class="font-semibold text-white text-sm truncate">{exp.data.role}</p> <p class="font-semibold text-white text-sm">{exp.data.role}</p>
<p class="text-xs text-white/50 mt-0.5"> <p class="text-sm text-white/45 mt-0.5">
{exp.data.companyUrl ? ( {exp.data.companyUrl ? (
<a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="text-purple-200 hover:text-white transition-colors">{exp.data.company}</a> <a href={exp.data.companyUrl} target="_blank" rel="noopener noreferrer" class="text-purple-200 hover:text-white transition-colors">{exp.data.company}</a>
) : exp.data.company} ) : exp.data.company}
{exp.data.location && ` · ${exp.data.location}`} {exp.data.location && ` · ${exp.data.location}`}
</p> </p>
</div> </div>
<span class:list={["text-xs whitespace-nowrap flex-shrink-0", isOngoing ? "text-purple-200 font-semibold" : "text-white/40"]}> <span class:list={["text-sm whitespace-nowrap flex-shrink-0", isOngoing ? "text-purple-200" : "text-white/35"]}>
{start} — {end} {start} — {end}
</span> </span>
</div> </div>
@ -94,12 +88,12 @@ function formatMonth(dateStr: string) {
</div> </div>
</div> </div>
<div class="mb-12"> <div class="mb-16">
<div class="flex items-center justify-between mb-5"> <div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white ">Projets</h2> <h2 class="text-2xl font-bold text-white ">Projets</h2>
<a href="/code/projets" class="text-sm text-purple-200 hover:text-white transition-colors">Voir tous &rarr;</a> <a href="/code/projets" class="text-sm text-purple-200 hover:text-white transition-colors">Voir tous &rarr;</a>
</div> </div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
{projects.map((project) => ( {projects.map((project) => (
<ProjectCard <ProjectCard
title={project.data.title} title={project.data.title}
@ -114,12 +108,12 @@ function formatMonth(dateStr: string) {
</div> </div>
{recommendationTexts.length > 0 && ( {recommendationTexts.length > 0 && (
<div class="mb-12"> <div class="mb-16">
<div class="flex items-center justify-between mb-5"> <div class="flex items-center justify-between mb-6">
<h2 class="text-2xl font-bold text-white ">Recommandations</h2> <h2 class="text-2xl font-bold text-white ">Recommandations</h2>
<a href="/code/recommandations" class="text-sm text-purple-200 hover:text-white transition-colors">Voir toutes &rarr;</a> <a href="/code/recommandations" class="text-sm text-purple-200 hover:text-white transition-colors">Voir toutes &rarr;</a>
</div> </div>
<div class="space-y-4"> <div class="space-y-5">
{recommendationTexts.map((rec) => ( {recommendationTexts.map((rec) => (
<FeaturedRecommendation <FeaturedRecommendation
author={rec.data.author} author={rec.data.author}
@ -134,60 +128,42 @@ function formatMonth(dateStr: string) {
</div> </div>
)} )}
<div class="mb-12"> <div class="mb-16">
<div class="flex items-center justify-between mb-5"> <h2 class="text-2xl font-bold text-white mb-6">Valeurs & Approche</h2>
<h2 class="text-2xl font-bold text-white">Compétences</h2> <ul class="space-y-3 text-white/60">
<a href="/code/competences" class="text-sm text-purple-200 hover:text-white transition-colors">Voir toutes &rarr;</a>
</div>
<div class="space-y-4">
{topSkills.map((category) => (
<div>
<p class="text-xs font-semibold text-white/40 uppercase tracking-wider mb-2">{category.name[locale as keyof typeof category.name]}</p>
<div class="flex flex-wrap gap-1.5">
{category.skills.map((skill) => (
<SkillBadge name={skill} />
))}
</div>
</div>
))}
</div>
</div>
<h2 class="text-2xl font-bold text-white mb-5">Valeurs & Approche</h2>
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-6 mb-10">
<ul class="space-y-3 text-white/70">
<li class="flex items-start gap-3"> <li class="flex items-start gap-3">
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span> <span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
<span>Le mouvement <Link href="http://manifesto.softwarecraftsmanship.org/#/fr-fr" external>Software Craftsmanship</Link></span> <span>Le mouvement <Link href="http://manifesto.softwarecraftsmanship.org/#/fr-fr" external>Software Craftsmanship</Link></span>
</li> </li>
<li class="flex items-start gap-3"> <li class="flex items-start gap-3">
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span> <span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
<span>L'utilité sociale du développeur</span> <span>L'utilité sociale du développeur</span>
</li> </li>
<li class="flex items-start gap-3"> <li class="flex items-start gap-3">
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span> <span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
<span>Être fier de son travail, mais sans égo</span> <span>Être fier de son travail, mais sans égo</span>
</li> </li>
<li class="flex items-start gap-3"> <li class="flex items-start gap-3">
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span> <span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
<span>Approche <strong class="text-white">Domain Driven Design</strong></span> <span>Approche <strong class="text-white">Domain Driven Design</strong></span>
</li> </li>
<li class="flex items-start gap-3"> <li class="flex items-start gap-3">
<span class="w-1.5 h-1.5 rounded-full bg-purple-400 mt-2 flex-shrink-0"></span> <span class="w-1 h-1 rounded-full bg-purple-300/60 mt-2.5 flex-shrink-0"></span>
<span>Organisation <Link href="https://agilemanifesto.org/iso/fr/manifesto.html" external>agile</Link> : itération et amélioration continue</span> <span>Organisation <Link href="https://agilemanifesto.org/iso/fr/manifesto.html" external>agile</Link> : itération et amélioration continue</span>
</li> </li>
</ul> </ul>
</div> </div>
<h2 class="text-2xl font-bold text-white mb-5">Communauté & Enseignement</h2> <div class="mb-16">
<div class="facet-card rounded-2xl bg-white/[0.04] border border-white/[0.08] p-6 mb-10"> <h2 class="text-2xl font-bold text-white mb-6">Communauté & Enseignement</h2>
<p class="text-white/70 leading-relaxed"> <p class="text-white/60 leading-relaxed">
J'anime les <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters d'Albi</Link> depuis 2018. Enseignant en génie logiciel à <Link href="https://www.univ-jfc.fr/" external>l'université Champollion</Link> d'Albi depuis 2019. J'anime les <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters d'Albi</Link> depuis 2018. Enseignant en génie logiciel à <Link href="https://www.univ-jfc.fr/" external>l'université Champollion</Link> d'Albi depuis 2019.
</p> </p>
</div> </div>
<h2 class="text-2xl font-bold text-white mb-5">En ligne</h2> <div class="mb-16">
<div class="facet-card flex flex-wrap gap-3 mb-12"> <h2 class="text-2xl font-bold text-white mb-6">En ligne</h2>
<div class="flex flex-wrap gap-x-6 gap-y-2">
{[ {[
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/jalil' }, { label: 'LinkedIn', href: 'https://www.linkedin.com/in/jalil' },
{ label: 'Malt', href: 'https://www.malt.fr/profile/jalilarfaoui' }, { label: 'Malt', href: 'https://www.malt.fr/profile/jalilarfaoui' },
@ -200,20 +176,18 @@ function formatMonth(dateStr: string) {
href={link.href} href={link.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-full bg-white/[0.06] border border-white/[0.1] text-sm text-white/70 hover:bg-white/[0.12] hover:text-white transition-all duration-200" class="text-sm text-white/50 hover:text-white transition-colors py-1"
> >
{link.label} {link.label}
<svg class="w-3 h-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a> </a>
))} ))}
</div> </div>
</div>
<div class="text-center pt-8 border-t border-white/[0.08]"> <div class="text-center pt-10 border-t border-white/[0.06]">
<Image src={logoTiqa} alt="Logo Tiqa" class="mx-auto mb-4" width={160} /> <Image src={logoTiqa} alt="Logo Tiqa" class="mx-auto mb-4" width={160} />
<p class="text-sm text-white/40"> <p class="text-sm text-white/30">
<strong class="text-white/60">SAS Tiqa</strong><br /> <strong class="text-white/45">SAS Tiqa</strong><br />
12, rue Fabre d'Églantine — 81 000 Albi<br /> 12, rue Fabre d'Églantine — 81 000 Albi<br />
811 917 871 RCS Albi 811 917 871 RCS Albi
</p> </p>

View file

@ -5,6 +5,9 @@ import RecommendationCard from "../../components/code/RecommendationCard.astro";
const recommendations = (await getCollection("recommendations")) const recommendations = (await getCollection("recommendations"))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const leftCol = recommendations.filter((_, i) => i % 2 === 0);
const rightCol = recommendations.filter((_, i) => i % 2 === 1);
--- ---
<Layout <Layout
@ -24,9 +27,10 @@ const recommendations = (await getCollection("recommendations"))
<p class="mt-3 text-white/60 text-lg">Ce que disent les gens avec qui j'ai travaillé.</p> <p class="mt-3 text-white/60 text-lg">Ce que disent les gens avec qui j'ai travaillé.</p>
</div> </div>
<div class="columns-1 sm:columns-2 gap-5 space-y-5"> <div class="hidden sm:grid grid-cols-2 gap-5 items-start">
{recommendations.map((rec) => ( {[leftCol, rightCol].map((col) => (
<div class="break-inside-avoid"> <div class="space-y-5">
{col.map((rec) => (
<RecommendationCard <RecommendationCard
author={rec.data.author} author={rec.data.author}
authorRole={rec.data.authorRole} authorRole={rec.data.authorRole}
@ -37,8 +41,23 @@ const recommendations = (await getCollection("recommendations"))
url={rec.data.url} url={rec.data.url}
lang={rec.data.lang} lang={rec.data.lang}
/> />
))}
</div> </div>
))} ))}
</div> </div>
<div class="sm:hidden space-y-5">
{recommendations.map((rec) => (
<RecommendationCard
author={rec.data.author}
authorRole={rec.data.authorRole}
company={rec.data.company}
text={rec.body || ''}
date={rec.data.date}
avatar={rec.data.avatar}
url={rec.data.url}
lang={rec.data.lang}
/>
))}
</div>
</section> </section>
</Layout> </Layout>

View file

@ -18,16 +18,12 @@ const experiences = (await getCollection("experiences"))
const recentExperiences = experiences.slice(0, 4); const recentExperiences = experiences.slice(0, 4);
const projects = (await getCollection("projects")) const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev") .filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => { .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
return b.data.date.getTime() - a.data.date.getTime();
})
.slice(0, 3);
const recommendations = (await getCollection("recommendations")) const recommendations = (await getCollection("recommendations"))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()) .filter((r) => r.data.featured)
.slice(0, 3); .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const recommendationTexts = recommendations.map((rec) => ({ const recommendationTexts = recommendations.map((rec) => ({
...rec, ...rec,
@ -50,16 +46,18 @@ function formatMonth(dateStr: string) {
> >
<section class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-3xl mx-auto my-12 px-7 lg:px-0">
<div class="mb-10"> <div class="mb-10">
<h1 class="text-4xl sm:text-5xl font-bold text-white">Code</h1> <h1 class="text-4xl sm:text-5xl font-semibold text-white font-display tracking-wide">Software Craftsman</h1>
<p class="mt-4 text-lg text-white/65 leading-relaxed max-w-2xl"> <div class="mt-6 space-y-4 text-lg text-white/60 leading-relaxed max-w-2xl">
Over 20 years building software. Craftsmanship, TDD, DDD — and an obsession with the biases we unknowingly put into code. <p>
TDD, Clean Code, Domain-Driven Design: that's how I build software. I work with teams as a senior developer, tech lead or technical coach. My stack: TypeScript/JavaScript, but also PHP and Elixir.
</p>
<p>
What sets me apart, perhaps: I care as much about code quality as about what it produces. I favor free software and tools that address real needs. I also question the biases we embed in code, perpetuating social dynamics that deserve scrutiny.
</p>
<p>
I teach programming at <Link href="https://www.univ-jfc.fr/" external>Université Champollion</Link> and have been running the <Link href="https://www.meetup.com/software-crafters-albi/" external>Software Crafters Albi</Link> meetup since 2018.
</p> </p>
</div> </div>
<div class="facet-card rounded-2xl bg-white/[0.06] border border-white/[0.1] p-6 mb-10">
<p class="text-white/70 leading-relaxed">
Freelance developer based in <strong class="text-white">Albi, France</strong>, I work with teams as a senior developer, tech lead or technical coach. I favor free software and tools that address real needs.
</p>
</div> </div>
<div class="mb-12"> <div class="mb-12">

View file

@ -5,6 +5,9 @@ import RecommendationCard from "../../../components/code/RecommendationCard.astr
const recommendations = (await getCollection("recommendations")) const recommendations = (await getCollection("recommendations"))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); .sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const leftCol = recommendations.filter((_, i) => i % 2 === 0);
const rightCol = recommendations.filter((_, i) => i % 2 === 1);
--- ---
<Layout <Layout
@ -24,9 +27,10 @@ const recommendations = (await getCollection("recommendations"))
<p class="mt-3 text-white/60 text-lg">What people I've worked with say about me.</p> <p class="mt-3 text-white/60 text-lg">What people I've worked with say about me.</p>
</div> </div>
<div class="columns-1 sm:columns-2 gap-5 space-y-5"> <div class="hidden sm:grid grid-cols-2 gap-5 items-start">
{recommendations.map((rec) => ( {[leftCol, rightCol].map((col) => (
<div class="break-inside-avoid"> <div class="space-y-5">
{col.map((rec) => (
<RecommendationCard <RecommendationCard
author={rec.data.author} author={rec.data.author}
authorRole={rec.data.authorRole} authorRole={rec.data.authorRole}
@ -37,8 +41,23 @@ const recommendations = (await getCollection("recommendations"))
url={rec.data.url} url={rec.data.url}
lang={rec.data.lang} lang={rec.data.lang}
/> />
))}
</div> </div>
))} ))}
</div> </div>
<div class="sm:hidden space-y-5">
{recommendations.map((rec) => (
<RecommendationCard
author={rec.data.author}
authorRole={rec.data.authorRole}
company={rec.data.company}
text={rec.body || ''}
date={rec.data.date}
avatar={rec.data.avatar}
url={rec.data.url}
lang={rec.data.lang}
/>
))}
</div>
</section> </section>
</Layout> </Layout>

View file

@ -1,4 +1,5 @@
import type { Locale } from "./i18n"; import type { Locale } from "./i18n";
import { getPhotoAlbumsPath, getPhotoBlogPath } from "./i18n";
/** /**
* Groupes de pages traduites. * Groupes de pages traduites.
@ -14,6 +15,7 @@ const translationGroups: Record<Locale, string>[] = [
{ fr: "/code/competences", en: "/en/code/skills", ar: "/ar/برمجة/مهارات" }, { fr: "/code/competences", en: "/en/code/skills", ar: "/ar/برمجة/مهارات" },
{ fr: "/theatre", en: "/en/acting", ar: "/ar/مسرح" }, { fr: "/theatre", en: "/en/acting", ar: "/ar/مسرح" },
{ fr: "/photo", en: "/en/photo", ar: "/ar/تصوير" }, { fr: "/photo", en: "/en/photo", ar: "/ar/تصوير" },
{ fr: "/photo/blog", en: "/en/photo/blog", ar: "/ar/تصوير/مدونة" },
]; ];
/** Index inversé : pathname → groupe de traduction */ /** Index inversé : pathname → groupe de traduction */
@ -24,13 +26,59 @@ for (const group of translationGroups) {
} }
} }
/** Pattern dynamique pour les routes paramétrées */
interface DynamicPattern {
regex: RegExp;
buildUrls: (match: RegExpMatchArray) => Record<Locale, string>;
}
const dynamicPatterns: DynamicPattern[] = [
{
// Photo albums: /photo/albums/{cat}, /en/photo/albums/{cat}, /ar/تصوير/ألبومات/{cat}
regex: /^(?:\/photo\/albums|\/en\/photo\/albums|\/ar\/تصوير\/ألبومات)\/([^/]+)$/,
buildUrls: (match) => {
const category = match[1];
return {
fr: `${getPhotoAlbumsPath("fr")}/${category}`,
en: `${getPhotoAlbumsPath("en")}/${category}`,
ar: `${getPhotoAlbumsPath("ar")}/${category}`,
};
},
},
{
// Photo blog posts: /photo/blog/{year}/{slug}, /en/photo/blog/{year}/{slug}, /ar/تصوير/مدونة/{year}/{slug}
regex: /^(?:\/photo\/blog|\/en\/photo\/blog|\/ar\/تصوير\/مدونة)\/(\d{4})\/([^/]+)$/,
buildUrls: (match) => {
const [, year, slug] = match;
return {
fr: `${getPhotoBlogPath("fr")}/${year}/${slug}`,
en: `${getPhotoBlogPath("en")}/${year}/${slug}`,
ar: `${getPhotoBlogPath("ar")}/${year}/${slug}`,
};
},
},
];
function matchDynamicPattern(
pathname: string,
): Record<Locale, string> | undefined {
for (const pattern of dynamicPatterns) {
const match = pathname.match(pattern.regex);
if (match) {
return pattern.buildUrls(match);
}
}
return undefined;
}
/** /**
* Retourne les URLs alternatives pour un pathname donné. * Retourne les URLs alternatives pour un pathname donné.
* Les trailing slashes sont normalisés. * Les trailing slashes sont normalisés.
* Essaie d'abord le lookup statique, puis le matching dynamique.
*/ */
export function getAlternateUrls( export function getAlternateUrls(
pathname: string, pathname: string,
): Record<Locale, string> | undefined { ): Record<Locale, string> | undefined {
const normalized = pathname.replace(/\/$/, "") || "/"; const normalized = pathname.replace(/\/$/, "") || "/";
return pathIndex.get(normalized); return pathIndex.get(normalized) ?? matchDynamicPattern(normalized);
} }

View file

@ -3,7 +3,11 @@ export default {
darkMode: "class", darkMode: "class",
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: { theme: {
extend: {}, extend: {
fontFamily: {
display: ['Raleway', 'sans-serif'],
},
},
}, },
plugins: [require("@tailwindcss/typography")], plugins: [require("@tailwindcss/typography")],
}; };