Architecture
Ce projet est construit sur HonoX, un méta-framework au-dessus de Hono qui ajoute le routage basé sur les fichiers, les îlots (islands) serveur/client et la génération de sites statiques. Le style est géré par PandaCSS (CSS-in-JS typé, sans exécution à l'exécution), le contenu est rédigé via Sveltia CMS (/admin/), et l'ensemble du site est pré-rendu en HTML statique.
| Couche | Outil |
|---|---|
| Framework | HonoX |
| Routage | Basé sur les fichiers, sous app/routes/ |
| Style | PandaCSS → design-system/ |
| Contenu | Markdown / MDX / JSON sous content/ |
| CMS | Sveltia CMS, adossé à Git, sur /admin/ |
| SSG | @hono/vite-ssg |
| Déploiement | Cloudflare Pages (wrangler.jsonc) ou Vercel (vercel.json) |
Le build : deux passes Vite, un site statique
bun run build exécute vite build --mode client && vite build — deux passes distinctes sur le même vite.config.ts, commutées par mode :
--mode clientcompileapp/client.ts(createClient()depuishonox/client) avecjsxImportSource: "hono/jsx/dom". C'est le bundle navigateur : il hydrate les îlots et rien d'autre.- La passe par défaut (serveur) compile
app/server.ts(createApp()depuishonox/server) avecjsxImportSource: "hono/jsx"(le moteur JSX SSR), puis confie l'ensemble de l'application au pluginssg(), qui explore chaque route et écrit le HTML pré-rendu dansdist/.
Routage SSG et corrections d'URL localisées
Pour éviter les erreurs de routage 404 sur les hébergeurs de fichiers statiques après la compilation des routes, un fixSsgRoutingPlugin personnalisé dans vite.config.ts traite de façon récursive tous les fichiers .html de la sortie de build (dist/). Il renomme et déplace les fichiers d'index/page d'accueil localisés (par ex. zh.html, docs/fr.html) vers des chemins propres imbriqués (zh/index.html, docs/fr/index.html) si un répertoire correspondant existe ou si le nom correspond à une locale prise en charge. Cela garantit que /zh et les autres points de terminaison localisés se résolvent proprement comme index de répertoire sur n'importe quel hébergeur statique.
Résolution de l'environnement de test
Pour exécuter les tests unitaires des composants Hono JSX sous Bun, bunfig.toml est configuré spécifiquement avec :
[jsx]
runtime = "classic"
pragma = "h"
fragment = "Fragment"
importSource = "hono/jsx"
Cela garantit une résolution standard du moteur d'exécution Hono et évite les erreurs de runtime JSX de développement manquant lors de l'exécution des tests.
Le plugin mdx() est limité à include: /\.mdx$/ uniquement — le .md simple (articles de blog, la plupart des docs) est délibérément laissé hors champ afin que les imports ?raw de app/utils/markdown.ts ne soient pas corrompus par la transformation MDX.
Routage basé sur les fichiers
Les routes se trouvent sous app/routes/, enregistrées dans app/server.ts via import.meta.glob sur **/*.{ts,tsx,md,mdx}, en excluant les conventions de fichiers privés de HonoX (_*, -*, $*) et les fichiers de test. Un fichier de route exporte des handlers (GET, POST, …) ou un composant par défaut ; [slug].tsx / [[slug]].tsx fournissent des segments dynamiques/optionnels, conformes aux propres conventions de routage de HonoX.
Routes API statiques personnalisées
Dans HonoX, les routes API statiques personnalisées (par ex. app/routes/api/posts.json.ts) qui exportent une route standard renvoyant c.json(...) sont automatiquement compilées en fichiers JSON statiques (par ex. dist/api/posts.json) par le plugin @hono/vite-ssg lors du build SSG. Aucune configuration de paramètre dynamique n'est nécessaire pour ces points de terminaison statiques.
Pré-rendu des routes dynamiques via ssgParams
Toute route dynamique (telle que /blog/by-author/[author].tsx) doit implémenter et exporter le middleware ssgParams dans la définition de la route afin de déclarer toutes les valeurs de paramètres potentielles pour le pré-rendu au moment du build.
Routage localisé et redirections héritées
Les routes des collections traduisibles (docs, blog, pages) suivent /<collection>/<locale?>/<item>, la locale par défaut (en) ne prenant pas de segment :
/docs/AbsoluteCenter (en)
/docs/fr/AbsoluteCenter (fr)
/blog/my-post (en)
/blog/zh/my-post (zh)
Les pages d'accueil linguistiques indépendantes de la locale se trouvent sur le segment de locale nu (/fr, /zh, …). Tout cela est centralisé dans app/lib/i18n.ts (detectLocale, localiseHref, stripLocale, localeToggleUrl) — aucun fichier de route ne gère manuellement la logique de locale. Une ancienne forme de route, /<locale>/<collection>/<item>, est redirigée en 301 vers la forme actuelle par un middleware dans app/server.ts, afin que les signets/liens anciens continuent de fonctionner.
Les locales prises en charge sont déclarées une seule fois, dans ALL_LOCALES / TRANSLATED_LOCALES (app/lib/i18n.ts) — cette liste doit rester synchronisée avec i18n.locales de public/admin/config.yml et les répertoires de routes miroirs app/routes/<locale>/.
Architecture des composants
La base de code maintient deux arbres parallèles sous app/ :
app/components/ui/— l'API de composants publique (~100 composants).app/islands/— les contreparties hydratées côté client, une par composant interactif, intégrées au bundle client et montées parhonox/client.
Sécurité serveur sans hook
Pour garantir une génération de site statique sans accroc, tous les hooks réactifs côté client (useEffect, useRef, useState depuis hono/jsx) sont strictement restreints au répertoire /islands/. Les fichiers sous le répertoire /components/ui/ restent entièrement sans hook et sûrs pour le rendu statique/SSR côté serveur. Les wrappers statiques (comme Dialog et Drawer dans components/ui/) qui transmettent des références utilisent un repli d'objet simple statique ({ current: null }) à la place de useRef pour éviter l'exécution de hooks client sur le serveur.
Résolution sûre des styles entre îlots
Les composants multiparties comme HoverCard qui rendent des enfants à travers les frontières d'îlots HonoX doivent implémenter une résolution de style de repli sûre (par ex. context?.styles || recipe()) dans leurs sous-composants primitifs afin de garantir que les noms de classe sont entièrement renseignés à la fois dans les états SSR/SSG pré-rendus et les états client hydratés.
Positionnement des superpositions et astuces d'interaction
- Positionnement correct : Les wrappers racine des composants
PopoveretHoverCardutilisent les styles en ligneposition: 'relative'etdisplay: 'inline-block'(pour les implémentations statiques et interactives/îlots). Cela les empêche de prendre de l'espace en ligne de niveau bloc et positionne correctement leur contenu superposé absolu par rapport au déclencheur. - Gestion du focus : Dans
app/components/ui/popover-primitive.tsx,InteractivePopoverRootutilise une référenceisFirstRenderpour garantir queclosePopoverne focalise pas l'élément déclencheur lors du rendu/montage initial lorsque la popover est fermée, évitant ainsi une auto-focalisation inattendue au chargement de la page. - Transparence des événements pointeur : Pour éviter une imbrication HTML invalide de balises d'ancrage (
<a>) à l'intérieur de grands éléments parents cliquables (comme des diapositives de carte ou de carrousel), le conteneur de texte de superposition est structuré avecpointer-events: noneetpointer-events: autoest appliqué aux éléments<Anchor>ou<a>imbriqués ciblés.
Mécanismes avancés des composants
- Composant Menu interactif (
app/islands/menu.tsx) : Gère les événements de défilement et de redimensionnement de la fenêtre en recalculant et repositionnant dynamiquement le conteneur du menu déroulant (viaupdatePosition()), garantissant qu'il reste ancré à son déclencheur. Il prend en charge un état ouvert contrôlé (openetonOpenChange), des placements mappés à partir de configurations classiques et en kebab-case avec détection de collision aux limites, et des actions de déclencheur personnalisables avec minuteurs d'entrée/sortie de survol. - API Menu simplifiée (
app/components/ui/menu.tsx) : Rend de manière récursive des sous-menus en cascade lorsqu'elle rencontre un élément de menu de type"submenu", affichant une icône chevron et tirant parti de primitivesMenucomposées imbriquées. Elle exposeMenu.Arrow,Menu.ArrowTipetMenu.TriggerItemcomme sous-composants composés. - Vérifications des références de nœuds VDOM : Pour vérifier correctement la référence de nœud VDOM d'un composant enfant (comme
MenuTriggerItemdansTrigger) en Hono JSX, le code vérifie à la foischild.tagetchild.typecar les nœuds de fonction JSX classiques sont mappés surtagplutôt quetypesous la compilation JSX classique. - DatePicker : Prend en charge des vues granulaires via la prop
picker("date" | "month" | "year"), mappant de manière transparente les tailles et variantes vers les configurations de token Panda CSS. Il prend en charge un style sémantique profondément personnalisable via les propsclassNamesetstylessur des éléments internes spécifiques (par ex. label, control, input, positioner, clearTrigger). - Composant Tabs : Entièrement porté vers Hono/JSX. Les primitives de mise en page SSR statiques sont définies dans
app/components/ui/tabs-primitive.tsx, tandis que le wrapper d'îlot client interactif et actifapp/islands/tabs.tsxgère l'état actif, le suivi de l'indicateur via unResizeObserver, et les règles standard de navigation ARIA/clavier. Il mappe les propriétés de style Ant Design (activeKey,defaultActiveKey,onChange,onTabClick, tailles et types) vers les primitives sous-jacentes. - Composant Select : Mappe dynamiquement les entrées de framework traditionnelles comme
size="small"/"medium"/"large"etvariant="outlined"/"flushed"vers les échelles standard Panda CSS (sm/md/lgetoutline/underlined) avant de calculer les classes de slot, afin d'assurer une compatibilité trans-framework. Il a été affiné pour prendre en charge la recherche/filtrage côté client dans les listes déroulantes via la propshowSearch, ainsi que le rendu des éléments sélectionnés sous forme de Tags interactifs et fermables en mode sélection multiple (personnalisable viatagRender). - Composant PinField : Implémenté avec une primitive SSR statique (
app/components/ui/pin-field-primitive.tsx) et un îlot interactif (app/islands/pin-field.tsx). Il normalisevalueetdefaultValuepour prendre en charge à la fois les types chaîne et tableau, définitselectOnFocusàtruepar défaut, prend en charge l'exécution de formulaireautoSubmit, assainit les caractères collés en supprimant les espaces et les traits d'union, et gère la navigation clavier RTL. - Système de mise en page en grille : Fournit un conteneur flexbox 24 colonnes haute performance via les composants
RowetCol, mappant les paramètres de points d'arrêt responsives (commexs,sm,md,lg,xl,xxl) vers les points d'arrêt standard Panda CSS. Row mappe les gouttières statiques, basées sur tableau et responsives en sorties abrégées d'espacement Panda CSS (cgetrg), tandis que Col convertit les props responsives et les objets de points d'arrêt en classes de système de design correspondantes de manière dynamique. - Mise en page en grille aplatie : Les composants de mise en page
GridetGridItemplats dansapp/components/ui/grid.tsxsont basés sur les motifs de mise en page natifs de Panda CSS, prenant en charge le contrôle 2D viacolumnsetrows. Ces motifs sont enregistrés dansstaticCss.patternsdepanda.config.ts(gridetgridItem) et liés de manière récursive dansconfig.ymlde Sveltia CMS souspagespour simplifier les mises en page multi-colonnes sans éléments Row/Col imbriqués. Les points d'arrêt responsives prennent en charge des objets responsives sérialisés en JSON (par ex."columns": "{\"base\": 1, \"md\": 3}"). - Recettes de grille de mise en page : Les recettes de grille de mise en page pour
rowetcolsont compilées par programmation en variantes statiques et discrètes (portées, décalages, ordres 0 à 24) et enregistrées dans le CSS statique depanda.config.tspour prendre en charge l'imbrication de mise en page de page statique dans Sveltia CMS et PageRenderer sans hydratation JavaScript dynamique. - Répertoire d'icônes SVG centralisé : La base de code utilise des composants d'icônes SVG individuels et réutilisables situés dans
app/icons/*(par ex.CloseIcon,ChevronDownIcon,CheckIcon, etc.) qui acceptentJSX.IntrinsicElements["svg"]pour transmettre des attributs commewidth,heightet des styles personnalisés. Les SVG en ligne codés en dur à travers les composants UI et les routes ont été refactorisés pour importer depuis ce répertoire d'icônes centralisé afin de promouvoir la réutilisation du code et d'éviter la duplication.
Pipelines de contenu et i18n
Tout ce qui se trouve sous content/ est découvert au moment du build via import.meta.glob de Vite et pré-rendu par SSG.
Partitionnement des collections CMS
Le dépôt partitionne le contenu documentaire en deux collections CMS distinctes définies dans public/admin/config.yml :
"docs": Guides situés sous/content/docs/sous forme de fichiers.md."components": Références de composants situées sous/content/components/sous forme de fichiers.mdx.
Les liens de page d'édition d'administration Sveltia CMS sont construits au format /admin/#/collections/[docs|components]/entries/[slug].
Modèle de classification d'hydratation
Le dépôt utilise un modèle de classification d'hydratation à trois niveaux, configuré via le frontmatter Sveltia CMS et documenté dans Hydration :
- « Interactive avec empressement » (Niveau 1) : S'hydrate par défaut avec empressement comme îlot client.
- « Adaptive intelligente » (Niveau 2) : S'hydrate conditionnellement selon les signaux comportementaux.
- « Statique zéro-JS » (Niveau 3) : Composants purement statiques sans hydratation JS.
i18n et ajout d'une nouvelle locale de traduction
Sveltia CMS est configuré pour l'internationalisation (i18n) sous public/admin/config.yml en prenant en charge les locales en, zh, es, pt, fr et de, l'anglais (en) étant la locale par défaut. Il utilise la structure multiple_folders avec omit_default_locale_from_file_path: true, en conservant les fichiers de locale par défaut dans les chemins racine d'origine et en plaçant les traductions sous des sous-dossiers de locale (pour docs/components) ou en utilisant des suffixes .<locale> (pour configs et posts).
Pour ajouter une nouvelle locale de traduction au dépôt, suivez ce flux de travail étape par étape :
- Configuration CMS : Ajoutez le code de locale (par ex.
froude) à la sectioni18n.localesdepublic/admin/config.yml. - Clés de traduction : Créez un fichier de configuration correspondant sous
content/configs.<locale>.jsonavec les clés de traduction localisées. - Enregistrement du sélecteur de langue : Enregistrez le code de locale et son nom lisible par un humain dans
ALL_LOCALESetLOCALE_NAMESà l'intérieur deapp/components/language-switcher.tsx. - Tableau du chargeur de docs : Ajoutez le code de locale au tableau
LOCALESà l'intérieur deapp/lib/docs.ts. - Réexportation de route : Réexportez les routes standard en créant un répertoire
app/routes/<locale>/correspondant à la structure des fichiers de route racine. - Traductions : Fournissez les traductions des docs markdown/MDX et des références de composants respectivement sous
content/docs/<locale>/*.mdetcontent/components/<locale>/*.mdx.
Style
PandaCSS génère tout le CSS à l'avance — il n'y a pas de moteur de style à l'exécution. panda.config.ts étend le thème de base depuis app/theme/, analyse app/**/*.{js,jsx,ts,tsx} pour l'usage des styles, et écrit le système généré (recettes, tokens, patterns, helpers JSX) dans design-system/, que les composants importent via l'alias Vite design-system.
Conceptions de recettes à emplacements et composants multiparties
Les recettes de thème pour les composants multiparties (par ex. RadioGroup, SegmentGroup, Tabs, ToggleGroup, Select, Avatar, Pagination, HoverCard) doivent définir explicitement leurs slots comme un tableau de chaînes dans defineSlotRecipe plutôt que d'importer depuis @ark-ui/react/anatomy ou @ark-ui/anatomy afin d'éliminer les dépendances React dans la couche de style.
Les composants multiparties utilisant defineSlotRecipe doivent être enregistrés dans slotRecipes dans app/theme/recipes/index.ts et explicitement inclus dans staticCss.recipes dans panda.config.ts (par ex. radioGroup: ['*'], select: ['*'], tabs: ['*']) pour garantir que toutes les variantes comme size sont correctement générées pour les îlots Hono.
Conflits de nommage de recettes personnalisées
Nommer une recette personnalisée stack entre en conflit avec les motifs de mise en page intégrés de Panda CSS, déclenchant un avertissement lors de codegen, bien que la recette reste fonctionnelle.
Tokens de couleur vs tokens sémantiques
Dans le système de design PandaCSS du projet :
- Tokens (
tokens.colors) : Les couleurs statiques pures (comme le noir et le blanc) sont définies comme valeurs brutes sousapp/theme/tokens/colors.ts. - Tokens sémantiques (
semanticTokens.colors) : Les palettes d'échelles conditionnelles ou adaptatives (comme slate/gray, blue, red, etc.) sont déclarées ici pour permettre la compilation automatique des variables des modes clair et sombre.
Directives d'utilisation explicite des tokens sémantiques
Dans la config Panda CSS et les styles personnalisés, évitez d'utiliser des tokens de couleur génériques comme bg et fg (qui compilent en CSS transparent/invalide). Utilisez plutôt des tokens sémantiques explicites comme gray.surface.bg, fg.default et gray.outline.border pour préserver les bons états de thème.
De plus, lors du style de superpositions contextuelles, de listes déroulantes ou de composants d'auto-complétion (comme app/islands/search.tsx), utilisez le token de fond sémantique gray.surface.bg pour garantir un fond opaque en modes clair/sombre et éviter le chevauchement de texte.
CMS
Sveltia CMS s'exécute entièrement côté client sur /admin/, configuré par public/admin/config.yml. app/server.ts sert les fichiers statiques de ce répertoire (config, HTML, assets) directement depuis public/admin/ plutôt que via le routage normal, donc l'interface CMS fonctionne de manière identique en développement et une fois déployée. Elle est adossée à Git : les modifications apportées dans l'interface CMS sont validées directement dans les fichiers de contenu sous content/, que le prochain build récupère comme n'importe quel autre changement.
Outils de développement et intégrité
Commandes Node et Bun
Pour configurer l'environnement de développement, installer les dépendances et exécuter la génération de code PandaCSS :
bun install
Pour exécuter le serveur de développement local (Vite sur le port 5173 par défaut) :
bun run dev
Pour construire la sortie de site statique (dist/) :
bun run build
Tests unitaires proactifs
Pour exécuter les tests unitaires de la base de code :
bun test unit
Note : Exécutez toujours les tests unitaires avec bun test unit pour contourner les échecs de dépendances manquantes potentiels des tests d'intégration qui dépendent de paquets externes lourds comme @playwright/test.
Biome Linter et qualité de code
Le dépôt utilise Biome pour le lint et le formatage du code. Pour garantir que bun run check et bun run fix s'exécutent avec succès avec un code de sortie 0, les règles restrictives et très bruyantes qui génèrent des faux positifs sur les attributs de composants dynamiques standard sont explicitement désactivées dans biome.json. Ces règles incluent :
useExportsLastuseAriaPropsSupportedByRolenoLabelWithoutControluseSemanticElementsnoNoninteractiveElementToInteractiveRole
Restriction des CLI orientées React
L'exécution de commandes CLI orientées React (comme @park-ui/cli) directement dans ce dépôt écrasera les implémentations Hono/JSX personnalisées et les recettes à emplacements par des modèles spécifiques à React, cassant le modèle SSG/îlots HonoX. Vérifiez toujours les fichiers existants de la base de code avant d'exécuter des scripts d'installation de composants externes.
Déploiement
La sortie de build (dist/) est un site entièrement statique — aucun processus serveur n'est requis au moment de la requête. Deux cibles sont configurées prêtes à l'emploi :
- Cloudflare Pages (
wrangler.jsonc) —assets.directorypointe versdist/;bun run deployconstruit puis exécutewrangler pages deploy ./dist. - Vercel (
vercel.json) — même commande de build,outputDirectory: "dist",cleanUrls: true(ainsi la réécriture clean-URL propre de Vercel complète les corrections d'index de répertoire defixSsgRoutingPlugin).
bun run preview (wrangler dev) sert le dist/ construit localement via le runtime local de Cloudflare, distinct de bun run dev (vite), qui exécute le serveur de développement HonoX en direct avec HMR.