Static Site Integration
Step-by-step guide to display theStacc blogs on Astro, Next.js, Hugo, and other static site generators.
This guide walks you through connecting theStacc to your static site. By the end, your site will automatically display AI-generated blog posts — fully rendered as static HTML for perfect SEO.
Prerequisites
Before starting, make sure you have:
- A theStacc project with Content SEO enabled and at least one published blog
- Your Public Blog API key from Settings > Publishing > API Access
- A static site built with Astro, Next.js, Hugo, or any other SSG
- Your site deployed on Cloudflare Pages, Vercel, Netlify, or similar
Overview
The integration has three parts:
- Fetch — Your site calls the theStacc API at build time to get blog data
- Render — Your SSG generates static HTML pages from the API response
- Rebuild — A deploy hook triggers a new build whenever content is published
This guide covers the first two parts. See Deploy Hooks for automatic rebuilds.
Environment setup
Store your API key as an environment variable. Never hardcode it in your source files.
Create or update your .env file:
THESTACC_API_KEY=your_api_key_here
THESTACC_API_URL=https://api.thestacc.com/blog/api/v1/public
Add .env to your .gitignore if it's not already there.
For your hosting platform, add the same variables in the dashboard:
- Cloudflare Pages — Settings > Environment variables
- Vercel — Settings > Environment Variables
- Netlify — Site configuration > Environment variables
Astro
Astro is the most straightforward integration since it supports static and server-rendered pages natively.
Create the API helper
Create src/lib/thestacc.ts:
const API_KEY = import.meta.env.THESTACC_API_KEY;
const API_URL = import.meta.env.THESTACC_API_URL;
export async function getAllBlogs() {
const res = await fetch(`${API_URL}/blogs?api_key=${API_KEY}`);
if (!res.ok) throw new Error(`Failed to fetch blogs: ${res.status}`);
const data = await res.json();
return data.blogs;
}
export async function getBlogBySlug(slug: string) {
const res = await fetch(`${API_URL}/blogs/${slug}?api_key=${API_KEY}`);
if (!res.ok) return null;
return await res.json();
}
Create the blog listing page
Create src/pages/blog/index.astro:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAllBlogs } from '../../lib/thestacc';
const blogs = await getAllBlogs();
---
<BaseLayout title="Blog">
<h1>Blog</h1>
<div class="blog-grid">
{blogs.map((blog) => (
<a href={`/blog/${blog.slug}`}>
<img src={blog.featured_image_url} alt={blog.title} />
<h2>{blog.title}</h2>
<p>{blog.excerpt}</p>
<time>{new Date(blog.published_at).toLocaleDateString()}</time>
</a>
))}
</div>
</BaseLayout>
Create the blog detail page
Create src/pages/blog/[slug].astro:
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getAllBlogs, getBlogBySlug } from '../../lib/thestacc';
export async function getStaticPaths() {
const blogs = await getAllBlogs();
return blogs.map((blog) => ({
params: { slug: blog.slug },
}));
}
const { slug } = Astro.params;
const blog = await getBlogBySlug(slug);
if (!blog) return Astro.redirect('/404');
---
<BaseLayout title={blog.meta_title} description={blog.meta_description}>
<article>
<img src={blog.featured_image_url} alt={blog.title} />
<h1>{blog.title}</h1>
<time>{new Date(blog.published_at).toLocaleDateString()}</time>
<div class="blog-content" set:html={blog.content} />
</article>
</BaseLayout>
set:html renders the HTML content directly. Style the .blog-content class in your CSS to match your site's design.
Generate the sitemap
If you use @astrojs/sitemap, the blog pages are automatically included since they're generated as static routes.
Next.js (App Router)
Create the API helper
Create lib/thestacc.ts:
const API_KEY = process.env.THESTACC_API_KEY;
const API_URL = process.env.THESTACC_API_URL;
export async function getAllBlogs() {
const res = await fetch(`${API_URL}/blogs?api_key=${API_KEY}`, {
next: { revalidate: false },
});
if (!res.ok) throw new Error('Failed to fetch blogs');
const data = await res.json();
return data.blogs;
}
export async function getBlogBySlug(slug: string) {
const res = await fetch(`${API_URL}/blogs/${slug}?api_key=${API_KEY}`, {
next: { revalidate: false },
});
if (!res.ok) return null;
return await res.json();
}
Create the blog listing page
Create app/blog/page.tsx:
import { getAllBlogs } from '@/lib/thestacc';
import Link from 'next/link';
import Image from 'next/image';
export default async function BlogPage() {
const blogs = await getAllBlogs();
return (
<main>
<h1>Blog</h1>
<div className="blog-grid">
{blogs.map((blog) => (
<Link key={blog.id} href={`/blog/${blog.slug}`}>
<Image src={blog.featured_image_url} alt={blog.title} width={800} height={400} />
<h2>{blog.title}</h2>
<p>{blog.excerpt}</p>
</Link>
))}
</div>
</main>
);
}
Create the blog detail page
Create app/blog/[slug]/page.tsx:
import { getAllBlogs, getBlogBySlug } from '@/lib/thestacc';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const blogs = await getAllBlogs();
return blogs.map((blog) => ({ slug: blog.slug }));
}
export async function generateMetadata({ params }) {
const blog = await getBlogBySlug(params.slug);
if (!blog) return {};
return {
title: blog.meta_title,
description: blog.meta_description,
openGraph: { images: [blog.featured_image_url] },
};
}
export default async function BlogPost({ params }) {
const blog = await getBlogBySlug(params.slug);
if (!blog) notFound();
return (
<article>
<img src={blog.featured_image_url} alt={blog.title} />
<h1>{blog.title}</h1>
<time>{new Date(blog.published_at).toLocaleDateString()}</time>
<div
className="blog-content"
dangerouslySetInnerHTML={{ __html: blog.content }}
/>
</article>
);
}
For static export, add output: 'export' to your next.config.js and use generateStaticParams as shown above.
Hugo
Create the data fetch script
Hugo doesn't fetch API data natively. Use a build script that runs before Hugo.
Create fetch-blogs.sh:
#!/bin/bash
mkdir -p content/blog
# Fetch all blogs
RESPONSE=$(curl -s "$THESTACC_API_URL/blogs?api_key=$THESTACC_API_KEY")
# Generate a markdown file for each blog
echo "$RESPONSE" | jq -c '.blogs[]' | while read -r blog; do
SLUG=$(echo "$blog" | jq -r '.slug')
TITLE=$(echo "$blog" | jq -r '.title')
DESC=$(echo "$blog" | jq -r '.meta_description')
DATE=$(echo "$blog" | jq -r '.published_at')
IMAGE=$(echo "$blog" | jq -r '.featured_image_url')
CONTENT=$(echo "$blog" | jq -r '.content')
cat > "content/blog/$SLUG.html" <<EOF
---
title: "$TITLE"
description: "$DESC"
date: "$DATE"
featured_image: "$IMAGE"
markup: html
---
$CONTENT
EOF
done
The script uses .html extension and markup: html frontmatter so Hugo renders the API's HTML content as-is instead of processing it as markdown. In your Hugo template, use {{ .Content | safeHTML }} to output the content without escaping.
Update your build command on Cloudflare Pages (or wherever you deploy):
bash fetch-blogs.sh && hugo
Nuxt 3 (Vue.js)
Create the API composable
Create composables/useThestacc.ts:
export async function getAllBlogs() {
const config = useRuntimeConfig()
const data = await $fetch(`${config.public.thestaccApiUrl}/blogs`, {
query: { api_key: config.thestaccApiKey },
})
return data.blogs
}
export async function getBlogBySlug(slug: string) {
const config = useRuntimeConfig()
return await $fetch(`${config.public.thestaccApiUrl}/blogs/${slug}`, {
query: { api_key: config.thestaccApiKey },
})
}
Add runtime config
In your nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
thestaccApiKey: process.env.THESTACC_API_KEY,
public: {
thestaccApiUrl: process.env.THESTACC_API_URL,
},
},
})
Create the blog listing page
Create pages/blog/index.vue:
<script setup>
const blogs = await getAllBlogs()
</script>
<template>
<div>
<h1>Blog</h1>
<div class="blog-grid">
<NuxtLink v-for="blog in blogs" :key="blog.id" :to="`/blog/${blog.slug}`">
<img :src="blog.featured_image_url" :alt="blog.title" />
<h2>{{ blog.title }}</h2>
<p>{{ blog.excerpt }}</p>
<time>{{ new Date(blog.published_at).toLocaleDateString() }}</time>
</NuxtLink>
</div>
</div>
</template>
Create the blog detail page
Create pages/blog/[slug].vue:
<script setup>
const route = useRoute()
const blog = await getBlogBySlug(route.params.slug as string)
useHead({
title: blog?.meta_title,
meta: [
{ name: 'description', content: blog?.meta_description },
],
})
</script>
<template>
<article v-if="blog">
<img :src="blog.featured_image_url" :alt="blog.title" />
<h1>{{ blog.title }}</h1>
<time>{{ new Date(blog.published_at).toLocaleDateString() }}</time>
<div class="blog-content" v-html="blog.content" />
</article>
</template>
v-html renders the HTML content directly. Style the .blog-content class in your CSS to match your site's design.
Static generation
For static export on Cloudflare Pages, Vercel, or Netlify, run:
npx nuxi generate
Nuxt pre-renders all pages at build time — the output is pure static HTML with the same SEO as any other static site generator.
Styling the content
The API returns HTML content. Add CSS to style it within your site's design:
.blog-content h2 {
font-size: 1.5rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 0.75rem;
}
.blog-content h3 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
.blog-content p {
line-height: 1.75;
margin-bottom: 1rem;
}
.blog-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1.5rem 0;
}
.blog-content a {
color: var(--color-primary);
text-decoration: underline;
}
.blog-content ul, .blog-content ol {
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.blog-content blockquote {
border-left: 3px solid var(--color-primary);
padding-left: 1rem;
font-style: italic;
color: #6b7280;
}
If you use Tailwind CSS, apply the @tailwindcss/typography plugin and wrap the content in a prose class:
<div class="prose prose-lg" set:html={blog.content} />
Testing locally
Run your site locally and verify:
- Blog listing page shows all published blogs
- Each blog detail page renders content correctly
- Images load properly
- Meta tags are set (check with View Source)
- Internal links within blog content work
Next steps
- Set up Deploy Hooks so your site rebuilds automatically when blogs are published
- Add an XML sitemap for SEO using the sitemap endpoint