Theme Development Guide
This guide explains how to create, customize, and manage themes programmatically in Multi-Domain AutoBlogger. Learn how to build theme systems, create themes via code, and extend the theming capabilities.
How Themes Work
The theme system operates in three layers:
Layer 1: Database Storage
Themes are stored in PostgreSQL with all customization values as database columns:
- Core Colors - Primary, Secondary, Accent, Body Text, Background
- Enhanced Colors - Link Hover, Border, Code Block Background/Text, Shadow Opacity
- Typography - Body Font, Heading Font
- Layout Config - Header/Footer Layouts, Menu Style, Sidebar Position, Content Width
- Visuals - Background Patterns
Layer 2: CSS Generation
Theme data is transformed into CSS variables at request time using the generateThemeCSS()
function. This creates a stylesheet with variables like --color-primary,
--color-link-hover, and --font-heading.
Layer 3: Application
The generated CSS is injected into the domain layout and applied to all child components via inline
<style> tags.
Theme Data Structure
TypeScript Interface
All themes conform to this interface:
interface Theme {
id: number;
name: string;
// Colors
primaryColor: string;
secondaryColor: string;
accentColor: string;
bodyTextColor: string;
backgroundColor: string;
linkHoverColor?: string;
borderColor?: string;
codeBgColor?: string;
codeTextColor?: string;
shadowOpacity?: number;
// Typography
fontFamily: string;
headingFont: string;
// Layout & Visuals
headerLayout?: string;
footerLayout?: string;
menuStyle?: string;
sidebarPosition?: string;
contentWidth?: string;
backgroundPattern?: string;
createdAt: Date;
updatedAt: Date;
}
Database Schema
The themes table is defined in lib/db/schema.ts:
export const themes = pgTable("themes", {
id: serial("id").primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
// Colors
primaryColor: varchar("primary_color", { length: 7 }).notNull(),
secondaryColor: varchar("secondary_color", { length: 7 }).notNull(),
accentColor: varchar("accent_color", { length: 7 }).notNull(),
bodyTextColor: varchar("body_text_color", { length: 7 }).notNull(),
backgroundColor: varchar("background_color", { length: 7 }).notNull(),
linkHoverColor: varchar("link_hover_color", { length: 7 }),
borderColor: varchar("border_color", { length: 7 }),
codeBgColor: varchar("code_bg_color", { length: 7 }),
codeTextColor: varchar("code_text_color", { length: 7 }),
// Layout
headerLayout: varchar("header_layout", { length: 50 }),
footerLayout: varchar("footer_layout", { length: 50 }),
// ... other layout fields
});
Example Theme Object
{
"id": 1,
"name": "Dark Professional",
"primaryColor": "#1f2937",
"secondaryColor": "#6b7280",
"accentColor": "#3b82f6",
"linkHoverColor": "#2563eb",
"borderColor": "#374151",
"fontFamily": "Inter, sans-serif",
"headingFont": "Roboto, sans-serif",
"headerLayout": "centered",
"createdAt": "2024-01-15T10:30:00Z"
}
Creating Themes Programmatically
Method 1: Via REST API (Recommended)
Create themes using the REST API endpoint:
const response = await fetch('/api/themes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Modern Startup',
primaryColor: '#0c3c26',
secondaryColor: '#475569',
accentColor: '#2563eb',
bodyTextColor: '#1f2937',
backgroundColor: '#ffffff',
fontFamily: 'Inter, sans-serif',
headingFont: 'Poppins, sans-serif',
}),
});
const theme = await response.json();
console.log(`Theme created with ID: ${theme.id}`);
Endpoint: POST /api/themes
Authentication: Required (admin)
Returns: Theme object with generated ID
Method 2: Via Drizzle ORM (Backend)
Create themes directly from backend code or scripts:
import { db } from '@/lib/db/client';
import { themes } from '@/lib/db/schema';
async function createTheme() {
const newTheme = await db.insert(themes).values({
name: 'Modern Startup',
primaryColor: '#0c3c26',
secondaryColor: '#475569',
accentColor: '#2563eb',
bodyTextColor: '#1f2937',
backgroundColor: '#ffffff',
fontFamily: 'Inter, sans-serif',
headingFont: 'Poppins, sans-serif',
}).returning();
return newTheme[0];
}
Method 3: Bulk Seeding
Create multiple theme presets in a seed script:
// /scripts/seed-themes.ts
import { db } from '@/lib/db/client';
import { themes } from '@/lib/db/schema';
const themePresets = [
{
name: 'Modern Startup',
primaryColor: '#0c3c26',
secondaryColor: '#475569',
accentColor: '#2563eb',
bodyTextColor: '#1f2937',
backgroundColor: '#ffffff',
fontFamily: 'Inter, sans-serif',
headingFont: 'Poppins, sans-serif',
},
{
name: 'Minimalist Tech',
primaryColor: '#1f2937',
secondaryColor: '#6b7280',
accentColor: '#0891b2',
bodyTextColor: '#374151',
backgroundColor: '#ffffff',
fontFamily: 'Open Sans, sans-serif',
headingFont: 'Inter, sans-serif',
},
];
async function seedThemes() {
for (const themeData of themePresets) {
const existing = await db.query.themes.findFirst({
where: (themes, { eq }) => eq(themes.name, themeData.name),
});
if (!existing) {
await db.insert(themes).values(themeData);
console.log(`✅ Created: ${themeData.name}`);
}
}
}
seedThemes().catch(console.error);
Run with: npx tsx scripts/seed-themes.ts
Understanding CSS Generation
The generateThemeCSS() Function
Located in lib/themes/utils.ts, this function converts a theme object into CSS:
export function generateThemeCSS(theme: Theme): string {
return `
:root {
--color-primary: ${theme.primaryColor};
--color-secondary: ${theme.secondaryColor};
--color-accent: ${theme.accentColor};
--font-family: ${theme.fontFamily};
--font-heading: ${theme.headingFont};
--color-body-text: ${theme.bodyTextColor};
--color-background: ${theme.backgroundColor};
}
body {
font-family: var(--font-family);
color: var(--color-body-text);
background-color: var(--color-background);
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
}
`;
}
Generated CSS Output Example
:root {
--color-primary: #0c3c26;
--color-link-hover: #0a2e1d;
--color-border: #e2e8f0;
--color-code-bg: #f8fafc;
--font-family: Inter, sans-serif;
--font-heading: Poppins, sans-serif;
--pattern-background: url('data:image/svg+xml...');
}
body {
font-family: var(--font-family);
color: var(--color-body-text);
background-image: var(--pattern-background);
}
a:hover {
color: var(--color-link-hover);
}
code {
background-color: var(--color-code-bg);
}
Using CSS Variables in Components
Once CSS variables are available, use them in your components:
// In JSX
<h1 style={{ color: 'var(--color-primary)' }}>
Site Title
</h1>
<p style={{ color: 'var(--color-body-text)' }}>
Article content
</p>
<a style={{ color: 'var(--color-accent)' }}>
Link
</a>
// In CSS
.header {
color: var(--color-primary);
background-color: var(--color-background);
}
a {
color: var(--color-accent);
}
Working with Themes in Code
Getting a Theme by ID
import { db } from '@/lib/db/client';
const theme = await db.query.themes.findFirst({
where: (themes, { eq }) => eq(themes.id, themeId),
});
if (theme) {
console.log(`Theme: ${theme.name}`);
}
Getting All Themes
const allThemes = await db.query.themes.findMany();
Getting a Domain's Theme
const domainWithTheme = await db.query.domains.findFirst({
where: (domains, { eq }) => eq(domains.id, domainId),
with: {
theme: true,
},
});
if (domainWithTheme?.theme) {
const css = generateThemeCSS(domainWithTheme.theme);
}
Updating a Theme
import { eq } from 'drizzle-orm';
await db.update(themes)
.set({
primaryColor: '#0c3c26',
updatedAt: new Date(),
})
.where(eq(themes.id, themeId));
Deleting a Theme
await db.delete(themes)
.where(eq(themes.id, themeId));
Assigning Themes to Domains
Via API
await fetch(`/api/domains/${domainId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
themeId: 1,
}),
});
Via Drizzle ORM
import { eq } from 'drizzle-orm';
await db.update(domains)
.set({
themeId: 1,
updatedAt: new Date(),
})
.where(eq(domains.id, domainId));
Extending the Theme System
Adding a New Color Property
To add a new customizable color (example: error color):
1. Update Database Schema
// lib/db/schema.ts
export const themes = pgTable("themes", {
// ... existing fields
errorColor: varchar("error_color", { length: 7 })
.notNull()
.default("#dc2626"),
});
2. Update TypeScript Interface
// lib/themes/utils.ts
export interface Theme {
// ... existing fields
errorColor: string;
}
3. Update CSS Generation
export function generateThemeCSS(theme: Theme): string {
return `
:root {
// ... existing variables
--color-error: ${theme.errorColor};
}
// ... existing rules
`;
}
4. Update API Endpoints
// /app/api/themes/route.ts
const newTheme = await sql`
INSERT INTO themes (..., error_color)
VALUES (..., $9)
RETURNING *
`;
5. Update Admin Form (optional)
// /components/admin/ThemeForm.tsx
<div className="space-y-2">
<label htmlFor="errorColor">Error Color</label>
<input
type="color"
id="errorColor"
{...form.register('errorColor')}
className="w-12 h-10"
/>
</div>
6. Use in Components
<div style={{ color: 'var(--color-error)' }}>
Error message
</div>
Theme Provider & Context
Using the ThemeProvider
The theme system includes a React Context for accessing theme data in client components:
// lib/themes/provider.tsx
import { createContext, useContext } from 'react';
const ThemeContext = createContext<{ theme: Theme | null }>({ theme: null });
export function ThemeProvider({ children, theme }) {
return (
<ThemeContext.Provider value={{ theme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
return useContext(ThemeContext);
}
Accessing Theme in Client Components
'use client';
import { useTheme } from '@/lib/themes/provider';
export function MyComponent() {
const { theme } = useTheme();
return (
<div>
<p>Current theme: {theme?.name}</p>
<p>Primary color: {theme?.primaryColor}</p>
</div>
);
}
Best Practices
Color Management
- Always use hex format (#RRGGBB) for colors
- Validate contrast ratios for accessibility (WCAG AA)
- Test colors on light and dark backgrounds
- Use semantic names (primary, accent) not literal ones (blue, red)
Font Management
- Use complete font stacks ('Font Name', fallback, generic)
- Load fonts at layout level (not per-component)
- Limit to 2-3 fonts max for performance
- Use system fonts as fallbacks
CSS Generation
- Generate CSS server-side (not in the browser)
- Cache generation results when possible
- Inject via <style> tag (not multiple <link> tags)
- Use CSS variables for all themeable properties
Database Operations
- Always update the updatedAt timestamp when modifying themes
- Validate input before storing (regex for hex colors)
- Use database defaults for new properties when extending schema
Complete Example: Theme Creation Workflow
Scenario: Create Theme and Assign to Domain
// Step 1: Create a theme
async function setupTheme() {
// Create theme
const themeResponse = await fetch('/api/themes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Brand 2024',
primaryColor: '#1e3a8a',
secondaryColor: '#0284c7',
accentColor: '#dc2626',
fontFamily: 'Inter, sans-serif',
headingFont: 'Poppins, sans-serif',
bodyTextColor: '#111827',
backgroundColor: '#ffffff',
}),
});
const theme = await themeResponse.json();
console.log(`✅ Theme created: ${theme.id}`);
// Step 2: Assign to domain
const domainResponse = await fetch(`/api/domains/${domainId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ themeId: theme.id }),
});
const updatedDomain = await domainResponse.json();
console.log(`✅ Theme assigned to domain: ${updatedDomain.domain_name}`);
return { theme, domain: updatedDomain };
}
Testing Themes
Manual Testing Checklist
- ✓ Theme creates without errors
- ✓ Theme is retrievable by ID
- ✓ Theme assigns to domain successfully
- ✓ CSS generates without syntax errors
- ✓ Colors appear correctly on pages
- ✓ Fonts load and display properly
- ✓ Theme works on multiple domains simultaneously
- ✓ Updating theme applies immediately
- ✓ Contrast ratios meet accessibility standards
Automated Testing Example
import { generateThemeCSS } from '@/lib/themes/utils';
describe('Theme CSS Generation', () => {
it('should generate CSS variables from theme', () => {
const theme = {
primaryColor: '#000000',
secondaryColor: '#666666',
accentColor: '#0066cc',
fontFamily: 'Arial, sans-serif',
headingFont: 'Georgia, serif',
bodyTextColor: '#333333',
backgroundColor: '#ffffff',
};
const css = generateThemeCSS(theme);
expect(css).toContain('--color-primary: #000000');
expect(css).toContain('--font-family: Arial, sans-serif');
});
});
API Endpoints Reference
Theme CRUD Operations
GET /api/themes- List all themesPOST /api/themes- Create new themeGET /api/themes/[id]- Get theme by IDPUT /api/themes/[id]- Update themeDELETE /api/themes/[id]- Delete theme
Domain Theme Assignment
GET /api/domains/[id]- Get domain (includes theme)PUT /api/domains/[id]- Update domain (assign theme)
Key Files Reference
Theme System Core
- /lib/db/schema.ts - Theme table definition
- /lib/themes/utils.ts - CSS generation and TypeScript interfaces
- /lib/themes/provider.tsx - React context for theme access
- /lib/themes/fonts.ts - Font configuration and loading
API & Admin
- /app/api/themes/route.ts - Theme CRUD endpoints
- /app/api/themes/[id]/route.ts - Single theme operations
- /components/admin/ThemeForm.tsx - Admin UI for theme creation
- /app/[domain]/layout.tsx - Theme injection point
Troubleshooting
Theme Not Applying
- Verify theme is assigned to domain:
GET /api/domains/[id] - Check browser console for errors
- Hard refresh page: Ctrl+Shift+R (Windows) or Cmd+Shift+R (Mac)
- Verify CSS is injected in page source
Font Not Loading
- Check font name matches exactly (case-sensitive)
- Verify font stack includes fallbacks
- Wait 30 seconds for Google Fonts to load
- Check Network tab in DevTools for font requests
Colors Look Wrong
- Verify hex color format is correct (#RRGGBB)
- Check contrast ratio meets accessibility standards
- Hard refresh to clear cached CSS
- Test in incognito/private mode
Next Steps
Now that you understand theme development:
- Create themes programmatically for your domains
- Build custom theme creation workflows
- Integrate themes with your plugin system
- Extend the theme system with additional properties
- Review
THEME-DEVELOPER-GUIDE.mdfor detailed information