# Render Posts in Next.js or Astro

> Complete examples for rendering Revnu posts on your own site with Next.js App Router or Astro, plus how updates reach a build-time site.

Canonical: https://revnu.com/docs/host-your-blog

Two complete examples. Both keep the token in a server-side environment variable and render on the server, which is [the one hard rule](/docs/content-api).

## Next.js App Router

```ts
// lib/revnu.ts
const BASE = "https://revnu.com/api/content/v1";
const HEADERS = { Authorization: `Bearer ${process.env.REVNU_TOKEN}` };

export async function getPosts() {
  const r = await fetch(`${BASE}/posts`, { headers: HEADERS, next: { revalidate: 300 } });
  if (!r.ok) throw new Error(`revnu: ${r.status}`);
  return r.json();
}

export async function getPost(slug: string) {
  const r = await fetch(`${BASE}/posts/${slug}`, { headers: HEADERS, next: { revalidate: 300 } });
  if (r.status === 404) return null;
  if (!r.ok) throw new Error(`revnu: ${r.status}`);
  return r.json();
}
```

```tsx
// app/blog/[...slug]/page.tsx
import { notFound, permanentRedirect } from "next/navigation";
import { getPost, getPosts } from "@/lib/revnu";

export async function generateStaticParams() {
  const { posts } = await getPosts();
  return posts.map((p) => ({ slug: p.canonicalPath.slice(1).split("/") }));
}

export default async function Post({ params }) {
  const { slug } = await params;
  const data = await getPost(slug[slug.length - 1]);
  if (!data) notFound();
  // One URL per post: a catch-all matches ANY path ending in the slug.
  if (`/${slug.join("/")}` !== data.post.canonicalPath) {
    permanentRedirect(`/blog${data.post.canonicalPath}`);
  }
  const html = data.post.body.html.replace(/href="\/(?!\/)/g, 'href="/blog/');
  return (
    <article>
      {data.jsonLd.map((schema, i) => (
        <script key={i} type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }} />
      ))}
      <h1>{data.post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </article>
  );
}
```

With `revalidate` set, new posts appear within minutes and you need nothing else.

## Astro

```astro
---
// src/pages/blog/[...slug].astro
export async function getStaticPaths() {
  const r = await fetch("https://revnu.com/api/content/v1/posts", {
    headers: { Authorization: `Bearer ${import.meta.env.REVNU_TOKEN}` },
  });
  if (!r.ok) throw new Error(`revnu: ${r.status}`);
  const { posts } = await r.json();
  return posts.map((p) => ({ params: { slug: p.canonicalPath.slice(1) } }));
}
const parts = String(Astro.params.slug).split("/");
const r = await fetch(`https://revnu.com/api/content/v1/posts/${parts[parts.length - 1]}`, {
  headers: { Authorization: `Bearer ${import.meta.env.REVNU_TOKEN}` },
});
if (!r.ok) throw new Error(`revnu: ${r.status}`);
const data = await r.json();
const html = data.post.body.html.replace(/href="\/(?!\/)/g, 'href="/blog/');
---
<html>
  <head>
    <title>{data.headMeta.title}</title>
    <link rel="canonical" href={data.headMeta.canonical} />
    {data.jsonLd.map((schema) => <script type="application/ld+json" set:html={JSON.stringify(schema)} />)}
  </head>
  <body><article><h1>{data.post.title}</h1><div set:html={html} /></article></body>
</html>
```

## Updates on a build-time site

Astro and other static builders render once at deploy. Give Revnu a deploy-hook URL and it POSTs `{ event, blogSlug, postSlug }` on every publish, update and delete. Treat the ping only as "rebuild now"; it is unsigned by design and carries nothing worth trusting.

## Frequently asked questions

### Do I need to handle the redirect for non-canonical paths?

Yes, if you use a catch-all route. Without it every post is reachable at infinitely many paths, which splits ranking. Compare the requested path with `canonicalPath` and redirect permanently.

### What about robots.txt and the sitemap?

Serve a robots.txt that allows crawling and points at your sitemap, and build the sitemap from the list endpoint using `canonicalPath` and `updatedAt`.
