Meursault « Les Trois Pierres »
24,00 € · millésime au choix
Starter T-IN · guide des composants
Une boutique sans paiement en ligne : le visiteur compose son panier dans son navigateur, envoie sa commande par le formulaire, et règle au retrait. Aucun backend, aucune donnée bancaire, aucun stock à tenir. Tout se règle dans le bloc `boutique` de src/data/site.ts et la collection `produits` — un site vitrine peut retirer l'ensemble en trois minutes (voir README).
Un article : packshot carré sur aplat, rayon, nom, prix, et la liste des choix quand l'article en propose (« taille, coloris au choix »). Toute la carte est un seul lien — le choix des options se fait sur la fiche, jamais dans une grille, sinon deux cibles cliquables se disputent le même geste. Un article indisponible garde sa carte et reçoit un bandeau. Le libellé du rayon vient de lib/rayons.ts (ou de la prop rayons), l'aplat de boutique.fondPackshot (ou de la variable CSS --produit-fond). Props : produit, heading, loading, rayons, fond, labels.
---
import CarteProduit from '../components/CarteProduit.astro';
import { lireProduits } from '../lib/boutique';
const produits = await lireProduits();
---
<CarteProduit produit={produits[0]} heading="h3" loading="eager" />
<!-- autre vocabulaire de rayons, autre aplat -->
<CarteProduit produit={produit} rayons={MES_RAYONS} fond="var(--paper-soft)" /> Le pendant clair et marchand de GrilleProduits : celui-là met une gamme en scène sur fond de nuit, celui-ci sert à trouver un article et à l'acheter. Filtre par rayon en îlot — sans JavaScript toutes les cartes sont dans le HTML, la page reste complète et indexable ; avec, une vingtaine de lignes basculent hidden et aria-pressed, et un compteur role=status annonce le résultat. La barre disparaît d'elle-même sous deux rayons représentés. filtres={false} donne une simple grille : sélection d'accueil, bandeau « à voir aussi » en bas d'une fiche.
<GrilleBoutique produits={produits} />
<!-- simple grille, sans filtres, titres en h3 -->
<GrilleBoutique produits={autres} filtres={false} heading="h3" colonnes={4} />
<!-- vocabulaire et libellés sur mesure -->
<GrilleBoutique
produits={produits}
rayons={MES_RAYONS}
labels={{ tout: 'Tout le domaine', filtrer: 'Filtrer par rayon',
compteUn: '{n} article affiché', comptePlusieurs: '{n} articles affichés' }}
/> Le formulaire d'une fiche produit : une liste par option déclarée dans le contenu (millésime, taille, coloris…), une quantité plafonnée par boutique.quantiteMax, un bouton. Les choix partent dans le panier déjà mis en phrase (« Millésime : 2022 ») : le stockage du navigateur ne contient que du texte et des quantités. La confirmation s'annonce en role=status avec un lien vers le panier. Sans JavaScript, pas de bouton qui ne fait rien : un renvoi vers le contact. Un article indisponible n'a pas de formulaire du tout. Props : produit, hrefPanier, hrefContact, labels.
Article disponible, avec une option et une quantité :
Article retiré de la vente (disponible: false) :
Cet article n’est plus disponible.
<AjoutPanier produit={produit} />
<!-- libellés sur mesure -->
<AjoutPanier
produit={produit}
labels={{ ajouter: 'Réserver cet article', quantite: 'Nombre' }}
/> « Panier (3) », tenu à jour à chaque ajout, y compris depuis un autre onglet (événement storage). Rendu en lien sur les pages boutique ; avec lien={false} il ne rend rien de visible et se contente de mettre à jour les cibles déjà dans la page — posé dans Base.astro, il transforme le CTA de l'en-tête en panier (ajouter data-panier-cta="Panier" au lien de Header.astro et régler site.cta). Toute cible [data-panier-cta] reçoit « Libellé (n) » + un aria-label rédigé, tout [data-panier-compteur] le nombre seul. Le texte passe par textContent : jamais d'innerHTML avec une donnée du localStorage. Il vit dans son propre composant, et non dans le layout, pour qu'un site vitrine n'embarque pas le JavaScript du panier sur chacune de ses pages.
Ajoutez un article ci-dessus : le libellé se met à jour sans rechargement. Pastille seule : 0 article(s).
<!-- sur une page boutique : un lien visible -->
<CompteurPanier />
<!-- dans Base.astro : îlot silencieux qui tient l'en-tête à jour -->
<CompteurPanier lien={false} />
<!-- une pastille ailleurs dans la page -->
<span data-panier-compteur>0</span> Le contenu du panier : lignes modifiables, sous-totaux, total, passage à la commande, et l'état vide rédigé. Le panier ne retient que { id, options, qte } — noms, prix, vignettes et disponibilités sont relus dans le catalogue du build, déposé par <Catalogue /> (inclus par le composant) : un prix modifié à la main dans le stockage du navigateur n'a aucun effet, et un article retiré de la vente disparaît du panier au premier rechargement. Les lignes sont clonées d'un <template> et remplies par textContent, jamais par innerHTML. La page /panier/ n'est plus qu'un appel à ce composant.
---
import PanierVue from '../components/PanierVue.astro';
---
<Base title="Votre panier" description="…" noindex>
<PageHeader eyebrow="La boutique" title="Votre panier" />
<section class="container"><PanierVue /></section>
</Base>
<!-- liens et libellés sur mesure -->
<PanierVue hrefBoutique="/la-cave/" note={null} labels={{ commander: 'Réserver' }} /> La commande sans paiement en ligne : coordonnées, mode de retrait, récapitulatif, et c'est tout. Deux modes comme ContactForm, selon site.formEndpoint : POST amélioré en fetch avec écran de remerciement et panier vidé, ou repli mailto: qui ouvre la commande déjà rédigée dans la messagerie du visiteur (le panier est alors conservé — on ne vide pas un panier dont on ne sait pas si le courriel est parti). Le récapitulatif envoyé est recomposé depuis le catalogue du build, jamais depuis le localStorage. Pot-de-miel anti-spam. La case de majorité n'apparaît que si le panier contient un article marqué alcool. Modes de retrait, mention de paiement, lien de CGV et mention d'alcool viennent du bloc boutique ou des props.
Votre panier est vide : il n’y a rien à commander pour l’instant.
Voir la boutiqueNous vous écrivons très vite pour confirmer la disponibilité des articles et convenir du retrait.
Voir la boutique---
import CommandeForm from '../components/CommandeForm.astro';
---
<CommandeForm />
<!-- retraits, CGV et objet du courriel sur mesure -->
<CommandeForm
retraits={['Retrait à la boutique', 'Retrait sur le marché du samedi']}
cgv={{ href: '/conditions-de-vente/', label: 'conditions de vente' }}
sujet="Nouvelle commande — boutique"
/>
// src/data/site.ts
formEndpoint: 'https://formspree.io/f/xxxxxxx', Rien de marchand n'est codé dans un composant. Un article est un fichier Markdown de src/content/produits/ ; les rayons sont un objet de src/lib/rayons.ts dont le schéma tire son enum (un rayon inexistant casse le build, pas la page) ; tout le reste — clé du panier, devise, modes de retrait, mention de paiement, CGV, mention d'alcool — vit dans le bloc boutique de src/data/site.ts. Le champ images accepte les deux écritures d'Img.astro : chemin public/ (les placeholders du starter) ou chemin relatif vers src/assets/, optimisé au build.
trois-pierres:panier/boutique/<id>/EUR · fr-FR20#ffffff// src/lib/rayons.ts — le seul endroit où les rayons sont nommés
export const RAYONS = {
vins: 'Les vins',
coffrets: 'Coffrets',
'art-de-la-table': 'Art de la table',
} as const;
---
# src/content/produits/les-trois-pierres.md
nom: Meursault « Les Trois Pierres »
rayon: vins
prix: 24
accroche: Le chardonnay de haut de coteau, dix mois de fût.
images:
- src: ../../assets/produits/trois-pierres.jpg # ou /images/packshot-1.svg
alt: Bouteille de la cuvée Les Trois Pierres
options:
- nom: Millésime
valeurs: ['2022', '2021']
details:
- Élevage 10 mois en fût
alcool: true
ordre: 10
disponible: true
---
Le corps du fichier est le texte long de la fiche.