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
- Zéro JS par défaut — Astro n'envoie AUCUN JavaScript sauf si vous ajoutez une directive client
- TypeScript TOUJOURS — comme tous les projets Vera Sesiom, mode strict
- Utiliser les Content Collections pour le contenu structuré — articles de blog, documentation, études de cas
- Architecture en îlots — Composants Vue/React uniquement là où l'interactivité est nécessaire
- Sortie statique (SSG) par défaut — SSR uniquement lorsque le contenu dynamique l'exige
- Tailwind CSS pour le style — comme le frontend Vue
- Screaming Architecture — les dossiers expriment le domaine, pas le framework
Critères de décision : Astro vs Vue
| Quand utiliser Astro | Quand utiliser Vue |
|---|---|
| Page d'atterrissage, site marketing | Application web (tableau de bord, admin) |
| Riche en contenu (blog, documentation) | Interface utilisateur très interactive |
| Le SEO est critique | Fonctionnalités temps réel nécessaires |
| Contenu majoritairement statique | Gestion 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
| Directive | Quand l'utiliser | JS envoyé |
|---|---|---|
| Aucune directive | Contenu statique, aucune interactivité | Zéro JS |
client:visible | Interactivité sous la ligne de flottaison (formulaires, carrousels) — PAR DÉFAUT | Lorsque défilé dans la vue |
client:load | Interactivité critique au-dessus de la ligne de flottaison | Immédiatement |
client:idle | Améliorations non critiques (analytics, chat) | Après que la page soit inactive |
client:media | Conditionnel par point de rupture (navigation mobile) | Lorsque la requête média correspond |
client:only | Composants de type SPA (ignorer SSR) | Immédiatement, pas de SSR |
Bonnes pratiques :
- Par défaut, aucune directive — purement statique
- Par défaut,
client:visiblelorsque l'interactivité est nécessaire - Utilisez
client:loadavec parcimonie — uniquement pour le contenu critique au-dessus de la ligne de flottaison - Utilisez
client:idlepour 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
widthetheightpour la prévention du CLS - Utilisez
loading="lazy"pour les images sous la ligne de flottaison - Préférez les formats
webpouavif - 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/sitemappour le référencement@astrojs/vuepour les composants interactifs@astrojs/tailwindpour l'intégration Tailwind
Résumé des bonnes pratiques
- Toujours commencer par zéro JS — n'ajoutez du JavaScript que lorsque c'est absolument nécessaire
- Utiliser les Content Collections pour tout contenu structuré — schéma validé, typage automatique
- Préférer
client:visibleàclient:loadpour l'hydratation des composants - Toujours optimiser les images — dimensions, formats et chargement différé
- Suivre la Screaming Architecture — organisez par domaine, pas par type de fichier
- Implémenter le SEO dès le départ — balises meta, Open Graph, JSON-LD
- Utiliser les layouts pour maintenir la cohérence entre les pages
- 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.