Un refactor interne pour la croissance future de notre blog

Fait partie de la série Créer un Blog Complet avec VitePress et Vue.js de Zéro

Notre blog a été établi, mettant l'accent principalement sur le contenu et un bon fonctionnement.

Mais nous visons à faire plus. Nous avons envisagé d'ajouter un système de commentaires. Une fois que vous avez une API, de nombreuses possibilités s'offrent à vous. De plus, nous avons beaucoup appris depuis le lancement de ce blog, et nous souhaitons appliquer ces connaissances pour le rendre plus maintenable et scalable, surtout pour une croissance future, même si ce refactor interne peut ne pas sembler l'améliorer immédiatement.

Ce refactor se concentre sur deux points principaux :

  1. Ajouter Tailwind Variants pour faciliter la réutilisation des composants
  2. Passer d'une structure de dossiers basée sur le type à une structure de dossiers basée sur les fonctionnalités

Laissez-moi expliquer pourquoi nous voulons faire cela.

Pourquoi utiliser Tailwind Variants ?

Commençons par comprendre le problème auquel nous faisons face. Actuellement, notre base de code a deux boutons :

  • ButtonPrimary.vue : un bouton principal
  • ButtonSecondary.vue : un bouton secondaire

Le problème est que notre site est simple et n'a que ces deux boutons. Mais que se passe-t-il si nous voulons ajouter un bouton tertiaire, un bouton avec une bordure au lieu d'une couleur de fond, ou un bouton de taille différente ? Pire encore, que se passe-t-il si nous voulons que notre Button soit un vrai button au lieu d'une simple balise a ? Mettre à jour chaque composant individuellement serait un cauchemar à maintenir, et des changements mineurs nécessiteraient tellement de temps, entraînant une perte de productivité—souvent la raison pour laquelle les projets échouent.

Pour résoudre cela, nous pouvons créer un unique composant regroupant tous nos boutons et c'est plus facile qu'il n'y paraît. Considérez les composants ButtonPrimary.vue et ButtonSecondary.vue :

vue
<script lang="ts" setup>
defineProps<{
  href: string
}>()
</script>

<template>
  <a
    :href
    class="border-4 border-black bg-[var(--color-yellow)] px-8 py-4 shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:bg-[#ffdf1b] hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]"
  >
    <slot />
  </a>
</template>
vue
<script lang="ts" setup>
defineProps<{
  href: string
}>()
</script>

<template>
  <a
    :href
    class="inline-block border-4 border-black bg-[var(--color-purple)] px-8 py-4 text-white shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:bg-[#828cd2] hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]"
  >
    <slot />
  </a>
</template>

Ils sont presque identiques ! La seule différence est la couleur : bg-[var(--color-yellow)] hover:bg-[#ffdf1b] pour le premier, et bg-[var(--color-purple)] hover:bg-[#828cd2] pour le second. Cela peut être fusionné en un seul composant :

vue
<script lang="ts" setup>
const props = defineProps<{
  href: string
  color: string
}>()

const color = computed(() => {
  switch (props.color) {
    case 'primary':
      return 'bg-[var(--color-yellow)] hover:bg-[#ffdf1b]'
    case 'secondary':
      return 'bg-[var(--color-purple)] hover:bg-[#828cd2]'
    default:
      throw new Error('Couleur inconnue')
  }
})
</script>

<template>
  <a
    :href
    class="inline-block border-4 border-black px-8 py-4 text-white shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]" :class="[color]"
  >
    <slot />
  </a>
</template>

C'est un bon départ, mais ce n'est pas encore scalable. Notre bouton pourrait varier en taille, en couleur, avoir plusieurs états... Gérer cela dans plusieurs propriétés calculées n'est pas idéal. C'est là que Tailwind Variants entre en jeu.

Une approche puissante est de découpler le thème, pas le style, du composant et du template. Avec Tailwind Variants, nous pouvons définir un thème indépendant pour un composant adaptable à différents contextes de manière déclarative :

ts
import { tv } from 'tailwind-variants'

const button = tv({
  base: 'inline-block border-4 border-black px-8 py-4 text-white shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:shadow-[6px_6px_0_black] hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem]',
  variants: {
    color: {
      primary: 'bg-[var(--color-yellow)] hover:bg-[#ffdf1b]',
      secondary: 'bg-[var(--color-purple)] hover:bg-[#828cd2]',
    },
  },
})

Avec cet exemple, vous devriez commencer à comprendre la puissance de cette approche. Le style de base, partagé entre les variantes, est défini de manière centrale. Ensuite, nous définissons des variantes, comme la couleur. D'autres propriétés, comme la taille, peuvent également être ajoutées ou des variantes composées définies pour des styles spécifiques, comme lorsque le bouton est principal et petit.

Réécrivons notre composant Button.vue :

vue
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'

const button = tv({
  base: 'border-4 border-black px-8 py-4 shadow-[4px_4px_0_black] transition duration-150 ease-linear hover:-translate-x-[0.125rem] hover:-translate-y-[0.125rem] hover:shadow-[6px_6px_0_black]',
  variants: {
    color: {
      primary: 'bg-[var(--color-yellow)] hover:bg-[#ffdf1b]',
      secondary: 'text-white bg-[var(--color-purple)] hover:bg-[#828cd2]',
    },
  },
  defaultVariants: {
    color: 'primary',
  },
})

type ButtonVariants = VariantProps<typeof button>

export interface ButtonProps {
  href: string
  label: string
  color?: ButtonVariants['color']
  class?: any
}
export interface ButtonEmits {}
export interface ButtonSlots {}
</script>

<script lang="ts" setup>
const props = defineProps<ButtonProps>()
defineEmits<ButtonEmits>()
defineSlots<ButtonSlots>()

const ui = computed(() => button({ class: props.class, color: props.color }))
</script>

<template>
  <a :href="props.href" :class="ui">
    {{ props.label }}
  </a>
</template>

Un aspect intéressant est l'extraction des variantes avec TypeScript et leur application aux props. Le type ButtonVariants inclut toutes les variantes de Button en fonction du thème button. Donc, tout changement de thème se reflète automatiquement dans le composant Button.

Vous vous demandez probablement : comment Tailwind Variants sait-il quelle variante utiliser ? Regardez ce code :

ts
const ui = computed(() => button({ class: props.class, color: props.color }))

La variable button est une fonction qui prend un objet avec des variantes comme arguments. Ainsi, les variantes sont calculées à l'exécution en fonction des props et du thème prédéfini.

Nous pouvons utiliser notre composant Button de cette manière :

vue
<Button href="https://soubiran.dev" label="Bouton Principal" />

<Button href="https://soubiran.dev" label="Bouton Secondaire" color="secondary" />

Automatiquement, le composant Button applique le style correct en fonction de la prop color.

Pourquoi passer à une structure de dossiers basée sur les fonctionnalités ?

Actuellement, notre base de code est dans .vitepress/theme, divisée en dossiers :

    Par Type

  • .vitepress/theme

Nous pourrions avoir des dossiers supplémentaires comme directives, utils, queries, ou mutations. Cette structure, basée sur les types de fichiers, place tous les composants dans un dossier, quelle que soit leur utilisation, ce qui n'est pas idéal.

Il est difficile d'identifier les fonctionnalités clés du blog à partir de la structure des dossiers. Avec une seule fonctionnalité, cela reste gérable, mais au fur et à mesure que le blog grandit, localiser des composants ou des pages spécifiques devient un défi. D'où l'avantage d'une structure de dossiers basée sur les fonctionnalités.

Cette approche regroupe plusieurs structures de dossiers par type par fonctionnalité. Il est facile de trouver des composants spécifiques, et lorsque vous travaillez sur une fonctionnalité, tous les fichiers associés sont facilement accessibles. C'est un moyen efficace de gérer et d'évoluer un projet.

Dans notre cas, les principales fonctionnalités sont le blog et les projets. La structure pourrait ressembler à ceci :

    Par Fonctionnalité

  • .vitepress/theme
  • blog
  • projects

De cette façon, les fonctionnalités clés du blog sont claires et les fichiers associés sont facilement trouvables. L'ajout de nouvelles fonctionnalités implique la création d'un nouveau dossier et l'inclusion de fichiers liés.

Un dossier manquant ici est common ou shared. Il contient des fichiers partagés entre les fonctionnalités. Par exemple, le composant Button partagé par le blog et les projets devrait être placé ici.

    Par Fonctionnalité avec Commun

  • .vitepress/theme
  • blog
  • projects
  • common

Note

Je l'ai nommé common, mais vous pouvez choisir n'importe quel nom qui transmet la nature partagée des fichiers ne relevant pas d'une fonctionnalité spécifique.

Restructurer la base de code

Outre les deux points principaux, de petits changements de la base de code amélioreront la lisibilité et réduiront la répétition de code, ce qui est crucial pour maintenir la cohérence du style du blog.

Tout d'abord, nous allons consolider tout ce qui est lié à Page. Chaque composant de page partage la même structure d'enveloppe, de titre et de pied de page. Nous pouvons facilement extraire chaque partie en trois composants :

  1. Page.vue
vue
<script lang="ts">
const page = tv({
  base: 'space-y-16',
})
export interface PageProps {
  class?: any
}
export interface PageEmits {}
export interface PageSlots {
  default: (props?: object) => any
}
</script>

<script lang="ts" setup>
const props = defineProps<PageProps>()
defineEmits<PageEmits>()
defineSlots<PageSlots>()
const ui = computed(() => page({ class: props.class }))
</script>

<template>
  <section :class="ui">
    <slot />
  </section>
</template>
  1. PageTitle.vue
vue
<script lang="ts">
const pageTitle = tv({
  base: 'text-6xl font-semibold',
})
export interface PageTitleProps {
  title: string
  class?: any
}
export interface PageTitleEmits {}
export interface PageTitleSlots {}
</script>

<script lang="ts" setup>
const props = defineProps<PageTitleProps>()
defineEmits<PageTitleEmits>()
defineSlots<PageTitleSlots>()
const ui = computed(() => pageTitle())
</script>

<template>
  <h1 :class="ui">
    {{ props.title }}
  </h1>
</template>
  1. PageFooter.vue
vue
<script lang="ts">
const pageFooter = tv({
  base: 'flex justify-center',
})
export interface PageFooterProps {
  backTo: { href: string, label: string }
  class?: any
}
export interface PageFooterEmits {}
export interface PageFooterSlots {}
</script>

<script lang="ts" setup>
const props = defineProps<PageFooterProps>()
defineEmits<PageFooterEmits>()
defineSlots<PageFooterSlots>()
const ui = computed(() => pageFooter({ class: props.class }))
</script>

<template>
  <div :class="ui">
    <Button :href="props.backTo.href" :label="props.backTo.label" />
  </div>
</template>

Ces composants simples aident à réduire la répétition de code et simplifient la création de nouvelles pages. De plus, nous pouvons regrouper chaque prose en un seul composant Prose.vue, garantissant un style de prose de blog cohérent.

Ensuite, nous pouvons unifier ProjectCard.vue et BlogCard.vue en Card.vue. Leurs seules différences sont la taille du titre, la description et le remplissage. Tailwind Variants gère efficacement ces différences :

vue
<script lang="ts">
import type { VariantProps } from 'tailwind-variants'
const card = tv({
  slots: {
    base: 'group relative border-4 border-black bg-white shadow-[8px_8px_0_black] transition duration-150 ease-linear hover:-translate-x-1 hover:-translate-y-1 hover:shadow-[12px_12px_0_black]',
    title: 'font-medium text-black',
    description: 'text-black',
  },
  variants: {
    size: {
      sm: {
        base: 'space-y-4 p-6',
        title: 'text-2xl',
        description: 'text-lg',
      },
      md: {
        base: 'space-y-6 p-12',
        title: 'text-4xl',
        description: 'text-xl',
      },
    },
  },
  defaultVariants: {
    size: 'md',
  },
})
type CardVariants = VariantProps<typeof card>
export interface CardProps {
  href: string
  title: string
  description?: string
  size: CardVariants['size']
  class?: any
  ui?: Partial<typeof card.slots>
}
export interface CardEmits {}
export interface CardSlots {}
</script>

<script lang="ts" setup>
const props = defineProps<CardProps>()
defineEmits<CardEmits>()
defineSlots<CardSlots>()
const ui = computed(() =>
  card({
    size: props.size,
  }),
)
</script>

<template>
  <article :class="ui.base({ class: [props.class, props.ui?.base] })">
    <h2 :class="ui.title({ class: props.ui?.title })">
      <a :href="props.href">
        {{ props.title }}
        <span class="absolute inset-0" />
      </a>
    </h2>
    <p :class="ui.description({ class: props.ui?.description })">
      {{ props.description }}
    </p>
  </article>
</template>

Enfin, déplacez le fichier .env.example vers le dossier src, car VitePress définit la racine du projet dans le dossier src.

Pour le refactor complet, reportez-vous au commit 26b1dd4 de Garabit car je ne peux pas couvrir tous les changements dans cet article.

J'espère que vous avez trouvé cet article instructif et avez appris quelque chose de nouveau. Les objectifs étaient de démontrer la puissance de Tailwind Variants avec une structure de dossier basée sur les fonctionnalités et de montrer que vous pouvez commencer simplement et grandir étape par étape au besoin. Dans notre cas, ce refactor prépare le terrain pour ajouter le système de commentaires.

Note

Tous mes projets, même les plus petits, sont basés sur à la fois Tailwind Variants et une structure de dossiers basée sur les fonctionnalités. Cette configuration améliore ma productivité et, grâce à des composants et à une structure de dossiers cohérents, me permet de passer d'un projet à l'autre sans perdre de temps à comprendre l'architecture du projet. Maintenant, créer un composant semble plus être un réflexe qu'une tâche.

Pd

Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web.

Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.

Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.

J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous—cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !

Continuer la lectureMigration du blog VitePress vers Tailwind CSS Version 4

Réactions

Discussions

Ajouter un commentaire

Vous devez être connecté pour accéder à cette fonctionnalité.

Se connecter avec GitHub