Creare un tema

In questa pagina

Un tema EmDash è un progetto Astro che un altro sviluppatore può generare con create-astro. Costruisci e testa il progetto come un sito completo, poi includi un seed che crea il modello di contenuto atteso dalle sue route e dai suoi componenti.

Partire da un modello attuale

Scegli il modello esistente più vicino al sito previsto e alla destinazione di deploy. Le varianti Node e Cloudflare mantengono insieme la loro configurazione di database, storage, adattatore e middleware.

Il modello blog è una base utile per un sito di contenuti:

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

Usa @emdash-cms/template-blog-cloudflare quando il tema risultante ha come destinazione Cloudflare Workers, D1 e R2.

Mantenere la struttura del modello

Il modello blog attuale usa questi percorsi rilevanti:

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/

I modelli starter, portfolio e marketing usano route diverse. Copia i file reali dalla base scelta invece di presumere che ogni tema abbia una route di pagina catch-all.

Puntare al seed

I modelli attuali dichiarano il percorso del loro seed in package.json:

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

EmDash rileva anche .emdash/seed.json e il fallback convenzionale seed/seed.json. Usa il campo del pacchetto quando distribuisci un modello, così il file previsto è esplicito.

Definire il modello di contenuto

Se parti dal modello blog, modifica il suo seed/seed.json esistente invece di sostituirlo con un modello non correlato. Il seed ridotto seguente mantiene le collection e i dati strutturali usati dagli esempi di questa guida:

{
  "$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 della voce di contenuto è un identificatore locale al seed usato dai riferimenti. Non è forzato a diventare l’ID di database di una voce routable. Il suo slug diventa l’identificatore di route che l’API di query espone come entry.id.

Leggi File seed prima di aggiungere tassonomie, bylines, riferimenti di menu, media, reindirizzamenti, aree widget, section, localizzazione o comportamento di conflitto.

Costruire route renderizzate lato server

I modelli EmDash attuali usano output: "server". Le route interrogano il contenuto live a ogni richiesta. Non aggiungere getStaticPaths() alle route di contenuto di un tema, a meno che il modello non usi deliberatamente EmDash solo come sorgente in fase di build.

L’archivio seguente ordina nel database usando il campo salvato 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 è un oggetto da campo a direzione. Usa orderBy: { published_at: "desc" }, non sort, sortBy o una callback JavaScript.

La route seguente risolve e renderizza un singolo articolo:

---
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>

Usa post.id negli URL delle route e post.data.id dove un helper richiede l’ID di contenuto salvato.

Interrogare la navigazione gestita dal sito

I valori gestiti dal CMS devono provenire dalle API corrispondenti. I modelli attuali usano getSiteSettings(), getMenu() e <WidgetArea /> nei loro layout:

---
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>

Il testo statico del design può restare nei file Astro. I valori che gli amministratori dovrebbero modificare devono essere rappresentati in impostazioni, contenuto, menu o widget.

Renderizzare campi immagine

I campi immagine sono valori media, non stringhe URL. Passa il valore completo al componente Image, così lo storage locale e i provider di immagini vengono risolti in modo coerente:

---
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>

Il componente usa il testo alternativo proprio del campo, a meno che tu non lo sostituisca. Riserva priority a un’immagine che si prevede sia nella parte visibile senza scorrere; le altre immagini restano caricate in modo differito.

Offrire scelte di layout pagina

Il seed iniziale qui sopra include un campo select per i siti in cui gli editor hanno bisogno di più di un layout di pagina. Quando aggiungi la funzione a un altro seed, usa valori stabili che corrispondano a componenti noti:

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

La route può quindi scegliere da una mappa esplicita di componenti:

---
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 mappa esplicita impedisce che un valore di campo salvato diventi un percorso di modulo arbitrario.

Aggiungere la ricerca

Abilita search in ogni collection che deve comparire nei risultati e contrassegna i campi rilevanti come searchable. I modelli attuali usano LiveSearch per una route di ricerca già pronta:

---
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>

Su un sito Astro con i18n, LiveSearch usa Astro.currentLocale. Passa locale={null} solo quando la pagina deve cercare deliberatamente in tutti i locale.

Seed di section riutilizzabili

Le section offrono agli editor punti di partenza Portable Text riutilizzabili. Aggiungile quando il design include un pattern di contenuto ripetuto, come una call to 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" }
          ]
        }
      ]
    }
  ]
}

La procedura guidata di configurazione permette all’utente di omettere le voci, i bylines e i termini di tassonomia inseriti dal seed. Le section e il resto del modello strutturale vengono comunque applicati.

Aggiungere block di pagina strutturati

Usa un campo blocks quando la pagina stessa è una composizione ordinata di section tipizzate. Usa un block Portable Text personalizzato quando l’oggetto appartiene all’interno di un documento di testo ricco e partecipa al flusso dei suoi paragrafi.

Il modello marketing inserisce con il seed cinque tipi di block conservati e li consente nel campo content di Pages. Il suo wrapper mappa ogni _type generato a un componente 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’unione PageContentBlock generata mantiene la mappa dei componenti allineata a ogni tipo di block consentito e a ogni versione conservata. Ogni valore di block inserito dal seed include _type, _version e un _key stabile.

Usa un block Portable Text personalizzato per un oggetto che sta tra i paragrafi, come un diagramma incorporato in un articolo. Mappa quell’oggetto tramite l’opzione components.type del componente PortableText. Un seed può includere il valore salvato, ma un plugin nativo deve registrare un editor personalizzato per creare e modificare quell’oggetto Portable Text.

Usa un plugin nativo quando il block ha bisogno di un editor personalizzato riutilizzabile e di componenti di rendering impacchettati. I plugin in sandbox non possono includere componenti di rendering Astro nella build di un sito.

Testare il modello

  1. Genera il modello in una directory pulita con create-astro.

  2. Installa le dipendenze ed esegui i comandi di build e di controllo dei tipi del sito.

  3. Parti da un database vuoto e completa /_emdash/admin/setup.

  4. Testa la configurazione con il contenuto di esempio incluso ed escluso.

  5. Apri ogni route con contenuto inserito dal seed, contenuto appena creato, senza immagine facoltativa e con una collection vuota.

  6. Modifica le impostazioni del sito, i menu, le assegnazioni di tassonomia, il Portable Text e i block dall’amministrazione. Per i block, aggiungi, riordina, duplica e rimuovi ogni tipo consentito. Verifica che le route pubbliche usino i valori modificati.

  7. Applica la configurazione specifica del deploy in un ambiente usa e getta e verifica lo storage dei media, le anteprime e il rendering lato server.

Pubblicare il modello

Un repository GitHub può essere usato direttamente con la sintassi di modello github: di Astro:

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

Prima di pubblicare, rimuovi i database e i caricamenti locali, tieni i segreti fuori dal repository, verifica i percorsi del pacchetto da un checkout pulito e documenta quale destinazione di deploy configura il modello.

Checklist

  • astro.config.mjs usa l’output server e gli adattatori di deploy previsti.
  • src/live.config.ts registra emdashLoader().
  • package.json#emdash.seed punta a un file esistente.
  • Ogni collection e ogni campo interrogati sono dichiarati dal seed.
  • Gli esempi di query usano orderBy e il corretto identificatore di voce.
  • I valori del sito gestiti dal CMS non sono duplicati come costanti permanenti del modello.
  • Una configurazione pulita funziona con e senza contenuto di esempio.
  • La build e il controllo dei tipi del modello passano per la sua destinazione supportata.