Skip to content
Docs

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.
Terminal window
npm install @citedseo/astro
  • .env
  • Directorysrc
    • content.config.ts the collection
    • Directorypages
      • Directoryblog
        • index.astro the index
        • [slug].astro one article
  1. Set your site ID.

    .env
    CITEDSEO_SITE_ID=YOUR_SITE_ID
  2. 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 };
  3. 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>
  4. Add the article page. Each entry’s id is the article’s slug, and render() 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>
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.

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:

A function on your host
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.

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.