Communitygithub.com

qalimero/qalim-portfolio

Guide complet pour construire et déployer des sites web statiques avec le framework Astro.

Qu'est-ce que qalim-portfolio ?

Ce skill fournit une assistance complète pour la construction et le déploiement de sites web statiques utilisant le framework Astro. Il couvre la génération de sites statiques (SSG), les collections de contenu, le format Markdown/MDX, l'hydratation partielle, l'architecture en îles, et le déploiement sur Netlify, Vercel, GitHub Pages ou GCP/Firebase. Idéal pour les développeurs créant des blogs, des sites de documentation ou des sites marketing, ce skill guide à travers les concepts fondamentaux d'Astro, l'optimisation des performances, la recherche côté client, la documentation versionnée, l'internationalisation, et l'intégration de diagrammes avec Mermaid ou PlantUML. Il est conçu pour être utilisé avec des outils comme Claude Code, Cursor ou Codex, et inclut des exemples détaillés de code, des configurations et des bonnes pratiques.

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/qalimero/qalim-portfolio/tree/HEAD/.claude/skills/publishing-astro-websites

Demander à votre IA préférée

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

Aperçu

Depuis le README du skill

qalim-portfolio preview

Documentation

Publication de sites web Astro

Construisez des sites web statiques rapides et orientés contenu avec l'approche SSG sans runtime d'Astro, l'hydratation partielle et la prise en charge étendue de Markdown.

Sommaire

Démarrage rapide

# Créer un nouveau projet (utiliser le modèle Blog pour les sites Markdown)
npm create astro@latest

# Développement
npm run dev          # Serveur local sur http://localhost:4321
npm run build        # Générer les fichiers statiques dans dist/
npm run preview      # Prévisualiser la version de production

Quand ne pas utiliser

Ce skill se concentre sur la génération de sites statiques (SSG). Envisagez d'autres approches pour :

  • Applications de données en temps réel — Utilisez le mode SSR avec des connexions à une base de données
  • Flux d'authentification utilisateur — Nécessite la gestion des sessions côté serveur
  • Commerce électronique avec inventaire dynamique — Utilisez le mode hybride ou SSR complet
  • Applications monopages (SPA) — Envisagez directement les frameworks React/Vue

Pour les schémas hybrides SSG+SSR, consultez la documentation des adaptateurs Astro.

Structure du projet

src/
  components/     # Composants Astro, React, Vue, Svelte
  content/        # Collections de contenu (Markdown/MDX)
    config.ts     # Schémas de collections
    docs/         # Exemple de collection
  layouts/        # Wrappers de page avec slots
  pages/          # Routage basé sur les fichiers
public/           # Actifs statiques (images, polices, favicons)
astro.config.mjs  # Configuration du framework

SSG vs SSR vs Hybride

ModeQuand les pages sont renduesCas d'utilisation
SSG (par défaut)Au moment de la constructionBlogs, documentation, sites marketing
SSRÀ chaque requêteDonnées dynamiques, personnalisation
HybrideMélange des deuxPages statiques + points de terminaison dynamiques

Pour les sites purement statiques, utilisez output: 'static' par défaut — aucun adaptateur nécessaire.

Collections de contenu

Schéma hérité (Astro 4.x)

Définissez les schémas dans src/content/config.ts :

import { defineCollection, z } from "astro:content";

export const collections = {
  docs: defineCollection({
    schema: z.object({
      title: z.string(),
      description: z.string().optional(),
      tags: z.array(z.string()).optional(),
      order: z.number().optional(),
      draft: z.boolean().default(false)
    })
  })
};

API Content Layer (Astro 5.0+)

Nouveau schéma avec le chargeur glob() — jusqu'à 75% de constructions plus rapides pour les grands sites :

// src/content.config.ts (note : nom de fichier différent)
import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/data/blog' }),
  schema: ({ image }) => z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    draft: z.boolean().default(false),
    cover: image(),  // Valide que l'image existe
    author: reference('authors'),  // Référence entre collections
  })
});

export const collections = { blog };

Schémas avancés

schema: ({ image }) => z.object({
  cover: image(),                    // Valide l'image dans src/
  category: z.enum(['tech', 'news']),
  author: reference('authors'),      // Référence entre collections
  relatedPosts: z.array(reference('blog')).optional(),
})

Chargeurs personnalisés (contenu distant)

Récupérez du contenu depuis des API externes (versions GitHub, CMS, etc.) :

// src/loaders/github-releases.ts
import type { Loader } from 'astro/loaders';

export function githubReleasesLoader(repo: string): Loader {
  return {
    name: 'github-releases',
    load: async ({ store, logger }) => {
      logger.info(`Récupération des versions pour ${repo}`);
      const response = await fetch(`https://api.github.com/repos/${repo}/releases`);
      const releases = await response.json();

      for (const release of releases) {
        store.set({
          id: release.tag_name,
          data: {
            version: release.tag_name,
            published_at: release.published_at,
            body: release.body  // Notes de version en Markdown
          }
        });
      }
    }
  };
}

Enregistrez dans content.config.ts :

import { githubReleasesLoader } from './loaders/github-releases';

const releases = defineCollection({
  loader: githubReleasesLoader('owner/repo'),
  schema: z.object({
    version: z.string(),
    published_at: z.string(),
    body: z.string(),
  })
});

Interrogez et affichez les collections :

---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const docs = await getCollection("docs");
  return docs.map(doc => ({
    params: { slug: doc.slug },
    props: { doc }
  }));
}

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

<article>
  <h1>{doc.data.title}</h1>
  <Content />
</article>

Coloration syntaxique

Configuration de base de Shiki

import { defineConfig } from "astro/config";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: "github-dark",
      wrap: true
    }
  }
});

Thème double clair/sombre

shikiConfig: {
  themes: {
    light: 'github-light',
    dark: 'github-dark',
  },
}

Ajoutez du CSS pour basculer entre les thèmes :

@media (prefers-color-scheme: dark) {
  .astro-code, .astro-code span {
    color: var(--shiki-dark) !important;
    background-color: var(--shiki-dark-bg) !important;
  }
}

Surlignage de lignes et transformateurs

```typescript {2,4}
const a = 1;
const b = 2;  // surligné
const c = 3;
console.log(a + b + c);  // surligné
```

Transformateurs Shiki (Astro 4.14+) :

import { transformerNotationFocus, transformerNotationDiff } from '@shikijs/transformers';

shikiConfig: {
  transformers: [transformerNotationFocus(), transformerNotationDiff()],
}

Utilisez des commentaires de notation dans le code :

  • // [!code focus] - Met cette ligne en focus
  • // [!code ++] - Marque comme ajout (vert)
  • // [!code --] - Marque comme suppression (rouge)

Expressive Code (Recommandé pour la documentation)

Blocs de code enrichis avec bouton de copie, noms de fichiers, surlignage des différences :

npm install astro-expressive-code
import expressiveCode from 'astro-expressive-code';

export default defineConfig({
  integrations: [expressiveCode()],
});

Fonctionnalités : Bouton de copie, onglets de fichiers, marqueurs de ligne, cadres de terminal, marqueurs de texte.

Intégration de diagrammes

Mermaid (Recommandé)

Installez l'intégration Astro :

npm install astro-mermaid mermaid
// astro.config.mjs
import { defineConfig } from 'astro/config';
import mermaid from 'astro-mermaid';

export default defineConfig({
  integrations: [mermaid({ theme: 'default' })]
});

Utilisation en Markdown :

```mermaid
graph TD;
    A-->B;
    B-->C;
```

Fonctionnalités : Rendu côté client, basculement automatique du thème, fonctionne hors ligne, pas besoin de Playwright.

Alternative (SVG statique au moment de la construction) : Utilisez rehype-mermaid avec Playwright pour des diagrammes pré-rendus (npx playwright install --with-deps requis).

Stratégies de thème sombre :

  1. Variables CSS - Laissez le navigateur résoudre les couleurs au moment de l'exécution :
// configuration mermaid
mermaid.initialize({
  theme: 'base',
  themeVariables: {
    primaryColor: 'var(--diagram-primary)',
    lineColor: 'var(--diagram-line)'
  }
});
  1. Élément Picture - Générez les deux thèmes, échangez avec la requête média :
<picture>
  <source srcset="/diagrams/flow-dark.svg" media="(prefers-color-scheme: dark)">
  <img src="/diagrams/flow-light.svg" alt="Diagramme de flux">
</picture>
  1. SVG en ligne - Ciblez les classes SVG avec CSS (risque : collisions de styles) - ajoutez des critères d'accessibilité avec la possibilité d'avoir role='image' ou role="presentation" :
.dark .mermaid-svg .node rect {
  fill: var(--bg-dark);
}

PlantUML

npx astro add plantuml

Utilisation en Markdown :

```plantuml
@startuml
Alice -> Bob: Bonjour
Bob --> Alice: Salut!
@enduml
```

Recherche côté client

Pagefind (Recommandé pour les grands sites)

Recherche statique sans configuration qui indexe au moment de la construction :

npm install pagefind

Ajoutez à package.json :

{
  "scripts": {
    "build": "astro build && npx pagefind --site dist",
    "postbuild": "pagefind --site dist"
  }
}

Utilisation dans les composants :

<link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
<script src="/pagefind/pagefind-ui.js" type="text/javascript"></script>

<div id="search"></div>
<script>
  window.addEventListener('DOMContentLoaded', () => {
    new PagefindUI({ element: '#search', showSubResults: true });
  });
</script>

Fonctionnalités : Aucun service externe, fonctionne hors ligne, indexation automatique, petit bundle (~10KB).

Contrôle d'indexation granulaire :

<!-- Indexer uniquement le contenu principal, pas les en-têtes/barres latérales -->
<main data-pagefind-body>
  <h1 data-pagefind-meta="title">Titre de la page</h1>
  <p data-pagefind-weight="10">Texte d'introduction important</p>

  <!-- Exclure des extraits de recherche -->
  <nav data-pagefind-ignore>
    <a href="/related">Articles connexes</a>
  </nav>
</main>
AttributObjectif
data-pagefind-bodyLimiter l'indexation à cet élément uniquement
data-pagefind-ignoreExclure l'élément de l'index
data-pagefind-meta="key"Définir un champ de métadonnées
data-pagefind-weight="10"Augmenter la pertinence (par défaut : 1)

Pagefind vs Fuse.js

FonctionnalitéPagefindFuse.js
ArchitectureMorceaux binaires pré-construitsEn mémoire lors de l'exécution
Bande passanteFaible (charge uniquement les morceaux nécessaires)Élevée (télécharge l'index complet)
Évolutivité10 000+ pages< 500 pages
MultilingueStemming natifConfiguration manuelle
Cas d'utilisationRecherche globale du siteFiltrage de petites listes

Fuse.js (Alternative légère)

Pour les petits sites avec des besoins d'interface personnalisée :

---
import { getCollection } from "astro:content";
const posts = await getCollection('blog');
const searchIndex = JSON.stringify(posts.map(post => ({
  title: post.data.title,
  slug: post.slug,
  body: post.body.slice(0, 500)
})));
---

<input type="search" id="search" placeholder="Recherche..." />
<ul id="results"></ul>

<script define:vars={{ searchIndex }}>
  import Fuse from 'fuse.js';

  const fuse = new Fuse(JSON.parse(searchIndex), {
    keys: ['title', 'body'],
    threshold: 0.3
  });

  document.getElementById('search').addEventListener('input', (e) => {
    const results = fuse.search(e.target.value);
    const resultsEl = document.getElementById('results');

    // Effacer les résultats précédents en toute sécurité
    resultsEl.replaceChildren();

    // Construire les résultats avec des méthodes DOM sécurisées
    results.forEach(r => {
      const li = document.createElement('li');
      const link = document.createElement('a');
      link.href = `/blog/${r.item.slug}`;
      link.textContent = r.item.title;
      li.appendChild(link);
      resultsEl.appendChild(li);
    });
  });
</script>

Pour les besoins d'entreprise, envisagez Algolia (API de recherche hébergée).

Documentation versionnée

Schéma de documentation multi-version

Utilisez une structure basée sur des dossiers :

src/content/docs/
  v1/
    getting-started.md
    api-reference.md
  v2/
    getting-started.md
    api-reference.md
    new-feature.md

Internationalisation (i18n)

Configurez dans astro.config.mjs :

export default defineConfig({
  i18n: {
    defaultLocale: "fr",
    locales: ["fr", "en", "es"],
    routing: {
      prefixDefaultLocale: false
    }
  }
});

Structurez le contenu par langue :

src/content/docs/
  fr/
    getting-started.md
  en/
    getting-started.md

Détectez la langue dans les composants :

---
const locale = Astro.currentLocale || 'fr';
---

Repli pour les traductions manquantes

Affichez le contenu en anglais avec une bannière lorsque les traductions n'existent pas :

---
// src/pages/[lang]/[...slug].astro
import { getCollection, getEntry } from "astro:content";

const languages = ['fr', 'en', 'es'];
const defaultLang = 'fr';

export async function getStaticPaths() {
  const frenchDocs = await getCollection('docs', ({ id }) => id.startsWith('fr/'));
  const paths = [];

  for (const doc of frenchDocs) {
    const slug = doc.id.replace(/^fr\//, '');

    for (const lang of languages) {
      const localizedId = `${lang}/${slug}`;
      const localizedDoc = await getEntry('docs', localizedId);

      paths.push({
        params: { lang, slug },
        props: {
          entry: localizedDoc || doc,  // Repli vers le français
          isFallback: !localizedDoc
        }
      });
    }
  }
  return paths;
}

const { entry, isFallback } = Astro.props;
const { Content } = await entry.render();
---

{isFallback && (
  <div class="translation-notice">
    Cette page n'est pas encore disponible dans votre langue.
  </div>
)}
<Content />

Schémas courants

Listes paginées

Pour les sites avec plus de 50 articles, divisez en pages :

---
// src/pages/blog/page/[page].astro
import { getCollection } from "astro:content";

const POSTS_PER_PAGE = 10;

export async function getStaticPaths() {
  const allPosts = await getCollection("blog");
  const totalPages = Math.ceil(allPosts.length / POSTS_PER_PAGE);
  
  const paths = [];
  for (let i = 1; i <= totalPages; i++) {
    paths.push({
      params: { page: String(i) },
      props: { page: i, totalPages }
    });
  }
  return paths;
}

const { page, totalPages } = Astro.props;
const start = (page - 1) * POSTS_PER_PAGE;
const end = start + POSTS_PER_PAGE;
const pagePosts = (await getCollection("blog")).slice(start, end);
---

<ul>
  {pagePosts.map(post => (
    <li>
      <a href={`/blog/${post.slug}`}>{post.data.title}</a>
      <p>{post.data.description}</p>
    </li>
  ))}
</ul>

<nav>
  {page > 1 && <a href={`/blog/page/${page - 1}`}>Précédent</a>}
  Page {page} sur {totalPages}
  {page < totalPages && <a href={`/blog/page/${page + 1}`}>Suivant</a>}
</nav>

Pages de tags

Regroupez les articles par tags :

---
// src/pages/tags/[tag].astro
import { getCollection } from "astro:content";

const posts = await getCollection("blog");
const tag = Astro.params.tag;

const filteredPosts = posts.filter(post => 
  post.data.tags?.includes(tag)
);
---

<h1>Articles tagués : {tag}</h1>
<ul>
  {filteredPosts.map(post => (
    <li><a href={`/blog/${post.slug}`}>{post.data.title}</a></li>
  ))}
</ul>

Flux RSS

Utilisez le package @astrojs/rss :

npm install @astrojs/rss
---
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET() {
  const posts = await getCollection('blog');
  
  return rss({
    title: 'Mon Blog',
    description: 'Articles récents',
    site: 'https://monsite.com',
    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>`,
  });
}
---

Formulaires

Pour les formulaires statiques, utilisez des services de backend comme Formspree ou Netlify Forms :

---
// src/pages/contact.astro
---

<form method="POST" action="https://formspree.io/f/xxxxxxxx">
  <label for="name">Nom</label>
  <input type="text" id="name" name="name" required />
  
  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />
  
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  
  <button type="submit">Envoyer</button>
</form>

Bonnes pratiques de performances

Préchargement

Astro gère le préchargement pour les liens visibles :

<!-- Activer manuellement si nécessaire -->
<a href="/blog" data-astro-prefetch>Blog</a>

<!-- Désactiver le préchargement pour un lien spécifique -->
<a href="/logout" data-astro-prefetch="false">Déconnexion</a>

CSS critique

Minimisez le CSS bloquant avec l'intégration critique :

npm install astro-critters
import critters from 'astro-critters';

export default defineConfig({
  integrations: [critters()],
});

Images

Utilisez les composants Image d'Astro pour les images optimisées :

---
import { Image } from 'astro:assets';
import heroImage from '../images/hero.jpg';
---

<Image src={heroImage} alt="Image d'accueil" width={800} height={400} />

Déploiement

Netlify

npm install @astrojs/netlify
import netlify from '@astrojs/netlify';

export default defineConfig({
  output: 'static',
  adapter: netlify(),
});

Déployez via l'interface Netlify ou CLI :

npx netlify deploy --prod --dir=dist

Vercel

npm install @astrojs/vercel
import vercel from '@astrojs/vercel/serverless';

export default defineConfig({
  output: 'server',  // Pour SSR ou hybride ; pour SSG, utilisez output: 'static'
  adapter: vercel(),
});

Déployez via l'interface Vercel ou CLI :

npx vercel --prod

GitHub Pages

npm install @astrojs/github-pages
import githubPages from '@astrojs/github-pages';

export default defineConfig({
  site: 'https://nomutilisateur.github.io',
  base: '/nom-projet',
  output: 'static',
  adapter: githubPages(),
});

`

Déployez via un workflow GitHub Actions.

GCP/Firebase

npm install @astrojs/firebase
import firebase from '@astrojs/firebase';

export default defineConfig({
  output: 'server',  // Pour SSR ; pour SSG, utilisez output: 'static'
  adapter: firebase(),
});

Déployez avec Firebase CLI :

firebase deploy --only hosting

Liste de contrôle pré-déploiement

  • Tous les liens internes fonctionnent (utilisez astro check)
  • Images optimisées et avec attributs alt
  • Balises meta SEO (titre, description, Open Graph)
  • Sitemap généré (intégration @astrojs/sitemap)
  • Flux RSS (si applicable)
  • Page 404 personnalisée
  • Fichier robots.txt
  • Performances vérifiées avec Lighthouse ou PageSpeed Insights
  • Tests sur mobile et ordinateur
  • Accessibilité de base (navigation au clavier, contrastes)

Tests et qualité

Tests unitaires avec Vitest

npm install vitest
// vite.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
  },
});

Exemple de test :

import { describe, it, expect } from 'vitest';

function getPostUrl(slug: string): string {
  return `/blog/${slug}`;
}

describe('Fonctions utilitaires', () => {
  it('devrait générer l\'URL correcte pour un article', () => {
    expect(getPostUrl('mon-article')).toBe('/blog/mon-article');
  });
});

Tests de bout en bout avec Playwright

npm install playwright
npx playwright install
// e2e/home.test.ts
import { test, expect } from '@playwright/test';

test('la page d\'accueil devrait s\'afficher correctement', async ({ page }) => {
  await page.goto('http://localhost:4321');
  await expect(page).toHaveTitle(/Mon Site/);
  
  const mainContent = page.locator('main');
  await expect(mainContent).toBeVisible();
});

Vérification des liens

Utilisez astro check ou des outils comme broken-link-checker :

npm install broken-link-checker
npx broken-link-checker http://localhost:4321 --recursive

Dépannage

Problèmes courants

SymptômeCause probableSolution
Erreur "Cannot find module 'astro:content'"Astro non installé ou version incorrecteExécutez npm install astro@latest
Les images ne s'affichent pasChemin incorrect ou image manquanteVérifiez les chemins dans src/ et utilisez public/ pour les actifs statiques
Le déploiement échoueConfiguration de l'adaptateur manquanteVérifiez output et adapter dans astro.config.mjs
Les pages 404 après déploiementBase URL incorrecteConfigurez site et base correctement
Performances lentes en développementTrop d'articles ou d'images lourdesUtilisez Image d'Astro pour les images et limitez la taille des collections

Ressources supplémentaires

Skills associés