@rankveo/astro
Run your rankveo blog on an Astro site. rankveo is the CMS: you approve and publish there, your site reads the article and renders it under your own domain, design, and URLs.
npm install @rankveo/astro
Content collection
The integration is a Content Layer loader, so articles behave like any other
collection — typed, cached between builds, and rendered with <Content />.
// src/content.config.ts
import { defineCollection } from 'astro:content';
import { rankveoLoader } from '@rankveo/astro';
const blog = defineCollection({
loader: rankveoLoader({ apiKey: import.meta.env.RANKVEO_BLOG_API_KEY }),
});
export const collections = { blog };
Pass the key explicitly. Astro loads .env into import.meta.env, not
process.env, so the client's own environment fallback cannot see it here. A
variable without a PUBLIC_ prefix never reaches the browser.
RANKVEO_BLOG_API_KEY=rk_your_key_here
Then render:
---
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const articles = await getCollection('blog');
return articles.map((article) => ({ params: { slug: article.id }, props: { article } }));
}
const { article } = Astro.props;
const { Content } = await render(article);
---
<article class="article-body"><Content /></article>
The loader stores the body under rendered, which is what makes render() and
<Content /> work, and keeps a large HTML blob out of the validated data.
Starter
cp -r node_modules/@rankveo/astro/starter/src/* src/
| Route | File |
|---|---|
/blog, /blog/2 … |
pages/blog/[...page].astro |
/blog/[slug] |
pages/blog/[slug].astro |
/blog/tag/[slug] |
pages/blog/tag/[slug]/[...page].astro |
/blog/sitemap.xml |
pages/blog/sitemap.xml.ts |
/blog/rss.xml |
pages/blog/rss.xml.ts |
Set your site URL and page size in src/rankveo.ts.
Pagination uses Astro's paginate(), so pages are real paths — static output
has no request-time search params, and there is no zero-based/one-based
conversion to get wrong.
Images
Ask the API which hosts your articles use, rather than guessing:
import { remotePatterns } from '@rankveo/astro';
const { imageHosts } = await blog.getSite();
console.log(JSON.stringify(remotePatterns(imageHosts), null, 2));
Paste into astro.config.mjs. Note the key nests under image, singular —
unlike Next.js:
image: { remotePatterns: [{ protocol: 'https', hostname: 'cdn.example.com' }] }
Article styling
starter/src/styles/article.css styles the article HTML under .article-body.
It has no dependencies and is meant to be edited.
Do not drop it and rely on your own reset. Most resets — Tailwind's preflight
included — ship ol, ul, menu { list-style: none } and
h1…h6 { font-size: inherit; font-weight: inherit }. With those in force and
nothing restoring them, an article renders as one flat wall of text: headings
the same size as body copy, lists with no markers.
Keeping the blog fresh
Static output has no runtime cache to invalidate — the site must be rebuilt. Point rankveo at your host's deploy hook (Cloudflare Pages, Netlify, Vercel) and publishing triggers a build.
Server output can invalidate a single page. Copy the revalidate endpoint:
cp -r node_modules/@rankveo/astro/starter/server/pages/api src/pages/api
It is kept out of the default starter deliberately: it carries
export const prerender = false, which requires an adapter, so copying it into a
static project fails the build with NoAdapterInstalled.
Set RANKVEO_REVALIDATE_SECRET, then paste the URL and secret into
rankveo → Integrations. Wire cache.invalidate() inside the handler to whatever
your adapter uses — the endpoint verifies the bearer token in constant time and
leaves the invalidation call to you, since the exact surface depends on your
adapter and Astro version.
Things to know
article.htmlis the body only. The loader hands it to Astro throughrendered; use<Content />rather thanset:htmlwhere you can.- Body images are already in the article HTML.
imageslists them so you can allowlist hosts and build an image sitemap, not so you render them twice. Show a hero only whenimage.role === 'featured'. imagecan be a body image. With no featured image it falls back to the first one in the body, so cards are never blank.- The collection is frozen at build, even with
output: 'server'. Anything that must be fresh per request has to go throughBlogClient. - A loader failure fails the build, which leaves your previous deploy live. That is the right default.
Docs and setup guide · All integrations
MIT.