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

  1. Create a new migration file (e.g., 005_custom.sql)
  2. 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);
  1. Run the migration in Supabase SQL Editor
  2. 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):

  1. Remove the route: app/(admin)/admin/newsletter/
  2. Remove the Server Actions: entries in lib/actions/newsletter.ts
  3. Remove the sidebar link in components/layout/admin-sidebar.tsx
  4. Remove the public signup form component from the blog layout
  5. Optionally remove the newsletter_subscribers table

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:

  1. Add your provider's SDK (e.g., npm install resend)
  2. Create an integration file in lib/integrations/email.ts
  3. Update the newsletter signup Server Action to call your provider's API alongside the database insert
  4. Use the admin newsletter page to compose and send campaigns through your provider