What You'll Build
A content management system powered by Google Sheets. Write and publish blog posts from a spreadsheet, no database required. Perfect for JAMstack sites, portfolios, and static site generators.
Step 1: Set Up Your Content Sheet
Create a Google Sheet with these columns:
- Title - Post title
- Slug - URL-friendly identifier (e.g., "my-first-post")
- Content - Full post content (supports HTML)
- Author - Author name
- Published Date - Publication date
- Status - Published, Draft, or Archived
Add a few sample posts to test with different statuses.
Step 2: Connect Your Sheet
Add your Google Sheet to SheetAPI.pro and get your API credentials. Your content will be instantly available via the API.
Step 3: Build Your Blog Frontend
Choose your stack:
- Vanilla JS - Use the HTML/JS snippet for client-side rendering
- Next.js - Use getStaticProps for server-side generation
- Hugo - Fetch content at build time with Hugo data templates
- Custom - Any framework that can make HTTP requests
Step 4: Create Post Pages
Implement dynamic routing based on the Slug field. The snippets show examples for different frameworks.
Step 5: Publish & Update
To publish new content, simply add a row to your Sheet and set Status to "Published". Changes appear immediately (or at next rebuild for SSG).
Use Cases
- Personal blogs - Simple content management without WordPress
- Portfolio sites - Project case studies managed from Sheets
- Documentation sites - Keep docs in sync with Sheet updates
- News sections - Company news/updates managed by non-technical team
Advanced Features
- Categories & Tags - Add columns for taxonomies
- Featured Images - Add an "Image URL" column
- SEO Meta - Add columns for meta descriptions, keywords
- Multi-author - Filter posts by author
- Search - Implement client-side or API-based search
- Comments - Integrate with comment systems
- RSS Feed - Generate RSS from the API response
Pro Tips
- Use Google Sheets formulas to auto-generate slugs from titles
- Add data validation to ensure Status values are consistent
- Use conditional formatting to highlight drafts vs published posts
- Set up webhooks to trigger rebuilds on content changes
- For Markdown content, use a Markdown-to-HTML converter
- Cache API responses for better performance
Performance Considerations
- For SSG (Static Site Generation), fetch at build time
- For SSR, cache API responses (Redis, CDN)
- For client-side, implement loading states
- Use incremental regeneration (Next.js revalidate) for fresh content
<!DOCTYPE html>
<html>
<head>
<title>My Blog</title>
<style>
body { font-family: Georgia, serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; }
header { text-align: center; padding: 40px 0; border-bottom: 2px solid #ddd; margin-bottom: 40px; }
header h1 { font-size: 3rem; margin: 0; }
.post-list { margin-bottom: 60px; }
.post-preview { margin-bottom: 40px; padding-bottom: 30px; border-bottom: 1px solid #eee; }
.post-preview h2 { margin: 0 0 10px 0; }
.post-preview h2 a { color: #333; text-decoration: none; }
.post-preview h2 a:hover { color: #667eea; }
.post-meta { color: #666; font-size: 0.9rem; margin-bottom: 15px; }
.post-excerpt { color: #444; }
.read-more { color: #667eea; text-decoration: none; font-weight: bold; }
.read-more:hover { text-decoration: underline; }
</style>
</head>
<body>
<header>
<h1>My Awesome Blog</h1>
<p>Powered by Google Sheets</p>
</header>
<div id="posts" class="post-list"></div>
<script src="blog.js"></script>
</body>
</html>
const API_URL = 'https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data';
const API_KEY = 'YOUR_API_KEY';
async function loadPosts() {
try {
const response = await fetch(API_URL, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const data = await response.json();
let posts = data.data || [];
// Filter only published posts
posts = posts.filter(post => post.Status === 'Published');
// Sort by date (newest first)
posts.sort((a, b) => new Date(b['Published Date']) - new Date(a['Published Date']));
renderPosts(posts);
} catch (error) {
console.error('Failed to load posts:', error);
}
}
function renderPosts(posts) {
const postsDiv = document.getElementById('posts');
if (posts.length === 0) {
postsDiv.innerHTML = '<p>No posts yet. Check back soon!</p>';
return;
}
postsDiv.innerHTML = posts.map(post => {
const date = new Date(post['Published Date']).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
const excerpt = post.Content.substring(0, 200) + '...';
return `
<article class="post-preview">
<h2><a href="/blog/${post.Slug}">${post.Title}</a></h2>
<div class="post-meta">By ${post.Author} on ${date}</div>
<div class="post-excerpt">${excerpt}</div>
<a href="/blog/${post.Slug}" class="read-more">Read more →</a>
</article>
`;
}).join('');
}
loadPosts();
// pages/blog.js (Next.js with SSG)
export async function getStaticProps() {
const API_URL = 'https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data';
const API_KEY = process.env.SHEETAPI_KEY;
const response = await fetch(API_URL, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const data = await response.json();
let posts = data.data || [];
// Filter published posts and sort by date
posts = posts
.filter(post => post.Status === 'Published')
.sort((a, b) => new Date(b['Published Date']) - new Date(a['Published Date']));
return {
props: { posts },
revalidate: 60 // Revalidate every 60 seconds
};
}
export default function Blog({ posts }) {
return (
<div className="blog">
<h1>Blog</h1>
{posts.map(post => (
<article key={post.Slug}>
<h2><a href={`/blog/${post.Slug}`}>{post.Title}</a></h2>
<p className="meta">By {post.Author} on {new Date(post['Published Date']).toLocaleDateString()}</p>
<p>{post.Content.substring(0, 200)}...</p>
<a href={`/blog/${post.Slug}`}>Read more →</a>
</article>
))}
</div>
);
}
// pages/blog/[slug].js (Individual post page)
export async function getStaticPaths() {
const API_URL = 'https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data';
const API_KEY = process.env.SHEETAPI_KEY;
const response = await fetch(API_URL, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const data = await response.json();
const posts = data.data || [];
const paths = posts
.filter(post => post.Status === 'Published')
.map(post => ({ params: { slug: post.Slug } }));
return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }) {
const API_URL = 'https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data';
const API_KEY = process.env.SHEETAPI_KEY;
const response = await fetch(API_URL, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const data = await response.json();
const post = data.data.find(p => p.Slug === params.slug);
if (!post || post.Status !== 'Published') {
return { notFound: true };
}
return {
props: { post },
revalidate: 60
};
}
export default function Post({ post }) {
return (
<article>
<h1>{post.Title}</h1>
<p className="meta">By {post.Author} on {new Date(post['Published Date']).toLocaleDateString()}</p>
<div dangerouslySetInnerHTML={{ __html: post.Content }} />
</article>
);
}
<!-- Hugo data template -->
<!-- In your hugo.toml or config.yaml, add: -->
<!--
[params]
sheetapi_url = "https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data"
sheetapi_key = "YOUR_API_KEY"
-->
<!-- layouts/blog/list.html -->
{{ $url := .Site.Params.sheetapi_url }}
{{ $key := .Site.Params.sheetapi_key }}
{{ $headers := dict "Authorization" (printf "Bearer %s" $key) }}
{{ $posts := getJSON $url $headers }}
<div class="blog-list">
{{ range $posts.data }}
{{ if eq .Status "Published" }}
<article class="post">
<h2><a href="/blog/{{ .Slug }}">{{ .Title }}</a></h2>
<p class="meta">By {{ .Author }} on {{ .PublishedDate }}</p>
<p>{{ substr .Content 0 200 }}...</p>
<a href="/blog/{{ .Slug }}">Read more →</a>
</article>
{{ end }}
{{ end }}
</div>
import requests
from flask import Flask, render_template, abort
app = Flask(__name__)
SHEET_ID = 'YOUR_SHEET_ID'
API_KEY = 'YOUR_API_KEY'
API_URL = f'https://sheetapi.pro/api/v1/sheets/{SHEET_ID}/data'
def get_posts():
headers = {'Authorization': f'Bearer {API_KEY}'}
response = requests.get(API_URL, headers=headers)
if response.status_code == 200:
posts = response.json().get('data', [])
# Filter published posts
posts = [p for p in posts if p.get('Status') == 'Published']
# Sort by date
posts.sort(key=lambda p: p.get('Published Date', ''), reverse=True)
return posts
return []
@app.route('/blog')
def blog_index():
posts = get_posts()
return render_template('blog.html', posts=posts)
@app.route('/blog/<slug>')
def blog_post(slug):
posts = get_posts()
post = next((p for p in posts if p.get('Slug') == slug), None)
if not post:
abort(404)
return render_template('post.html', post=post)
if __name__ == '__main__':
app.run(debug=True)
<?php
// blog.php
$sheetId = 'YOUR_SHEET_ID';
$apiKey = 'YOUR_API_KEY';
$apiUrl = "https://sheetapi.pro/api/v1/sheets/{$sheetId}/data";
function getPosts() {
global $apiUrl, $apiKey;
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer {$apiKey}"
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);
$posts = $data['data'] ?? [];
// Filter published posts
$posts = array_filter($posts, function($p) {
return $p['Status'] === 'Published';
});
// Sort by date
usort($posts, function($a, $b) {
return strtotime($b['Published Date']) - strtotime($a['Published Date']);
});
return $posts;
}
$posts = getPosts();
?>
<!DOCTYPE html>
<html>
<head>
<title>Blog</title>
</head>
<body>
<h1>Blog Posts</h1>
<?php foreach ($posts as $post): ?>
<article>
<h2><a href="/blog/<?= htmlspecialchars($post['Slug']) ?>">
<?= htmlspecialchars($post['Title']) ?>
</a></h2>
<p>By <?= htmlspecialchars($post['Author']) ?> on
<?= date('F j, Y', strtotime($post['Published Date'])) ?></p>
<p><?= htmlspecialchars(substr($post['Content'], 0, 200)) ?>...</p>
</article>
<?php endforeach; ?>
</body>
</html>
# Get all posts
curl -X GET https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data \
-H "Authorization: Bearer YOUR_API_KEY"
# Create a new blog post
curl -X POST https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"Title": "Getting Started with SheetAPI",
"Slug": "getting-started-sheetapi",
"Content": "Full blog post content here...",
"Author": "John Doe",
"Published Date": "2024-01-15",
"Status": "Published"
}'
# Update post status to Draft
curl -X PATCH https://sheetapi.pro/api/v1/sheets/YOUR_SHEET_ID/data/3 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"Status": "Draft"}'