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:
parent
7cb920f773
commit
23510f59b1
22 changed files with 388 additions and 88 deletions
|
|
@ -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,
|
||||
|
|
|
|||
102
package-lock.json
generated
102
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,4 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://jalil.arfaoui.net/sitemap-index.xml
|
||||
|
|
|
|||
|
|
@ -1,70 +1,23 @@
|
|||
---
|
||||
// Mapping des URLs entre langues
|
||||
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'
|
||||
}
|
||||
};
|
||||
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 = [
|
|||
</span>
|
||||
) : (
|
||||
<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"
|
||||
title={lang.name}
|
||||
hreflang={lang.code}
|
||||
|
|
|
|||
135
src/components/SEO.astro
Normal file
135
src/components/SEO.astro
Normal 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)} />
|
||||
)}
|
||||
|
|
@ -1,14 +1,21 @@
|
|||
---
|
||||
import PhotoFooter from '../components/photo/PhotoFooter.astro';
|
||||
import SEO from '../components/SEO.astro';
|
||||
import type { Locale } from '../utils/i18n';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
enableScroll?: boolean;
|
||||
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>
|
||||
|
|
@ -17,6 +24,7 @@ const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'f
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{title}</title>
|
||||
<SEO title={title} description={description} />
|
||||
|
||||
<!-- Google Fonts - Karla -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
|
|
@ -29,6 +37,7 @@ const { title = "Galerie Photo - Jalil Arfaoui", enableScroll = false, lang = 'f
|
|||
</script>
|
||||
|
||||
<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 -->
|
||||
<script is:inline async src="https://plausible.io/js/pa-fP2pF1VtXKDIjQczHCynl.js"></script>
|
||||
<script is:inline>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,34 @@
|
|||
import Footer from "../components/footer.astro";
|
||||
import Header from "../components/header.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>
|
||||
<html lang="en">
|
||||
<html lang={locale} dir={locale === "ar" ? "rtl" : "ltr"}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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 -->
|
||||
<script is:inline>
|
||||
|
|
@ -71,6 +90,7 @@ const { title, facet } = Astro.props;
|
|||
}
|
||||
</style>
|
||||
<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>
|
||||
<!-- Privacy-friendly analytics by Plausible -->
|
||||
<script is:inline async src="https://plausible.io/js/pa-fP2pF1VtXKDIjQczHCynl.js"></script>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import Layout from "./main.astro";
|
|||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={frontmatter.title}>
|
||||
<Layout
|
||||
title={frontmatter.title}
|
||||
description={frontmatter.description || ""}
|
||||
ogType={frontmatter.description ? "article" : undefined}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import Link from "../components/Link.astro";
|
|||
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">
|
||||
<PageHeading
|
||||
title="À propos"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import Layout from "../../layouts/main.astro";
|
|||
import jalilPhoto from "../../assets/images/jalil-2.jpg";
|
||||
---
|
||||
|
||||
<Layout title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر">
|
||||
<Layout
|
||||
title="جليل عرفاوي - حِرَفي برمجة • ممثل ارتجالي • مصوّر"
|
||||
description="جليل عرفاوي: مطوّر مستقل (Software Craftsmanship، TDD، DDD)، ممثل ارتجالي ومصوّر مقيم في ألبي، فرنسا."
|
||||
>
|
||||
<!-- 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 class="flex flex-col items-center md:flex-row-reverse">
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import Link from "../../components/Link.astro";
|
|||
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">
|
||||
<PageHeading
|
||||
title="برمجة"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import Layout from "../../layouts/main.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">
|
||||
<PageHeading
|
||||
title="مسرح"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import Link from "../../components/Link.astro";
|
|||
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">
|
||||
<PageHeading
|
||||
title="نبذة عني"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import Link from "../components/Link.astro";
|
|||
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">
|
||||
<PageHeading
|
||||
title="Code"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ import Link from "../../components/Link.astro";
|
|||
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">
|
||||
<PageHeading
|
||||
title="About"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import Layout from "../../layouts/main.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">
|
||||
<PageHeading
|
||||
title="Acting"
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import Link from "../../components/Link.astro";
|
|||
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">
|
||||
<PageHeading
|
||||
title="Code"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import Layout from "../../layouts/main.astro";
|
|||
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 -->
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import Layout from "../layouts/main.astro";
|
|||
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 -->
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import Layout from "../layouts/main.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">
|
||||
<PageHeading
|
||||
title="Théâtre"
|
||||
|
|
|
|||
32
src/utils/page-translations.ts
Normal file
32
src/utils/page-translations.ts
Normal 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);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue