Astro
@citedseo/astro is a content loader. Your articles become a collection, typed and queried like any other in your site, and each one renders with render(). It needs Astro 6 or later.
Hand this to your coding agent
Read https://developer.citedseo.ai/llms-full.txt. My CitedSEO site ID is <YOUR_SITE_ID>. Install @citedseo/astro. Add a content collection that loads my articles, plus a /blog index and /blog/[slug] pages with the article's JSON-LD in this Astro app, reading my articles with that package. The feed needs no key. Use only the functions and addresses that file lists.
Install
Section titled “Install”npm install @citedseo/astropnpm add @citedseo/astroyarn add @citedseo/astrobun add @citedseo/astroWhat you’ll add
Section titled “What you’ll add”- .env
Directorysrc
- content.config.ts the collection
Directorypages
Directoryblog
- index.astro the index
- [slug].astro one article
-
Set your site ID.
.env CITEDSEO_SITE_ID=YOUR_SITE_ID -
Load the feed as a collection. The loader brings its own schema, so there is none to write.
src/content.config.ts import { defineCollection } from 'astro:content';import { citedseoLoader } from '@citedseo/astro';const articles = defineCollection({loader: citedseoLoader({ siteId: import.meta.env.CITEDSEO_SITE_ID }),});export const collections = { articles }; -
Add the index.
src/pages/blog/index.astro ---import { getCollection } from 'astro:content';const articles = (await getCollection('articles')).sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf(),);---<ul>{articles.map((article) => (<li><a href={`/blog/${article.id}`}>{article.data.title}</a><p>{article.data.description}</p></li>))}</ul> -
Add the article page. Each entry’s
idis the article’s slug, andrender()gives its body.src/pages/blog/[slug].astro ---import { getCollection, render } from 'astro:content';import { jsonLd } from '@citedseo/astro';export async function getStaticPaths() {const articles = await getCollection('articles');// An article's slug is URL-encoded; Astro encodes a param itself, so hand it over decoded.return articles.map((article) => ({params: { slug: decodeURIComponent(article.id) },props: { article },}));}const { article } = Astro.props;const { Content } = await render(article);const data = jsonLd(article.data);---<html lang="en"><head><title>{article.data.title}</title><meta name="description" content={article.data.description} /><link rel="canonical" href={article.data.url} />{data && <script type="application/ld+json" set:html={data} />}</head><body><article><h1>{article.data.title}</h1>{article.data.cover && <img src={article.data.cover.src} alt={article.data.cover.alt} />}<Content /></article></body></html>
What an entry holds
Section titled “What an entry holds”entry.id |
The article’s slug, URL-encoded as the feed has it. Use it as it is in a link, and decoded as a route param. |
entry.data |
The article’s fields, with publishedAt and modifiedAt as Dates. The type is ArticleData, exported with its schema, articleSchema. |
entry.body |
The body as Markdown. |
render(entry) |
The body as HTML, as a <Content /> component. |
The loader keeps what it read between builds: an article that hasn’t changed is skipped, and one that was taken down is removed from the collection.
Rebuild when you publish
Section titled “Rebuild when you publish”A static Astro site shows the feed as it was when the site was last built. To pick up a new article, rebuild.
Most hosts give you a deploy hook: an address that starts a build when it is posted to. Don’t add that address as a CitedSEO webhook directly, since it can’t check our signature. Put a small function in between that verifies the post, then calls the deploy hook:
import { webhookHandler } from '@citedseo/astro';
export const POST = webhookHandler({ secret: process.env.CITEDSEO_WEBHOOK_SECRET!, onPost: async () => { const res = await fetch(process.env.DEPLOY_HOOK_URL!, { method: 'POST' }); // Throwing answers 500, so CitedSEO tries the post again. if (!res.ok) throw new Error(`The deploy hook answered ${res.status}`); },});See Update the moment you publish.
Keep the address the same
Section titled “Keep the address the same”Each article’s url is where CitedSEO expects it to live, and its structured data and canonical address say so. If your blog isn’t at /blog, set the site’s blog path in the app’s settings to match your route. See SEO for your article pages.