Communitygithub.com

asolis87/vera-sesiom-skills

Conventions Astro pour les pages d'atterrissage, les sites de contenu et les projets SSG chez Vera Sesiom. Zéro JS par défaut, Vue Islands pour l'interactivité, et architecture axée sur le contenu.

Qu'est-ce que vera-sesiom-skills ?

Ce skill définit les conventions d'architecture et les bonnes pratiques pour les sites statiques (SSG) basés sur Astro chez Vera Sesiom. Il est conçu pour les pages d'atterrissage, sites marketing, blogs, documentations, et tout projet où la performance et le référencement (SEO) sont primordiaux. L'approche privilégie le 'Zero JS by default' (aucun JavaScript n'est envoyé par défaut), l'utilisation de TypeScript en mode strict, et une architecture en îlots (Islands Architecture) avec des composants Vue pour les interactions ponctuelles. Le skill s'appuie sur Tailwind CSS pour le style, les Content Collections pour la gestion de contenu structuré, et le pattern d'architecture 'Screaming Architecture' où les dossiers reflètent le domaine métier plutôt que le framework.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/asolis87/vera-sesiom-skills/tree/HEAD/skills/astro-landing

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Quand l'utiliser

  • Pages d'atterrissage et sites marketing
  • Sites vitrine d'entreprise
  • Sites riches en contenu (blogs, documentation)
  • Tout projet où le SEO et la performance sont critiques
  • Sites principalement statiques avec un minimum d'interactivité
  • Sites nécessitant un chargement initial rapide et de bons Core Web Vitals

PAS pour les applications web complètes — utilisez vue-frontend pour les applications interactives (tableaux de bord, applications CRUD, applications temps réel).


Règles essentielles

  1. Zéro JS par défaut — Astro n'envoie AUCUN JavaScript sauf si vous ajoutez une directive client
  2. TypeScript TOUJOURS — comme tous les projets Vera Sesiom, mode strict
  3. Utilisez les Content Collections pour le contenu structuré — articles de blog, documentation, études de cas
  4. Architecture en îlots — Composants Vue/React uniquement là où l'interactivité est nécessaire
  5. Sortie statique (SSG) par défaut — SSR uniquement lorsque le contenu dynamique l'exige
  6. Tailwind CSS pour le style — comme pour le frontend Vue
  7. Screaming Architecture — les dossiers crient le domaine, pas le framework

Critères de décision : Astro vs Vue

Quand utiliser AstroQuand utiliser Vue
Page d'atterrissage, site marketingApplication web (tableau de bord, administration)
Riche en contenu (blog, documentation)Interface utilisateur très interactive
Le SEO est critiqueFonctionnalités temps réel nécessaires
Contenu principalement statiqueGestion d'état complexe
Interactivité minimale (formulaires, carrousels)Navigation SPA complète
Zéro JS est une fonctionnalitéLes avantages de l'écosystème Vue comptent

Règle : Si le site est fait de contenu que les gens LISENT → Astro. Si c'est une application que les gens UTILISENT → Vue.


Structure du projet (Screaming Architecture)

apps/landing/   (ou apps/website/)
├── src/
│   ├── pages/                    # Routage basé sur les fichiers (fichiers .astro)
│   │   ├── index.astro           # Page d'accueil
│   │   ├── about.astro           # Page À propos
│   │   ├── pricing.astro         # Page Tarifs
│   │   ├── blog/
│   │   │   ├── index.astro      # Liste des articles
│   │   │   └── [slug].astro      # Article de blog (dynamique)
│   │   └── contact.astro
│   ├── layouts/                  # Layouts de page
│   │   ├── BaseLayout.astro      # Structure HTML de base
│   │   ├── BlogLayout.astro      # Layout d'article de blog
│   │   └── LandingLayout.astro   # Layout de page d'atterrissage
│   ├── components/               # Composants Astro (statiques par défaut)
│   │   ├── sections/             # Sections de page
│   │   │   ├── Hero.astro
│   │   │   ├── Features.astro
│   │   │   ├── Pricing.astro
│   │   │   ├── Testimonials.astro
│   │   │   └── CTA.astro
│   │   ├── ui/                   # Atomes d'interface réutilisables
│   │   │   ├── Button.astro
│   │   │   ├── Card.astro
│   │   │   ├── Badge.astro
│   │   │   └── Input.astro
│   │   └── interactive/          # Îlots Vue/React
│   │       ├── ContactForm.vue   # Formulaire avec validation
│   │       ├── Carousel.vue      # Carrousel d'images
│   │       └── MobileMenu.vue    # Navigation mobile
│   ├── content/                  # Collections de contenu
│   │   ├── blog/                 # Articles de blog en Markdown/MDX
│   │   │   ├── post-1.md
│   │   │   └── post-2.mdx
│   │   └── config.ts             # Schéma de collection (Zod)
│   ├── styles/                   # Styles globaux
│   │   └── global.css            # Imports Tailwind
│   ├── assets/                   # Images optimisées, polices
│   │   ├── images/
│   │   └── fonts/
│   └── utils/                    # Fonctions utilitaires
│       ├── date-format.ts
│       └── seo.ts
├── public/                       # Actifs statiques (non traités)
│   ├── favicon.ico
│   ├── robots.txt
│   └── fonts/
├── astro.config.mjs              # Configuration Astro
├── tailwind.config.mjs           # Configuration Tailwind
├── tsconfig.json
└── package.json

Modèle de composant Astro

Chaque fichier .astro comporte deux parties : le frontmatter (côté serveur) et le template (HTML).

---
// src/components/ui/Button.astro
// Frontmatter : s'exécute côté serveur, PAS de JS navigateur

export interface Props {
  variant?: 'primary' | 'secondary' | 'outline'
  size?: 'sm' | 'md' | 'lg'
  href?: string
}

const {
  variant = 'primary',
  size = 'md',
  href,
  ...rest
} = Astro.props

const baseClasses = 'inline-flex items-center justify-center font-medium rounded-lg transition-colors'
const variantClasses = {
  primary: 'bg-blue-600 text-white hover:bg-blue-700',
  secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
  outline: 'border-2 border-blue-600 text-blue-600 hover:bg-blue-50'
}
const sizeClasses = {
  sm: 'px-3 py-1.5 text-sm',
  md: 'px-4 py-2 text-base',
  lg: 'px-6 py-3 text-lg'
}

const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]}`
---

{href ? (
  <a href={href} class={classes} {...rest}>
    <slot />
  </a>
) : (
  <button class={classes} {...rest}>
    <slot />
  </button>
)}

Points clés :

  • Le frontmatter s'exécute au moment de la construction (ou de la requête pour le SSR)
  • Pas besoin d'export default — le template EST l'export
  • <slot /> pour le contenu enfant
  • Les props sont typées avec des interfaces TypeScript

Modèle de layout

---
// src/layouts/BaseLayout.astro
import Nav from '../components/Nav.astro'
import Footer from '../components/Footer.astro'

export interface Props {
  title: string
  description?: string
  image?: string
}

const { title, description = '', image = '/og-default.png' } = Astro.props
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
---

<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="canonical" href={canonicalURL} />
    <title>{title}</title>
    <meta name="description" content={description} />
    
    <!-- OpenGraph -->
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={image} />
    <meta property="og:url" href={canonicalURL} />
    <meta property="og:type" content="website" />
    
    <!-- Twitter -->
    <meta name="twitter:card" content="summary_large_image" />
    
    <slot name="head" />
  </head>
  <body class="bg-white text-gray-900 antialiased">
    <Nav />
    <main>
      <slot />
    </main>
    <Footer />
  </body>
</html>

Utilisation :

---
// src/pages/index.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import Hero from '../components/sections/Hero.astro'
---

<BaseLayout title="Vera Sesiom - Développement logiciel" description="Nous construisons des logiciels qui comptent.">
  <Hero />
</BaseLayout>

Modèle de Content Collection

Définissez du contenu structuré avec une validation de schéma Zod.

// src/content/config.ts
import { defineCollection, z } from 'astro:content'

const blogCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    author: z.string().default('Vera Sesiom'),
    image: z.object({
      url: z.string(),
      alt: z.string()
    }).optional(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false)
  })
})

export const collections = {
  blog: blogCollection
}
---
title: "Pourquoi nous avons choisi Astro pour les pages d'atterrissage"
description: "La performance et l'expérience développeur comptent. Voici pourquoi Astro est notre choix pour les sites de contenu."
pubDate: 2024-01-15
author: "Lead Technique"
image:
  url: "/images/blog/astro-landing.jpg"
  alt: "Logo Astro sur un site web moderne"
tags: ["astro", "performance", "architecture"]
---

Le contenu va ici...
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'

const posts = (await getCollection('blog'))
  .filter(post => !post.data.draft)
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())
---

<BaseLayout title="Blog" description="Derniers articles de Vera Sesiom">
  <h1>Blog</h1>
  <ul>
    {posts.map(post => (
      <li>
        <a href={`/blog/${post.slug}`}>
          <h2>{post.data.title}</h2>
          <p>{post.data.description}</p>
          <time>{post.data.pubDate.toLocaleDateString()}</time>
        </a>
      </li>
    ))}
  </ul>
</BaseLayout>
---
// src/pages/blog/[slug].astro
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'

export async function getStaticPaths() {
  const posts = await getCollection('blog')
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }))
}

const { post } = Astro.props
const { Content } = await post.render()
---

<BaseLayout title={post.data.title} description={post.data.description}>
  <article>
    <header>
      <h1>{post.data.title}</h1>
      <time>{post.data.pubDate.toLocaleDateString()}</time>
      <p>Par {post.data.author}</p>
    </header>
    <Content />
  </article>
</BaseLayout>

Architecture en îlots (composants Vue)

Les composants interactifs n'envoient du JS que lorsque cela est nécessaire. Utilisez les directives client pour contrôler l'hydratation.

<!-- src/components/interactive/ContactForm.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const form = ref({
  name: '',
  email: '',
  message: ''
})

const errors = ref<Record<string, string>>({})

const isValid = computed(() => {
  return form.value.name && 
         form.value.email.includes('@') && 
         form.value.message.length > 10
})

async function submitForm() {
  if (!isValid.value) return
  
  // Soumettre à l'API
  const response = await fetch('/api/contact', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(form.value)
  })
  
  if (response.ok) {
    // Gérer le succès
    form.value = { name: '', email: '', message: '' }
  }
}
</script>

<template>
  <form @submit.prevent="submitForm" class="space-y-4">
    <div>
      <label for="name">Nom</label>
      <input v-model="form.name" type="text" id="name" required />
    </div>
    <div>
      <label for="email">Email</label>
      <input v-model="form.email" type="email" id="email" required />
    </div>
    <div>
      <label for="message">Message</label>
      <textarea v-model="form.message" id="message" rows="4" required />
    </div>
    <button type="submit" :disabled="!isValid">
      Envoyer le message
    </button>
  </form>
</template>

Utilisation dans Astro :

---
// src/pages/contact.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import ContactForm from '../components/interactive/ContactForm.vue'
---

<BaseLayout title="Contactez-nous">
  <h1>Prenez contact</h1>
  <p>Nous serions ravis de vous entendre.</p>
  
  <!-- client:visible = hydrater lorsque défilé dans la vue (BONNE PRATIQUE) -->
  <ContactForm client:visible />
</BaseLayout>

Arbre de décision des directives client

DirectiveQuand l'utiliserJS livré
Aucune directiveContenu statique, sans interactivitéZéro JS
client:visibleInteractivité sous la ligne de flottaison (formulaires, carrousels) — PAR DÉFAUTLorsque défilé dans la vue
client:loadInteractivité critique au-dessus de la ligne de flottaisonImmédiatement
client:idleAméliorations non critiques (analytique, chat)Après que la page soit inactive
client:mediaConditionnel par point de rupture (navigation mobile)Lorsque la requête média correspond
client:onlyComposants de type SPA (ignorer le SSR)Immédiatement, pas de SSR

Bonnes pratiques :

  • Par défaut, aucune directive — purement statique
  • Par défaut, client:visible lorsque l'interactivité est nécessaire
  • Utilisez client:load avec parcimonie — uniquement pour le contenu critique au-dessus de la ligne de flottaison
  • Utilisez client:idle pour les améliorations non bloquantes
---
<!-- Bons exemples -->
import Hero from './Hero.astro'                    <!-- Statique, pas de JS -->
import Carousel from './Carousel.vue'             
import MobileMenu from './MobileMenu.vue'         
import Chat from './Chat.vue'                     
import Stats from './Stats.astro'                  <!-- Compteur statique = CSS -->

<Hero />                                          <!-- Statique, pas d'hydratation -->
<Carousel client:visible />                       <!-- Hydrater lorsque visible -->
<MobileMenu client:media="(max-width: 768px)" />  <!-- Mobile uniquement -->
<Chat client:idle />                               <!-- Après que la page se soit stabilisée -->
<Stats />                                          <!-- Animations CSS pures, pas de JS nécessaire -->

Optimisation des images

Utilisez astro:assets pour une optimisation automatique.

---
// src/pages/index.astro
import { Image } from 'astro:assets'
import heroImage from '../assets/images/hero.png'
---

<Image 
  src={heroImage} 
  alt="Image hero" 
  width={800} 
  height={600}
  loading="lazy"
  format="webp"
/>

Pour les images provenant d'un CMS ou de sources externes :

---
import { Image } from 'astro:assets'
---

<Image 
  src="https://example.com/image.jpg" 
  alt="Image externe" 
  width={800}
  height={600}
/>

Règles :

  • Spécifiez toujours width et height pour la prévention du CLS
  • Utilisez loading="lazy" pour les images sous la ligne de flottaison
  • Préférez les formats webp ou avif
  • Gardez les images hero optimisées et dimensionnées de manière appropriée

Modèle SEO

---
// src/components/SEO.astro
export interface Props {
  title: string
  description: string
  image?: string
  article?: boolean
  publishedTime?: Date
  author?: string
  tags?: string[]
}

const { 
  title, 
  description, 
  image = '/og-default.png',
  article = false,
  publishedTime,
  author,
  tags = []
} = Astro.props

const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const imageURL = new URL(image, Astro.site)
---

<!-- Balises Meta principales -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />

<!-- Open Graph -->
<meta property="og:type" content={article ? 'article' : 'website'} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={imageURL} />

<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={canonicalURL} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
<meta property="twitter:image" content={imageURL} />

<!-- Article spécifique -->
{article && publishedTime && (
  <meta property="article:published_time" content={publishedTime.toISOString()} />
)}
{article && author && (
  <meta property="article:author" content={author} />
)}
{article && tags.length > 0 && (
  tags.map(tag => <meta property="article:tag" content={tag} />)
)}

Utilisation :

---
// src/pages/index.astro
import SEO from '../components/SEO.astro'
---

<SEO 
  title="Vera Sesiom - Développement logiciel"
  description="Nous construisons des logiciels qui comptent."
  image="/images/og-home.png"
/>

Modèle RSS (optionnel)

// src/pages/rss.xml.ts
import rss from '@astrojs/rss'
import { getCollection } from 'astro:content'

export async function GET(context: any) {
  const posts = (await getCollection('blog'))
    .filter(post => !post.data.draft)
    .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())

  return rss({
    title: 'Blog Vera Sesiom',
    description: 'Derniers articles de Vera Sesiom',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/blog/${post.slug}/`,
    })),
    customData: `<language>fr-fr</language>`,
  })
}

Scripts npm essentiels

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview",
    "astro": "astro",
    "lint": "eslint src/",
    "format": "prettier --write src/"
  }
}

Intégrations Astro courantes

// astro.config.mjs
import { defineConfig } from 'astro/config'
import vue from '@astrojs/vue'
import tailwind from '@astrojs/tailwind'
import sitemap from '@astrojs/sitemap'
import mdx from '@astrojs/mdx'

export default defineConfig({
  site: 'https://verasesiom.com',
  output: 'static',
  integrations: [
    vue(),
    tailwind(),
    sitemap(),
    mdx()
  ]
})

Intégrations recommandées :

  • @astrojs/vue — pour les îlots Vue
  • @astrojs/tailwind — pour l'intégration Tailwind
  • @astrojs/sitemap — pour le SEO
  • @astrojs/mdx — pour le contenu MDX
  • @astrojs/image — pour l'optimisation des images (si vous utilisez un CMS externe)

Bonnes pratiques rapides

  1. Utilisez client:visible comme directive par défaut pour les composants interactifs
  2. Gardez le frontmatter maigre — les transformations lourdes doivent être dans les utilitaires
  3. Utilisez Astro.url pour les URLs actuelles — pas de chaînes codées en dur
  4. Préfixez les composants globaux (Hero, Features) pour éviter les collisions de noms
  5. Validez les schémas de contenu avec Zod (z.string(), z.date(), etc.)
  6. Exportez les Props en tant qu'interface pour la documentation et la sécurité des types
  7. Utilisez slot pour les composants réutilisables (cards, boutons avec contenu personnalisé)
  8. Préférez les animations CSS aux bibliothèques JS pour les micro-interactions
  9. Gardez les images hero en WebP, larges de 1200px maximum
  10. Utilisez getStaticPaths() pour les routes dynamiques avec des sources de données connues

Skills associés