Turn a Google Drive folder into a static JSON index — a framework-agnostic headless CMS with zero runtime dependencies.
You organize your content in a Google Drive folder. drive-cms scans the folder and writes a JSON file you can serve statically alongside your app. Each Google Doc (or HTML/PDF file) becomes one entry in the array.
Folder convention:
my-drive-folder/
├── My First Article ← Google Doc → becomes an entry
├── My First Article.jpg ← Same name → used as previewImage
└── Another Project ← No image → previewImage is ""
npm install -D drive-cmsOr use without installing:
npx drive-cms init
npx drive-cms sync1. Scaffold a config file:
npx drive-cms initThis creates drive-cms.config.js in your project root.
2. Edit the config:
// drive-cms.config.js
import 'dotenv/config';
export default {
apiKey: process.env.DRIVE_API_KEY,
output: './public/data',
collections: [
{
name: 'projects-en',
folderId: process.env.DRIVE_FOLDER_ID_EN,
},
{
name: 'projects-es',
folderId: process.env.DRIVE_FOLDER_ID_ES,
},
],
};3. Make your Drive folder public:
Right-click the folder in Google Drive → Share → Anyone with the link → Viewer.
4. Run:
npx drive-cms syncThis writes public/data/projects-en.json and public/data/projects-es.json.
- Go to console.cloud.google.com
- Enable the Google Drive API
- Create an API Key under Credentials
- (Optional) Restrict the key to the Google Drive API
Tip: Create a separate unrestricted key for the CLI and a referrer-restricted key for your browser app.
Each entry in the JSON array has this shape by default:
{
id: string; // Google Drive file ID
mimeType: string; // e.g. "application/vnd.google-apps.document"
title: string; // File name (extension stripped)
description: string; // Drive file description field
previewImage: string; // Drive ID of the paired image file (or "")
}Set the description in Google Drive via right-click → File information → Details.
Supply a transform function to control exactly what goes into the JSON:
export default {
apiKey: process.env.DRIVE_API_KEY,
output: './public/data',
collections: [{ name: 'posts', folderId: process.env.FOLDER_ID }],
transform: (doc, image) => ({
slug: doc.name.toLowerCase().replace(/\s+/g, '-').replace(/\.[^.]+$/, ''),
title: doc.name.replace(/\.[^.]+$/, ''),
summary: doc.description ?? '',
cover: image ? `https://lh3.googleusercontent.com/d/${image.id}=w800` : null,
driveId: doc.id,
}),
};The transform function receives:
doc— the Google Drive file object ({ id, name, mimeType, description, thumbnailLink })image— the paired image file, ornullif none was found
drive-cms sync # sync all collections
drive-cms sync --collection posts # sync a single collection by name
drive-cms init # scaffold drive-cms.config.jsimport { loadConfig, syncAll } from 'drive-cms';
const config = await loadConfig();
const results = await syncAll(config);
for (const result of results) {
console.log(`${result.collection}: ${result.entryCount} entries → ${result.outputPath}`);
}| Field | Type | Required | Default |
|---|---|---|---|
apiKey |
string |
Yes | — |
output |
string |
No | "./public/data" |
collections |
Collection[] |
Yes | — |
transform |
(doc, image) => T |
No | Default shape above |
Collection:
| Field | Type | Required |
|---|---|---|
name |
string |
Yes — used as output filename |
folderId |
string |
Yes — Google Drive folder ID |
MIT