Astro SSG : Guide Complet Performance et SEO 2024
Astro révolutionne le développement web statique avec son approche Islands Architecture. Après avoir développé plusieurs sites avec Astro, voici mon guide complet pour créer des sites ultra-performants.
1. Pourquoi Choisir Astro en 2024 ?
Avantages Clés d’Astro
- Zero JavaScript par défaut : HTML statique ultra-rapide
- Islands Architecture : Hydratation sélective
- Multi-framework : React, Vue, Svelte dans le même projet
- SEO optimisé : Génération statique complète
- Performance exceptionnelle : Score 100/100 PageSpeed possible
Comparaison avec d’Autres Solutions
// Temps de chargement moyens (First Contentful Paint)
const performanceComparison = {
'Astro SSG': '0.8s',
'Next.js SSG': '1.2s',
'Gatsby': '1.5s',
'Nuxt SSG': '1.1s',
'SPA traditionnelle': '2.5s+'
}
2. Architecture Optimisée d’un Projet Astro
Structure de Projet Recommandée
src/
├── components/
│ ├── ui/ # Composants UI réutilisables
│ ├── layout/ # Composants de mise en page
│ └── islands/ # Composants interactifs
├── content/
│ ├── blog/ # Articles de blog (Markdown)
│ └── config.ts # Configuration du contenu
├── layouts/
│ ├── BaseLayout.astro # Layout de base
│ └── BlogLayout.astro # Layout pour le blog
├── pages/
│ ├── index.astro # Page d'accueil
│ ├── blog/
│ │ ├── index.astro # Index du blog
│ │ └── [...slug].astro # Pages dynamiques
│ └── api/ # Routes API (optionnel)
├── styles/
│ ├── global.css # Styles globaux
│ └── components/ # Styles par composant
└── utils/
├── seo.ts # Utilitaires SEO
└── content.ts # Helpers pour le contenu
Configuration Astro Optimisée
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import tailwind from '@astrojs/tailwind';
import compress from 'astro-compress';
import { remarkReadingTime } from './src/utils/reading-time.mjs';
export default defineConfig({
site: 'https://mathieumont.cc',
integrations: [
mdx({
remarkPlugins: [remarkReadingTime],
shikiConfig: {
theme: 'github-dark-dimmed',
wrap: true
}
}),
sitemap({
customPages: [
'https://mathieumont.cc/services',
'https://mathieumont.cc/portfolio'
]
}),
tailwind({
config: {
applyBaseStyles: false // Pour un contrôle total des styles
}
}),
compress({
css: true,
html: true,
js: true,
img: true,
svg: true
})
],
markdown: {
shikiConfig: {
theme: 'github-dark-dimmed',
wrap: true
}
},
build: {
inlineStylesheets: 'auto'
},
compressHTML: true,
server: {
port: 3000,
host: true
}
});
3. Islands Architecture en Pratique
Composant Island Interactif
---
// src/components/islands/ContactForm.astro
---
<contact-form>
<form id="contact-form" class="space-y-4">
<div>
<label for="name">Nom</label>
<input
type="text"
id="name"
name="name"
required
class="w-full p-3 border rounded-lg"
/>
</div>
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
class="w-full p-3 border rounded-lg"
/>
</div>
<div>
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows="5"
required
class="w-full p-3 border rounded-lg"
></textarea>
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700 transition-colors"
>
<span class="default-text">Envoyer le message</span>
<span class="loading-text hidden">Envoi en cours...</span>
</button>
</form>
<div id="success-message" class="hidden p-4 bg-green-100 text-green-800 rounded-lg">
Merci ! Votre message a été envoyé avec succès.
</div>
<div id="error-message" class="hidden p-4 bg-red-100 text-red-800 rounded-lg">
Une erreur s'est produite. Veuillez réessayer.
</div>
</contact-form>
<script>
class ContactForm extends HTMLElement {
constructor() {
super();
this.form = this.querySelector('#contact-form');
this.successMessage = this.querySelector('#success-message');
this.errorMessage = this.querySelector('#error-message');
this.submitButton = this.querySelector('button[type="submit"]');
this.form.addEventListener('submit', this.handleSubmit.bind(this));
}
async handleSubmit(e) {
e.preventDefault();
// UI Loading state
this.setLoadingState(true);
this.hideMessages();
try {
const formData = new FormData(this.form);
const response = await fetch('/api/contact', {
method: 'POST',
body: formData
});
if (response.ok) {
this.showSuccess();
this.form.reset();
} else {
throw new Error('Erreur réseau');
}
} catch (error) {
console.error('Erreur:', error);
this.showError();
} finally {
this.setLoadingState(false);
}
}
setLoadingState(isLoading) {
const defaultText = this.submitButton.querySelector('.default-text');
const loadingText = this.submitButton.querySelector('.loading-text');
if (isLoading) {
defaultText.classList.add('hidden');
loadingText.classList.remove('hidden');
this.submitButton.disabled = true;
} else {
defaultText.classList.remove('hidden');
loadingText.classList.add('hidden');
this.submitButton.disabled = false;
}
}
showSuccess() {
this.successMessage.classList.remove('hidden');
}
showError() {
this.errorMessage.classList.remove('hidden');
}
hideMessages() {
this.successMessage.classList.add('hidden');
this.errorMessage.classList.add('hidden');
}
}
customElements.define('contact-form', ContactForm);
</script>
Optimisation du Chargement des Islands
---
// src/components/islands/LazyChart.astro
const { data, title } = Astro.props;
---
<lazy-chart data-chart-data={JSON.stringify(data)} data-title={title}>
<div class="chart-placeholder bg-gray-100 h-64 flex items-center justify-center">
<div class="text-gray-500">
📊 Graphique en cours de chargement...
</div>
</div>
<canvas id="chart" class="hidden" width="400" height="200"></canvas>
</lazy-chart>
<script>
class LazyChart extends HTMLElement {
constructor() {
super();
this.chartData = JSON.parse(this.dataset.chartData);
this.title = this.dataset.title;
// Intersection Observer pour le lazy loading
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadChart();
this.observer.disconnect();
}
});
});
this.observer.observe(this);
}
async loadChart() {
try {
// Charger Chart.js seulement quand nécessaire
const { Chart, registerables } = await import('chart.js');
Chart.register(...registerables);
const canvas = this.querySelector('#chart');
const placeholder = this.querySelector('.chart-placeholder');
// Créer le graphique
new Chart(canvas, {
type: 'line',
data: this.chartData,
options: {
responsive: true,
plugins: {
title: {
display: true,
text: this.title
}
}
}
});
// Afficher le graphique et cacher le placeholder
canvas.classList.remove('hidden');
placeholder.classList.add('hidden');
} catch (error) {
console.error('Erreur lors du chargement du graphique:', error);
}
}
}
customElements.define('lazy-chart', LazyChart);
</script>
4. Optimisations de Performance Avancées
Compression et Optimisation d’Images
---
// src/components/OptimizedImage.astro
const {
src,
alt,
width,
height,
loading = "lazy",
class: className = "",
...rest
} = Astro.props;
// Générer les différentes tailles d'image
const sizes = [400, 800, 1200];
const formats = ['webp', 'avif', 'jpg'];
const generateSrcSet = (baseSrc, format) => {
return sizes.map(size =>
`${baseSrc}?w=${size}&f=${format} ${size}w`
).join(', ');
};
---
<picture class={className}>
<!-- AVIF pour les navigateurs modernes -->
<source
srcset={generateSrcSet(src, 'avif')}
type="image/avif"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<!-- WebP comme fallback -->
<source
srcset={generateSrcSet(src, 'webp')}
type="image/webp"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<!-- JPEG comme fallback final -->
<img
src={`${src}?w=800&f=jpg`}
srcset={generateSrcSet(src, 'jpg')}
alt={alt}
width={width}
height={height}
loading={loading}
decoding="async"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
{...rest}
/>
</picture>
Preloading Stratégique
---
// src/layouts/BaseLayout.astro
const { title, description, image, type = "website" } = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Preconnect aux domaines externes -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://analytics.google.com" />
<!-- Preload des ressources critiques -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/css/critical.css" as="style" />
<!-- DNS-prefetch pour les domaines secondaires -->
<link rel="dns-prefetch" href="https://cdn.example.com" />
<!-- SEO Meta Tags -->
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
<!-- Open Graph -->
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
{image && <meta property="og:image" content={new URL(image, Astro.site)} />}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={canonicalURL} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{image && <meta name="twitter:image" content={new URL(image, Astro.site)} />}
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Styles critiques inlinés -->
<style>
/* Critical CSS pour éviter le FOUC */
body {
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.6;
}
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
</style>
</head>
<body>
<slot />
<!-- JavaScript non-critique chargé en différé -->
<script async src="/js/analytics.js"></script>
</body>
</html>
5. SEO Avancé avec Astro
Génération de Rich Snippets
// src/utils/seo.ts
export interface SEOData {
title: string;
description: string;
image?: string;
type?: string;
publishDate?: Date;
author?: string;
tags?: string[];
}
export function generateStructuredData(data: SEOData, url: string) {
const baseSchema = {
"@context": "https://schema.org",
"@type": data.type === "article" ? "Article" : "WebPage",
"url": url,
"headline": data.title,
"description": data.description,
"image": data.image ? [data.image] : undefined,
};
if (data.type === "article") {
return {
...baseSchema,
"@type": "Article",
"datePublished": data.publishDate?.toISOString(),
"dateModified": data.publishDate?.toISOString(),
"author": {
"@type": "Person",
"name": data.author || "Mathieu Mont",
"url": "https://mathieumont.cc"
},
"publisher": {
"@type": "Organization",
"name": "Mathieu Mont - Développeur Fullstack",
"logo": {
"@type": "ImageObject",
"url": "https://mathieumont.cc/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": url
},
"keywords": data.tags?.join(", ")
};
}
return baseSchema;
}
export function generateBreadcrumbs(path: string) {
const segments = path.split('/').filter(Boolean);
const breadcrumbs = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Accueil",
"item": "https://mathieumont.cc"
}
]
};
segments.forEach((segment, index) => {
const url = `https://mathieumont.cc/${segments.slice(0, index + 1).join('/')}`;
const name = segment.charAt(0).toUpperCase() + segment.slice(1);
breadcrumbs.itemListElement.push({
"@type": "ListItem",
"position": index + 2,
"name": name,
"item": url
});
});
return breadcrumbs;
}
Composant SEO Réutilisable
---
// src/components/SEO.astro
import { generateStructuredData, generateBreadcrumbs } from '../utils/seo';
const {
title,
description,
image,
type = "website",
publishDate,
author,
tags,
noindex = false
} = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const fullTitle = title ? `${title} | Mathieu Mont - Développeur Fullstack` : 'Mathieu Mont - Développeur Fullstack';
const structuredData = generateStructuredData({
title: fullTitle,
description,
image: image ? new URL(image, Astro.site).toString() : undefined,
type,
publishDate,
author,
tags
}, canonicalURL.toString());
const breadcrumbs = generateBreadcrumbs(Astro.url.pathname);
---
<!-- Title -->
<title>{fullTitle}</title>
<!-- Basic Meta Tags -->
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />
{noindex && <meta name="robots" content="noindex, nofollow" />}
<!-- Open Graph -->
<meta property="og:type" content={type} />
<meta property="og:url" content={canonicalURL} />
<meta property="og:title" content={fullTitle} />
<meta property="og:description" content={description} />
<meta property="og:site_name" content="Mathieu Mont - Développeur Fullstack" />
{image && <meta property="og:image" content={new URL(image, Astro.site)} />}
{image && <meta property="og:image:alt" content={title} />}
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@MathieuMont" />
<meta name="twitter:creator" content="@MathieuMont" />
<meta name="twitter:url" content={canonicalURL} />
<meta name="twitter:title" content={fullTitle} />
<meta name="twitter:description" content={description} />
{image && <meta name="twitter:image" content={new URL(image, Astro.site)} />}
<!-- Article specific meta -->
{type === "article" && publishDate && (
<>
<meta property="article:published_time" content={publishDate.toISOString()} />
<meta property="article:author" content={author || "Mathieu Mont"} />
{tags && tags.map(tag => <meta property="article:tag" content={tag} />)}
</>
)}
<!-- Structured Data -->
<script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
<script type="application/ld+json" set:html={JSON.stringify(breadcrumbs)} />
<!-- Additional SEO enhancements -->
<meta name="theme-color" content="#667eea" />
<meta name="color-scheme" content="light dark" />
6. Déploiement et CI/CD
Configuration Netlify
# netlify.toml
[build]
publish = "dist/"
command = "npm run build"
[build.environment]
NODE_VERSION = "18"
NPM_FLAGS = "--prefix=/dev/null"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline' https://analytics.google.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com"
[[headers]]
for = "*.js"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "*.css"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[headers]]
for = "*.woff2"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"
[[redirects]]
from = "/old-blog/*"
to = "/blog/:splat"
status = 301
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
GitHub Actions pour CI/CD
# .github/workflows/deploy.yml
name: Deploy to Netlify
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Run lint
run: npm run lint
- name: Build site
run: npm run build
- name: Deploy to Netlify
uses: nwtgck/actions-netlify@v1.2
with:
publish-dir: './dist'
production-branch: main
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
7. Monitoring et Analytics
Web Vitals Tracking
// src/scripts/web-vitals.js
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Envoyer les métriques à votre service d'analytics
console.log('Web Vital:', metric);
// Exemple avec Google Analytics 4
if (typeof gtag !== 'undefined') {
gtag('event', metric.name, {
event_category: 'Web Vitals',
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
non_interaction: true,
});
}
}
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
Conclusion
Astro offre une approche révolutionnaire pour créer des sites web ultra-rapides et SEO-friendly. Avec les techniques présentées dans ce guide, vous pouvez atteindre des performances exceptionnelles tout en maintenant une excellente expérience développeur.
Résultats typiques avec Astro :
- PageSpeed Score : 95-100/100
- First Contentful Paint : < 1s
- Largest Contentful Paint : < 1.2s
- Cumulative Layout Shift : < 0.1
Besoin d’aide pour votre projet Astro ? Contactez-moi pour un audit technique ou du développement sur mesure.
Mathieu Mont - Expert Développement Web Moderne : Astro, React, Vue.js, PHP Symfony