Build a Dynamic Website with Google Sheets as CMS in 2025
Build a Dynamic Website with Google Sheets as CMS in 2025
Tired of complex CMS platforms like WordPress, Contentful, or Strapi? What if your content team could manage your entire website using something they already know—Google Sheets?
In this comprehensive tutorial, you'll learn how to build a fully dynamic, production-ready website using Google Sheets as your headless CMS. No expensive subscriptions, no learning curve for your team, just a simple spreadsheet powering a modern website.
🎯 What You'll Build
A complete Next.js website with:
✅ Dynamic pages from Google Sheets content ✅ Blog system with posts, categories, authors ✅ Product catalog with categories and filtering ✅ Team member profiles with bios and photos ✅ Testimonials section ✅ Real-time updates when content changes ✅ SEO optimization with dynamic meta tags
Live Example: We'll build a portfolio/agency website that your content team can update without touching code.
🏗️ Architecture Overview
[Google Sheets] ← Content Team edits here
↓
[SheetToAPI] ← Provides REST API
↓
[Next.js App] ← Your website fetches data
↓
[Vercel/Netlify] ← Deployed site with ISR
Key Benefits:
- For Developers: Clean API, no CMS backend to maintain
- For Content Teams: Familiar spreadsheet interface
- For Users: Fast, modern website experience
📋 Step 1: Set Up Your Google Sheets CMS
Create Your Content Structure
Create a new Google Spreadsheet with multiple sheets (tabs):
Sheet 1: "Pages"
| id | title | slug | content | seo_title | seo_description | status |
|----|-------|------|---------|-----------|-----------------|--------|
| 1 | Home | / | Welcome to our site... | Best Agency 2025 | We build... | published |
| 2 | About | /about | Our story... | About Us | Learn more... | published |
Sheet 2: "Blog_Posts"
| id | title | slug | excerpt | content | author | category | featured_image | published_date | status |
|----|-------|------|---------|---------|--------|----------|----------------|----------------|--------|
| 1 | Getting Started | getting-started | Learn how... | Full content... | John Doe | Tutorial | /img/post1.jpg | 2025-01-15 | published |
Sheet 3: "Products"
| id | name | slug | description | price | category | image | in_stock |
|----|------|------|-------------|-------|----------|-------|----------|
| 1 | Pro Plan | pro-plan | Our best... | 49 | subscription | /img/pro.jpg | true |
Sheet 4: "Team"
| id | name | role | bio | photo | linkedin | twitter |
|----|------|------|-----|-------|----------|---------|
| 1 | Jane Smith | CEO | Jane has 10 years... | /img/jane.jpg | /in/jane | @jane |
Sheet 5: "Testimonials"
| id | author | company | role | quote | rating | photo |
|----|--------|---------|------|-------|--------|-------|
| 1 | John D | Acme Inc | CTO | Best service ever! | 5 | /img/john.jpg |
🔌 Step 2: Connect to SheetToAPI
- Go to SheetToAPI.com
- Click "Create New API"
- Paste your Google Sheet URL
- Copy your endpoint and API key
- Save in
.env.local:
SHEETTOAPI_ENDPOINT=https://api.sheettoapi.com/api/v1/data/your_endpoint
SHEETTOAPI_KEY=your_api_key_here
💻 Step 3: Create Next.js Project
npx create-next-app@latest my-website
cd my-website
npm install
Create API Helper
// lib/api.ts
const API_ENDPOINT = process.env.SHEETTOAPI_ENDPOINT!;
const API_KEY = process.env.SHEETTOAPI_KEY!;
interface FetchOptions {
sheet: string;
filter?: Record<string, string>;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
fields?: string[];
}
export async function fetchFromSheet<T>({
sheet,
filter,
sortBy,
sortOrder = 'desc',
fields
}: FetchOptions): Promise<T[]> {
const params = new URLSearchParams();
params.append('sheet', sheet);
if (filter) {
Object.entries(filter).forEach(([key, value]) => {
params.append(`filter[${key}]`, value);
});
}
if (sortBy) {
params.append('sortBy', sortBy);
params.append('sortOrder', sortOrder);
}
if (fields) {
params.append('fields', fields.join(','));
}
const response = await fetch(`${API_ENDPOINT}?${params}`, {
headers: { 'X-API-Key': API_KEY },
next: { revalidate: 60 } // ISR: Revalidate every 60 seconds
});
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const data = await response.json();
return data.rows || [];
}
export async function fetchSingleFromSheet<T>(
sheet: string,
slug: string
): Promise<T | null> {
const results = await fetchFromSheet<T>({
sheet,
filter: { slug, status: 'published' }
});
return results[0] || null;
}
🏠 Step 4: Build Dynamic Pages
Home Page
// app/page.tsx
import { fetchFromSheet } from '@/lib/api';
import Hero from '@/components/Hero';
import Testimonials from '@/components/Testimonials';
interface PageContent {
title: string;
content: string;
seo_title: string;
seo_description: string;
}
interface Testimonial {
author: string;
company: string;
quote: string;
rating: number;
photo: string;
}
export async function generateMetadata() {
const [page] = await fetchFromSheet<PageContent>({
sheet: 'Pages',
filter: { slug: '/' }
});
return {
title: page.seo_title,
description: page.seo_description
};
}
export default async function HomePage() {
const [page] = await fetchFromSheet<PageContent>({
sheet: 'Pages',
filter: { slug: '/', status: 'published' }
});
const testimonials = await fetchFromSheet<Testimonial>({
sheet: 'Testimonials',
sortBy: 'rating',
sortOrder: 'desc'
});
return (
<div>
<Hero title={page.title} content={page.content} />
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4">
<h2 className="text-3xl font-bold text-center mb-12">
What Our Clients Say
</h2>
<Testimonials data={testimonials} />
</div>
</section>
</div>
);
}
Blog System
// app/blog/page.tsx
import { fetchFromSheet } from '@/lib/api';
import BlogCard from '@/components/BlogCard';
interface BlogPost {
id: number;
title: string;
slug: string;
excerpt: string;
author: string;
category: string;
featured_image: string;
published_date: string;
}
export const metadata = {
title: 'Blog | My Website',
description: 'Read our latest articles and insights'
};
export default async function BlogPage() {
const posts = await fetchFromSheet<BlogPost>({
sheet: 'Blog_Posts',
filter: { status: 'published' },
sortBy: 'published_date',
sortOrder: 'desc'
});
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-4xl font-bold mb-12">Our Blog</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{posts.map(post => (
<BlogCard key={post.id} post={post} />
))}
</div>
</div>
);
}
// app/blog/[slug]/page.tsx
import { fetchFromSheet, fetchSingleFromSheet } from '@/lib/api';
import { notFound } from 'next/navigation';
interface BlogPost {
title: string;
slug: string;
content: string;
author: string;
category: string;
featured_image: string;
published_date: string;
}
export async function generateStaticParams() {
const posts = await fetchFromSheet<BlogPost>({
sheet: 'Blog_Posts',
filter: { status: 'published' },
fields: ['slug']
});
return posts.map(post => ({
slug: post.slug
}));
}
export async function generateMetadata({ params }: { params: { slug: string } }) {
const post = await fetchSingleFromSheet<BlogPost>('Blog_Posts', params.slug);
if (!post) return {};
return {
title: post.title,
description: post.content.substring(0, 160)
};
}
export default async function BlogPostPage({ params }: { params: { slug: string } }) {
const post = await fetchSingleFromSheet<BlogPost>('Blog_Posts', params.slug);
if (!post) {
notFound();
}
return (
<article className="container mx-auto px-4 py-16 max-w-3xl">
<img
src={post.featured_image}
alt={post.title}
className="w-full h-96 object-cover rounded-lg mb-8"
/>
<h1 className="text-5xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-gray-600 mb-8">
<span>By {post.author}</span>
<span>•</span>
<time>{new Date(post.published_date).toLocaleDateString()}</time>
<span>•</span>
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm">
{post.category}
</span>
</div>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: post.content }}
/>
</article>
);
}
Product Catalog
// app/products/page.tsx
'use client'
import { useState, useEffect } from 'react';
import ProductCard from '@/components/ProductCard';
interface Product {
id: number;
name: string;
slug: string;
description: string;
price: number;
category: string;
image: string;
in_stock: boolean;
}
export default function ProductsPage() {
const [products, setProducts] = useState<Product[]>([]);
const [category, setCategory] = useState<string>('all');
const [categories, setCategories] = useState<string[]>([]);
useEffect(() => {
fetchProducts();
}, [category]);
async function fetchProducts() {
const url = category === 'all'
? '/api/products'
: `/api/products?category=${category}`;
const res = await fetch(url);
const data = await res.json();
setProducts(data);
// Extract unique categories
const uniqueCategories = [...new Set(data.map((p: Product) => p.category))];
setCategories(['all', ...uniqueCategories]);
}
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-4xl font-bold mb-8">Our Products</h1>
{/* Category Filter */}
<div className="flex gap-4 mb-12">
{categories.map(cat => (
<button
key={cat}
onClick={() => setCategory(cat)}
className={`px-6 py-2 rounded-full ${
category === cat
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{cat.charAt(0).toUpperCase() + cat.slice(1)}
</button>
))}
</div>
{/* Products Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
// app/api/products/route.ts
import { fetchFromSheet } from '@/lib/api';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const products = await fetchFromSheet({
sheet: 'Products',
filter: {
status: 'published',
...(category && { category })
},
sortBy: 'price'
});
return Response.json(products);
}
Team Page
// app/team/page.tsx
import { fetchFromSheet } from '@/lib/api';
import TeamMemberCard from '@/components/TeamMemberCard';
interface TeamMember {
id: number;
name: string;
role: string;
bio: string;
photo: string;
linkedin: string;
twitter: string;
}
export const metadata = {
title: 'Our Team | My Website',
description: 'Meet the people behind our success'
};
export default async function TeamPage() {
const team = await fetchFromSheet<TeamMember>({
sheet: 'Team',
sortBy: 'id'
});
return (
<div className="container mx-auto px-4 py-16">
<h1 className="text-4xl font-bold text-center mb-4">Meet Our Team</h1>
<p className="text-center text-gray-600 mb-12 max-w-2xl mx-auto">
Our talented team of experts is dedicated to delivering exceptional results.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-8">
{team.map(member => (
<TeamMemberCard key={member.id} member={member} />
))}
</div>
</div>
);
}
🎨 Step 5: Create Reusable Components
Product Card Component
// components/ProductCard.tsx
interface Product {
name: string;
slug: string;
description: string;
price: number;
image: string;
in_stock: boolean;
}
export default function ProductCard({ product }: { product: Product }) {
return (
<div className="border rounded-lg overflow-hidden hover:shadow-xl transition-shadow">
<img
src={product.image}
alt={product.name}
className="w-full h-64 object-cover"
/>
<div className="p-6">
<h3 className="text-xl font-bold mb-2">{product.name}</h3>
<p className="text-gray-600 mb-4">{product.description}</p>
<div className="flex justify-between items-center">
<span className="text-2xl font-bold text-blue-600">
${product.price}
</span>
{product.in_stock ? (
<button className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Add to Cart
</button>
) : (
<span className="text-red-600 font-semibold">Out of Stock</span>
)}
</div>
</div>
</div>
);
}
Testimonials Component
// components/Testimonials.tsx
interface Testimonial {
author: string;
company: string;
quote: string;
rating: number;
photo: string;
}
export default function Testimonials({ data }: { data: Testimonial[] }) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{data.map((testimonial, index) => (
<div key={index} className="bg-white p-6 rounded-lg shadow-lg">
<div className="flex items-center gap-4 mb-4">
<img
src={testimonial.photo}
alt={testimonial.author}
className="w-16 h-16 rounded-full object-cover"
/>
<div>
<h4 className="font-bold">{testimonial.author}</h4>
<p className="text-sm text-gray-600">{testimonial.company}</p>
</div>
</div>
<div className="flex mb-3">
{[...Array(testimonial.rating)].map((_, i) => (
<span key={i} className="text-yellow-400">★</span>
))}
</div>
<p className="text-gray-700 italic">"{testimonial.quote}"</p>
</div>
))}
</div>
);
}
⚡ Step 6: Optimize Performance
Incremental Static Regeneration (ISR)
Already implemented with next: { revalidate: 60 } in our API helper. This means:
- ✅ Pages are statically generated
- ✅ Cached for 60 seconds
- ✅ Rebuilt in background when cache expires
- ✅ Users always get fast response times
Image Optimization
import Image from 'next/image';
<Image
src={product.image}
alt={product.name}
width={400}
height={300}
className="w-full h-64 object-cover"
/>
🔄 Content Management Workflow
For Content Team:
-
Add new blog post:
- Open Google Sheet
- Go to "Blog_Posts" tab
- Add new row with all details
- Set status to "published"
- Done! Post appears on website within 60 seconds
-
Update product pricing:
- Go to "Products" tab
- Change price in spreadsheet
- Save
- Website updates automatically
-
Add team member:
- Go to "Team" tab
- Add new row
- Website updates within 60 seconds
No deployment, no code changes, no technical knowledge required!
🎯 Advantages Over Traditional CMS
| Feature | Google Sheets CMS | WordPress | Contentful |
|---|---|---|---|
| Setup Time | 30 min | 2-4 hours | 1-2 hours |
| Monthly Cost | $0-$9 | $25-$100 | $0-$300 |
| Learning Curve | None (everyone knows Excel) | Moderate | Steep |
| Team Collaboration | Excellent (familiar tool) | Good | Good |
| Developer Freedom | Full control | Limited by PHP | Full control |
| Performance | Excellent (static) | Slow (dynamic) | Excellent |
| Maintenance | Zero | High | Low |
🔐 Security Best Practices
// ✅ Do: Use server components or API routes
export async function GET() {
const data = await fetchFromSheet({
sheet: 'Products',
filter: { status: 'published' }
});
return Response.json(data);
}
// ❌ Don't: Expose API keys in client components
'use client'
const data = await fetch(endpoint, {
headers: { 'X-API-Key': 'exposed_key' } // NEVER DO THIS
});
🎯 Conclusion
You now have a production-ready, dynamic website powered by Google Sheets as a CMS. This approach gives you:
✅ $0-$9/month total cost (vs $25-$300 for traditional CMS) ✅ Zero maintenance - no CMS backend to update or secure ✅ Instant updates - content changes appear within 60 seconds ✅ Team-friendly - anyone can edit a spreadsheet ✅ Developer-friendly - full control over frontend ✅ Fast performance - static generation with ISR ✅ SEO-optimized - proper meta tags and structure
Pikebyte.dev
Expert contributor sharing insights and best practices for building with SheetToAPI.
Ready to build?
Start using SheetToAPI today and turn your spreadsheets into powerful APIs.
Explore Documentation
