Skip to content
Docs

Nuxt

@citedseo/nuxt gives your server routes the feed and your pages their head tags. Two small server routes, and two pages that read them.

Hand this to your coding agent

Read https://developer.citedseo.ai/llms-full.txt.
My CitedSEO site ID is <YOUR_SITE_ID>.
Install @citedseo/nuxt. Add pages/blog/index.vue and pages/blog/[slug].vue with the article's head tags and JSON-LD in this Nuxt 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/nuxt
  • .env
  • Directoryserver
    • Directoryapi
      • Directoryarticles
        • index.get.ts every article
        • [slug].get.ts one article
      • citedseo.post.ts the webhook
  • Directorypages
    • Directoryblog
      • index.vue the index
      • [slug].vue one article
  1. Set your site ID.

    .env
    CITEDSEO_SITE_ID=YOUR_SITE_ID
  2. Add the two routes.

    server/api/articles/index.get.ts
    import { fetchArticles } from '@citedseo/nuxt';
    export default defineEventHandler(() => fetchArticles({ siteId: process.env.CITEDSEO_SITE_ID! }));
    server/api/articles/[slug].get.ts
    import { fetchArticle } from '@citedseo/nuxt';
    export default defineEventHandler(async (event) => {
    const article = await fetchArticle({
    siteId: process.env.CITEDSEO_SITE_ID!,
    slug: getRouterParam(event, 'slug')!,
    });
    if (!article) throw createError({ statusCode: 404, statusMessage: 'Article not found' });
    return article;
    });
  3. Add the index.

    pages/blog/index.vue
    <script setup lang="ts">
    const { data: articles } = await useFetch('/api/articles');
    </script>
    <template>
    <ul>
    <li v-for="article in articles" :key="article.id">
    <NuxtLink :to="`/blog/${article.slug}`">{{ article.title }}</NuxtLink>
    <p>{{ article.description }}</p>
    </li>
    </ul>
    </template>
  4. Add the article page. articleHead gives the title, description, canonical link, Open Graph tags and JSON-LD in the shape useHead takes.

    pages/blog/[slug].vue
    <script setup lang="ts">
    import { articleHead } from '@citedseo/nuxt';
    const route = useRoute();
    const { data: article, error } = await useFetch(`/api/articles/${route.params.slug}`);
    if (error.value || !article.value) {
    throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true });
    }
    useHead(articleHead(article.value));
    </script>
    <template>
    <article v-if="article">
    <h1>{{ article.title }}</h1>
    <img v-if="article.cover" :src="article.cover.src" :alt="article.cover.alt" />
    <div v-html="article.html" />
    </article>
    </template>

Each request reads the manifest, which is cached for a minute, so a new article shows within a minute with no webhook.

If you prerender the blog or cache your pages, refresh from a server route. webhookHandler checks the signature for you:

server/api/citedseo.post.ts
import { webhookHandler } from '@citedseo/nuxt';
const handle = webhookHandler({
secret: process.env.CITEDSEO_WEBHOOK_SECRET!,
onPost: async (post) => {
// Clear your cache, start a build, or store post.data.
},
});
export default defineEventHandler((event) => handle(toWebRequest(event)));

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.