Créer un thème

Sur cette page

Un thème EmDash est un projet Astro qu’un autre développeur peut générer avec create-astro. Construisez et testez le projet comme un site complet, puis incluez un seed qui crée le modèle de contenu attendu par ses routes et ses composants.

Partir d’un modèle actuel

Choisissez le modèle existant le plus proche du site visé et de la cible de déploiement. Les variantes Node et Cloudflare conservent ensemble leur configuration de base de données, de stockage, d’adaptateur et de middleware.

Le modèle de blog est une base utile pour un site de contenu :

npm create astro@latest -- --template @emdash-cms/template-blog

Utilisez @emdash-cms/template-blog-cloudflare lorsque le thème obtenu cible Cloudflare Workers, D1 et R2.

Conserver la structure du modèle

Le modèle de blog actuel utilise ces chemins pertinents :

astro.config.mjs
emdash-env.d.ts
package.json
seed/
└── seed.json
src/
├── components/
│   └── PostCard.astro
├── layouts/
│   └── Base.astro
├── live.config.ts
├── pages/
│   ├── index.astro
│   ├── category/[slug].astro
│   ├── pages/[slug].astro
│   ├── posts/index.astro
│   ├── posts/[slug].astro
│   ├── search.astro
│   └── tag/[slug].astro
└── styles/

Les modèles starter, portfolio et marketing utilisent d’autres routes. Copiez de vrais fichiers depuis la base choisie plutôt que de supposer que chaque thème possède une route de page fourre-tout.

Pointer vers le seed

Les modèles actuels déclarent le chemin de leur seed dans package.json :

{
  "name": "@example/emdash-theme-publication",
  "private": true,
  "type": "module",
  "emdash": {
    "seed": "seed/seed.json"
  }
}

EmDash découvre aussi .emdash/seed.json et le repli conventionnel seed/seed.json. Utilisez le champ de package lorsque vous distribuez un modèle afin que le fichier voulu soit explicite.

Définir le modèle de contenu

Si vous partez du modèle de blog, modifiez son seed/seed.json existant plutôt que de le remplacer par un modèle sans rapport. Le seed réduit suivant conserve les collections et les données structurelles utilisées par les exemples de ce guide :

{
  "$schema": "https://emdashcms.com/seed.schema.json",
  "version": "1",
  "meta": {
    "name": "Publication",
    "description": "A publication with posts"
  },
  "settings": {
    "title": "Publication",
    "tagline": "Latest articles"
  },
  "collections": [
    {
      "slug": "posts",
      "label": "Posts",
      "labelSingular": "Post",
      "supports": ["drafts", "revisions", "search", "seo"],
      "fields": [
        {
          "slug": "title",
          "label": "Title",
          "type": "string",
          "required": true,
          "searchable": true
        },
        {
          "slug": "excerpt",
          "label": "Excerpt",
          "type": "text"
        },
        {
          "slug": "featured_image",
          "label": "Featured image",
          "type": "image"
        },
        {
          "slug": "content",
          "label": "Content",
          "type": "portableText",
          "searchable": true
        }
      ]
    },
    {
      "slug": "pages",
      "label": "Pages",
      "labelSingular": "Page",
      "supports": ["drafts", "revisions", "search"],
      "fields": [
        {
          "slug": "title",
          "label": "Title",
          "type": "string",
          "required": true,
          "searchable": true
        },
        {
          "slug": "content",
          "label": "Content",
          "type": "portableText",
          "searchable": true
        },
        {
          "slug": "template",
          "label": "Page template",
          "type": "select",
          "defaultValue": "default",
          "validation": {
            "options": ["default", "full-width", "landing"]
          }
        }
      ]
    }
  ],
  "menus": [
    {
      "name": "primary",
      "label": "Primary navigation",
      "items": [
        { "type": "custom", "label": "Home", "url": "/" },
        { "type": "custom", "label": "Posts", "url": "/posts" }
      ]
    }
  ],
  "widgetAreas": [
    {
      "name": "sidebar",
      "label": "Sidebar",
      "widgets": []
    }
  ],
  "content": {
    "posts": [
      {
        "id": "post-welcome",
        "slug": "welcome",
        "status": "published",
        "data": {
          "title": "Welcome",
          "excerpt": "The first article",
          "content": []
        }
      }
    ]
  }
}

L’id de l’entrée de contenu est un identifiant local au seed utilisé par les références. Il n’est pas forcé de devenir l’ID de base de données d’une entrée routable. Son slug devient l’identifiant de route que l’API de requête expose sous le nom entry.id.

Lisez Fichiers seed avant d’ajouter des taxonomies, des bylines, des références de menu, des médias, des redirections, des zones de widgets, des sections, la localisation ou un comportement de conflit.

Construire des routes rendues côté serveur

Les modèles EmDash actuels utilisent output: "server". Les routes interrogent le contenu en direct à chaque requête. N’ajoutez pas getStaticPaths() aux routes de contenu d’un thème, sauf si le modèle utilise délibérément EmDash uniquement comme source au moment du build.

L’archive suivante trie dans la base de données à l’aide du champ stocké published_at :

---
import { getEmDashCollection } from "emdash";
import Base from "../../layouts/Base.astro";

const { entries: posts, error, cacheHint } = await getEmDashCollection("posts", {
  orderBy: { published_at: "desc" },
});

if (error) return new Response("Could not load posts", { status: 500 });
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

<Base title="Posts">
  {posts.map((post) => (
    <article>
      <h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
      {post.data.excerpt && <p>{post.data.excerpt}</p>}
    </article>
  ))}
</Base>

orderBy est un objet associant un champ à une direction. Utilisez orderBy: { published_at: "desc" }, et non sort, sortBy ou un callback JavaScript.

La route suivante résout et affiche un article :

---
import { decodeSlug, getEmDashEntry } from "emdash";
import { PortableText } from "emdash/ui";
import Base from "../../layouts/Base.astro";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/404");

const { entry: post, error, cacheHint } = await getEmDashEntry("posts", slug);
if (error) return new Response("Could not load the post", { status: 500 });
if (!post) return Astro.rewrite("/404");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);
---

<Base title={post.data.title} content={{ collection: "posts", id: post.data.id, slug }}>
  <article>
    <h1 {...post.edit.title}>{post.data.title}</h1>
    <PortableText value={post.data.content} />
  </article>
</Base>

Utilisez post.id dans les URL de route et post.data.id là où un helper requiert l’ID de contenu stocké.

Interroger la navigation gérée par le site

Les valeurs gérées par le CMS doivent provenir des API correspondantes. Les modèles actuels utilisent getSiteSettings(), getMenu() et <WidgetArea /> dans leurs layouts :

---
import { getMenu, getSiteSettings } from "emdash";
import { WidgetArea } from "emdash/ui";

const [settings, primary] = await Promise.all([
  getSiteSettings(),
  getMenu("primary"),
]);
---

<header>
  <a href="/">{settings.title}</a>
  <nav>
    {primary?.items.map((item) => <a href={item.url}>{item.label}</a>)}
  </nav>
</header>

<main><slot /></main>
<aside><WidgetArea name="sidebar" /></aside>

Le texte statique du design peut rester dans les fichiers Astro. Les valeurs que les administrateurs sont censés modifier doivent être représentées dans les réglages, le contenu, les menus ou les widgets.

Afficher les champs image

Les champs image sont des valeurs de média, pas des chaînes d’URL. Transmettez la valeur complète au composant Image afin que le stockage local et les fournisseurs d’images soient résolus de manière cohérente :

---
import { Image } from "emdash/ui";

const { post } = Astro.props;
---

<article>
  {post.data.featured_image && (
    <Image
      image={post.data.featured_image}
      alt={post.data.title}
      width={800}
      height={450}
    />
  )}
  <h2><a href={`/posts/${post.id}`}>{post.data.title}</a></h2>
</article>

Le composant utilise le texte alternatif propre au champ, sauf si vous le remplacez. Réservez priority à une image censée se trouver au-dessus de la ligne de flottaison ; les autres images restent chargées en différé.

Proposer des choix de mise en page

Le seed de départ ci-dessus inclut un champ select pour les sites où les éditeurs ont besoin de plus d’une mise en page. Lorsque vous ajoutez cette fonctionnalité à un autre seed, utilisez des valeurs stables qui correspondent à des composants connus :

{
  "slug": "template",
  "label": "Page template",
  "type": "select",
  "defaultValue": "default",
  "validation": {
    "options": ["default", "full-width", "landing"]
  }
}

La route peut alors choisir à partir d’une table de composants explicite :

---
import { decodeSlug, getEmDashEntry } from "emdash";
import PageDefault from "../../layouts/PageDefault.astro";
import PageFullWidth from "../../layouts/PageFullWidth.astro";
import PageLanding from "../../layouts/PageLanding.astro";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.rewrite("/404");

const { entry: page } = await getEmDashEntry("pages", slug);
if (!page) return Astro.rewrite("/404");

const layouts = {
  default: PageDefault,
  "full-width": PageFullWidth,
  landing: PageLanding,
};
const Layout = layouts[page.data.template as keyof typeof layouts] ?? PageDefault;
---

<Layout {page} />

La table explicite empêche une valeur de champ stockée de devenir un chemin de module arbitraire.

Ajouter la recherche

Activez search dans chaque collection qui doit apparaître dans les résultats et marquez les champs pertinents comme searchable. Les modèles actuels utilisent LiveSearch pour une route de recherche prête à l’emploi :

---
import LiveSearch from "emdash/ui/search";
import Base from "../layouts/Base.astro";
---

<Base title="Search">
  <h1>Search</h1>
  <LiveSearch placeholder="Search posts and pages" collections={["posts", "pages"]} />
</Base>

Sur un site Astro avec i18n, LiveSearch utilise Astro.currentLocale. Ne transmettez locale={null} que lorsque la page doit délibérément rechercher dans toutes les locales.

Seed de sections réutilisables

Les sections offrent aux éditeurs des points de départ Portable Text réutilisables. Ajoutez-les lorsque le design comprend un motif de contenu répété, tel qu’un appel à l’action :

{
  "version": "1",
  "sections": [
    {
      "slug": "newsletter-signup",
      "title": "Newsletter signup",
      "description": "Heading and copy for the newsletter form",
      "keywords": ["newsletter", "email"],
      "content": [
        {
          "_type": "block",
          "_key": "newsletter-heading",
          "style": "h2",
          "children": [
            { "_type": "span", "_key": "newsletter-heading-text", "text": "Get new articles by email" }
          ]
        }
      ]
    }
  ]
}

L’assistant de configuration permet à l’utilisateur d’omettre les entrées, bylines et termes de taxonomie semés. Les sections et le reste du modèle structurel sont tout de même appliqués.

Ajouter des blocks de page structurés

Utilisez un champ blocks lorsque la page elle-même est une composition ordonnée de sections typées. Utilisez un block Portable Text personnalisé lorsque l’objet appartient à l’intérieur d’un document de texte enrichi et participe au flux de ses paragraphes.

Le modèle marketing sème cinq types de block conservés et les autorise dans le champ content de Pages. Son wrapper associe chaque _type généré à un composant Astro :

---
import { Blocks, defineBlockComponents } from "emdash/ui";
import type { PageContentBlock } from "../../emdash-env";
import FAQ from "./blocks/FAQ.astro";
import Features from "./blocks/Features.astro";
import Hero from "./blocks/Hero.astro";
import Pricing from "./blocks/Pricing.astro";
import Testimonials from "./blocks/Testimonials.astro";

const components = defineBlockComponents<PageContentBlock>({
  marketing_hero: Hero,
  marketing_features: Features,
  marketing_testimonials: Testimonials,
  marketing_pricing: Pricing,
  marketing_faq: FAQ,
});
---

<Blocks value={Astro.props.value} components={components} />

L’union PageContentBlock générée maintient la table de composants alignée avec chaque type de block autorisé et chaque version conservée. Chaque valeur de block semée inclut _type, _version et un _key stable.

Utilisez un block Portable Text personnalisé pour un objet qui se place entre des paragraphes, comme un diagramme intégré dans un article. Associez cet objet via l’option components.type du composant PortableText. Un seed peut inclure la valeur stockée, mais un plugin natif doit enregistrer un éditeur personnalisé pour créer et modifier cet objet Portable Text.

Utilisez un plugin natif lorsque le block a besoin d’un éditeur personnalisé réutilisable et de composants de rendu empaquetés. Les plugins en sandbox ne peuvent pas livrer de composants de rendu Astro dans le build d’un site.

Tester le modèle

  1. Générez le modèle dans un répertoire propre avec create-astro.

  2. Installez les dépendances et exécutez les commandes de build et de vérification des types du site.

  3. Démarrez avec une base de données vide et terminez /_emdash/admin/setup.

  4. Testez la configuration avec et sans le contenu d’exemple.

  5. Ouvrez chaque route avec du contenu semé, du contenu nouvellement créé, sans image facultative et avec une collection vide.

  6. Modifiez les réglages du site, les menus, les attributions de taxonomie, le Portable Text et les blocks depuis l’administration. Pour les blocks, ajoutez, réordonnez, dupliquez et supprimez chaque type autorisé. Vérifiez que les routes publiques utilisent les valeurs modifiées.

  7. Appliquez la configuration propre au déploiement dans un environnement jetable et vérifiez le stockage des médias, les aperçus et le rendu côté serveur.

Publier le modèle

Un dépôt GitHub peut être utilisé directement avec la syntaxe de modèle github: d’Astro :

npm create astro@latest -- --template github:example/emdash-theme-publication

Avant de publier, supprimez les bases de données et les téléversements locaux, gardez les secrets hors du dépôt, vérifiez les chemins du package depuis un checkout propre et documentez la cible de déploiement que le modèle configure.

Liste de contrôle

  • astro.config.mjs utilise la sortie serveur et les adaptateurs de déploiement prévus.
  • src/live.config.ts enregistre emdashLoader().
  • package.json#emdash.seed pointe vers un fichier existant.
  • Chaque collection et chaque champ interrogés sont déclarés par le seed.
  • Les exemples de requête utilisent orderBy et le bon identifiant d’entrée.
  • Les valeurs du site gérées par le CMS ne sont pas dupliquées comme constantes permanentes du modèle.
  • Une configuration propre fonctionne avec et sans contenu d’exemple.
  • Le build et la vérification des types du modèle réussissent pour sa cible prise en charge.