Fonctionnalités du site

Liste des outils utilisés, des mises en page, palettes et composants disponibles sur ce site.

Technologies utilisées

Astro

Astro est le générateur de site statique avec lequel j’ai réécrit le site au début de l’année 2023. (Auparavant, j’utilisais Hugo.) C’est un excellent outil, simple, bien documenté et flexible, qui rend la création fun et l’expérimentation plus rapide. Je le conseille vivement !

J’ai eu la chance d’être accompagné par mon amie Erika, qui travaille chez Astro, ainsi que par mon ami Goulven, qui utilise lui aussi Astro pour son blog. Ils m’ont fait découvrir des features en cours de développement que j’ai pu utiliser sur mon site : les content collections (voir Structure du contenu) et l’optimisation des assets.

Astro supporte la syntaxe MDX, activée en renommant un fichier en .mdx, et qui permet d’utiliser des composants au sein des articles. Cela m’aide à encrichir les pages de plein de composants rigolos. Voir Blocs et mentions.1

Tailwind CSS

Tailwind CSS est un framework CSS que je n’ai sans doute pas besoin de présenter tant il est populaire. Je l’ai utilisé pour la première fois en réalisant ce site et il m’a rendu plus efficace et créatif, tout en produisant un code plus élégant et léger.

Tailwind propose par défaut un reset CSS appelé Preflight, injecté en incluant @tailwind base dans le CSS. Sur ce site, Preflight est désactivé dans les pages Markdown comme celle-ci et les articles de blog, mais il est activé dans toutes les autres pages, par exemple l’accueil du blog et le portfolio.

Dans les pages de présentation du portfolio, le contenu en Markdown est stylisé avec @tailwindcss/typography.

API de Notion

2Le site peut récupérer le contenu d’une base de données sur Notion, mais aussi afficher le contenu d’une page. Ces fonctionnalités sont actuellement utilisées pour afficher la liste de la page /games et les « impressions » notées pour certains jeux.

Utilisé comme un CMS, Notion est parfois plus pratique que des pages Markdown, notamment pour organiser de grandes quantités de données. La page /games, qui contient plusieurs centaines d’entrées, en est un bon exemple.

Actuellement, le site supporte la plupart des blocs de Notion, mais pas tous les éléments « enfant ». Par exemple, les listes de deuxième niveau ne sont pas affichés. Pour un aperçu de toutes les fonctionnalités disponibles, voir /notion/test.

Structure du contenu

Le site contient un blog, un portfolio, un journal, des lieux, ainsi que quelques pages volantes telles que /about ou la présente page.

Chaque type de contenu dispose d’un sous-site très distinct, n’utilisant que très peu de logique en commun si ce n’est du code boilerplate pour alimenter le <head> et l’optimisation des images. L’idéal serait de pouvoir continuer à créer autant de ces environnements que possible au sein du même dépôt, afin de correspondre à ma vision d’un site qui peut accueillir toute ma vie, des contenus les plus anciens et brouillons aux plus récents et professionnels. Pour que ces sous-sites continuent de fonctionner sans que des mises à jour ne brisent discrètement leur mise en page, il est important de pouvoir les isoler autant que possible. Pour cela, j’utilise les content collections d’Astro, qui permettent de regrouper tous les documents Markdown d’un dossier sous une même logique.

Ainsi, le dossier content/blog est une collection regroupant tous mes articles en Markdown, avec un schéma de frontmatter permettant de renseigner la date de publication de l’article, une position géographique, les films et livres mentionnés, etc. Le dossier content/portfolio est une collection au schéma de frontmatter bien distinct. Le fichier des schémas étant inaccessible car stocké dans un submodule privé, voici son contenu :

src/content/config.ts
288 collapsed lines
import { defineCollection, reference, z } from "astro:content"
import { PaletteName } from "$utils/design/palettes"
import { Layouts } from "$utils/design/layouts"
enum Lang {
fr = "fr",
en = "en",
es = "es",
jp = "jp",
it = "it",
}
export const multilingualText = z.object({
...Object.fromEntries(Object.values(Lang).map((lang) => [lang, z.string().optional()])),
original: z.nativeEnum(Lang),
})
export const months = [
"jan",
"feb",
"mar",
"apr",
"may",
"jun",
"jul",
"aug",
"sep",
"oct",
"nov",
"dec",
] as const
export const date = z.object({
y: z.number().optional(),
m: z.number().or(z.enum(months)).optional(),
d: z.number().optional(),
precision: z.enum(["decade", "around"]).optional(),
})
export const period = z.array(date).length(2)
const sidenotesOptions = z.object({
label: z.enum(["number", "symbol", "none"]).default("none"),
toggle: z.boolean().default(false),
visible: z.boolean().default(true),
removeFootnotes: z.boolean().default(true),
layoutOnly: z.boolean().default(false),
})
const links = {
tags: z.array(reference("tags")).default([]),
games: z.array(z.string()).default([]),
books: z.array(z.string()).default([]),
movies: z.array(z.number()).default([]),
places: z.array(reference("places")).default([]),
gear: z.array(reference("gear")).default([]),
muses: z.array(reference("muses")).default([]),
}
const articleView = {
title: z.string(),
description: z.string().optional(),
longDesc: z.string().optional(),
date: z.date().optional(),
license: z.string().optional(),
imageAnchorTop: z.boolean().optional(),
cover: z.boolean().default(false),
toc: z.boolean().default(false),
depth: z.onumber(),
palette: z.nativeEnum(PaletteName).default(PaletteName.default),
layouts: z.array(z.nativeEnum(Layouts)).default([Layouts.classic]),
customLayout: z.boolean().default(false),
forceNarrow: z.boolean().default(false),
sidenotes: sidenotesOptions.optional(),
draft: z.boolean().default(false),
}
// The following schemas define the shape of the frontmatter or entry data in each
// collection. Each key of `collections` matches a directory name in `src/content`.
export const collections = {
tags: defineCollection({
schema: z.object({
title: z.string(),
plural: z.string().optional(),
type: z.enum(["tool", "type"]).optional(),
}),
}),
muses: defineCollection({
schema: z.object({
title: z.string(),
fullTitle: z.string().optional(),
}),
}),
blog: defineCollection({
schema: ({ image }) =>
z.object({
...articleView,
date: z.date(),
highlight: z.boolean().default(false),
image: image().optional(),
opengraph: image().optional(),
...links,
}),
}),
pages: defineCollection({
schema: ({ image }) =>
z.object({
...articleView,
image: image().optional(),
opengraph: image().optional(),
parent: z.string().optional(),
seeAlso: z.array(z.string()).default([]),
...links,
}),
}),
diary: defineCollection({
schema: ({ image }) =>
z.object({
...articleView,
end: z.date().optional(),
highlight: z.boolean().default(false),
image: image().optional(),
mapLat: z.number().optional(),
mapLng: z.number().optional(),
mapZoom: z.number().optional(),
hidePlaces: z.boolean().default(false),
palette: z.nativeEnum(PaletteName).optional(),
customLayout: z.boolean().default(false),
...links,
}),
}),
portfolio: defineCollection({
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string().optional(),
toc: z.boolean().default(false),
lede: z.string().optional(),
date: z.date().optional(),
client: z.string().optional(),
roles: z.array(z.string()).default([]),
moreRoles: z.array(z.string()).default([]),
tools: z.array(z.string()).default([]),
moreTools: z.array(z.string()).default([]),
link: z.string().optional(),
linkTitle: z.string().optional(),
image: image().optional(),
imageAnchorTop: z.number().optional(),
video: z.string().optional(),
...links,
}),
}),
resume: defineCollection({
schema: z.object({
title: z.string(),
}),
}),
places: defineCollection({
schema: z.object({
title: z.string(),
id: z.string(),
status: z.enum(["todo", "done"]),
review: z.enum(["loved", "liked", "okay", "disliked"]).optional(),
hide: z.boolean().default(false),
}),
}),
gear: defineCollection({
schema: z.object({
title: z.string(),
etat: z.string(),
utilisation: z.string().optional(),
obtained: z.date().optional(),
obtainedInfo: z.string().optional(),
price: z.string().optional(),
clickable: z.boolean().default(false),
}),
}),
collections: defineCollection({
schema: z.object({
title: z.string().or(multilingualText),
highlight: reference("pieces"),
}),
}),
pieces: defineCollection({
schema: ({ image }) =>
z.object({
// If a work is untitled: false. It the title is unknown: undefined.
title: z.string().or(z.literal(false)).or(multilingualText).optional(),
author: z.string().or(multilingualText).optional(),
date: date.or(period).optional(),
collections: z.array(reference("collections")).default([]),
image: image().optional(),
audio: z.string().optional(),
audioFormat: z.enum(["mp3", "flac"]).optional(),
fileSource: z.string().url().optional(),
type: z.nativeEnum(PieceType),
technique: z
.enum([
"oil",
"oil-cardboard",
"watercolor",
"pencil-and-watercolor",
"graphite-on-tracing-paper",
])
.optional(),
style: z.enum(["color", "black-and-white"]).optional(),
subjects: z
.array(z.enum(["portrait", "nude", "landscape", "still-life", "abstract"]))
.optional(),
album: z.string().optional(),
}),
}),
wiki: defineCollection({
type: "data",
schema: z.object({
id: z.string(),
slug: z.string(),
title: z.string(),
description: z.string(),
related: z.array(z.string()),
tags: z.array(z.string()),
editedTime: z.string(),
status: z.object({
icon: z.string(),
text: z.string(),
}),
blocks: z.array(z.any()),
}),
}),
games: defineCollection({
type: "data",
schema: z.object({
title: z.string(),
slug: z.string().nullish(),
quickReview: z
.enum([
"Coup de cœur",
"Aimé",
"Sympa un moment",
"Whatever",
"Mitigé",
"Décevant",
"J'aime pas",
"Mauvais",
"Pas pour moi",
])
.nullish(),
review: z.string().nullish(),
firstPlayedYear: z.number().nullish(),
progress: z.string(),
multiplayer: z.array(z.enum(["En ligne", "Local", "Coop", "Versus"])),
myPlatforms: z.array(z.string()),
blocks: z.array(z.any()),
notionUrl: z.string(),
lastEditedTime: z.string(),
igdb: z.any().nullish(),
}),
}),
recipes: defineCollection({
schema: ({ image }) =>
z.object({
title: z.string(),
ingredients: z
.array(
reference("ingredients").or(z.tuple([reference("ingredients"), z.string().nullish()]))
)
.default([]),
tags: z.array(z.string()).default([]),
cover: image().optional(),
draft: z.boolean().default(false),
}),
}),
ingredients: defineCollection({
schema: ({ image }) =>
z.object({
title: z.string(),
tags: z.array(z.string()).default([]),
icon: image().optional(),
}),
}),
}
export enum PieceType {
painting = "painting",
drawing = "drawing",
photo = "photo",
digital = "digital",
album = "album",
track = "track",
movie = "movie",
}

Layouts

Attention à ne pas confondre les layouts de mes articles avec les layouts d’Astro ! Le nom est le même car initialement, j’utilisais ces derniers, mais j’ai finalement simplifié mon code. Aujourd’hui, ce que j’appelle layout est plus concrètement un nom de classe CSS.

Layouts principaux

  • classic est un design rétro et humble, présentant souvent des couleurs de fond vives et des polices par défaut comme Times New Roman. Ce layout reste fidèle aux valeurs par défaut du navigateur conçernant la taille du texte et des marges, n’intervenant que pour appliquer de petites corrections, notamment la largeur maximale de la page. Les galeries sont pensées pour apporter de la vie à ce layout en brisant la verticalité du texte.

    Exemples : « Remnant: From the Ashes » (), « Commited Language » (), « L'introduction de Destiny : analyse du level design » ()

  • typography, utilisé sur cette page, tente de proposer un design minimaliste mais plus travaillé et évoquant la matérialité d’un livre. Il utilise les fonctionnalités typographiques des polices OpenType pour afficher des petites capitales et les différents types de chiffre en fonction du contexte.

  • tufte est une adaptation de TufteCSS pour Astro. Il doit être utilisé avec des composants réalisés spécifiquement pour cette mise en page : <Sidenote>, <MarginNote>, <TufteFigure> et <MarginImage>. Pour créer un changement de section qui ajoute un espacement, écrire <section-switch/>.

Layouts secondaires

Les layouts secondaires proposent des variantes concernant certains éléments d’une page. Ils doivent être combinés avec un layout principal.

  • cute applique des coins arrondis aux images et des paramètres différents pour les légendes.

    Exemple : « L'antipoésie de Breath of the Wild » ()

  • future apporte de la couleur et de la fantaisie sans concession sur la lisibilité du texte. Les titres, les <em>, les puces et les numéros de liste sont colorés différemment du texte. Les liens ont un soulignage et un fond coloré qui réagissent au survol.

    Exemple : « Programmer des mini-jeux rétro avec PICO-8 » ()

Palettes

3Les palettes sont des combinaisons de couleurs, de polices d’écriture et d’autres paramètres servant à personnaliser les pages. Si le layout détermine la structure générale d’une page, la palette est une variante de style pour en modifier l’ambiance. Certaines palettes présentent une couleur de fond vive à la manière du web des années 90, tandis que d’autres proposent une variante plus subtile, comme la palette nature avec ses nuances de gris discrètes, à l’exception d’un vert clair pour colorer les liens.

Les palettes sont composées des éléments suivants, certains étant optionnels :

  • Couleurs : texte principal, secondaire et spécial ; fond de la page, des liens et des mentions.
  • Polices : titres ; texte principal et spécial ; taille et hauteur de ligne.

Certaines ont un style rétro (default, black, white, darkgrey, brown, green), tandis que d’autres sont plus modernes. La prévisualisation (/palettes) présente les éléments principaux de chaque palette.

Voici une liste non exhaustive des palettes existantes :

  • Palettes rétro : default, black, white, darkgrey, blue, brown, green
  • Palettes modernes : guidebook, whiteboard, notebook, desolate, nature

En explorant l’accueil du blog (/blog), vous aurez un bon aperçu de la variété des palettes et de leurs effets.

Barre de navigation

4La barre de navigation en bas de l’écran est un élément persistant entre les pages du site. Elle permet d’ajouter des fonctionnalités, par exemple le téléchargement d’images, sans surcharger les pages elles-mêmes, qui peuvent conserver une certaine pureté.

Pour adapter la barre à toutes les palettes (voir Palettes) et limiter son effet distrayant, son design est sobre avec un noir et blanc purs (et une pointe de gris pour les éléments secondaires). Elle peut être masquée avec un raccourci clavier (Ctrl+K ou ⌘K) ou en cliquant sur le bouton à droite.

La section gauche de la barre s’adapte au contexte :

  • Par défaut, elle affiche le titre de la page actuelle (qui est cliquable et copie l’URL), un lien vers la page parente, et si applicable, la table des matières et une liste d’articles similaires ou de pages recommandées.
  • Lorsqu’une image est agrandie, elle affiche les liens de téléchargement de toutes les versions disponibles de l’image : petite taille, grande taille et fichier d’origine.
  • Lorsqu’une pop-up est ouverte (voir Pop-ups), elle affiche un lien vers la source de données qui a alimenté le contenu la pop-up, et si applicable, un lien vers l’œuvre en question dans ma bibliothèque.

La section droite de la barre est fixe, avec une barre de recherche, des liens vers les pages principales du site, ainsi que le bouton pour masquer la barre.

Chacun des éléments de la barre dispose de sa propre logique pour rapetisser sur mobile tout en maintenant la meilleure lisibilité possible.

Recherche

5La barre de recherche, dont la logique est tirée du site de Goulven, permet de retrouver les entrées des content collections ainsi que quelques autres pages ajoutées manuellement.

Tous les données nécessaires à la recherche sont indexées au build puis stockées dans une variable JavaScript qui est recopiée dans chaque page. Ainsi, lorsque le visiteur tape du texte dans la barre, la logique de la recherche s’exécute côté client.

Cela fonctionne bien sur la majorité des pages du site, qui sont générées statiquement, mais cela pose problème pour les pages du /wiki qui sont dynamiques (à chaque requête d’un visiteur, le serveur communique avec l’API de Notion puis génère la page). La logique d’indexation, qui prépare le contenu de la variable, doit alors s’effectuer à chaque chargement de page, et c’est malheureusement un processus qui prend deux ou trois secondes pour les serveurs de Vercel. Cela n’est pas bloquant lorsque le visiteur arrive sur le site : grâce au HTML streaming, le contenu principal, qui est en haut du fichier HTML, s’affiche dès qu’il est prêt, et la barre de recherche, qui est tout en bas, s’affiche lorsque l’indexation est terminée. Cependant, le processus est bloquant lorsque le visiteur change de page : comme j’utilise les View Transitions, la page doit être complètement chargée avant que la transition ne s’exécute. Je dois donc différer cette logique après le chargement de la page.

La solution est de générer statiquement un chemin d’API qui contient l’indexation de la recherche. Dans les pages dynamiques du site, la variable JavaScript est initialement vide, mais après le chargement de la page, le client effectue une requête vers l’API pour alimenter la variable. Cela signifie que la recherche ne peut rien trouver durant les premiers instants sur la page, mais aucun visiteur ne devrait être assez rapide pour le remarquer. De plus, l’état de la barre étant conservé entre les pages, si le visiteur provient d’une page statique et arrive sur une page dynamique, la variable JavaScript aura déjà été alimentée et ce processus est ignoré.

Ajustements réalisés aux pages

Masquer la barre permet de retirer tout élément de distraction lors de la lecture, ce qui peut être agréable mais aussi poser problème car c’est un outil de navigation essentiel pour sortir de certains culs-de-sac. Pour encourager son utilisation, la barre s’affiche de nouveau lorsqu’on actualise le site, et un bouton discret est présent à la fin des articles.

La barre étant affichée au-dessus du reste, elle peut donner l’illusion que la marge en bas de certaines pages est trop petite. Une marge dynamique supplémentaire est donc présente en bas de chaque page. Dans le même esprit, le positionnement des images agrandies prend en compte la présence de la barre.

Images

Optimisation

6Ce site utilise l’optimisation des images d’Astro. Dans le dépôt, les images ne sont pas traitées et sont conservées dans leur format d’origine, mais lors du build, elles sont redimensionnées et converties au format WebP. Certains sites vont plus loin en proposant également le format AVIF, plus moderne, mais je n’ai pas trouvé de réglage offrant un compromis satisfaisant entre le poids et la qualité.

Images de substitution

7J’ai repris la technique développée par mon amie Erika pour son site personnel et consistant à créer, pour chaque image, une image de substition (ou placeholder) à la résolution très faible afin de l’encoder dans la page HTML, plus précisément dans le background de <img>. Ainsi, avant que la véritable image ne soit chargée, le navigateur affiche un dégradé abstrait, bien plus esthétique qu’un simple fond blanc.

Erika a ajusté les paramètres de l’image de substition (flou ; luminosité ; contraste) pour qu’elle évoque au mieux la véritable image et que la transition entre les deux soit plus douce. J’ai légèrement modifié ces valeurs pour correspondre à mes besoins, mais le reste du code est inchangé.

8J’utilise la librairie Medium Zoom pour afficher une version en haute résolution de l’image lors du clic. La lightbox utilise la couleur de fond de la palette pour une expérience plus immersive.

Blocs et mentions

Galeries

9Une galerie est une succession d’images sur la même ligne dans une flexbox. Pour optimiser le poids de la page, les images d’une galerie peuvent être deux ou trois fois plus petites qu’une image normale. Le défaut de cette galerie est que chaque élément de la flexbox fait la même taille, ce qui signifie que les images doivent toutes faire la même taille pour éviter les espaces vides. Une astuce pour redimensionner les éléments de la flexbox dynamiquement en fonction de la taille des images serait bienvenue !

Exemple : « L'introduction de Destiny : analyse du level design » ()

Sommaire

10Le sommaire, dont vous pouvez voir un exemple en haut de cette page, peut avoir une profondeur variable.

Onglets

11Les onglets permettent de choisir parmi plusieurs alternatives de contenu. Ils sont particulièrement utile dans les tutoriels, pour séparer le contenu en fonction du système d’exploitation.

Exemple : « Créer un portfolio ou un blog sans programmer avec Hugo » ()

Google Maps

12Cette idée provient de mon ami Goulven qui, pour son article « Discovering coffee in Toulouse », a réalisé des blocs adorables pour représenter ses cafés préférés sur Google Maps. Dans son élan, il a aussi réalisé des blocs pour référencer des livres (idée que j’ai également reprise, voir Open Library) et des produits Amazon.

Les infos de l’établissement sont récupérées à la compilation. Le bloc n’est donc pas dynamique côté visiteur et peut afficher des informations légèrement datées. Voici un exemple présentant, en hommage à Goulven, le café que je fréquente près de chez moi.

Kopi Coffee Shop
  • 20 Place du Salin, Toulouse
  • Fermé le lundi et le dimanche.
  • ⭐️ ⭐️ ⭐️ ⭐️ ⭐️ (250 avis)

J’ai également réalisé un composant inline permettant de mentionner un établissement comme Kopi Coffee Shop au sein d’un paragraphe. L’icône est distribuée par Google Maps en fonction du type d’établissement, tandis que le texte peut être récupéré automatiquement ou personnalisé. Vous trouverez plusieurs exemples d’utilisation des mentions dans l’entrée de journal Grottes des Eaux-Chaudes, Laruns .

Open Library

13Comme Goulven, j’utilise Open Library pour afficher la couverture et les informations d’un livre, à partir d’un ISBN ou de l’identifiant Open Library (OLID) si le premier n’est pas renseigné.

Open Library a l’avantage d’avoir des données relativement complètes, mais surtout qui sont éditables instantanément, ce qui m’a été utile plusieurs fois pour préciser les données d’un livre que je souhaitais présenter. Vous trouverez des exemples d’utilisation du bloc dans l’article « Utiliser les petites capitales sur le web ».

Discogs

14Le bloc Discogs peut afficher une version précise d’un album ou une master release, c’est-à-dire une représentation de l’ensemble des versions d’un même album. Vous trouverez deux exemples d’utilisation dans l’article « Commited Language ».

IGDB

15L’Internet Game Database (IGDB) est une sorte d’IMDB du jeu vidéo. Lancé en 2014 et possédé depuis 2019 par Twitch qui l’utilise pour alimenter son site, les données d’IGDB sont très complètes. L’API permet d’obtenir un jeu à partir de son slug, par exemple remnant-from-the-ashes, ce qui est très pratique et lisible dans un document Markdown.

Le bloc est utilisé dans « Remnant: From the Ashes » () et « Pourquoi est-ce arrivé ? » ().

TMDB

16The Movie Database (TMDB) est une alternative gratuite à IMDB et son API payante. C’est un service utilisé par de nombreux sites dont Letterboxd. Il est presque toujours possible d’obtenir un trailer et le casting, mais les images ont tendance à manquer sur les films peu populaires.

Pop-ups

17Les pop-ups affichent des informations sur un jeu, un film ou une personnalité (voir IGDB et TMDB). Dans le cas d’une œuvre, la pop_up contient le titre, l’affiche, les auteurs, l’année de parution et jusqu’à trois images. Une pop-up s’ouvre en cliquant sur un texte dont le lien est d’une apparence plus discrète que les liens externes. Selon la palette, le lien peut être coloré ou en pointillé.

Exemple : Le film First Cow, le jeu Sword & Sorcery.

Références bibliographiques

18Le bloc de référence permet d’indiquer le titre, l’auteur et d’autres données d’une référence pour l’afficher dans un format standardisé à travers tout le site. Plusieurs règles sont prises en compte. Par exemple, le titre d’un article est entouré de guillemets, mais pas le titre d’un livre, qui sera plutôt en italique.

Il est important que le site reste entièrement utilisable au fil des années, ce qui veut dire que les références doivent rester consultables ! Ce bloc m’aide à m’en assurer, puisque lorsque j’oublie de fournir un lien d’archive, un rappel est affiché dans la console.

Vous trouverez plusieurs utilisations du bloc dans les sidenotes de « Utiliser les petites capitales sur le web » et dans les notes de bas de page de « Sur les traces de RPG Maker : 1987–1997 ».

Mentions d’article

19Ces mentions homogénéisent les références aux autres articles du site, et vérifient que l’adresse soit valide. Elles peuvent indiquer le titre et l’année : « Sur les traces de RPG Maker : 1987–1997 » ().

Code

Le site utilise Expressive Code pour afficher les blocs de code dans différents thèmes de couleur en fonction de la palette (voir Palettes) et différents cadres selon le contexte : bordure simple, nom de fichier ou fenêtre de terminal.

content/index.md
Un fichier _Markdown_ et son **nom de fichier**.
Terminal
Un fichier _Markdown_ dans une **fenêtre de terminal**.

Le plugin collapsible sections permet de masquer un ensemble de lignes. Pour un exemple, voir Structure du contenu.

Autres fonctionnalités

  • L’interface de navigation du blog (/tags, etc.) s’adapte au mode clair ou sombre de l’appareil du visiteur. En mode sombre, la liste des articles est enveloppée d’une lueur afin de préparer le visiteur qui pourrait être ébloui en cliquant sur un article dont la palette est claire.
  • Les pages de jeu, accessibles depuis /games, contiennent une porte dérobée. Un double-clic sur le titre renvoie vers la page Notion qui sert à en générer le contenu. Je m’inspire de Gwern qui rend la source accessible en ajoutant .page à une URL, mais ma variante est plus pratique sur mobile.
  • Le site utilise les View Transitions d’Astro, affichant un fondu entre les pages et parfois une transition plus travaillée. Cela nécessite d’être particulièrement vigilant quant à la logique d’exécution du code JavaScript au sein des pages.

Ce que ce site ne contient pas

Commentaires

Je ne crois pas que les espaces commentaires aient beaucoup de sens la plupart du temps, car tous les contenus sur Internet n’ont pas vocation à être transformés en espaces de discussion publics. Mes articles sont souvent un espace d’expression personnelle, avec la vulnérabilité que cela implique. J’ose à peine les publier, alors s’ils devaient être directement confrontés à l’expression publique, ce serait encore plus dur ! Je préfère encourager les lecteurs à m’envoyer un mail ou à me contacter sur les réseaux sociaux.

Je suis attaché au phénomène qui s’opère lorsqu’on ouvre un livre, on va au cinéma ou au musée. Ce sont des espaces dédiés à l’immersion, à l’échange intime et invisible entre l’auteur et le lecteur, le réalisateur et le spectateur. Les commentaires et critiques peuvent exister, mais dans leur propre espace, ailleurs. Si je ne vois pas mes articles comme des œuvres d’art (pour la plupart), j’espère que certains recoins de mon site pourront créer ce même rapport intime avec le lecteur.

Pagination

La pagination sur le web est souvent dûe à la peur de surcharger le lecteur ou la page avec trop de contenu et d’options. C’est une méthode encore présente sur de nombreux blogs et journaux ; cependant, c’est un design restrictif, difficile à utiliser et qui masque trop de contenu. La pagination la plus iconique d’Internet est probablement celle de Google, et elle a disparu un beau jour sans que personne ne s’en rende compte.

Pour remplacer la page suivante, il serait plus intuitif d’avoir un chargement automatique ou un grand bouton « voir plus ». Cela permet de garder visible le contenu de la « première page », qui est probablement important.

Lorsque j’aurai suffisamment de contenu pour le justifier, je réfléchirai à un menu de navigation, qui fonctionnerait comme la barre de défilement de Google Photos couplée à des options de filtrage. Sur ordinateur, il serait parfait sur un côté de l’écran. Sur mobile, l’idéal serait de le placer en bas de l’écran pour une accessibilité optimale.

Todo

  • Améliorer le design :
    • Blocs de code : Coloration différente pour la variante sombre des palettes.
  • Pop-ups : ajouter les données de /games au pop-ups de jeu.
  • Emplacement du sommaire : Ajouter la possibilité de l’inclure à un endroit précis du document Markdown.
  • Date de modification des articles et des pages. Elle pourrait être récupéré de l’API de GitHub afin d’éviter de la mettre à jour manuellement. Le but n’est pas que cette date soit exacte, mais de représenter l’idée que les pages peuvent toujours être affinées au fil du temps, malgré qu’elles soient inscrites dans une époque dans le cas du blog.
  • Ajouter une galerie des films que j’ai mentionnés sur ce site ou aimés. C’est un problème épineux, car Letterboxd a refusé ma demande d’accès à l’API.
  • Ajouter un bloc GitHub pour mentionner un dépôt ou un utilisateur.
  • Ajouter l’historique git sur la page /changelog.
  • Écrire un plugin Remark pour ajouter automatiquement les guillemets françaises et leurs espaces insécables.

Voir aussi : À propos de ce site, Manuel de rédaction, Changelog

Notes

  1. Pour faciliter la lecture, j’utiliserai dorénavant le terme « Markdown » pour désigner un fichier pouvant être au format Markdown ou MDX.

  2. Requêtes : utils/notion
    Rendu : components/notion

  3. palettes.ts
  4. <NavBar>
  5. <Image>
  6. imageService.ts
  7. <MediumZoom>
  8. <Gallery>
    <PhotoGallery>
  9. <TableOfContents>
  10. <Tabs>
  11. <GoogleMaps>
  12. <Book>
  13. <Discogs>
  14. <Game>
  15. <Movie>
    <MoviePerson>
  16. <Popup>
    <PopupV2>
  17. <Ref>
  18. <BlogRef>
    <DiaryRef>