Guide: Blog Integration via Data Mode
π€ Using Cursor? Install the PageGun rules for the best experience: Cursor Rules
Build a high-performance blog powered by PageGun. Use the API to manage articles, and the public CDN to serve them β no rate limits, instant global delivery.
Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WRITE PATH (API) β
β β
β Your CMS / Script / AI Agent β
β β β
β βΌ β
β POST /pages βββΆ Create article (markdown) β
β PUT /pages/:id βββΆ Update article β
β POST /pages/:id/publish βββΆ Encrypt + push to CDN β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β READ PATH (CDN) β
β β
β content.pagegun.com/{project_id}/nav.enc β
β βββ Article index (titles, slugs, metadata) β
β β
β content.pagegun.com/{project_id}/{subroute}/{slug}.enc β
β βββ Full article content (encrypted) β
β β
β Your App (Next.js / Nuxt / etc.) β
β βββ Fetch .enc β Decrypt server-side β Render β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββWhy this architecture?
| API (write) | CDN (read) | |
|---|---|---|
| Rate limit | 100 req/min | Unlimited |
| Latency | ~200ms | ~20ms (global edge) |
| Auth required | Yes (API key) | No (public, encrypted) |
| Cost | Free tier | Free (Cloudflare R2) |
Prerequisites
- A PageGun account with an API key (
pgk_live_...) - Your project ID and Content Key (found in your Dashboard under Settings)
- Node.js 18+ (for decryption examples)
Step 1: Get Your API Key
Get your API key from the PageGun Dashboard under Settings β API Keys.
export PAGEGUN_API_KEY="pgk_live_your_key_here"
export PROJECT_ID="your_project_id"Step 2: Get Your Content Key (Encryption Key)
The Content Key is used to decrypt content fetched from the CDN. You can find it in your Dashboard under Settings β Data Mode.
export PAGEGUN_CONTENT_KEY="your_content_key_hex"β οΈ Keep your Content Key secret. Store it as a server-side environment variable β never expose it in client-side code.
Step 3: Create Blog Articles
curl -X POST "https://api.pagegun.com/pages" \
-H "Authorization: Bearer $PAGEGUN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "How to Use Our Platform",
"slug": "how-to-use-our-platform",
"subroute": "blog",
"type": "article",
"project_id": "'"$PROJECT_ID"'",
"description": "A step-by-step guide to getting started with our platform.",
"og_image_url": "https://cdn.example.com/blog/how-to-use-og.jpg",
"markdown_content": "# How to Use Our Platform\n\nWelcome! In this guide we will walk you through...\n\n## Getting Started\n\nFirst, sign up for an account...\n\n## Key Features\n\n- **Dashboard** β manage everything in one place\n- **API access** β automate your workflow\n- **Analytics** β track performance\n\n## Next Steps\n\nCheck out our [API docs](/docs) for more details."
}'Response:
{
"data": {
"id": "page_abc123",
"title": "How to Use Our Platform",
"slug": "how-to-use-our-platform",
"subroute": "blog",
"type": "article",
"status": "draft"
}
}Article Fields Reference
| Field | Required | Description |
|---|---|---|
title | Yes | Article title (H1 + page title) |
slug | Yes | URL path segment (lowercase, hyphens) |
subroute | Yes | URL prefix, e.g. "blog" β /blog/slug |
type | Yes | Always "article" |
project_id | Yes | Your project ID |
description | No | SEO meta description (155 chars recommended) |
og_image_url | No | Social sharing image (1200Γ630px) |
markdown_content | Yes | Full article body in Markdown |
π Need author support? The
authorfield for articles is coming soon. If you need author attribution now, please contact the developer.
Supported Markdown
- Headings (h1βh6)
- Bold, italic, strikethrough
- Ordered and unordered lists
- Links and images
- Code blocks with syntax highlighting
- Tables
- Blockquotes
Adding Images
Images use standard Markdown syntax with external URLs. Host your images on any CDN or storage service (S3, Cloudflare R2, Cloudinary, Imgur, etc.):
# Inline image

# Image with link
[](https://example.com/features)In the API request, include images directly in markdown_content:
curl -X POST "https://api.pagegun.com/pages" \
-H "Authorization: Bearer $PAGEGUN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Product Update: New Dashboard",
"slug": "new-dashboard",
"subroute": "blog",
"type": "article",
"project_id": "'"$PROJECT_ID"'",
"og_image_url": "https://cdn.example.com/blog/new-dashboard-og.jpg",
"markdown_content": "# Product Update: New Dashboard\n\nWe are excited to announce our redesigned dashboard.\n\n\n\n## Key Changes\n\n- Faster navigation\n- Real-time analytics\n\n"
}'Hosting Images on PageGun CDN
You can also upload images directly to PageGun's CDN using the Media API, instead of hosting them yourself.
Method 1: Upload from URL
curl -X POST "https://api.pagegun.com/media" \
-H "Authorization: Bearer $PAGEGUN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/photo.jpg",
"project_id": "'"$PROJECT_ID"'",
"alt_text": "Dashboard overview screenshot",
"label": "dashboard-overview"
}'Method 2: Upload file directly
curl -X POST "https://api.pagegun.com/media" \
-H "Authorization: Bearer $PAGEGUN_API_KEY" \
-F "file=@./dashboard.png" \
-F "project_id=$PROJECT_ID" \
-F "alt_text=Dashboard overview screenshot"The response includes a hosted url β use it in your markdown content:
{
"data": {
"url": "https://content.pagegun.com/YOUR_PROJECT_ID/media/dashboard-overview.png",
"alt_text": "Dashboard overview screenshot"
}
}Then reference it in your article:
Tips:
- Set
og_image_urlfor social sharing previews (1200Γ630px recommended) - Use descriptive alt text for SEO and accessibility
- Optimize image file size before uploading
- Use
labelto give images readable names on the CDN
Step 4: Publish to CDN
Publishing encrypts the article and pushes it to the global CDN:
curl -X POST "https://api.pagegun.com/pages/page_abc123/publish" \
-H "Authorization: Bearer $PAGEGUN_API_KEY"After publishing, the article is available at:
https://content.pagegun.com/{project_id}/blog/how-to-use-our-platform.encThe navigation index is also updated:
https://content.pagegun.com/{project_id}/nav.encStep 5: Fetch and Decrypt in Your App
Decryption Utility (Node.js / TypeScript)
// lib/pagegun.ts
import crypto from 'crypto';
const PROJECT_ID = process.env.PAGEGUN_PROJECT_ID!;
const CONTENT_KEY = process.env.PAGEGUN_CONTENT_KEY!;
const CDN_BASE = `https://content.pagegun.com/${PROJECT_ID}`;
interface PageData {
title: string;
slug: string;
subroute: string;
description: string;
og_image_url: string | null;
markdown_content: string;
published_at: string;
}
interface NavItem {
title: string;
slug: string;
subroute: string;
description: string;
og_image_url: string | null;
published_at: string;
}
function decrypt(encrypted: string): string {
const [ivHex, authTagHex, ciphertext] = encrypted.split(':');
const iv = Buffer.from(ivHex, 'hex');
const authTag = Buffer.from(authTagHex, 'hex');
const key = Buffer.from(CONTENT_KEY, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(ciphertext, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Fetch the navigation index (all published pages).
* Filter by subroute to get blog articles only.
*/
export async function getBlogArticles(): Promise<NavItem[]> {
const res = await fetch(`${CDN_BASE}/nav.enc`);
if (!res.ok) return [];
const encrypted = await res.text();
const nav = JSON.parse(decrypt(encrypted));
return nav.items
.filter((item: NavItem) => item.subroute === 'blog')
.sort((a: NavItem, b: NavItem) =>
new Date(b.published_at).getTime() - new Date(a.published_at).getTime()
);
}
/**
* Fetch a single blog article by slug.
*/
export async function getBlogArticle(slug: string): Promise<PageData | null> {
const res = await fetch(`${CDN_BASE}/blog/${slug}.enc`);
if (!res.ok) return null;
const encrypted = await res.text();
return JSON.parse(decrypt(encrypted));
}Next.js App Router Example
// app/blog/page.tsx β Blog listing
import { getBlogArticles } from '@/lib/pagegun';
import Link from 'next/link';
export default async function BlogPage() {
const articles = await getBlogArticles();
return (
<div>
<h1>Blog</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{articles.map((article) => (
<Link key={article.slug} href={`/blog/${article.slug}`}>
<article className="border rounded-lg p-4 hover:shadow-lg transition">
{article.og_image_url && (
<img src={article.og_image_url} alt={article.title} className="rounded mb-3" />
)}
<h2 className="text-xl font-bold">{article.title}</h2>
<p className="text-gray-600 mt-2">{article.description}</p>
<time className="text-sm text-gray-400 mt-2 block">
{new Date(article.published_at).toLocaleDateString()}
</time>
</article>
</Link>
))}
</div>
</div>
);
}// app/blog/[slug]/page.tsx β Single article
import { getBlogArticle, getBlogArticles } from '@/lib/pagegun';
import { notFound } from 'next/navigation';
import ReactMarkdown from 'react-markdown';
// Generate static paths at build time
export async function generateStaticParams() {
const articles = await getBlogArticles();
return articles.map((a) => ({ slug: a.slug }));
}
// SEO metadata
export async function generateMetadata({ params }: { params: { slug: string } }) {
const article = await getBlogArticle(params.slug);
if (!article) return {};
return {
title: article.title,
description: article.description,
openGraph: {
title: article.title,
description: article.description,
images: article.og_image_url ? [article.og_image_url] : [],
},
};
}
export default async function ArticlePage({ params }: { params: { slug: string } }) {
const article = await getBlogArticle(params.slug);
if (!article) notFound();
return (
<article className="prose lg:prose-xl mx-auto py-10">
<h1>{article.title}</h1>
<time className="text-gray-500">
{new Date(article.published_at).toLocaleDateString()}
</time>
<ReactMarkdown>{article.markdown_content}</ReactMarkdown>
</article>
);
}Environment Variables
Add to your .env.local:
PAGEGUN_PROJECT_ID=your_project_id
PAGEGUN_CONTENT_KEY=your_content_key_hexβ οΈ Never expose
PAGEGUN_CONTENT_KEYto client-side code. Always decrypt server-side (in Server Components,getServerSideProps, or API routes).
Step 6: Update and Re-publish
When you update an article, re-publish to push changes to CDN:
# Update content
curl -X PUT "https://api.pagegun.com/pages/page_abc123" \
-H "Authorization: Bearer $PAGEGUN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"markdown_content": "# Updated Title\n\nNew content here..."}'
# Re-publish to CDN
curl -X POST "https://api.pagegun.com/pages/page_abc123/publish" \
-H "Authorization: Bearer $PAGEGUN_API_KEY"Changes appear on CDN within seconds.
Caching
CDN content updates immediately after publishing. How you cache and revalidate on your end is entirely up to your hosting setup.
SEO Checklist
- β
Use descriptive, keyword-rich
slugvalues (e.g.how-to-use-our-platform) - β
Write
descriptionunder 155 characters with target keyword - β
Set
og_image_url(1200Γ630px) for social sharing - β Structure markdown with proper heading hierarchy (H1 β H2 β H3)
- β
Use
generateMetadatain Next.js for dynamic meta tags - β
Generate a sitemap from
nav.encdata
Complete Workflow Summary
1. Create article β POST /pages (type: "article", markdown_content: "...")
2. Publish β POST /pages/:id/publish
3. CDN delivers β content.pagegun.com/{project_id}/blog/{slug}.enc
4. Your app reads β fetch .enc β decrypt β render markdown
5. Update β PUT /pages/:id β POST /pages/:id/publishRelated
- Guide: Data Mode Integration β Data Mode deep dive
- Guide: Managing Articles β Article management reference
- API: Create Page β Full create endpoint docs
- Page Type: Article β Article type reference
- Articles Grid Section β Display articles on landing pages