Communitygithub.com

asolis87/application-alimentaire-complementaire

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

Qu'est-ce que application-alimentaire-complementaire ?

Ce skill définit les conventions strictes pour construire des sites web statiques optimisés (SSG) avec Astro chez Vera Sesiom. Il est conçu pour les pages d'atterrissage, les sites marketing, les blogs, la documentation et tous les projets où la performance, le SEO et les Core Web Vitals sont critiques. Le principe fondamental est « Zéro JS par défaut » : Astro n'envoie aucun JavaScript dans le navigateur, sauf si un composant Vue (ou React) est expressément hydraté via une directive client (comme `client:visible` ou `client:load`). Le skill impose une architecture Screaming Architecture (les noms de dossiers reflètent le domaine métier, pas le framework), l'utilisation systématique de TypeScript strict, Tailwind CSS pour le style, et les Content Collections d'Astro pour gérer les contenus structurés (articles de blog, documentation, études de cas). Il fournit des modèles de code complets pour les composants Astro, les layouts, les collections de contenu, les îlots interactifs en Vue, l'optimisation des images via `astro:assets`, et un pattern SEO complet. Un tableau d'aide à la décision (Astro vs. Vue) permet de choisir l'outil adapté : si le site est principalement du contenu à lire → Astro ; si c'est une application interactive (tableau de bord, CRUD, temps réel) → Vue. Ce skill est à utiliser avec les plateformes Astro, VS Code / Cursor / Windsurf, et tout environnement de développement web moderne.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/asolis87/complementary-food-app/tree/HEAD/.agents/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 d'entreprise
  • Sites riches en contenu (blogs, documentation)
  • Tout projet où le SEO et la performance sont critiques
  • Sites majoritairement statiques avec une interactivité minimale
  • 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 en temps réel).


Règles critiques

  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. Utiliser 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 le frontend Vue
  7. Screaming Architecture — les dossiers expriment 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, admin)
Riche en contenu (blog, documentation)Interface utilisateur très interactive
Le SEO est critiqueFonctionnalités temps réel nécessaires
Contenu majoritairement statiqueGestion d'état complexe
Interactivité minimale (formulaires, carrousels)Navigation SPA complète
Zéro JS est une fonctionnalitéL'écosystème Vue est important

Règle : Si le site est du contenu que les gens LISENT → Astro. S'il s'agit d'une application que les gens UTILISENT → Vue.


Structure du projet (Screaming Architecture)

apps/landing/   (ou apps/website/)
├── src/
│   ├── pages/                    # Routage basé sur les 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/                  # Mises en page
│   │   ├── BaseLayout.astro      # Structure HTML de base
│   │   ├── BlogLayout.astro      # Mise en page d'article
│   │   └── LandingLayout.astro   # Mise en 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 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 (aucun traitement)
│   ├── 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 a 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 à la requête pour SSR)
  • Pas besoin d'export default — le template EST l'exportation
  • <slot /> pour le contenu enfants
  • Les props sont typées avec des interfaces TypeScript

Modèle de mise en page

---
// 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="en">
  <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 collection de contenu

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"]
---

Contenu 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 envoient du JS uniquement lorsque 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 = hydrate lorsque défilé dans la vue (BONNE PRATIQUE) -->
  <ContactForm client:visible />
</BaseLayout>

Arbre de décision des directives client

DirectiveQuand l'utiliserJS envoyé
Aucune directiveContenu statique, aucune 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 (analytics, 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 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 />                       <!-- Hydrate lorsque visible -->
<MobileMenu client:media="(max-width: 768px)" />  <!-- Mobile uniquement -->
<Chat client:idle />                               <!-- Après que la page se stabilise -->
<Stats />                                          <!-- Animations CSS pures, pas besoin de JS -->

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 héroïque" 
  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 :

  • Toujours spécifier 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 héroïques optimisées et dimensionnées correctement

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 (optionnel) -->
{article && publishedTime && (
  <meta property="article:published_time" content={publishedTime.toISOString()} />
)}
{article && author && (
  <meta property="article:author" content={author} />
)}
{tags.length > 0 && tags.map(tag => (
  <meta property="article:tag" content={tag} />
))}

<!-- JSON-LD (Schema.org) -->
<script type="application/ld+json" set:html={{
  "@context": "https://schema.org",
  "@type": article ? 'Article' : 'WebPage',
  "headline": title,
  "description": description,
  "image": imageURL,
  "url": canonicalURL,
  ...(article && publishedTime ? { "datePublished": publishedTime.toISOString() } : {}),
  ...(article && author ? { "author": { "@type": "Person", "name": author } } : {})
}} />

Utilisation :

---
// src/pages/about.astro
import BaseLayout from '../layouts/BaseLayout.astro'
import SEO from '../components/SEO.astro'
---

<html>
  <head>
    <SEO 
      title="À propos de Vera Sesiom"
      description="Apprenez-en plus sur notre équipe et notre mission."
      image="/images/about-hero.jpg"
    />
  </head>
  <body>
    <BaseLayout>
      <h1>À propos de nous</h1>
    </BaseLayout>
  </body>
</html>

Configuration du site

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

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

Points clés :

  • output: 'static' est le paramètre par défaut
  • Utilisez output: 'server' uniquement si vous avez besoin de rendu SSR
  • @astrojs/sitemap pour le référencement
  • @astrojs/vue pour les composants interactifs
  • @astrojs/tailwind pour l'intégration Tailwind

Résumé des bonnes pratiques

  1. Toujours commencer par zéro JS — n'ajoutez du JavaScript que lorsque c'est absolument nécessaire
  2. Utiliser les Content Collections pour tout contenu structuré — schéma validé, typage automatique
  3. Préférer client:visible à client:load pour l'hydratation des composants
  4. Toujours optimiser les images — dimensions, formats et chargement différé
  5. Suivre la Screaming Architecture — organisez par domaine, pas par type de fichier
  6. Implémenter le SEO dès le départ — balises meta, Open Graph, JSON-LD
  7. Utiliser les layouts pour maintenir la cohérence entre les pages
  8. Garder les composants petits et ciblés — un composant = une responsabilité

Exemple de schéma de décision

Le site est principalement du contenu ?
├── OUI → Principalement statique ?
│   ├── OUI → Avez-vous besoin d'interactivité ?
│   │   ├── NON → Page purement statique : zéro composant interactif
│   │   └── OUI → Composants interactifs isolés : utiliser des îlots Vue avec client:visible
│   └── NON → Contenu dynamique (forum, commentaires) : SSR Astro avec endpoints API
└── NON → C'est une application riche : utiliser le skill `vue-frontend`

Rappelez-vous : Zéro JS par défaut n'est pas une limitation — c'est une fonctionnalité de performance. Cela conduit à des sites plus rapides, un meilleur SEO et une meilleure expérience utilisateur.

Skills associés