Ouverture de mon blog !
Que ce soit du choix de la plateforme, à la façon dont je gère mes données avec DatoCMS, ou pour parler des diverses difficultés que j'ai pu rencontrer. Je reviens, dans cet article, sur les détails de l'ouverture de mon blog, antoinelin.com.
Introduction
Après plus de 3 ans de "site en construction", mon blog est enfin en ligne. Créer son site internet, c'est bâtir un espace qui est censé refléter votre personnalité. Un petit coin d'Internet qui vous appartient sur lequel vous voulez vous exprimer. Que ce soit par le contenu, mais aussi par la direction artistique, ou l'attention portée aux détails, c'est votre image en ligne que vous défendez. Subtilement, mais de façon discernable, chaque décision que vous prenez et chaque élément que vous montrez à son importance. Et si vous êtes indépendant, c'est également une vitrine marketing que vous construisez. Le site où vous lisez ces quelques lignes représente, pour moi, l'aboutissement d'un profond travail d'introspection. De la première maquette, au choix de quelle librairie de CSS in JS j'ai utilisé, ou pour parler des difficultés que j'ai rencontré. J'aimerais revenir, dans cet article, sur le pourquoi et sur le comment j'ai conçu et développé mon site internet antoinelin.com.
Solution clé en main, ou from scratch ?
Si vous avez déjà envisagé de créer un blog, et surtout si vous êtes un développeur, vous vous êtes peut-être déjà demandé, comme moi, quelle est la solution à plébisciter en matière de plateforme de publication en ligne. Développer un site, from scratch, indépendant ? Passer par un thème WordPress ? Ou utiliser un service tel que Medium pour ne pas "réinventer la roue" et bénéficier de son écosystème de partage ? Au final, le temps qu'on met à concevoir et développer son site internet, c'est le temps qu'on peut mettre à créer du contenu, n'est-ce pas ?
Personnellement, je voulais que ma plateforme soit une expression de ma personnalité. On éjecte donc d'emblée la piste du thème WordPress qui représente un frein à la créativité. D'autant plus que je préfère avoir la main sur la technologie pour m'assurer du niveau de qualité du code.
Concernant les services comme Medium, j'estime qu'ils représentent l'équivalent des fast-foods du contenu rédactionnel en ligne. Je ne souhaite pas que mes articles se perdent parmis les millions d'autres sur ces plateformes.
Bonjour Monsieur Lin. Voici les cinq articles du jour. Vous avez terminé de les lires ? Vous en reprendrez bien un de plus pour le dessert. Celui-ci ne vous plaît pas ? Aucun problème, vous pouvez swipper au prochain d'un mouvement de doigt.
Enfin, créer son site internet, c'est se soucier des composantes fondamentales d'un projet web "classique". UX, UI, travail de SEO, contenu, et pour les plus tech, c'est un moyen de montrer son code en dehors des dépôts Git. Dans mon cas, c'est aussi une vitrine pour mes clients. Par ce biais, je leur prouve que je suis capable de développer un produit, rien de plus utile pour prouver que je suis légitime de construire le leur.
Si je devais résumer, c'est surtout par critère d'indépendance que j'ai choisi de ne pas passer par un thème ou un service. Beaucoup de développeurs pensent que prendre le temps de créer son propre site est un travail supplémentaire inutile. Ce qu'ils ne comprennent pas, c'est que par ce biais, ils ont la possibilité de se démarquer des autres profils, qui eux ne le font pas.
L'inconvénient, c'est que j'ai mis bien plusieurs mois à le terminer. En cause, mon envie de vouloir (trop) bien faire, qui m'a valut beaucoup d'itérations sur la partie graphique et beaucoup d'hésitations sur le choix des technologies.
Alors, blog from scratch, thème pré-fait, ou service dédié ? Vous avez mon avis sur la question.
Un blog ? Pour y proposer quel contenu ?
C'est bien beau de développer un blog, mais ce qui en fait sa pertinence, c'est avant tout son contenu vous ne pensez pas ? Voici une courte liste des différents articles que vous pourrez trouver ici :
Du contenu éditorial sur l'univers du développement web,
Des tutoriels, sous la forme de solutions étapes par étapes à des problématiques précises,
Des nouvelles de mon activité professionnelle,
Des retours sur mes expériences et sur les difficultés que j'ai pu rencontrer dans ma jeune carrière de développeur web,
Y écrire, par moments, des billets d'humeurs sur des sujets qui peuvent me toucher.
Dans la généralité, j'aimerais que ce blog soit orienté autour du monde du développement web et de la technologie. Mais je souhaite également que cette plateforme soit un endroit d'expression personnel, où je me sens libre de publier ce que bon me semble.
Également (et attention, interlude publicitaire) vous trouverez sur ma chaîne YouTube des vidéos en rapport avec certains de mes articles. Le plus souvent, vous aurez le droit à la vidéo en fin de page ! Donc n'hésitez pas à vous abonner 👀
Et niveau tech, ça donne quoi ?
Rapide coup d'oeil sur la stack
Front-end
Le site a été construit sous TypeScript et React.js avec le framework Next.js.
Par défaut, Next.js effectue un "pré-rendu" de chaque page : il génère le contenu à
l'avance via deux solutions différentes, au choix. La première est la génération statique, qui récupère la donnée et transmet le contenu lors de la construction (au build) du site. Et la seconde est le rendu côté serveur, qui pré-rend, côté serveur donc, une page à la demande de utilisateur qui la visite (au runtime). Pour ce blog, j'ai opté pour la génération statique afin de limiter les pics de charge serveur et optimiser les performances de rendu. Si vous souhaitez en savoir plus sur ce concept, je vous invite à lire cette page de la documentation officielle de Next.js.
Pour le style, j'utilise TailwindCSS en mode preview avec l'option Just-in-time activé. Pour le moment, j'écris mon style en inline (directement rédigé dans le DOM virtuel de React) mais qui devrait passer par du CSS Modules d'ici peu. Et pour composer mes classNames j'utilise l'utilitaire éponyme : classNames.
Enfin, les animations sont faites avec Framer Motion pour la manipulation du DOM et avec Paper.js pour le curseur "iPad OS".
Back-endPour le back-end, Next.js nous met à disposition des routes API accessibles sous REST dans lesquels on peut déclarer du code qui s'exécutera côté serveur. Très utile si vous souhaitez faire de la persistence avec une base de donnée ou faire appel à des services tiers en évitant de compiler vos tokens dans le code front-end. En parlant de services tiers et pour la gestion du contenu du site, j'utilise un Headless CMS qui s'appelle DatoCMS et je récupère la donnée en GraphQL avec la librairie Urql. L'ensemble de ma logique est écrite sous observables avec RxJS et j'utilise les API suivantes pour afficher les différents petits modules sur le site :
- Spotify API pour le composant dans le footer qui affiche la musique que j'écoute en direct sur Spotify,
- Twitch API pour afficher la bannière qui avertit quand je suis en direct sur Twitch,
- Google Analytics pour récupérer et afficher le nombre de vues de chacun de mes articles.
Pour réduire le nombre d'appels aux API ci-dessus et réduire le temps de mes requêtes entre mon front-end et mon back-end, j'ai ajouté de la persistence avec un cache normalisé lru-cache.
HébergementCe blog est déployé avec Vercel. Ayant essayé la première version quand le service s'appelait encore Now, je dois dire que le produit a beaucoup changé depuis. Super simple à mettre en place et plutôt bien optimisé avec Next.js, chaque fois que je modifie du code et que je le push sur Github, le site est déployé automatiquement et mis à jour en production. Moi qui suis friand des solutions d'orchestration à la Kubernetes et des CI/CD automatisé, je cherchais une solution qui soit moins overkill pour un blog, je suis très satisfait par Vercel.
Services tiersPour analyser les comportements des utilisateurs, je m'aide des services tiers suivants :
- Google Analytics pour récupérer les statistiques,
- Hotjar pour rejouer la visite d'utilisateurs,
- Google Tag Manager pour implémenter les deux services précédents,
- Sentry pour détecter et remonter les bugs,
Gestion, flux et affichage des données
J'utilise un Headless CMS pour gérer le contenu de mon blog : DatoCMS.
Vous voyez WordPress ? DatoCMS, c'est le même principe, à la différence que je n'ai pas besoin de l'héberger et que je n'ai qu'une interface d'administration avec laquelle j'ajoute mon contenu que je récupère par requêtes API. Ce service à l'immense avantage de me laisser la liberté d'utiliser importe quelle technologie pour le front-end.
Avec DatoCMS, je gère :
Mes articles,
La structure et le contenu de chaque page,
Les meta, globales au site, et par page.
J'ai même une connexion via webhook avec Vercel pour déployer mon site.
De la même manière que WordPress, DatoCMS me permet de créer mes différents modèles de données. Voici ceux que j'utilise pour le blog :
all_posts
, où je définis la structure de ma page/posts
,category
, où je définis l'ensemble de mes catégories d'articles, avec le titre et la couleur,home
, où je définis la structure de ma page d'accueil/
,legal
, même chose pour la page de mentions légales/legal
,navigation
, pour éditer ma situation géographique et ma première disponibilité,post
, modèle de donnée pour mes articles,question
, pour les questions FAQ que j'ajoute en fin d'article,single_post
, pour la structure de ma page/posts/[post_slug]
, là où je mets le contenu qui doit être le même pour chaque article (les articles les plus populaires, l'article en avant, etc.).
À l'image d'un ACF pour WordPress, je peux définir les différents champs de mon modèle de données. Voici l'ensemble des types de champs que DatoCMS nous fournit :
Je n'ai plus qu'à créer ou modifier du contenu :
Derrière, je n'ai plus qu'à préparer ma requête GraphQL pour récupérer cet article :
# SinglePost.query.graphqlquery SinglePost($locale: SiteLocale!, $slug: String!) {site: _site {favicon: faviconMetaTags {attributescontenttag}}post(locale: $locale, filter: {slug: {eq: $slug}}) {idslug_firstPublishedAtseo: _seoMetaTags {tagattributescontent}_allSlugLocales {valuelocale}displayPinnedPostdisplayNetworkstitleintroduction[...]}}
À tout moment, je peux explorer les options de ma requête en utilisant l'éditeur GraphiQL intégré à DatoCMS (l'onglet API Explorer dans la barre de navigation)
Il me suffit, ensuite, de récupérer ce contenu et de l'afficher dans mon front-end.
Travaillant sous TypeScript, j'utilise une commande
codegenqui me lance une introspection de mon serveur GraphQL distant (soit mon endpoint GraphQL DatoCMS) pour récupérer le schéma et le transformer en typage TypeScript via GraphQL Code Generator.
Si vous êtes intéressé par le sujet, je ferai un tutoriel complet dédié au fonctionnement d'un Headless CMS avec GraphQL et TypeScript.
Comme expliqué plus haut, j'ai choisi la solution SSG (génération statique du contenu) avec Next.js qui me permet de récupérer mon contenu à la construction du site. Il me faut alors utiliser une fonction
getStaticPropsdans la page qui m'intéresse qui s'occupera d'initialiser mon client GraphQL (Urql) et d'exécuter la requête précédente :
// pages/posts/[post_slug].tsxexport const getStaticProps: GetStaticProps = async ({ params, preview = false, locale = 'fr' }) => {const ssrCache = ssrExchange({ isClient: false })const client = initGraphQLClient({ preview, ssrCache })const slug = params?.post_slugconst { data, error } = await client.query<SinglePostQuery>(SinglePostDocument, {locale,slug,}).toPromise()if (error || !data || !data.post) {return {notFound: true,}}return {props: {urqlState: ssrCache.extractData(),data: data ?? null,preview,previewTitle: data?.post?.title ?? 'Single Post',},revalidate: 1,}}
Puis de définir les URL qui nécessitent d'être procédées et générées statiquement par Next.js en utilisant
getStaticPath:
// pages/posts/[post_slug].tsxexport const getStaticPaths: GetStaticPaths = async () => {const client = initGraphQLClient({})const { data: frData } = await client.query<AllPostQuery>(AllPostDocument, {locale: 'fr',}).toPromise()const { data: enData } = await client.query<AllPostQuery>(AllPostDocument, {locale: 'en',}).toPromise()const formatPath = (post: { slug: string }) => ({params: {post_slug: post.slug,},})const frPaths = (frData?.allPosts as { slug: string }[]).map(formatPath) ?? []const enPaths = (enData?.allPosts as { slug: string }[]).map(formatPath) ?? []return {paths: [...frPaths, ...enPaths],fallback: true,}}
Next.js récupère déjà la route courante
/posts/[post_slug].tsx, ce qu'il a besoin, c'est qu'on lui communique la liste des slug qui correspondent aux articles qui doivent être générés. Pour éviter d'avoir à relancer la construction complète du site à chaque fois qu'on ajoute un article, il suffit d'ajouter
fallback: truedans la réponse, ce qui fera en sorte de lancer une génération statique pour cette page uniquement à la première visite d'un utilisateur.
Astuce : les robots d'indexations (comme celui de Google) déclenchent les fallback quand une page n'a pas été générée à la construction du site. Si vous avez des soucis d'indexation de vos pages (mauvaise URL, balises canonical étrange, etc.) vérifiez que l'erreur ne vient pas de là.
Vous savez, maintenant, comment je gère et récupère la donnée de mon blog. Derrière, j'apporte du traitement pour le contenu textuel "riche" uniquement où j'utilise Remark pour transformer ce que je reçois en Markdown de DatoCMS vers de l'HTML.
Astuce : on en reparlera dans un article dédié, mais si vous souhaitez injecter de l'HTML dans votre contenu Markdown qui doit être utilisé comme tel, veillez à bien "nettoyer" vos chaînes de caractères pour vous protéger d'attaques XSS ou autre. Je vous conseille isomorphic-dompurify avec Next.js, qui est une variante
ìsomorphique(qui marche côté serveur et client) de DOMPurify.
Quelques détails en vrac
Thèmes de couleurs et réduction des animations visuellesLe site se base sur les caractéristiques média
prefers-color-schemeet
prefers-reduced-motionpour afficher le thème de couleur par défaut et désactiver, ou non, le curseur iPad OS ainsi que les animations susceptibles de provoquer un mal des transports (motion sickness).
Pour le choix du thème de couleurs :
useEffect(() => {const getColorScheme = (event: MediaQueryListEvent) => {const newColorScheme = event.matches ? 'dark' : 'light'setActiveTheme(newColorScheme)}if (isBrowser && window.matchMedia) {window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', getColorScheme)}return () => {if (window.matchMedia) {window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', getColorScheme)}}}, [])
Quelques points à noter :
J'attribue une classe CSS au
body
en fonction du choix du thème,Je stocke le choix de l'utilisateur en
localStorage
pour faire de la persistence,Je récupère la préférence du navigateur au
useEffect
pour assurer la présence de l'objetwindow
,Je gère la préférence du thème de couleur dans un
context
React.
Pour la définition des thèmes, j'ai un fichier
theme.cssqui contient les classes que j'attribue au
body. Dans chaque classe, je définis des variables CSS qui sont utilisés dans le fichier de configuration
tailwind.config.js.
/* themes.css */.theme-light {--color-bg-primary: #ffffff;...}.theme-dark {--color-bg-primary: #131313;...}
// tailwind.config.jsmodule.exports = {theme: {backgroundColor: {primary: 'var(--color-bg-primary)',}}...}
Reste à l'utiliser dans mon DOM React avec TailwindCSS :
<div className="bg-primary">...</div>
La gestion des préférences motion est similaire :
useEffect(() => {const getMotionPreference = (event: MediaQueryListEvent) => {const newMotionPreference: ReduceMotionPreference = event.matches ? 'no-preference' : 'reduce'setPrefersReduceMotion(newMotionPreference)}if (isBrowser && window.matchMedia) {window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', getMotionPreference)}return () => {if (window.matchMedia) {window.matchMedia('(prefers-reduced-motion: reduce)').removeEventListener('change', getMotionPreference)}}}, [])
Quelques points à noter :
Comme pour la gestion du thème, j'attribue une classe CSS au
body
en fonction des préférences de l'utilisateur,Comme pour la gestion du thème, je gère la logique dans un
context
React pour récupérer la valeur dans chaque composant facilement,La valeur de la préférence est stockée dans un
useState
transmis par lecontext
, ainsi qu'une méthode pour la modifier.
Il me reste plus qu'à modifier les animations de mouvement (
rotate,
translate,
scale). Un exemple avec le composant qui gère l'animation de mon texte :
import { useContext } from 'react'export default function SplitText({ text, ...props }: Props) {const { motionPreference } = useContext(MotionPreferenceContext)const words = text.split(' ')return (<>{words.map((word, i) => (<span key={`word-${i}`} className="inline-block overflow-hidden"><motion.spanvariants={{initial: {y: motionPreference === 'reduce' ? 0 : '100%',opacity: motionPreference === 'reduce' ? 0 : 1,},show: i => ({y: 0,opacity: 1,transition: {delay: showDelay + i * 0.01,stiffness: 500,damping: 300,},}),hide: i => ({y: motionPreference === 'reduce' ? 0 : '-105%',opacity: motionPreference === 'reduce' ? 0 : 1,transition: {delay: i * 0.01,stiffness: 500,damping: 300,},}),}}className="inline-block"style={motionPreference === 'no-preference' ? { willChange: 'transform' } : {}}custom={i}>{word + (i !== words.length - 1 ? '\u00A0' : '')}</motion.span></span>))}</>)}
Je n'utilise pas de
target: _blank
sur mes ancres pour laisser le choix à l'utilisateur d'ouvrir un lien dans un nouvel onglet avecCMD
ouCTRL
s'il le souhaite,Chaque image du site est optimisée via Imgix grâce à DatoCMS. En savoir plus,
Conclusion
Je n'ai pas couvert tout ce qui a été fait sur ce blog pour ne pas faire un article à rallonge. Encore une fois, tous les sujets que j'estime intéressants comme la gestion de l'internationalisation du site, la connexion avec Spotify ou la création du curseur iPad OS, bénéficieront d'un article dédié. Pour ne rien louper, vous pouvez me suivre sur Twitter, je communiquerai chaque sortie d'article.
J'aimerais continuer d'améliorer ce blog et développer de nouvelles fonctionnalités. Pour le moment, j'ai prévu d'ajouter ces mises à jour :
De quoi s'abonner à un flux RSS pour rester au courant des prochaines sorties d'articles,
Une solution pour grossir les images des articles en mode "théâtre",
Des améliorations concernant l'accessibilité et l'optimisation de la navigation pour les personnes malvoyantes,
Une barre de progression pour la lecture des articles, et peut-être un mode "défilement automatique" optionnel,
Des petites mises à jour pour simplifier la navigation du site (rendre les catégories des articles cliquables par exemple),
Transformer le blog en PWA pour permettre la lecture hors ligne.
J'espère que ce premier article vous aura plus, c'est une discipline nouvelle pour moi et je suis super impatient de pouvoir partager plein d'autres sujets avec vous.
Si vous avez un avis ou un retour, vous pouvez utiliser ce formulaire, et je serais heureux d'en discuter avec vous sur Twitter (Merci par avance pour le temps que vous prendrez 🙏🏻).
Et si cet article vous a aidé dans une moindre mesure, n'hésitez pas à me le faire savoir également !
À plus dans le bus ! 👋🏻
FAQ en rapport avec l'article
Qui suis-je ?
Je m'appelle Antoine Lin, j'ai 25 et je vis près de Paris. J'ai grandi en Seine-et-Marne et j'ai fait mes études à l'école HETIC, Montreuil. Je suis développeur indépendant, et depuis 2015, j'accompagne des marques et des influenceurs en leur livrant des expériences web sur-mesure.
Quel contenu je vais trouver sur ce blog ?
Le type de contenu que vous trouverez sur ce blog concerne en majorité l'univers du développement web et de la technologie (du contenu éditorial, des tutoriels etc.). Mais je souhaite également que cette plateforme soit un endroit d'expression personnel, où je me sens libre de publier ce que bon me semble.
Comment le site a été construit ?
Le site a été construit de zéro en utilisant le language TypeScript et React.js avec le framework Next.js. Pour le style, j'utilise TailwindCSS, et pour les animations Framer Motion. Le site est déployé avec Vercel.
Les dernières publications
Si vous êtes familier avec GraphQL, vous avec sûrement déjà eu besoin de récupérer des données sur des services tiers en parallèle de votre API GraphQL principale. Dans cet article, je résume comment j'ai réussi à utiliser plusieurs API GraphQL via un seul et unique client Urql.