Héritage i18n : les fichiers EN/AR ne surchargent que les champs traduits, les métadonnées partagées sont héritées de la base FR

Ajout de getLocalizedCollection/getLocalizedEntry dans content-i18n.ts pour fusionner automatiquement les entrées localisées avec leur base FR. Schémas adaptés (champs partagés optionnels), 78 fichiers de contenu allégés, consommateurs migrés.
This commit is contained in:
Jalil Arfaoui 2026-03-11 23:17:19 +01:00
parent a79e075fbb
commit 366d18764b
103 changed files with 199 additions and 489 deletions

View file

@ -1,7 +1,8 @@
---
import { getCollection, render } from "astro:content";
import { render } from "astro:content";
import { Image } from "astro:assets";
import { t, getDateLocale, type Locale } from "../../utils/i18n";
import { getLocalizedCollection } from "../../utils/content-i18n";
import "../../styles/exp-card.css";
interface Props {
@ -53,9 +54,9 @@ function computeDuration(startStr: string, endStr?: string) {
return formatDuration(years, months);
}
const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const experiences = (await getLocalizedCollection("experiences", locale))
.filter((e) => !e.data.draft)
.sort((a, b) => (b.data.startDate! > a.data.startDate! ? 1 : -1));
const presentLabel = t('career', 'present', locale);
const logoAltPrefix = t('career', 'logoAlt', locale);

View file

@ -3,7 +3,7 @@ import CategoryNav from './CategoryNav.astro';
import HeroViewport from './HeroViewport.astro';
import Lightbox from './Lightbox.astro';
import { Picture } from 'astro:assets';
import { getEntry } from 'astro:content';
import { getLocalizedEntry } from '../../utils/content-i18n';
import { getCategoryEntryId, type Locale } from '../../utils/i18n';
interface Props {
@ -13,10 +13,9 @@ interface Props {
const { category, lang = 'fr' } = Astro.props;
// Récupérer les métadonnées de la catégorie (localisées avec fallback FR)
// Récupérer les métadonnées de la catégorie (localisées avec fusion FR)
const entryId = getCategoryEntryId(category, lang);
const categoryData = await getEntry('photoCategories', entryId)
?? await getEntry('photoCategories', category);
const categoryData = await getLocalizedEntry('photoCategories', entryId, lang);
// Auto-détection des images du dossier de la catégorie
const allImages = import.meta.glob<{ default: ImageMetadata }>('/src/assets/images/photos/categories/**/*.{jpg,jpeg,png,webp}');

View file

@ -1,5 +1,5 @@
---
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../utils/content-i18n';
import HomeIcon from '../icons/HomeIcon.astro';
import { t, getPhotoBasePath, getPhotoBlogPath, getPhotoAlbumsPath, getHomePath, type Locale } from '../../utils/i18n';
@ -15,10 +15,8 @@ const photoBasePath = getPhotoBasePath(lang);
const homePath = getHomePath(lang);
// Récupérer les catégories depuis la collection, filtrées par langue
const allCategories = await getCollection('photoCategories');
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
const sortedCategories = effectiveCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
const localizedCategories = await getLocalizedCollection('photoCategories', lang);
const sortedCategories = localizedCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
// Extraire l'id de base (sans suffixe de langue)
const categories = sortedCategories.map(cat => ({

View file

@ -1,5 +1,5 @@
---
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../utils/content-i18n';
import { t, getPhotoBlogPath, getPhotoAlbumsPath, type Locale } from '../../utils/i18n';
interface Props {
@ -9,10 +9,8 @@ interface Props {
const { lang = 'fr' } = Astro.props;
// Récupérer les catégories filtrées par langue
const allCategories = await getCollection('photoCategories');
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
const sortedCategories = effectiveCategories.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
const sortedCategories = (await getLocalizedCollection('photoCategories', lang))
.sort((a, b) => (a.data.order || 99) - (b.data.order || 99));
---
<section id="explore-section" class="explore-section">

View file

@ -1,5 +1,5 @@
---
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../utils/content-i18n';
import { t, type Locale } from '../../utils/i18n';
interface Props {
@ -15,12 +15,10 @@ const { images, albumTitle = '', showCategory = false, category = '', lang = 'fr
const imagesForJS = JSON.stringify(images);
// Construire les labels depuis la collection filtrée par langue
const allCategories = await getCollection('photoCategories');
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
const localizedCategories = await getLocalizedCollection('photoCategories', lang);
const categoryLabels: Record<string, string> = {
'blog': t('photo', 'photoFeed', lang),
...Object.fromEntries(effectiveCategories.map(cat => [cat.id.replace(/\.(en|ar)$/, ''), cat.data.title]))
...Object.fromEntries(localizedCategories.map(cat => [cat.id.replace(/\.(en|ar)$/, ''), cat.data.title]))
};
---

View file

@ -2,7 +2,7 @@
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
import CategoryGrid from '../CategoryGrid.astro';
import { getCategoryEntryId, type Locale } from '../../../utils/i18n';
import { getEntry } from 'astro:content';
import { getLocalizedEntry } from '../../../utils/content-i18n';
interface Props {
category: string;
@ -13,8 +13,7 @@ const { category, lang = 'fr' } = Astro.props;
// Récupérer le titre localisé pour le title de la page
const entryId = getCategoryEntryId(category, lang);
const categoryData = await getEntry('photoCategories', entryId)
?? await getEntry('photoCategories', category);
const categoryData = await getLocalizedEntry('photoCategories', entryId, lang);
const categoryLabel = categoryData?.data.title || category;
const categorySubtitle = categoryData?.data.subtitle || "";

View file

@ -1,7 +1,7 @@
---
import PhotoLayout from '../../../layouts/PhotoLayout.astro';
import CategoryNav from '../CategoryNav.astro';
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../../utils/content-i18n';
import { Picture } from 'astro:assets';
import { t, getPhotoBlogPath, getDateLocale, getPostBaseSlug, type Locale } from '../../../utils/i18n';
@ -20,8 +20,7 @@ const allImages = import.meta.glob<{ default: ImageMetadata }>(
);
// Récupération des posts photo filtrés par langue
const allPhotoBlogPosts = (await getCollection('photoBlogPosts'))
.filter(post => (post.data.lang ?? 'fr') === lang);
const allPhotoBlogPosts = await getLocalizedCollection('photoBlogPosts', lang);
// Tri par date (plus récent en premier)
const sortedPosts = allPhotoBlogPosts.sort((a, b) =>

View file

@ -37,19 +37,19 @@ const projectsCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
category: z.enum(['dev', 'comedy', 'photo']),
date: z.date().optional(),
category: z.enum(['dev', 'comedy', 'photo']).optional(),
technologies: z.array(z.string()).optional(),
url: z.string().url().optional(),
github: z.string().url().optional(),
image: z.string().optional(),
imageAlt: z.string().optional(),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
lang: z.enum(['fr', 'en', 'ar']).default('fr'),
}).transform((data) => ({
...data,
dateFormatted: formatDate(data.date, data.lang),
dateFormatted: data.date ? formatDate(data.date, data.lang) : undefined,
})),
});
@ -57,16 +57,16 @@ const experiencesCollection = defineCollection({
loader: glob({ pattern: "**/*.md", base: "./src/content/experiences", ...stripExtension('md') }),
schema: z.object({
role: z.string(),
company: z.string(),
company: z.string().optional(),
companyUrl: z.string().url().optional(),
logo: z.string().optional(),
location: z.string().optional(),
startDate: z.string(),
startDate: z.string().optional(),
endDate: z.string().optional(),
technologies: z.array(z.string()).optional(),
type: z.enum(['employment', 'freelance', 'teaching', 'community', 'entrepreneurship']),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
type: z.enum(['employment', 'freelance', 'teaching', 'community', 'entrepreneurship']).optional(),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
lang: z.enum(['fr', 'en', 'ar']).default('fr'),
}),
});
@ -110,11 +110,11 @@ const photoBlogPostsCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
date: z.date(),
coverImage: z.string(),
date: z.date().optional(),
coverImage: z.string().optional(),
tags: z.array(z.string()).optional(),
featured: z.boolean().default(false),
draft: z.boolean().default(false),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
lang: z.enum(['fr', 'en', 'ar']).default('fr'),
}),
});

View file

@ -1,14 +1,6 @@
---
role: "مهندس معماري للواجهات"
company: "ARaymond"
companyUrl: "https://www.araymond.com/"
logo: "araymond.png"
location: "غرونوبل"
startDate: "2022-01"
endDate: "2023-01"
technologies: ["TypeScript", "React.js", "Redux", "Nx", "Vite"]
type: "freelance"
featured: true
lang: "ar"
---

View file

@ -1,14 +1,5 @@
---
role: "Frontend Architect"
company: "ARaymond"
companyUrl: "https://www.araymond.com/"
logo: "araymond.png"
location: "Grenoble"
startDate: "2022-01"
endDate: "2023-01"
technologies: ["TypeScript", "React.js", "Redux", "Nx", "Vite"]
type: "freelance"
featured: true
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "أستاذ هندسة البرمجيات"
company: "جامعة شامبوليون"
companyUrl: "https://www.univ-jfc.fr/"
logo: "champollion.png"
location: "ألبي"
startDate: "2019-09"
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
type: "teaching"
featured: true
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Software Engineering Professor"
company: "Université Champollion"
companyUrl: "https://www.univ-jfc.fr/"
logo: "champollion.png"
location: "Albi"
startDate: "2019-09"
technologies: ["TypeScript", "JavaScript", "Node.js", "TDD", "Clean Code"]
type: "teaching"
featured: true
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "حرفي برمجيات / مؤسس مشارك"
company: "DisMoi"
companyUrl: "https://github.com/dis-moi"
logo: "dismoi.png"
location: "عن بُعد"
startDate: "2019-01"
endDate: "2021-06"
technologies: ["TypeScript", "React.js", "Redux", "Web Extension", "Node.js"]
type: "entrepreneurship"
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Software Craftsman / Cofounder"
company: "DisMoi"
companyUrl: "https://github.com/dis-moi"
logo: "dismoi.png"
location: "Remote"
startDate: "2019-01"
endDate: "2021-06"
technologies: ["TypeScript", "React.js", "Redux", "Web Extension", "Node.js"]
type: "entrepreneurship"
lang: "en"
---

View file

@ -1,12 +1,6 @@
---
role: "مدرّس تطوير"
company: "ESN 81"
companyUrl: "https://www.esn81.fr/"
location: "كاستر"
startDate: "2020-09"
endDate: "2021-06"
technologies: ["JavaScript", "Node.js"]
type: "teaching"
lang: "ar"
---

View file

@ -1,12 +1,5 @@
---
role: "Development Teacher"
company: "ESN 81"
companyUrl: "https://www.esn81.fr/"
location: "Castres"
startDate: "2020-09"
endDate: "2021-06"
technologies: ["JavaScript", "Node.js"]
type: "teaching"
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "مطوّر ويب"
company: "e-Themis"
companyUrl: "https://www.e-themis.com/"
logo: "ethemis.png"
location: "فرساي"
startDate: "2002-06"
endDate: "2002-12"
technologies: ["PHP", "JavaScript", "HTML", "MySQL"]
type: "employment"
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Web Developer"
company: "e-Themis"
companyUrl: "https://www.e-themis.com/"
logo: "ethemis.png"
location: "Versailles"
startDate: "2002-06"
endDate: "2002-12"
technologies: ["PHP", "JavaScript", "HTML", "MySQL"]
type: "employment"
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "مهندس برمجيات أول"
company: "e-Themis"
companyUrl: "https://www.e-themis.com/"
logo: "ethemis.png"
location: "فرساي"
startDate: "2011-01"
endDate: "2015-12"
technologies: ["PHP", "Symfony", "JavaScript", "MySQL", "Linux"]
type: "employment"
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Senior Software Engineer"
company: "e-Themis"
companyUrl: "https://www.e-themis.com/"
logo: "ethemis.png"
location: "Versailles"
startDate: "2011-01"
endDate: "2015-12"
technologies: ["PHP", "Symfony", "JavaScript", "MySQL", "Linux"]
type: "employment"
lang: "en"
---

View file

@ -1,11 +1,6 @@
---
role: "مطوّر ويب وبرمجيات"
company: "مستقل"
location: "إيل دو فرانس"
startDate: "2003-01"
endDate: "2006-12"
technologies: ["PHP", "JavaScript", "HTML", "MySQL", "Linux"]
type: "freelance"
lang: "ar"
---

View file

@ -1,11 +1,5 @@
---
role: "Web & Software Developer"
company: "Freelance"
location: "Île-de-France"
startDate: "2003-01"
endDate: "2006-12"
technologies: ["PHP", "JavaScript", "HTML", "MySQL", "Linux"]
type: "freelance"
lang: "en"
---

View file

@ -1,12 +1,6 @@
---
role: "مدير تقني"
company: "GoBuild (Go-Decision)"
companyUrl: "https://www.gobuild.fr"
location: "ليون"
startDate: "2020-06"
endDate: "2022-01"
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
type: "employment"
lang: "ar"
---

View file

@ -1,12 +1,5 @@
---
role: "CTO"
company: "GoBuild (Go-Decision)"
companyUrl: "https://www.gobuild.fr"
location: "Lyon"
startDate: "2020-06"
endDate: "2022-01"
technologies: ["TypeScript", "React.js", "Elixir", "PostgreSQL", "Docker"]
type: "employment"
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "مهندس برمجيات fullstack"
company: "Libeo"
companyUrl: "https://www.libeo.io/"
logo: "libeo.png"
location: "عن بُعد"
startDate: "2021-01"
endDate: "2021-06"
technologies: ["TypeScript", "React.js", "Node.js", "GraphQL"]
type: "freelance"
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Fullstack Software Engineer"
company: "Libeo"
companyUrl: "https://www.libeo.io/"
logo: "libeo.png"
location: "Remote"
startDate: "2021-01"
endDate: "2021-06"
technologies: ["TypeScript", "React.js", "Node.js", "GraphQL"]
type: "freelance"
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "مهندس برمجيات أول"
company: "Obat"
companyUrl: "https://www.obat.fr/"
logo: "obat.png"
location: "عن بُعد"
startDate: "2023-02"
endDate: "2024-01"
technologies: ["TypeScript", "React.js", "Node.js", "NestJS", "PostgreSQL"]
type: "freelance"
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Senior Software Engineer"
company: "Obat"
companyUrl: "https://www.obat.fr/"
logo: "obat.png"
location: "Remote"
startDate: "2023-02"
endDate: "2024-01"
technologies: ["TypeScript", "React.js", "Node.js", "NestJS", "PostgreSQL"]
type: "freelance"
lang: "en"
---

View file

@ -1,12 +1,6 @@
---
role: "مؤسّس ومدير عام"
company: "Team Logics"
location: "فرساي"
startDate: "2007-01"
endDate: "2011-12"
technologies: ["PHP", "CakePHP", "JavaScript", "MySQL", "Linux"]
type: "entrepreneurship"
featured: true
lang: "ar"
---

View file

@ -1,12 +1,5 @@
---
role: "Founder & CEO"
company: "Team Logics"
location: "Versailles"
startDate: "2007-01"
endDate: "2011-12"
technologies: ["PHP", "CakePHP", "JavaScript", "MySQL", "Linux"]
type: "entrepreneurship"
featured: true
lang: "en"
---

View file

@ -1,13 +1,6 @@
---
role: "مطوّر رئيسي"
company: "Urssaf Caisse nationale"
companyUrl: "https://www.urssaf.fr/"
logo: "urssaf.png"
location: "عن بُعد / باريس"
startDate: "2024-02"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
type: "freelance"
featured: true
lang: "ar"
---

View file

@ -1,13 +1,5 @@
---
role: "Lead Developer"
company: "Urssaf Caisse nationale"
companyUrl: "https://www.urssaf.fr/"
logo: "urssaf.png"
location: "Remote / Paris"
startDate: "2024-02"
technologies: ["TypeScript", "React.js", "Publicodes", "Node.js", "GitHub"]
type: "freelance"
featured: true
lang: "en"
---

View file

@ -1,14 +1,6 @@
---
role: "مطوّر رئيسي ← قائد تقني"
company: "Veepee"
companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "باريس"
startDate: "2016-02"
endDate: "2019-06"
technologies: ["TypeScript", "JavaScript", "React.js", "Redux", "redux-saga", "RxJS", "Node.js", "Webpack"]
type: "freelance"
featured: true
lang: "ar"
---

View file

@ -1,14 +1,5 @@
---
role: "Lead Developer → Tech Lead"
company: "Veepee"
companyUrl: "https://www.veepee.com/"
logo: "veepee.png"
location: "Paris"
startDate: "2016-02"
endDate: "2019-06"
technologies: ["TypeScript", "JavaScript", "React.js", "Redux", "redux-saga", "RxJS", "Node.js", "Webpack"]
type: "freelance"
featured: true
lang: "en"
---

View file

@ -1,11 +1,6 @@
---
title: "تصوير إيرول"
description: "أراد إيرول صورًا له لإعداد كتاب أعمال. عملنا طوال اليوم لتنويع الأجواء..."
date: 2011-10-02
coverImage: "18-Eroll-Shooting-1-19.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Shooting Eroll"
description: "Eroll wanted some photos of him in order to have a modeling book. We worked all day in order to have some ambiance variations..."
date: 2011-10-02
coverImage: "18-Eroll-Shooting-1-19.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "Inox Park Paris 2011"
description: "بعد نجاحه في 2010، يعود مهرجان Inox Park Paris إلى جزيرة شاتو في نسخته الثانية. ثلاث مسارح، 15 دي جي، 12 ساعة من الحفل في الهواء الطلق: Tiësto، Joachim Garraud، Sven Väth، Steve Aoki..."
date: 2011-09-10
coverImage: "01-Inox-Park-Paris-Chatou-2011.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Inox Park Paris 2011"
description: "After its 2010 success, the Inox Park Paris Electro Festival is back to the island of Chatou for its second edition. Three stages, 15 DJs, 12 hours of outdoor party: Tiësto, Joachim Garraud, Sven Väth, Steve Aoki..."
date: 2011-09-10
coverImage: "01-Inox-Park-Paris-Chatou-2011.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "عملية المحفظة 2012"
description: "توزيع محافظ مدرسية مجانية في مدارس محرومة من طرف جمعية محلية (JCI)، طنجة، المغرب."
date: 2012-09-30
coverImage: "35-Moroccan-Schoolgirls.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Schoolbag Operation 2012"
description: "During a distribution of free schoolbags in poor schools by a local association (JCI), Tangier, Morocco."
date: 2012-09-30
coverImage: "35-Moroccan-Schoolgirls.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "جولة في طنجة"
description: "جولة فوتوغرافية في شوارع طنجة."
date: 2012-05-26
coverImage: "01-Observer-le-changement.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Tangier Walk"
description: "A photographic walk through the streets of Tangier."
date: 2012-05-26
coverImage: "01-Observer-le-changement.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "هلسنكي"
description: "اختفى الثلج من هلسنكي وسرعان ما أفسح المجال للربيع..."
date: 2013-05-15
coverImage: "01-Library-of-University-of-Helsinki.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Helsinki"
description: "The snow has disappeared from Helsinki and quickly gave way to spring..."
date: 2013-05-15
coverImage: "01-Library-of-University-of-Helsinki.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "نزهة في إفران"
description: "نزهة شتوية في جبال الأطلس المتوسط."
date: 2013-01-13
coverImage: "03-3.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Ifrane Hike"
description: "Winter hike in the Middle Atlas mountains."
date: 2013-01-13
coverImage: "03-3.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "London Calling"
description: "عطلة نهاية أسبوع فوتوغرافية في لندن."
date: 2014-07-15
coverImage: "01-The-sky-inside.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "London Calling"
description: "A photographic weekend in London."
date: 2014-07-15
coverImage: "01-The-sky-inside.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "أحد سيكواني"
description: "نزهة يوم أحد على ضفاف نهر السين."
date: 2014-05-18
coverImage: "04-La-Defense-seen-from-Pont-de-Suresnes-2.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Sequanian Sunday"
description: "A sunday walk near the Seine."
date: 2014-05-18
coverImage: "04-La-Defense-seen-from-Pont-de-Suresnes-2.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "تجوال في مدينة طنجة القديمة"
description: "أثناء التجوال في أزقة طنجة القديمة، صادفت ساعاتيًا، ونجّارًا، وقمرًا عملاقًا..."
date: 2014-08-10
coverImage: "01-The-watchmaker.jpg"
tags: []
featured: true
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Wandering Tangier Medina"
description: "Walking in the streets of the old Tangier, met a watchmaker, a carpenter and a super-moon..."
date: 2014-08-10
coverImage: "01-The-watchmaker.jpg"
tags: []
featured: true
draft: false
lang: en
---

View file

@ -4,11 +4,6 @@ description: |
إنيغما كانت مسابقة بحث عن الكنز بين المدارس نظّمها المركز العالي للدراسات CESIM يوم السبت، تحت عنوان «البحث عن كنز ابن بطوطة المنسي». 4 فرق طنجاوية دُعيت لتمثيل مؤسساتها.
تغطية.
date: 2015-04-25
coverImage: "01-Enigma-v1.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -4,11 +4,6 @@ description: |
Enigma was an inter-school treasure hunt organized this saturday by the CESIM, on the theme "In search of forgotten treasure of Ibn Battuta". 4 Tangier teams were invited to represent their respective institutions.
Recap.
date: 2015-04-25
coverImage: "01-Enigma-v1.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "Field of Stones"
description: "كواليس تصوير غلاف ألبوم ماركو وولتر. أراد أن تُلتقط الصورة في سينماتيك طنجة. لا وقت، لا إضاءة، لكن لا خيار. حاولنا تقديم أفضل ما لدينا..."
date: 2015-04-02
coverImage: "01-Marco-Wolter-Field-of-Stones-2.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Field of Stones"
description: "Making of the album cover for Marco Wolter. He wanted it to be shot in the Cinémathèque of Tangier. We had no time, no light, but no choice. We tried to make the best of it..."
date: 2015-04-02
coverImage: "01-Marco-Wolter-Field-of-Stones-2.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "لا رياح في لاس كويفاس"
description: "كان من المفترض أن يكون يومًا مثاليًا لتطيير طائرتنا الورقية: مشمس وعاصف. مشمس كان، لكن الرياح لم تأتِ أبدًا."
date: 2015-01-10
coverImage: "13-No-wind-at-Las-Cuevas.jpg"
tags: []
featured: false
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "No Wind at Las Cuevas"
description: "It was supposed to be a perfect day for flying our kite: sunny and windy. Sunny it was, but the wind never came."
date: 2015-01-10
coverImage: "13-No-wind-at-Las-Cuevas.jpg"
tags: []
featured: false
draft: false
lang: en
---

View file

@ -1,11 +1,6 @@
---
title: "زفاف أورور وتوما"
description: "كان لي شرف ومتعة أن أكون شاهد توما في زفافه الجميل مع أورور. ليس سهلًا التصوير في نفس الوقت، لكن كل الصور مليئة بالحب."
date: 2015-09-26
coverImage: "10-Mariage-Aurore-Thomas-10.jpg"
tags: []
featured: true
draft: false
lang: ar
---

View file

@ -1,11 +1,6 @@
---
title: "Wedding Aurore & Thomas"
description: "I had the honor and pleasure to be Thomas' best man for his beautiful wedding with Aurore. Not easy to shoot pictures though, but all of them are filled with love."
date: 2015-09-26
coverImage: "10-Mariage-Aurore-Thomas-10.jpg"
tags: []
featured: true
draft: false
lang: en
---

View file

@ -1,6 +1,5 @@
{
"title": "ثقافات وتقاليد",
"subtitle": "ثراء التقاليد الإنسانية",
"order": 4,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Cultures & Traditions",
"subtitle": "Richness of human traditions",
"order": 4,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "محرّكات",
"subtitle": "ميكانيكا وقوة في حركة",
"order": 7,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Engines",
"subtitle": "Mechanics and power in motion",
"order": 7,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "يوميات",
"subtitle": "لحظات من الحياة اليومية",
"order": 8,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Everyday Life",
"subtitle": "Moments of everyday life",
"order": 8,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "موسيقى واحتفالات",
"subtitle": "نغمات وأغانٍ واهتزازات تحتفي بلحظات حياتنا الكبيرة والصغيرة",
"order": 5,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Music & Celebrations",
"subtitle": "Notes, songs and vibrations celebrating life's big and small moments",
"order": 5,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "طبيعة",
"subtitle": "سحر العالم الطبيعي",
"order": 3,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Nature",
"subtitle": "The magic of the natural world",
"order": 3,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "مناظر طبيعية",
"subtitle": "جمال المناظر والأماكن",
"order": 2,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Landscapes",
"subtitle": "Beauty of landscapes and places",
"order": 2,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "بورتريهات",
"subtitle": "تعابير ومشاعر ملتقطة",
"order": 1,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Portraits",
"subtitle": "Captured expressions and emotions",
"order": 1,
"lang": "en"
}

View file

@ -1,6 +1,5 @@
{
"title": "رياضة",
"subtitle": "حركة وجهد وتجاوز للذات",
"order": 6,
"lang": "ar"
}

View file

@ -1,6 +1,5 @@
{
"title": "Sports",
"subtitle": "Movement, effort and pushing limits",
"order": 6,
"lang": "en"
}

View file

@ -1,11 +1,6 @@
---
title: "Débats.co"
description: "منصّة تعاونية لتلخيص النقاشات المجتمعية. ويكيبيديا المواقف."
date: 2015-01-01
category: "dev"
technologies: ["Next.js", "React", "TypeScript", "Supabase", "PostgreSQL", "Effect TS"]
url: "https://debats.co"
featured: true
lang: "ar"
---

View file

@ -1,11 +1,6 @@
---
title: "Débats.co"
description: "Collaborative platform for synthesizing public debates. The Wikipedia of public stances."
date: 2015-01-01
category: "dev"
technologies: ["Next.js", "React", "TypeScript", "Supabase", "PostgreSQL", "Effect TS"]
url: "https://debats.co"
featured: true
lang: "en"
---

View file

@ -1,12 +1,6 @@
---
title: "DisMoi"
description: "إضافة متصفّح في مجال التكنولوجيا المدنية تضيف معلومات سياقية على أي صفحة ويب."
date: 2019-01-01
category: "dev"
technologies: ["TypeScript", "React.js", "Redux", "Web Extension", "PHP", "Symfony"]
url: "https://www.dismoi.io/"
github: "https://github.com/dis-moi"
featured: true
lang: "ar"
---

View file

@ -1,12 +1,6 @@
---
title: "DisMoi"
description: "Civic tech browser extension that adds contextual information to any web page."
date: 2019-01-01
category: "dev"
technologies: ["TypeScript", "React.js", "Redux", "Web Extension", "PHP", "Symfony"]
url: "https://www.dismoi.io/"
github: "https://github.com/dis-moi"
featured: true
lang: "en"
---

View file

@ -1,9 +1,6 @@
---
title: "ICU"
description: "منصة مشاركة صور عبر الإنترنت، مشروع شخصي تاريخي."
date: 2005-01-01
category: "dev"
technologies: ["PHP", "JavaScript", "MySQL"]
lang: "ar"
---

View file

@ -1,9 +1,6 @@
---
title: "ICU"
description: "Online photo sharing platform, a historical personal project."
date: 2005-01-01
category: "dev"
technologies: ["PHP", "JavaScript", "MySQL"]
lang: "en"
---

View file

@ -1,12 +1,6 @@
---
title: "mon-entreprise.urssaf.fr"
description: "المساعد الرسمي لروّاد الأعمال في فرنسا. أكثر من 20 محاكيًا اجتماعيًا-ضريبيًا، مليون مستخدم شهريًا."
date: 2024-01-01
category: "dev"
technologies: ["TypeScript", "React.js", "Publicodes", "Redux", "Node.js"]
url: "https://mon-entreprise.urssaf.fr/"
github: "https://github.com/betagouv/mon-entreprise"
featured: false
lang: "ar"
---

View file

@ -1,12 +1,6 @@
---
title: "mon-entreprise.urssaf.fr"
description: "The official assistant for entrepreneurs in France. Over 20 socio-fiscal simulators, one million users per month."
date: 2024-01-01
category: "dev"
technologies: ["TypeScript", "React.js", "Publicodes", "Redux", "Node.js"]
url: "https://mon-entreprise.urssaf.fr/"
github: "https://github.com/betagouv/mon-entreprise"
featured: false
lang: "en"
---

View file

@ -1,9 +1,6 @@
---
title: "N.Gine"
description: "نظام إدارة محتوى خاص بُني من الصفر، استُخدم للعديد من مشاريع العملاء."
date: 2004-01-01
category: "dev"
technologies: ["PHP", "JavaScript", "MySQL"]
lang: "ar"
---

View file

@ -1,9 +1,6 @@
---
title: "N.Gine"
description: "Proprietary CMS built from scratch, used for many client projects."
date: 2004-01-01
category: "dev"
technologies: ["PHP", "JavaScript", "MySQL"]
lang: "en"
---

View file

@ -1,16 +1,17 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../utils/content-i18n";
import Layout from "../../layouts/main.astro";
import jalilPhoto from "../../assets/images/jalil-2.jpg";
const currentMission = (await getCollection("experiences"))
.filter((e) => e.data.lang === "ar" && !e.data.draft && !e.data.endDate)
const currentMission = (await getLocalizedCollection("experiences", "ar"))
.filter((e) => !e.data.draft && !e.data.endDate)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1))[0];
const featuredProjects = (await getCollection("projects"))
.filter((p) => p.data.lang === "ar" && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
const featuredProjects = (await getLocalizedCollection("projects", "ar"))
.filter((p) => !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime())
.slice(0, 2);
const recommendationCount = (await getCollection("recommendations")).length;

View file

@ -1,5 +1,6 @@
---
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../../utils/content-i18n";
import { Image } from "astro:assets";
import Layout from "../../../layouts/main.astro";
import Link from "../../../components/Link.astro";
@ -17,15 +18,15 @@ import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
const locale = "ar";
const projectsBasePath = getProjectsBasePath(locale);
const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft)
const experiences = (await getLocalizedCollection("experiences", locale))
.filter((e) => !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const recentExperiences = experiences.filter((e) => e.data.featured).slice(0, 4);
const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const projects = (await getLocalizedCollection("projects", locale))
.filter((p) => !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime());
const recommendations = (await getCollection("recommendations"))
.filter((r) => r.data.featured)

View file

@ -1,5 +1,5 @@
---
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../../utils/content-i18n";
import Layout from "../../../layouts/main.astro";
import ProjectCard from "../../../components/code/ProjectCard.astro";
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
@ -7,11 +7,11 @@ import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
const locale = "ar";
const projectsBasePath = getProjectsBasePath(locale);
const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
const projects = (await getLocalizedCollection("projects", locale))
.filter((p) => !p.data.draft && p.data.category === "dev")
.sort((a, b) => {
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
return b.data.date.getTime() - a.data.date.getTime();
return b.data.date!.getTime() - a.data.date!.getTime();
});
---

View file

@ -1,11 +1,11 @@
---
import ProjectDetailContent from '../../../../components/code/ProjectDetailContent.astro';
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../../../utils/content-i18n';
import { getProjectBaseSlug } from '../../../../utils/i18n';
export async function getStaticPaths() {
const allProjects = await getCollection('projects');
const arProjects = allProjects.filter(p => p.data.lang === 'ar' && !p.data.draft && p.data.category === 'dev');
const arProjects = (await getLocalizedCollection('projects', 'ar'))
.filter(p => !p.data.draft && p.data.category === 'dev');
return arProjects.map(project => ({
params: { slug: getProjectBaseSlug(project.id) },
props: { project },

View file

@ -1,11 +1,10 @@
---
import PhotoBlogPostContent from '../../../../../components/photo/pages/PhotoBlogPostContent.astro';
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../../../../utils/content-i18n';
import { getPostBaseSlug } from '../../../../../utils/i18n';
export async function getStaticPaths() {
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
const arPosts = allPhotoBlogPosts.filter(post => post.data.lang === 'ar');
const arPosts = await getLocalizedCollection('photoBlogPosts', 'ar');
return arPosts.map(post => {
const slug = getPostBaseSlug(post.id);
return {

View file

@ -1,5 +1,6 @@
---
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../utils/content-i18n";
import { Image } from "astro:assets";
import Layout from "../../layouts/main.astro";
import Link from "../../components/Link.astro";
@ -17,15 +18,15 @@ import logoTiqa from "../../assets/images/logo-tiqa-blanc.png";
const locale = "fr";
const projectsBasePath = getProjectsBasePath(locale);
const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft)
const experiences = (await getLocalizedCollection("experiences", locale))
.filter((e) => !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const recentExperiences = experiences.filter((e) => e.data.featured).slice(0, 4);
const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const projects = (await getLocalizedCollection("projects", locale))
.filter((p) => !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime());
const recommendations = (await getCollection("recommendations"))
.filter((r) => r.data.featured)

View file

@ -1,5 +1,5 @@
---
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../utils/content-i18n";
import Layout from "../../layouts/main.astro";
import ProjectCard from "../../components/code/ProjectCard.astro";
import { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
@ -7,11 +7,11 @@ import { getProjectBaseSlug, getProjectsBasePath } from "../../utils/i18n";
const locale = "fr";
const projectsBasePath = getProjectsBasePath(locale);
const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
const projects = (await getLocalizedCollection("projects", locale))
.filter((p) => !p.data.draft && p.data.category === "dev")
.sort((a, b) => {
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
return b.data.date.getTime() - a.data.date.getTime();
return b.data.date!.getTime() - a.data.date!.getTime();
});
---

View file

@ -1,11 +1,11 @@
---
import ProjectDetailContent from '../../../components/code/ProjectDetailContent.astro';
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../../utils/content-i18n';
import { getProjectBaseSlug } from '../../../utils/i18n';
export async function getStaticPaths() {
const allProjects = await getCollection('projects');
const frProjects = allProjects.filter(p => p.data.lang === 'fr' && !p.data.draft && p.data.category === 'dev');
const frProjects = (await getLocalizedCollection('projects', 'fr'))
.filter(p => !p.data.draft && p.data.category === 'dev');
return frProjects.map(project => ({
params: { slug: getProjectBaseSlug(project.id) },
props: { project },

View file

@ -1,5 +1,6 @@
---
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../../utils/content-i18n";
import { Image } from "astro:assets";
import Layout from "../../../layouts/main.astro";
import Link from "../../../components/Link.astro";
@ -17,15 +18,15 @@ import logoTiqa from "../../../assets/images/logo-tiqa-blanc.png";
const locale = "en";
const projectsBasePath = getProjectsBasePath(locale);
const experiences = (await getCollection("experiences"))
.filter((e) => e.data.lang === locale && !e.data.draft)
const experiences = (await getLocalizedCollection("experiences", locale))
.filter((e) => !e.data.draft)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1));
const recentExperiences = experiences.filter((e) => e.data.featured).slice(0, 4);
const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const projects = (await getLocalizedCollection("projects", locale))
.filter((p) => !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime());
const recommendations = (await getCollection("recommendations"))
.filter((r) => r.data.featured)

View file

@ -1,5 +1,5 @@
---
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../../utils/content-i18n";
import Layout from "../../../layouts/main.astro";
import ProjectCard from "../../../components/code/ProjectCard.astro";
import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
@ -7,11 +7,11 @@ import { getProjectBaseSlug, getProjectsBasePath } from "../../../utils/i18n";
const locale = "en";
const projectsBasePath = getProjectsBasePath(locale);
const projects = (await getCollection("projects"))
.filter((p) => p.data.lang === locale && !p.data.draft && p.data.category === "dev")
const projects = (await getLocalizedCollection("projects", locale))
.filter((p) => !p.data.draft && p.data.category === "dev")
.sort((a, b) => {
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
return b.data.date.getTime() - a.data.date.getTime();
return b.data.date!.getTime() - a.data.date!.getTime();
});
---

View file

@ -1,11 +1,11 @@
---
import ProjectDetailContent from '../../../../components/code/ProjectDetailContent.astro';
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../../../utils/content-i18n';
import { getProjectBaseSlug } from '../../../../utils/i18n';
export async function getStaticPaths() {
const allProjects = await getCollection('projects');
const enProjects = allProjects.filter(p => p.data.lang === 'en' && !p.data.draft && p.data.category === 'dev');
const enProjects = (await getLocalizedCollection('projects', 'en'))
.filter(p => !p.data.draft && p.data.category === 'dev');
return enProjects.map(project => ({
params: { slug: getProjectBaseSlug(project.id) },
props: { project },

View file

@ -1,16 +1,17 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../../utils/content-i18n";
import Layout from "../../layouts/main.astro";
import jalilPhoto from "../../assets/images/jalil-2.jpg";
const currentMission = (await getCollection("experiences"))
.filter((e) => e.data.lang === "en" && !e.data.draft && !e.data.endDate)
const currentMission = (await getLocalizedCollection("experiences", "en"))
.filter((e) => !e.data.draft && !e.data.endDate)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1))[0];
const featuredProjects = (await getCollection("projects"))
.filter((p) => p.data.lang === "en" && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
const featuredProjects = (await getLocalizedCollection("projects", "en"))
.filter((p) => !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime())
.slice(0, 2);
const recommendationCount = (await getCollection("recommendations")).length;

View file

@ -1,11 +1,10 @@
---
import PhotoBlogPostContent from '../../../../../components/photo/pages/PhotoBlogPostContent.astro';
import { getCollection } from 'astro:content';
import { getLocalizedCollection } from '../../../../../utils/content-i18n';
import { getPostBaseSlug } from '../../../../../utils/i18n';
export async function getStaticPaths() {
const allPhotoBlogPosts = await getCollection('photoBlogPosts');
const enPosts = allPhotoBlogPosts.filter(post => post.data.lang === 'en');
const enPosts = await getLocalizedCollection('photoBlogPosts', 'en');
return enPosts.map(post => {
const slug = getPostBaseSlug(post.id);
return {

View file

@ -1,16 +1,17 @@
---
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import { getLocalizedCollection } from "../utils/content-i18n";
import Layout from "../layouts/main.astro";
import jalilPhoto from "../assets/images/jalil-2.jpg";
const currentMission = (await getCollection("experiences"))
.filter((e) => e.data.lang === "fr" && !e.data.draft && !e.data.endDate)
const currentMission = (await getLocalizedCollection("experiences", "fr"))
.filter((e) => !e.data.draft && !e.data.endDate)
.sort((a, b) => (b.data.startDate > a.data.startDate ? 1 : -1))[0];
const featuredProjects = (await getCollection("projects"))
.filter((p) => p.data.lang === "fr" && !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
const featuredProjects = (await getLocalizedCollection("projects", "fr"))
.filter((p) => !p.data.draft && p.data.category === "dev" && p.data.featured)
.sort((a, b) => b.data.date!.getTime() - a.data.date!.getTime())
.slice(0, 2);
const recommendationCount = (await getCollection("recommendations")).length;

Some files were not shown because too many files have changed in this diff Show more