Pourquoi j'ai construit mon portfolio avec TanStack Start et Strapi
La stack derrière ce site et ses raisons : des routes typées, des fonctions serveur, un CMS que j'héberge moi-même, et trois langues dès le premier jour.
Dahmane Abdelmalek
20 oct. 20263 min de lecturePartie 1 sur 3
Un portfolio est un petit site, et il est tentant de prendre un modèle tout fait et de passer à autre chose. Je voulais que le mien soit un vrai projet : le genre de code que je serais fier de confier à un client. Il devait présenter mon travail en anglais, en arabe et en français, me permettre de modifier n'importe quel texte sans toucher au code, et rester peu coûteux à faire tourner sur mon propre serveur.
Cette série explique comment il est construit. Cette première partie couvre les deux choix les plus importants : TanStack Start pour le site, et Strapi pour le contenu.
L'organisation du projet
Un seul dépôt contient deux applications, gérées avec pnpm et Turborepo :
apps/webest le site : TanStack Start, TanStack Router et TanStack Query, mis en forme avec Tailwind CSS.apps/cmsest Strapi 5, où vivent chaque texte, chaque projet et chaque poste, dans les trois langues.
Les deux tournent dans des conteneurs Docker sur mon propre VPS, déployés avec Dokploy. Aucune dépendance à une plateforme, et une seule facture mensuelle prévisible.
Pourquoi TanStack Start
J'utilise Next.js pour la plupart de mes projets clients, ce changement était donc délibéré. Trois choses m'ont convaincu.
Les routes sont typées. Un lien vers une page qui n'existe pas ne compile pas. Chaque adresse de ce site commence par une langue (/en, /ar, /fr), et le vérificateur de types s'assure qu'aucun lien ne l'oublie.
Les fonctions serveur valident leurs entrées. Tout ce qui lit le CMS passe par une fonction serveur, avec un schéma Zod pour ses arguments. Le navigateur ne parle jamais directement à Strapi et ne connaît jamais son adresse interne.
export const getProjects = createServerFn({ method: 'GET' })
.validator(z.object({ lang: languageSchema }))
.handler(async ({ data }) => {
const response = await getStrapiClient()
.collection('projects')
.find({
locale: data.lang,
sort: ['order:asc', 'title:asc'],
populate: CARD_POPULATE,
})
return projectListSchema.parse(response).data
})Le chargement des données se compose. Le loader de chaque route demande à TanStack Query exactement ce dont la page a besoin. Les pages qui partagent des données partagent le résultat en cache : la page d'accueil et la page des projets lisent la même liste, et passer de l'une à l'autre ne coûte aucune nouvelle requête.
export const Route = createFileRoute('/$lang/projects/')({
validateSearch: projectsSearchSchema,
loader: ({ context, params }) =>
context.queryClient.query({
...projectsQuery(params.lang),
staleTime: 'static',
}),
component: ProjectsPage,
})La contrepartie, c'est la maturité. Next.js a des années de réponses sur tous les forums ; TanStack Start est plus jeune, et il m'est arrivé de lire son code source plutôt qu'un article de blog. Pour un site que je maintiens moi-même, la sûreté des types en valait la peine.
Pourquoi Strapi
Pour le contenu, je voulais une vraie interface d'édition, des traductions dignes de ce nom et l'auto-hébergement. Strapi 5 coche les trois cases :
- Des traductions par champ. Chaque texte existe en anglais, en arabe et en français, tandis que les dates, les liens et les images sont partagés entre les langues.
- Brouillon et publication. Je peux préparer un projet ou un article et le publier quand il est prêt. Le site ne demande jamais que du contenu publié.
- Des routes sur mesure là où il le faut. Les commentaires et les messages de contact viennent du public : ils passent par de petites routes dédiées, avec des limites de requêtes, plutôt que par l'API par défaut.
Le modèle de contenu reflète le site : un profil, des postes, des diplômes, des projets, des services, et désormais des articles, des tags et des séries. Quand j'ajoute un projet dans l'interface d'administration, il apparaît sur le site sans nouveau déploiement.
Ce que je ferais autrement
Arrêter le design avant de construire les pages. J'ai construit les premières pages, puis je les ai redessinées deux fois. Aujourd'hui, le design validé vit dans un document du dépôt, et chaque page est construite à partir de lui.
La partie 2 traite de l'aspect de ce design qui me tient le plus à cœur : faire de l'arabe une langue à part entière, et non une version inversée ajoutée à la fin.
Newsletter
Recevez les nouveaux articles par e-mail
Un e-mail par nouvel article. Désabonnement à tout moment.
Vous préférez un lecteur de flux ? Flux RSS
Réponses
Contact
Un projet en tête ?
Construisons-le.
Ouvert aux postes en télétravail et aux missions freelance.