From 23510f59b18f113688a7cb832ffd662a51e76813 Mon Sep 17 00:00:00 2001 From: Jalil Arfaoui Date: Sat, 21 Feb 2026 14:32:56 +0100 Subject: [PATCH] =?UTF-8?q?Infrastructure=20SEO=20compl=C3=A8te=20:=20site?= =?UTF-8?q?map,=20meta=20descriptions,=20OG,=20Twitter=20Cards,=20JSON-LD,?= =?UTF-8?q?=20hreflang?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- astro.config.mjs | 16 ++- package-lock.json | 102 +++++++++++++++++-- package.json | 4 + public/robots.txt | 4 +- src/components/LanguageSwitcher.astro | 79 +++------------ src/components/SEO.astro | 135 ++++++++++++++++++++++++++ src/layouts/PhotoLayout.astro | 11 ++- src/layouts/main.astro | 24 ++++- src/layouts/post.astro | 6 +- src/pages/a-propos.astro | 5 +- src/pages/ar/index.astro | 5 +- src/pages/ar/برمجة.astro | 6 +- src/pages/ar/مسرح.astro | 5 +- src/pages/ar/نبذة-عني.astro | 5 +- src/pages/code.astro | 6 +- src/pages/en/about.astro | 5 +- src/pages/en/acting.astro | 5 +- src/pages/en/code.astro | 6 +- src/pages/en/index.astro | 5 +- src/pages/index.astro | 5 +- src/pages/theatre.astro | 5 +- src/utils/page-translations.ts | 32 ++++++ 22 files changed, 388 insertions(+), 88 deletions(-) create mode 100644 src/components/SEO.astro create mode 100644 src/utils/page-translations.ts diff --git a/astro.config.mjs b/astro.config.mjs index 0c77772..fa979ac 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,11 +1,25 @@ import { defineConfig } from "astro/config"; +import sitemap from "@astrojs/sitemap"; import tailwind from "@astrojs/tailwind"; // https://astro.build/config export default defineConfig({ + site: "https://jalil.arfaoui.net", devToolbar: { enabled: false }, - integrations: [tailwind()], + integrations: [ + tailwind(), + sitemap({ + i18n: { + defaultLocale: "fr", + locales: { + fr: "fr", + en: "en", + ar: "ar", + }, + }, + }), + ], redirects: { "/photos": { status: 301, diff --git a/package-lock.json b/package-lock.json index 6bcdcba..b645f74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,10 @@ "": { "name": "jalil.arfaoui.net", "version": "0.0.1", + "dependencies": { + "@astrojs/rss": "^4.0.15", + "@astrojs/sitemap": "^3.7.0" + }, "devDependencies": { "@astrojs/check": "^0.9.6", "@astrojs/tailwind": "^6.0.2", @@ -152,6 +156,57 @@ "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": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/@astrojs/tailwind/-/tailwind-6.0.2.tgz", @@ -2049,13 +2104,20 @@ "version": "25.0.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "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": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2333,7 +2395,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -6044,7 +6105,6 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", - "dev": true, "license": "ISC" }, "node_modules/picocolors": { @@ -6772,7 +6832,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -6861,6 +6920,31 @@ "dev": true, "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": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.0.tgz", @@ -6895,6 +6979,12 @@ "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": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -7811,7 +7901,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unified": { @@ -8848,7 +8937,6 @@ "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "dev": true, "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index 2747e8c..208a2c7 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,9 @@ "tsx": "^4.21.0", "typescript": "^5.7.3", "webdav": "^5.8.0" + }, + "dependencies": { + "@astrojs/rss": "^4.0.15", + "@astrojs/sitemap": "^3.7.0" } } diff --git a/public/robots.txt b/public/robots.txt index 14267e9..5f773ae 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,4 @@ User-agent: * -Allow: / \ No newline at end of file +Allow: / + +Sitemap: https://jalil.arfaoui.net/sitemap-index.xml diff --git a/src/components/LanguageSwitcher.astro b/src/components/LanguageSwitcher.astro index 4d901bf..d2953db 100644 --- a/src/components/LanguageSwitcher.astro +++ b/src/components/LanguageSwitcher.astro @@ -1,70 +1,23 @@ --- -// Mapping des URLs entre langues -const translations: Record> = { - '/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' - } -}; +import { getAlternateUrls } from "../utils/page-translations"; -// Détection de la langue courante -const pathname = Astro.url.pathname.replace(/\/$/, '') || '/'; -const currentLang = pathname.startsWith('/en') ? 'en' : pathname.startsWith('/ar') ? 'ar' : 'fr'; +const pathname = Astro.url.pathname.replace(/\/$/, "") || "/"; +const currentLang = pathname.startsWith("/en") + ? "en" + : pathname.startsWith("/ar") + ? "ar" + : "fr"; -// Récupération des liens traduits ou fallback vers les pages d'accueil -const links = translations[pathname] || { - fr: '/', - en: '/en', - ar: '/ar' +const links = getAlternateUrls(pathname) ?? { + fr: "/", + en: "/en", + ar: "/ar", }; const languages = [ - { code: 'fr', label: 'FR', name: 'Français' }, - { code: 'en', label: 'EN', name: 'English' }, - { code: 'ar', label: 'ع', name: 'العربية' } + { code: "fr", label: "FR", name: "Français" }, + { code: "en", label: "EN", name: "English" }, + { code: "ar", label: "ع", name: "العربية" }, ]; --- @@ -78,7 +31,7 @@ const languages = [ ) : ( ))} - \ No newline at end of file + diff --git a/src/components/SEO.astro b/src/components/SEO.astro new file mode 100644 index 0000000..3175672 --- /dev/null +++ b/src/components/SEO.astro @@ -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 = { + 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; +--- + + + + +{noindex && } + + + + + + + + + +{article?.publishedTime && ( + +)} +{article?.tags?.map((tag) => ( + +))} + + + + + + + + + +{alternateUrls && ( + <> + + + + + +)} + + + + diff --git a/src/layouts/post.astro b/src/layouts/post.astro index c907784..c2d29bb 100644 --- a/src/layouts/post.astro +++ b/src/layouts/post.astro @@ -3,7 +3,11 @@ import Layout from "./main.astro"; const { frontmatter } = Astro.props; --- - +
diff --git a/src/pages/a-propos.astro b/src/pages/a-propos.astro index 0b20718..a6d8aad 100644 --- a/src/pages/a-propos.astro +++ b/src/pages/a-propos.astro @@ -6,7 +6,10 @@ import Link from "../components/Link.astro"; import jalilPhoto from "../assets/images/jalil.jpg"; --- - +
+
diff --git a/src/pages/ar/برمجة.astro b/src/pages/ar/برمجة.astro index 85b13bb..c596f1e 100644 --- a/src/pages/ar/برمجة.astro +++ b/src/pages/ar/برمجة.astro @@ -6,7 +6,11 @@ import Link from "../../components/Link.astro"; import logoTiqa from "../../assets/images/logo-tiqa-blanc.png"; --- - +
+
+
+
+
+
+
+
diff --git a/src/pages/index.astro b/src/pages/index.astro index 302f60d..890fa46 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -4,7 +4,10 @@ import Layout from "../layouts/main.astro"; import jalilPhoto from "../assets/images/jalil-2.jpg"; --- - +
diff --git a/src/pages/theatre.astro b/src/pages/theatre.astro index 2051c58..aa6fff9 100644 --- a/src/pages/theatre.astro +++ b/src/pages/theatre.astro @@ -4,7 +4,10 @@ import Layout from "../layouts/main.astro"; import Link from "../components/Link.astro"; --- - +
[] = [ + { 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>(); +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 | undefined { + const normalized = pathname.replace(/\/$/, "") || "/"; + return pathIndex.get(normalized); +}