Skip to content

Astro

@easeo/astro injects the site config at build time and can emit an SEO contract when the build finishes.

Simple: configure the integration

// astro.config.mjs
import { defineConfig } from "astro/config";
import easeo from "@easeo/astro";

export default defineConfig({
  integrations: [
    easeo({
      config: {
        canonicalHost: "example.com",
        publicBaseUrl: "https://example.com",
        siteName: "Example",
      },
    }),
  ],
});

Then read the injected config in a layout through the Vite define __EASEO_CONFIG__.

Simple: one page

---
// src/pages/about.astro
import { buildSeoPayload } from "@easeo/core";

const config = { canonicalHost: "example.com", publicBaseUrl: "https://example.com" };
const payload = buildSeoPayload(
  { entityType: "page", title: "About", description: "About us." },
  "/about",
  config
);
---
<html>
  <head>
    <Fragment set:html={payload.renderHtml()} />
  </head>
  <body><h1>About</h1></body>
</html>

Complex: content collections

Build the entity from a content collection entry, and pass overrides for the fields the collection does not store.

---
// src/pages/blog/[slug].astro
import { getCollection } from "astro:content";
import { buildSeoPayload } from "@easeo/core";
import type { SEOConfig } from "@easeo/core";

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

const { post } = Astro.props;
const config: SEOConfig = {
  canonicalHost: "blog.example.com",
  publicBaseUrl: "https://blog.example.com",
  siteName: "Example Blog",
  titleTemplate: "{title} - Example Blog",
  defaultOgImage: "https://blog.example.com/assets/og-image.png",
};

const payload = buildSeoPayload(
  {
    entityType: "post",
    title: post.data.title,
    description: post.data.description,
    image: post.data.cover,
    authorName: post.data.author,
    publishedAt: post.data.publishedAt,
    breadcrumbs: [
      { name: "Home", url: "/" },
      { name: "Blog", url: "/blog" },
    ],
  },
  `/blog/${post.slug}`,
  config
);
---
<html>
  <head>
    <Fragment set:html={payload.renderHtml()} />
  </head>
  <body><article set:html={post.body} /></body>
</html>

Complex: emit a contract at build time

// astro.config.mjs
import { defineConfig } from "astro/config";
import easeo from "@easeo/astro";

export default defineConfig({
  integrations: [
    easeo({
      config: {
        canonicalHost: "blog.example.com",
        publicBaseUrl: "https://blog.example.com",
        siteName: "Example Blog",
      },
      contract: {
        canonicalHost: "blog.example.com",
        scheme: "https",
        defaults: { ogRequired: true, schemaRequired: true },
        rules: [{ match: "/blog/*", expect: { schemaTypes: ["Article"] } }],
      },
    }),
  ],
});

The contract is written to dist/.easeo/contract.json. Commit it and gate the deployment on a diff. See Contracts in CI.

Notes

  • The contract is resolved with fileURLToPath, so output paths with spaces work.
  • The integration works for static and SSR Astro projects. The metadata itself is generated by buildSeoPayload, so it works the same in both.