Firestore model (recommended)
Collection: articles
Document id: index (string or numeric converted to string)
Document fields:
{
"index": 1, // numeric index (also used as doc id: "1")
"title": "How to Build with Next.js",
"slug": "how-to-build-with-nextjs",
"author": "Rohit Kumar",
"body": "<p>Article markdown or HTML content...</p>",
"excerpt": "Short summary for listing",
"tags": ["nextjs", "react", "firebase"],
"coverImage": "https://.../cover.jpg",
"status": "published", // draft / published
"createdAt": 1696020000000, // timestamp (ms) or Firestore Timestamp
"updatedAt": 1696020000000
}
1) Firebase client init (firebaseClient.js)
Create this at src/lib/firebaseClient.js and replace config values with your Firebase project’s config.
// src/lib/firebaseClient.js
import { initializeApp, getApps } from "firebase/app";
import { getFirestore, serverTimestamp } from "firebase/firestore";
import { getAuth } from "firebase/auth";
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_PROJECT.firebaseapp.com",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT.appspot.com",
messagingSenderId: "SENDER_ID",
appId: "APP_ID"
};
let app;
if (!getApps().length) {
app = initializeApp(firebaseConfig);
} else {
app = getApps()[0];
}
export const db = getFirestore(app);
export const auth = getAuth(app);
export const now = () => serverTimestamp();
2) React component: ArticleForm — create / post an article (index as id)
This is a simple form that writes to articles/{index} using setDoc. It validates index uniqueness (optionally).
// src/components/ArticleForm.jsx
import React, { useState } from "react";
import { doc, setDoc, getDoc, serverTimestamp } from "firebase/firestore";
import { db } from "@/lib/firebaseClient";
export default function ArticleForm() {
const [index, setIndex] = useState("");
const [title, setTitle] = useState("");
const [slug, setSlug] = useState("");
const [excerpt, setExcerpt] = useState("");
const [body, setBody] = useState("");
const [author, setAuthor] = useState("");
const [status, setStatus] = useState("draft");
const [tags, setTags] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
// Helper to convert tags string to array
const tagsToArray = (s) => s.split(",").map(t => t.trim()).filter(Boolean);
async function handleSubmit(e) {
e.preventDefault();
if (!index || !title) {
setMessage("Index and title are required.");
return;
}
setLoading(true);
setMessage("");
try {
const docId = String(index);
const ref = doc(db, "articles", docId);
// OPTIONAL: check if article with same index already exists
const exists = await getDoc(ref);
if (exists.exists()) {
// choose to overwrite or prevent - here we prevent
setLoading(false);
setMessage(`Article with index ${docId} already exists. Choose a different index or delete existing.`);
return;
}
const payload = {
index: Number(index),
title,
slug: slug || title.toLowerCase().replace(/s+/g, "-"),
excerpt,
body,
author,
status,
tags: tagsToArray(tags),
coverImage: "", // add a URL if available
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
};
await setDoc(ref, payload);
setMessage(`Article ${docId} saved successfully.`);
// optionally clear form
setIndex("");
setTitle("");
setSlug("");
setExcerpt("");
setBody("");
setAuthor("");
setTags("");
} catch (err) {
console.error(err);
setMessage("Error saving article: " + (err.message || err));
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4 max-w-2xl">
<div>
<label>Index (number)</label>
<input value={index} onChange={e=>setIndex(e.target.value)} type="number" className="w-full" />
</div>
<div>
<label>Title</label>
<input value={title} onChange={e=>setTitle(e.target.value)} className="w-full" />
</div>
<div>
<label>Slug (optional)</label>
<input value={slug} onChange={e=>setSlug(e.target.value)} className="w-full" />
</div>
<div>
<label>Excerpt</label>
<textarea value={excerpt} onChange={e=>setExcerpt(e.target.value)} className="w-full" />
</div>
<div>
<label>Body (HTML or Markdown)</label>
<textarea value={body} onChange={e=>setBody(e.target.value)} className="w-full h-40" />
</div>
<div>
<label>Author</label>
<input value={author} onChange={e=>setAuthor(e.target.value)} className="w-full" />
</div>
<div>
<label>Tags (comma separated)</label>
<input value={tags} onChange={e=>setTags(e.target.value)} className="w-full" />
</div>
<div>
<label>Status</label>
<select value={status} onChange={e=>setStatus(e.target.value)} className="w-full">
<option value="draft">Draft</option>
<option value="published">Published</option>
</select>
</div>
<div>
<button type="submit" disabled={loading} className="px-4 py-2 bg-blue-600 text-white">
{loading ? "Saving..." : "Save Article"}
</button>
</div>
{message && <p className="mt-2">{message}</p>}
</form>
);
}
3) Sample article (JSON)
You can use this to quickly seed Firestore manually or via the form:
{
"index": 1,
"title": "Getting Started with Next.js + Firebase",
"slug": "getting-started-nextjs-firebase",
"author": "Rohit Kumar",
"body": "<h2>Intro</h2><p>Next.js + Firebase is a great combo...</p>",
"excerpt": "Quick guide to combine Next.js with Firebase.",
"tags": ["nextjs","firebase","react"],
"coverImage": "https://example.com/cover.jpg",
"status": "published",
"createdAt": 1696030000000,
"updatedAt": 1696030000000
}
4) Read / fetch an article by index
Example helper to fetch an article from Firestore by index (document id = index string).
// src/lib/articles.js
import { doc, getDoc } from "firebase/firestore";
import { db } from "./firebaseClient";
export async function fetchArticleByIndex(index) {
const docId = String(index);
const ref = doc(db, "articles", docId);
const snap = await getDoc(ref);
if (!snap.exists()) return null;
// Convert Firestore timestamp to JS date if needed
const data = snap.data();
return { id: snap.id, ...data };
}
Usage in a React component:
import React, { useEffect, useState } from "react";
import { fetchArticleByIndex } from "@/lib/articles";
export default function ArticlePage({ index }) {
const [article, setArticle] = useState(null);
useEffect(() => {
fetchArticleByIndex(index).then(setArticle);
}, [index]);
if (!article) return <div>Loading...</div>;
return (
<article>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
<p>Author: {article.author}</p>
</article>
);
}
5) Example Firestore security rules (basic)
Put this in your Firebase console → Firestore → Rules. Adjust for your auth model. This example allows authenticated users to create/update their own articles; you can tighten it.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /articles/{articleId} {
allow read: if true; // public read
allow create, update, delete: if request.auth != null
// Optional: restrict that index field must match doc id
&& request.resource.data.index == int(articleId)
// maybe require certain fields present
&& request.resource.data.title is string
&& request.resource.data.body is string;
}
}
}
Note:
int(articleId)will fail if docId is non-numeric — adjust to string comparison if you use string slugs.
6) Notes & best practices
- Doc id as index: using
indexas document id makes URLs predictable (articles/1). If you expect renumbering, consider using a stable slug or UUID and keepindexas a separate numeric field. - Timestamps: prefer Firestore
serverTimestamp()forcreatedAt/updatedAtto avoid client clock issues. - HTML vs Markdown: store Markdown in
bodyand render on the client with a Markdown renderer (safer). If storing HTML, sanitize before rendering. - Images: for cover images, use Firebase Storage and store the public URL in
coverImage. - Validation: validate index uniqueness if you rely on numeric indices.
- Admin writes: if you need restricted writes (only admins can create), use a server-side endpoint or Cloud Function with the Admin SDK.
7) If you want a Next.js API route (server-side write using Admin SDK)
I can provide that too — it’s safer for admin-only article creation. For now I kept the example client-side for quick posting.
If you want, I can:
- Convert the form into a full Next.js page with Tailwind UI.
- Add image upload to Firebase Storage and automatically save
coverImageURL. - Provide a server-side (Admin SDK) API route for admin-only article posting.
- Give a migration script to bulk import sample articles.
Tell me which of the above you’d like next and I’ll give the code.