Infrastructure SEO complète : sitemap, meta descriptions, OG, Twitter Cards, JSON-LD, hreflang

- Ajout de site: 'https://jalil.arfaoui.net' et @astrojs/sitemap avec support i18n dans astro.config.mjs
- Création de src/components/SEO.astro : meta description, canonical, Open Graph, Twitter Cards, hreflang (fr/en/ar/x-default), JSON-LD Person (11 liens sameAs) sur chaque page et JSON-LD WebSite sur les pages d'accueil
- Création de src/utils/page-translations.ts : mapping centralisé des URLs entre langues
- Fix lang="en" hardcodé dans main.astro → lang dynamique + dir="rtl" pour l'arabe
- Ajout de meta descriptions ciblées sur les 13 pages principales (FR/EN/AR)
- Refactorisation du LanguageSwitcher pour utiliser le mapping centralisé
- Ajout de la directive Sitemap dans robots.txt
This commit is contained in:
Jalil Arfaoui 2026-02-21 14:32:56 +01:00
parent 7cb920f773
commit 23510f59b1
22 changed files with 388 additions and 88 deletions

View file

@ -1,11 +1,25 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind"; import tailwind from "@astrojs/tailwind";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://jalil.arfaoui.net",
devToolbar: { enabled: false }, devToolbar: { enabled: false },
integrations: [tailwind()], integrations: [
tailwind(),
sitemap({
i18n: {
defaultLocale: "fr",
locales: {
fr: "fr",
en: "en",
ar: "ar",
},
},
}),
],
redirects: { redirects: {
"/photos": { "/photos": {
status: 301, status: 301,

102
package-lock.json generated
View file

@ -7,6 +7,10 @@
"": { "": {
"name": "jalil.arfaoui.net", "name": "jalil.arfaoui.net",
"version": "0.0.1", "version": "0.0.1",
"dependencies": {
"@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0"
},
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.9.6", "@astrojs/check": "^0.9.6",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^6.0.2",
@ -152,6 +156,57 @@
"node": "18.20.8 || ^20.3.0 || >=22.0.0" "node": "18.20.8 || ^20.3.0 || >=22.0.0"
} }
}, },
"node_modules/@astrojs/rss": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.15.tgz",
"integrity": "sha512-uXO/k6AhRkIDXmRoc6xQpoPZrimQNUmS43X4+60yunfuMNHtSRN5e/FiSi7NApcZqmugSMc5+cJi8ovqgO+qIg==",
"license": "MIT",
"dependencies": {
"fast-xml-parser": "^5.3.3",
"piccolore": "^0.1.3"
}
},
"node_modules/@astrojs/rss/node_modules/fast-xml-parser": {
"version": "5.3.7",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz",
"integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"strnum": "^2.1.2"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/@astrojs/rss/node_modules/strnum": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
"integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/@astrojs/sitemap": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.0.tgz",
"integrity": "sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA==",
"license": "MIT",
"dependencies": {
"sitemap": "^8.0.2",
"stream-replace-string": "^2.0.0",
"zod": "^3.25.76"
}
},
"node_modules/@astrojs/tailwind": { "node_modules/@astrojs/tailwind": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz",
@ -2049,13 +2104,20 @@
"version": "25.0.3", "version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
}, },
"node_modules/@types/sax": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz",
"integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/unist": { "node_modules/@types/unist": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@ -2333,7 +2395,6 @@
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/argparse": { "node_modules/argparse": {
@ -6044,7 +6105,6 @@
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
"integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
@ -6772,7 +6832,6 @@
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
"integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=11.0.0" "node": ">=11.0.0"
@ -6861,6 +6920,31 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sitemap": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz",
"integrity": "sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==",
"license": "MIT",
"dependencies": {
"@types/node": "^17.0.5",
"@types/sax": "^1.2.1",
"arg": "^5.0.0",
"sax": "^1.4.1"
},
"bin": {
"sitemap": "dist/cli.js"
},
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
}
},
"node_modules/sitemap/node_modules/@types/node": {
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz",
"integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==",
"license": "MIT"
},
"node_modules/smol-toml": { "node_modules/smol-toml": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz",
@ -6895,6 +6979,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/stream-replace-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz",
"integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==",
"license": "MIT"
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
@ -7811,7 +7901,6 @@
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unified": { "node_modules/unified": {
@ -8848,7 +8937,6 @@
"version": "3.25.76", "version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"funding": { "funding": {

View file

@ -24,5 +24,9 @@
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"webdav": "^5.8.0" "webdav": "^5.8.0"
},
"dependencies": {
"@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0"
} }
} }

View file

@ -1,2 +1,4 @@
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: https://jalil.arfaoui.net/sitemap-index.xml

View file

@ -1,70 +1,23 @@
--- ---
// Mapping des URLs entre langues import { getAlternateUrls } from "../utils/page-translations";
const translations: Record<string, Record<string, string>> = {
'/a-propos': {
fr: '/a-propos',
en: '/en/about',
ar: '/ar/نبذة-عني'
},
'/en/about': {
fr: '/a-propos',
en: '/en/about',
ar: '/ar/نبذة-عني'
},
'/ar/نبذة-عني': {
fr: '/a-propos',
en: '/en/about',
ar: '/ar/نبذة-عني'
},
// Photo
'/photo': {
fr: '/photo',
en: '/en/photo',
ar: '/ar/تصوير'
},
'/en/photo': {
fr: '/photo',
en: '/en/photo',
ar: '/ar/تصوير'
},
'/ar/تصوير': {
fr: '/photo',
en: '/en/photo',
ar: '/ar/تصوير'
},
// Page d'accueil
'/': {
fr: '/',
en: '/en',
ar: '/ar'
},
'/en': {
fr: '/',
en: '/en',
ar: '/ar'
},
'/ar': {
fr: '/',
en: '/en',
ar: '/ar'
}
};
// Détection de la langue courante const pathname = Astro.url.pathname.replace(/\/$/, "") || "/";
const pathname = Astro.url.pathname.replace(/\/$/, '') || '/'; const currentLang = pathname.startsWith("/en")
const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr'; ? "en"
: pathname.startsWith("/ar")
? "ar"
: "fr";
// Récupération des liens traduits ou fallback vers les pages d'accueil const links = getAlternateUrls(pathname) ?? {
const links = translations[pathname] || { fr: "/",
fr: '/', en: "/en",
en: '/en', ar: "/ar",
ar: '/ar'
}; };
const languages = [ const languages = [
{ code: 'fr', label: 'FR', name: 'Français' }, { code: "fr", label: "FR", name: "Français" },
{ code: 'en', label: 'EN', name: 'English' }, { code: "en", label: "EN", name: "English" },
{ code: 'ar', label: 'ع', name: 'العربية' } { code: "ar", label: "ع", name: "العربية" },
]; ];
--- ---
@ -78,7 +31,7 @@ const languages = [
</span> </span>
) : ( ) : (
<a <a
href={links[lang.code]} href={links[lang.code as keyof typeof links]}
class="text-gray-500 dark:text-neutral-400 hover:text-gray-800 dark:hover:text-neutral-200 transition-colors" class="text-gray-500 dark:text-neutral-400 hover:text-gray-800 dark:hover:text-neutral-200 transition-colors"
title={lang.name} title={lang.name}
hreflang={lang.code} hreflang={lang.code}
@ -88,4 +41,4 @@ const languages = [
)} )}
</> </>
))} ))}
</div> </div>

135
src/components/SEO.astro Normal file
View file

@ -0,0 +1,135 @@
---
import type { ImageMetadata } from "astro";
import { getAlternateUrls } from "../utils/page-translations";
import defaultOgImage from "../assets/images/jalil-2.jpg";
interface Props {
title: string;
description: string;
ogImage?: ImageMetadata;
ogType?: string;
article?: { publishedTime?: string; tags?: string[] };
noindex?: boolean;
}
const {
title,
description,
ogImage,
ogType = "website",
article,
noindex = false,
} = Astro.props;
const siteUrl = Astro.site!.origin;
const canonicalUrl = new URL(Astro.url.pathname, siteUrl).href;
const imageUrl = new URL((ogImage ?? defaultOgImage).src, siteUrl).href;
// Locale detection
const pathname = Astro.url.pathname.replace(/\/$/, "") || "/";
const locale = pathname.startsWith("/en")
? "en"
: pathname.startsWith("/ar")
? "ar"
: "fr";
const ogLocaleMap: Record<string, string> = {
fr: "fr_FR",
en: "en_US",
ar: "ar_SA",
};
// Hreflang
const alternateUrls = getAlternateUrls(pathname);
// Home pages for WebSite schema
const isHomePage = pathname === "/" || pathname === "/en" || pathname === "/ar";
// Person JSON-LD (on every page)
const personJsonLd = {
"@context": "https://schema.org",
"@type": "Person",
"@id": `${siteUrl}/#person`,
name: "Jalil Arfaoui",
url: siteUrl,
image: imageUrl,
jobTitle: "Software Craftsman",
knowsAbout: [
"Software Craftsmanship",
"TDD",
"DDD",
"Improv Theater",
"Photography",
],
sameAs: [
"https://www.linkedin.com/in/jalil/",
"https://github.com/JalilArfaoui",
"https://gitlab.gnome.org/Jalil",
"https://forge.tiqa.fr/jalil",
"https://framagit.org/jalil",
"https://www.malt.fr/profile/jalilarfaoui?overview",
"https://www.collective.work/profile/jalil-arfaoui-mrr",
"https://500px.com/p/jalilarfaoui",
"https://commons.wikimedia.org/wiki/User:JalilArfaoui",
"https://www.instagram.com/l.i.l.a.j",
"https://x.com/jalilarfaoui",
],
};
// WebSite JSON-LD (only on home pages)
const websiteJsonLd = isHomePage
? {
"@context": "https://schema.org",
"@type": "WebSite",
name: "Jalil Arfaoui",
url: siteUrl,
inLanguage: locale,
author: { "@id": `${siteUrl}/#person` },
}
: null;
---
<!-- Meta tags -->
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<!-- Open Graph -->
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={imageUrl} />
<meta property="og:type" content={ogType} />
<meta property="og:locale" content={ogLocaleMap[locale]} />
<meta property="og:site_name" content="Jalil Arfaoui" />
{article?.publishedTime && (
<meta property="article:published_time" content={article.publishedTime} />
)}
{article?.tags?.map((tag) => (
<meta property="article:tag" content={tag} />
))}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@jalilarfaoui" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
<!-- Hreflang -->
{alternateUrls && (
<>
<link rel="alternate" hreflang="fr" href={new URL(alternateUrls.fr, siteUrl).href} />
<link rel="alternate" hreflang="en" href={new URL(alternateUrls.en, siteUrl).href} />
<link rel="alternate" hreflang="ar" href={new URL(alternateUrls.ar, siteUrl).href} />
<link rel="alternate" hreflang="x-default" href={new URL(alternateUrls.fr, siteUrl).href} />
</>
)}
<!-- JSON-LD Person -->
<script type="application/ld+json" set:html={JSON.stringify(personJsonLd)} />
<!-- JSON-LD WebSite (home pages only) -->
{websiteJsonLd && (
<script type="application/ld+json" set:html={JSON.stringify(websiteJsonLd)} />
)}

View file

@ -1,14 +1,21 @@
--- ---
import PhotoFooter from '../components/photo/PhotoFooter.astro'; import PhotoFooter from '../components/photo/PhotoFooter.astro';
import SEO from '../components/SEO.astro';
import type { Locale } from '../utils/i18n'; import type { Locale } from '../utils/i18n';
interface Props { interface Props {
title?: string; title?: string;
description?: string;
enableScroll?: boolean; enableScroll?: boolean;
lang?: Locale; lang?: Locale;
} }
const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'fr' } = Astro.props; const {
title = "Galerie Photo - Jalil Arfaoui",
description = "Portfolio photo de Jalil Arfaoui. Portraits, paysages, cultures, musique, nature.",
enableScroll = false,
lang = 'fr',
} = Astro.props;
--- ---
<!doctype html> <!doctype html>
@ -17,6 +24,7 @@ const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'f
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title> <title>{title}</title>
<SEO title={title} description={description} />
<!-- Google Fonts - Karla --> <!-- Google Fonts - Karla -->
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
@ -29,6 +37,7 @@ const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'f
</script> </script>
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.png" /> <link rel="icon" type="image/x-icon" href="/assets/images/favicon.png" />
<link rel="alternate" type="application/rss+xml" title="Jalil Arfaoui — Photo Blog" href="/rss.xml" />
<!-- Privacy-friendly analytics by Plausible --> <!-- Privacy-friendly analytics by Plausible -->
<script is:inline async src="https://plausible.io/js/pa-fP2pF1VtXKDIjQczHCynl.js"></script> <script is:inline async src="https://plausible.io/js/pa-fP2pF1VtXKDIjQczHCynl.js"></script>
<script is:inline> <script is:inline>

View file

@ -2,15 +2,34 @@
import Footer from "../components/footer.astro"; import Footer from "../components/footer.astro";
import Header from "../components/header.astro"; import Header from "../components/header.astro";
import SquareLines from "../components/square-lines.astro"; import SquareLines from "../components/square-lines.astro";
const { title, facet } = Astro.props; import SEO from "../components/SEO.astro";
interface Props {
title: string;
facet?: string;
description?: string;
ogImage?: import("astro").ImageMetadata;
ogType?: string;
article?: { publishedTime?: string; tags?: string[] };
}
const { title, facet, description = "", ogImage, ogType, article } = Astro.props;
const pathname = Astro.url.pathname.replace(/\/$/, "") || "/";
const locale = pathname.startsWith("/en")
? "en"
: pathname.startsWith("/ar")
? "ar"
: "fr";
--- ---
<!doctype html> <!doctype html>
<html lang="en"> <html lang={locale} dir={locale === "ar" ? "rtl" : "ltr"}>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title> <title>{title}</title>
{description && <SEO title={title} description={description} ogImage={ogImage} ogType={ogType} article={article} />}
<!-- Used to add dark mode right away, adding here prevents any flicker --> <!-- Used to add dark mode right away, adding here prevents any flicker -->
<script is:inline> <script is:inline>
@ -71,6 +90,7 @@ const { title, facet } = Astro.props;
} }
</style> </style>
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.png" /> <link rel="icon" type="image/x-icon" href="/assets/images/favicon.png" />
<link rel="alternate" type="application/rss+xml" title="Jalil Arfaoui — Photo Blog" href="/rss.xml" />
<script src="../assets/css/main.css"></script> <script src="../assets/css/main.css"></script>
<!-- Privacy-friendly analytics by Plausible --> <!-- Privacy-friendly analytics by Plausible -->
<script is:inline async src="https://plausible.io/js/pa-fP2pF1VtXKDIjQczHCynl.js"></script> <script is:inline async src="https://plausible.io/js/pa-fP2pF1VtXKDIjQczHCynl.js"></script>

View file

@ -3,7 +3,11 @@ import Layout from "./main.astro";
const { frontmatter } = Astro.props; const { frontmatter } = Astro.props;
--- ---
<Layout title={frontmatter.title}> <Layout
title={frontmatter.title}
description={frontmatter.description || ""}
ogType={frontmatter.description ? "article" : undefined}
>
<main <main
class="relative z-30 max-w-4xl pb-1 mx-auto mt-10 bg-white dark:bg-neutral-950 md:rounded-t-md text-neutral-900" class="relative z-30 max-w-4xl pb-1 mx-auto mt-10 bg-white dark:bg-neutral-950 md:rounded-t-md text-neutral-900"
> >

View file

@ -6,7 +6,10 @@ import Link from "../components/Link.astro";
import jalilPhoto from "../assets/images/jalil.jpg"; import jalilPhoto from "../assets/images/jalil.jpg";
--- ---
<Layout title="À propos - Jalil Arfaoui"> <Layout
title="À propos - Jalil Arfaoui"
description="Qui est Jalil Arfaoui ? Développeur artisan, comédien improvisateur et photographe. Plus de 20 ans d'expérience en développement logiciel."
>
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="À propos" title="À propos"

View file

@ -4,7 +4,10 @@ import Layout from "../../layouts/main.astro";
import jalilPhoto from "../../assets/images/jalil-2.jpg"; import jalilPhoto from "../../assets/images/jalil-2.jpg";
--- ---
<Layout title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر"> <Layout
title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر"
description="جليل عرفاوي: مطوّر مستقل (Software Craftsmanship، TDD، DDD)، ممثل ارتجالي ومصوّر مقيم في ألبي، فرنسا."
>
<!-- Hero Section --> <!-- Hero Section -->
<div dir="rtl" lang="ar" class="relative z-20 w-full max-w-6xl mx-auto mt-16 px-7 md:mt-24 lg:mt-32 xl:px-0"> <div dir="rtl" lang="ar" class="relative z-20 w-full max-w-6xl mx-auto mt-16 px-7 md:mt-24 lg:mt-32 xl:px-0">
<div class="flex flex-col items-center md:flex-row-reverse"> <div class="flex flex-col items-center md:flex-row-reverse">

View file

@ -6,7 +6,11 @@ import Link from "../../components/Link.astro";
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png"; import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
--- ---
<Layout title="برمجة - جليل عرفاوي" facet="code"> <Layout
title="برمجة - جليل عرفاوي"
facet="code"
description="المسار المهني لجليل عرفاوي: مطوّر مستقل متخصص في Software Craftsmanship، TDD، DDD. TypeScript، PHP، Elixir."
>
<section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="برمجة" title="برمجة"

View file

@ -4,7 +4,10 @@ import Layout from "../../layouts/main.astro";
import Link from "../../components/Link.astro"; import Link from "../../components/Link.astro";
--- ---
<Layout title="مسرح - جليل عرفاوي"> <Layout
title="مسرح - جليل عرفاوي"
description="المسار المسرحي لجليل عرفاوي: ممثل ارتجالي منذ 2008، مؤسس مشارك لفرقة Les Particules في ألبي، ممثل مسرح مكتوب."
>
<section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="مسرح" title="مسرح"

View file

@ -6,7 +6,10 @@ import Link from "../../components/Link.astro";
import jalilPhoto from "../../assets/images/jalil.jpg"; import jalilPhoto from "../../assets/images/jalil.jpg";
--- ---
<Layout title="نبذة عني - جليل عرفاوي"> <Layout
title="نبذة عني - جليل عرفاوي"
description="من هو جليل عرفاوي؟ مطوّر حِرَفي، ممثل ارتجالي ومصوّر. أكثر من 20 عامًا من الخبرة في تطوير البرمجيات."
>
<section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section dir="rtl" lang="ar" class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="نبذة عني" title="نبذة عني"

View file

@ -6,7 +6,11 @@ import Link from "../components/Link.astro";
import logoTiqa from "../assets/images/logo-tiqa-blanc.png"; import logoTiqa from "../assets/images/logo-tiqa-blanc.png";
--- ---
<Layout title="Code - Jalil Arfaoui" facet="code"> <Layout
title="Code - Jalil Arfaoui"
facet="code"
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-2xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="Code" title="Code"

View file

@ -6,7 +6,10 @@ import Link from "../../components/Link.astro";
import jalilPhoto from "../../assets/images/jalil.jpg"; import jalilPhoto from "../../assets/images/jalil.jpg";
--- ---
<Layout title="About - Jalil Arfaoui"> <Layout
title="About - Jalil Arfaoui"
description="Who is Jalil Arfaoui? Software craftsman, improv actor and photographer. Over 20 years of experience in software development."
>
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="About" title="About"

View file

@ -4,7 +4,10 @@ import Layout from "../../layouts/main.astro";
import Link from "../../components/Link.astro"; import Link from "../../components/Link.astro";
--- ---
<Layout title="Acting - Jalil Arfaoui"> <Layout
title="Acting - Jalil Arfaoui"
description="Jalil Arfaoui's theater journey: improv actor since 2008, co-founder of Les Particules in Albi, scripted theater actor."
>
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="Acting" title="Acting"

View file

@ -6,7 +6,11 @@ import Link from "../../components/Link.astro";
import logoTiqa from "../../assets/images/logo-tiqa-blanc.png"; import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
--- ---
<Layout title="Code - Jalil Arfaoui" facet="code"> <Layout
title="Code - Jalil Arfaoui"
facet="code"
description="Jalil Arfaoui's professional journey: freelance developer specializing in Software Craftsmanship, TDD, DDD. TypeScript, PHP, Elixir."
>
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="Code" title="Code"

View file

@ -4,7 +4,10 @@ import Layout from "../../layouts/main.astro";
import jalilPhoto from "../../assets/images/jalil-2.jpg"; import jalilPhoto from "../../assets/images/jalil-2.jpg";
--- ---
<Layout title="Jalil Arfaoui - Software Craftsman • Improv Actor • Photographer"> <Layout
title="Jalil Arfaoui - Software Craftsman • Improv Actor • Photographer"
description="Jalil Arfaoui: freelance developer (Software Craftsmanship, TDD, DDD), improv actor and photographer based in Albi, France."
>
<!-- Hero Section --> <!-- Hero Section -->
<div class="relative z-20 w-full max-w-6xl mx-auto mt-16 px-7 md:mt-24 lg:mt-32 xl:px-0"> <div class="relative z-20 w-full max-w-6xl mx-auto mt-16 px-7 md:mt-24 lg:mt-32 xl:px-0">
<div class="flex flex-col items-center md:flex-row"> <div class="flex flex-col items-center md:flex-row">

View file

@ -4,7 +4,10 @@ import Layout from "../layouts/main.astro";
import jalilPhoto from "../assets/images/jalil-2.jpg"; import jalilPhoto from "../assets/images/jalil-2.jpg";
--- ---
<Layout title="Jalil Arfaoui - Développeur artisan • Comédien improvisateur • Photographe"> <Layout
title="Jalil Arfaoui - Développeur artisan • Comédien improvisateur • Photographe"
description="Jalil Arfaoui : développeur freelance (Software Craftsmanship, TDD, DDD), comédien improvisateur et photographe basé à Albi."
>
<!-- Hero Section --> <!-- Hero Section -->
<div class="relative z-20 w-full max-w-6xl mx-auto mt-16 px-7 md:mt-24 lg:mt-32 xl:px-0"> <div class="relative z-20 w-full max-w-6xl mx-auto mt-16 px-7 md:mt-24 lg:mt-32 xl:px-0">
<div class="flex flex-col items-center md:flex-row"> <div class="flex flex-col items-center md:flex-row">

View file

@ -4,7 +4,10 @@ import Layout from "../layouts/main.astro";
import Link from "../components/Link.astro"; import Link from "../components/Link.astro";
--- ---
<Layout title="Théâtre - Jalil Arfaoui"> <Layout
title="Théâtre - Jalil Arfaoui"
description="Parcours théâtral de Jalil Arfaoui : improvisateur depuis 2008, cofondateur des Particules à Albi, comédien de théâtre écrit."
>
<section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0"> <section class="relative z-20 max-w-2xl mx-auto my-12 px-7 lg:px-0">
<PageHeading <PageHeading
title="Théâtre" title="Théâtre"

View file

@ -0,0 +1,32 @@
import type { Locale } from "./i18n";
/**
* Groupes de pages traduites.
* Chaque groupe contient les URLs correspondantes pour chaque langue.
*/
const translationGroups: Record<Locale, string>[] = [
{ fr: "/", en: "/en", ar: "/ar" },
{ fr: "/a-propos", en: "/en/about", ar: "/ar/نبذة-عني" },
{ fr: "/code", en: "/en/code", ar: "/ar/برمجة" },
{ fr: "/theatre", en: "/en/acting", ar: "/ar/مسرح" },
{ fr: "/photo", en: "/en/photo", ar: "/ar/تصوير" },
];
/** Index inversé : pathname → groupe de traduction */
const pathIndex = new Map<string, Record<Locale, string>>();
for (const group of translationGroups) {
for (const path of Object.values(group)) {
pathIndex.set(path, group);
}
}
/**
* Retourne les URLs alternatives pour un pathname donné.
* Les trailing slashes sont normalisés.
*/
export function getAlternateUrls(
pathname: string,
): Record<Locale, string> | undefined {
const normalized = pathname.replace(/\/$/, "") || "/";
return pathIndex.get(normalized);
}