Customization
Last updated on 2026-09-03
This guide covers common customizations from rebranding to extending the database schema.
Changing Brand Colors
Update the oklch tokens in app/globals.css:
:root {
/* Change the primary hue from 250 (blue) to your brand hue */
--primary: oklch(0.55 0.15 YOUR_HUE);
--primary-foreground: oklch(0.98 0.005 YOUR_HUE);
}
.dark {
--primary: oklch(0.65 0.15 YOUR_HUE);
}
Common hue values: 0 (red), 30 (orange), 60 (yellow), 120 (green), 180 (teal), 210 (blue), 250 (indigo), 270 (purple), 330 (pink).
Update secondary, accent, chart, and sidebar tokens to maintain visual harmony. Because the token system shares a single hue angle, changing it in all declarations updates the entire theme at once.
Changing Fonts
In app/layout.tsx, swap the Google Fonts imports:
// Replace these with your preferred fonts
import { Inter, DM_Sans, JetBrains_Mono, Lora } from "next/font/google"
Update the CSS variables in globals.css to match:
--font-heading: "Your Heading Font", sans-serif;
--font-body: "Your Body Font", sans-serif;
--font-mono: "Your Mono Font", monospace;
--font-serif: "Your Serif Font", serif;
The --font-serif variable controls article body typography. If you prefer sans-serif article text, set it to your body font instead.
Replacing Seed Data
Option 1: Admin Panel
Use the built-in admin CMS to create posts, categories, tags, and upload media. The admin interface provides full WYSIWYG editing with validation.
Option 2: Direct Database Insert
Insert data directly into your Supabase tables:
INSERT INTO categories (name, slug, description)
VALUES ('Engineering', 'engineering', 'Technical posts about software engineering');
INSERT INTO posts (title, slug, content, excerpt, status, category_id, author_id)
VALUES ('My First Post', 'my-first-post', '...', 'A brief excerpt', 'published', 'category-uuid', 'author-uuid');
Extending the Database Schema
Adding a New Table
- Create a new migration file (e.g.,
005_custom.sql) - Define the table with RLS:
CREATE TABLE bookmarks (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users(id) NOT NULL,
post_id uuid REFERENCES posts(id) NOT NULL,
created_at timestamptz DEFAULT now(),
UNIQUE(user_id, post_id)
);
ALTER TABLE bookmarks ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read own bookmarks" ON bookmarks
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own bookmarks" ON bookmarks
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own bookmarks" ON bookmarks
FOR DELETE USING (auth.uid() = user_id);
- Run the migration in Supabase SQL Editor
- Regenerate types:
npx supabase gen types typescript --project-id xxx > types/database.ts
Adding Columns to Existing Tables
ALTER TABLE posts ADD COLUMN reading_time integer;
ALTER TABLE posts ADD COLUMN featured boolean DEFAULT false;
Update the TypeScript types and any queries or forms that use these columns.
Adding New Pages
In the (blog) Route Group
Add public-facing pages accessible to all visitors:
// app/(blog)/about/page.tsx
export default function AboutPage() {
return (
<div className="max-w-prose mx-auto space-y-6">
<h1 className="text-3xl font-bold font-heading">About</h1>
{/* Your content */}
</div>
)
}
In the (admin) Route Group
Add admin CMS pages protected by the auth middleware:
// app/(admin)/admin/analytics/page.tsx
import { createServerClient } from "@/lib/supabase/server"
export default async function AnalyticsPage() {
const supabase = await createServerClient()
const { data } = await supabase.from("posts").select("views, created_at")
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold font-heading">Analytics</h1>
{/* Your charts and metrics */}
</div>
)
}
Add the route to the admin sidebar in components/layout/admin-sidebar.tsx.
Customizing the Blog Design
Article Layout Width
The article content area uses max-w-prose (65ch) by default. To widen it:
// Change in the article layout component
<article className="max-w-3xl mx-auto"> {/* was max-w-prose */}
Typography Tokens
Article typography is controlled by the --font-serif variable and prose utility classes. Adjust line height, font size, and spacing in the article component or via Tailwind's typography plugin configuration.
Adding Integrations
Email Service for Newsletters
Connect the newsletter signup form to a real email provider:
// lib/integrations/email.ts
import { Resend } from "resend"
const resend = new Resend(process.env.RESEND_API_KEY)
export async function addSubscriber(email: string) {
await resend.contacts.create({
email,
audienceId: process.env.RESEND_AUDIENCE_ID!,
})
}
Supported providers: Resend, SendGrid, Mailchimp, ConvertKit, Buttondown.
Analytics
Add privacy-friendly analytics alongside Vercel Analytics:
- Plausible -- add the script tag in
app/layout.tsx - PostHog -- install the SDK and wrap the app in the PostHog provider
- Fathom -- add the script tag in
app/layout.tsx
Headless CMS
Use the Supabase backend for storage while sourcing content from a headless CMS:
- Fetch content from your CMS API in Server Components
- Store metadata (views, comments, newsletter signups) in Supabase
- Use webhooks to sync published content
Removing Features
To remove a feature (e.g., newsletter):
- Remove the route:
app/(admin)/admin/newsletter/ - Remove the Server Actions: entries in
lib/actions/newsletter.ts - Remove the sidebar link in
components/layout/admin-sidebar.tsx - Remove the public signup form component from the blog layout
- Optionally remove the
newsletter_subscriberstable
The kit is modular -- removing one feature does not break others. Posts, comments, categories, tags, media, and newsletter each operate independently and can be removed without affecting the rest.
Customizing Comment Moderation
By default, comments are held for moderation. To change this:
Auto-Approve All Comments
Update the comment creation Server Action to set status to approved by default:
// In lib/actions/comments.ts
const { data } = await supabase
.from("comments")
.insert({ ...commentData, status: "approved" })
Add Spam Filtering
Integrate a spam detection service before inserting comments:
// lib/integrations/spam.ts
import Akismet from "akismet-api"
const client = new Akismet.Client({ key: process.env.AKISMET_KEY!, blog: process.env.NEXT_PUBLIC_APP_URL! })
export async function isSpam(comment: { name: string; email: string; content: string }) {
return client.checkSpam({ ...comment, type: "comment" })
}
Customizing Newsletter
The built-in newsletter stores subscribers in the newsletter_subscribers table. To connect to a real email provider:
- Add your provider's SDK (e.g.,
npm install resend) - Create an integration file in
lib/integrations/email.ts - Update the newsletter signup Server Action to call your provider's API alongside the database insert
- Use the admin newsletter page to compose and send campaigns through your provider