Astro AI snippet
Adds a static /blog to an Astro site: pages, SEO tags, sitemap, deploy-hook rebuilds.
AstroA static /blog rendered at build time by the SEO for SaaS React components, with SEO tags, a sitemap, and deploy-hook rebuilds.astro@3 · api v1 · tested: Astro 7 static build on a fresh project, against the example article
# Add the SEO for SaaS blog to this Astro site
Articles are written and published by SEO for SaaS and read from its content API at build time. This site renders them. Follow the steps in order; the code below is tested, so use it as written and adapt only import paths.
## Done when
- `/blog` lists the published articles; `/blog/<slug>` renders the title as the h1 and every block.
- Each article page has title, description, canonical (no trailing slash), Open Graph and one JSON-LD script.
- The sitemap lists every article, each with a `<lastmod>` from the API.
- The user knows how to make publishes rebuild the site (a deploy hook).
## Before changing anything
1. Read `package.json` and `astro.config.*`. This targets a static Astro build. If the project uses a server adapter (`output: "server"`), tell the user this snippet builds pages at build time and ask how to proceed.
2. Ask the user to set Dashboard → your site → **Settings** → **Site and URLs** → **Articles live under** to `/blog` (canonical URLs and links between articles are built on it; unset means the site root). If they use another path, put the pages there instead of `/blog`.
3. If `src/pages/blog` exists, STOP and ask whether to replace it, merge, or use another path.
4. Note the package manager (use its lockfile's tool instead of npm) and any existing `site`, `trailingSlash` or sitemap settings in the Astro config: merge, never overwrite them, and tell the user if `trailingSlash` must change.
## 1. Environment
Add to `.env` (it must be gitignored) and ask the user for the content key. Never write it into any other file.
```
SEOFORSAAS_PROJECT_ID=YOUR_PROJECT_ID
SEOFORSAAS_CONTENT_KEY= # Delivery → Content API keys → Create key
```
## 2. Install
```
npx astro add react sitemap --yes
for f in lib/seoforsaas/types.ts lib/seoforsaas/client.ts \
components/seoforsaas/article.tsx components/seoforsaas/blocks.tsx components/seoforsaas/rich-text.tsx \
components/seoforsaas/video.tsx components/seoforsaas/json-ld.tsx components/seoforsaas/article.css; do
curl -fsSL --create-dirs -o "src/$f" "https://seoforsaas.dev/r/files/$f"
done
```
Leave the installed files as they are: customization goes through `<Article components={…}>` and CSS variables, so reinstalling stays a clean overwrite.
## 3. Config
Make `astro.config.mjs` match this, keeping the project's other settings. Ask the user for the production origin for `site`. If the config already passes options to `sitemap()`, add `serialize` to them; if it already has a `serialize`, chain it rather than replace it.
```js
// @ts-check
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
import sitemap from "@astrojs/sitemap";
import { loadEnv } from "vite";
import { createClient } from "./src/lib/seoforsaas/client.ts";
// The config runs before Astro loads .env into import.meta.env: read it here.
const env = loadEnv(process.env.NODE_ENV ?? "production", process.cwd(), "");
/**
* Path → last reader-visible change, fetched once per build. Keyed by path, not
* URL, so a www or http difference between `site` and the project cannot drop
* every date.
* @type {Promise<Map<string, string>> | undefined}
*/
let articleDates;
/** @param {string} url */
const lastModifiedOf = async (url) => {
// Inside the promise: a missing variable makes createClient throw, and that
// must reach the .catch below like any failed request.
articleDates ??= Promise.resolve()
.then(() =>
createClient({
projectId: env.SEOFORSAAS_PROJECT_ID,
contentKey: env.SEOFORSAAS_CONTENT_KEY,
}).getSitemap(),
)
.then((entries) => new Map(entries.map((e) => [new URL(e.url).pathname, e.lastModified])))
// @astrojs/sitemap drops the whole sitemap on a throw and still reports
// success; a sitemap without dates is the better failure.
.catch((error) => {
console.warn(`[seoforsaas] sitemap dates unavailable, writing it without them: ${error}`);
return new Map();
});
return (await articleDates).get(new URL(url).pathname);
};
// https://astro.build/config
export default defineConfig({
// Your production origin: canonical URLs and the sitemap resolve against it.
site: "https://example.com",
// Article URLs have no trailing slash (neither has seo.canonicalPath):
// serve them that way, or every canonical would point at a redirect.
trailingSlash: "never",
build: { format: "file" },
integrations: [
react(),
sitemap({
// Articles get their lastmod from the API; your own pages are left as they are.
serialize: async (item) => ({ ...item, lastmod: (await lastModifiedOf(item.url)) ?? item.lastmod }),
}),
],
});
```
## 4. Create these files
`src/lib/blog.ts`:
```ts
import { createClient } from "./seoforsaas/client";
// Server-only: Astro exposes variables without the PUBLIC_ prefix to server code alone.
export const seoforsaas = createClient({
projectId: import.meta.env.SEOFORSAAS_PROJECT_ID,
contentKey: import.meta.env.SEOFORSAAS_CONTENT_KEY,
});
```
`src/pages/blog/index.astro` (wrap it in the site's layout if it has one):
```astro
---
import { seoforsaas } from "../../lib/blog";
const articles = await seoforsaas.listAllArticles();
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Blog</title>
</head>
<body>
<main>
<h1>Blog</h1>
<ul>
{articles.map((article) => (
<li>
<a href={`/blog/${article.slug}`}><h2>{article.title}</h2></a>
<p>{article.seo.metaDescription}</p>
</li>
))}
</ul>
</main>
</body>
</html>
```
`src/pages/blog/[slug].astro`:
```astro
---
import type { GetStaticPaths } from "astro";
import { Article } from "../../components/seoforsaas/article";
import { JsonLd } from "../../components/seoforsaas/json-ld";
import { safeHref } from "../../components/seoforsaas/rich-text";
import "../../components/seoforsaas/article.css";
import { seoforsaas } from "../../lib/blog";
export const getStaticPaths = (async () => {
const articles = await seoforsaas.listAllArticles();
return articles.map(({ slug }) => ({ params: { slug } }));
}) satisfies GetStaticPaths;
const { slug } = Astro.params;
const article = await seoforsaas.getArticle(slug);
// Static build: every slug came from the list a moment ago.
if (!article) throw new Error(`Article "${slug}" was unpublished during the build`);
const { seo } = article;
const canonical = new URL(seo.canonicalPath, Astro.site);
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{seo.metaTitle}</title>
<meta name="description" content={seo.metaDescription} />
<link rel="canonical" href={canonical} />
<meta property="og:type" content="article" />
<meta property="og:title" content={seo.openGraph.title} />
<meta property="og:description" content={seo.openGraph.description} />
{seo.openGraph.image && <meta property="og:image" content={seo.openGraph.image} />}
<JsonLd data={seo.jsonLd} />
</head>
<body>
<main>
<article>
<h1>{article.title}</h1>
{article.thumbnailUrl && <img src={article.thumbnailUrl} alt="" width="1536" height="1024" />}
<Article blocks={article.blocks} />
{article.related.length > 0 && (
<nav aria-label="Related articles">
<h2>Related</h2>
<ul>
{article.related.map((link) => {
const href = safeHref(link.href);
return href && <li><a href={href}>{link.title}</a></li>;
})}
</ul>
</nav>
)}
</article>
</main>
</body>
</html>
```
## Rules
- The content key is read only in build and server code (`import.meta.env` without `PUBLIC_`).
- Render article bodies only through `<Article>`; never `set:html` for article content.
- Use `seo.jsonLd` as it is; never write article JSON-LD by hand.
- Pass every link from the API (`related`) through `safeHref`, as the code above does.
- Keep `trailingSlash: "never"` and `build.format: "file"`: `seo.canonicalPath` has no trailing slash, and a canonical must not point at a redirect.
## Check your work
1. `npx astro check` reports 0 errors (install `@astrojs/check` and `typescript` as dev dependencies if asked).
2. `npx astro build` writes `dist/blog/<slug>.html` for each article and `dist/sitemap-0.xml` listing them, every article with a `<lastmod>`. A `[seoforsaas] sitemap dates unavailable` warning means the API request failed: report it.
3. An article's HTML has one `<title>`, a meta description, a canonical link without a trailing slash, `og:` tags and exactly one `<script type="application/ld+json">`.
4. Without the user's key you can still check rendering: `https://api.seoforsaas.dev/public/v1/fixtures/article` needs no key and has every block type.
## Report back
- Files created or changed.
- What the user must do: fill the content key locally and in the host's build settings, create a deploy hook in the host, paste it into Delivery → Webhook, select Send test event.
- Anything you skipped, and why.The code in this snippet comes from the Astro guide, which you can also follow by hand.