Open app

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:

  1. A theStacc project with Content SEO enabled and at least one published blog
  2. Your Public Blog API key from Settings > Publishing > API Access
  3. A static site built with Astro, Next.js, Hugo, or any other SSG
  4. Your site deployed on Cloudflare Pages, Vercel, Netlify, or similar

Overview

The integration has three parts:

  1. Fetch — Your site calls the theStacc API at build time to get blog data
  2. Render — Your SSG generates static HTML pages from the API response
  3. 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:

  1. Blog listing page shows all published blogs
  2. Each blog detail page renders content correctly
  3. Images load properly
  4. Meta tags are set (check with View Source)
  5. Internal links within blog content work

Next steps