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
HTML
JavaScript
Next.js
Hugo
Python
PHP
cURL
<!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"}'