@xexr/blog-platform
v1.0.1
Published
SEO-friendly blog platform for Next.js 15 with Bloggy integration
Readme
@xexr/blog-platform
SEO-friendly blog platform for Next.js 15 with Bloggy integration.
Overview
This package provides a complete blog platform solution that integrates with Bloggy's AI-generated content. It includes:
- 📝 Blog pages with full article rendering and MDX support
- 👤 Author management with rich bylines and JSON-LD schema
- 🔍 SEO optimization with meta tags, OpenGraph, and structured data
- 📊 Publishing API with idempotency and rate limiting
- 🛡️ Security with authentication middleware and CORS support
Requirements
- Next.js 15.x
- React 19.x
- Node.js 18+ or 20+
Installation
npm install @xexr/blog-platform
# or
pnpm add @xexr/blog-platform
# or
yarn add @xexr/blog-platformTailwind CSS Setup (Required)
This package uses Tailwind CSS utility classes that must be processed by your application's Tailwind configuration. The blog components will automatically inherit your site's theme colors, fonts, and design system.
Tailwind v4 Configuration (Recommended)
If you're using Tailwind v4, the blog platform works with an existing tailwind install.
You will need to install the tailwind typography plugin and point tailwind at the installed blog-platform package.
Be sure your globals.css file is configured similarly to the below:
@import "tailwindcss";
@source "../../node_modules/@xexr/blog-platform";
@plugin "@tailwindcss/typography";
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
/* ... other colors */
}The blog will automatically use these theme variables! ✨
Theme Inheritance
The blog components use semantic Tailwind classes that automatically adapt to your site's theme:
text-foreground- Uses your site's primary text colortext-muted-foreground- Uses your site's muted text colorbg-card- Uses your site's card background colortext-primary- Uses your site's primary brand colordark:prose-invert- Automatically supports dark mode
This means:
- Blue primary → blog links are blue
- Dark mode → blog automatically adapts
- Custom fonts → blog inherits them
- Custom spacing → blog matches it
No additional CSS needed - the blog seamlessly blends with your design system!
Quick Start
1. Initialize the Platform Once
Create a small server-only module that configures authentication, storage, and rate limiting exactly once. Import it anywhere you need the platform (layouts, API routes, server components).
src/lib/blog-platform.ts:
import {
initBlogPlatformOnce,
DrizzleStorage,
EnvAuth,
} from "@xexr/blog-platform";
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
const url = process.env.BLOG_DATABASE_URL;
const authToken = process.env.BLOG_DATABASE_AUTH_TOKEN;
if (!url) {
throw new Error("BLOG_DATABASE_URL must be set before initializing the blog platform");
}
initBlogPlatformOnce({
auth: new EnvAuth(), // Reads from BLOG_PLATFORM_API_KEYS
storage: new DrizzleStorage(drizzle(createClient({ url, authToken }))),
// optional: override rate limiting here
});
declare global {
// eslint-disable-next-line no-var
var __BLOG_PLATFORM_INITIALISED__?: boolean;
}Import
"@/lib/blog-platform"as a side effect in any file that re-exports the platform handlers (API routes) or renders the blog pages. This ensures the same storage instance is used everywhere.
2. Add Blog Pages to Your Next.js App
Create the following files in your Next.js application:
app/blog/page.tsx - Blog home page:
import "@/lib/blog-platform";
import { BlogHomePage, getStorage } from "@xexr/blog-platform";
const ITEMS_PER_PAGE = 10;
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const params = await searchParams;
const storage = getStorage();
// Parse current page from URL (default to 1)
const currentPage = Math.max(1, parseInt(params.page ?? "1", 10));
const offset = (currentPage - 1) * ITEMS_PER_PAGE;
// Fetch articles for current page
const { articles, total } = await storage.listArticles({
sortBy: "publishedAt",
order: "desc",
limit: ITEMS_PER_PAGE,
offset,
});
// Calculate total pages
const totalPages = Math.ceil(total / ITEMS_PER_PAGE);
return (
<BlogHomePage
articles={articles}
siteName={articles[0]?.site.name ?? "Blog"}
siteTagline={articles[0]?.site.tagline ?? "Insights and updates"}
includeStructuredData
pagination={{
currentPage,
totalPages,
baseUrl: "/blog",
}}
/>
);
}app/blog/[slug]/page.tsx - Individual article pages:
import "@/lib/blog-platform";
import { BlogArticlePage, getStorage } from "@xexr/blog-platform";
import { notFound } from "next/navigation";
export default async function ArticlePage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const storage = getStorage();
// Fetch the article
const article = await storage.getArticle(slug);
if (!article) {
notFound();
}
// Fetch all articles to find previous/next
const { articles } = await storage.listArticles({
sortBy: "publishedAt",
order: "desc",
});
// Find current article index
const currentIndex = articles.findIndex((a) => a.slug === slug);
const previousArticle = currentIndex > 0 ? articles[currentIndex - 1] : undefined;
const nextArticle = currentIndex < articles.length - 1 ? articles[currentIndex + 1] : undefined;
return (
<BlogArticlePage
article={article}
navigation={{
backToHome: { url: "/blog" },
...(previousArticle && {
previousArticle: {
url: `/blog/${previousArticle.slug}`,
title: previousArticle.title,
},
}),
...(nextArticle && {
nextArticle: {
url: `/blog/${nextArticle.slug}`,
title: nextArticle.title,
},
}),
}}
/>
);
}Note: The
navigationprop is optional. You can omitpreviousArticleandnextArticleif you don't want article navigation links.
2. Set Up Publishing API
app/api/blog/publish/route.ts:
import "@/lib/blog-platform";
import { publishHandler } from "@xexr/blog-platform";
export const POST = publishHandler;app/api/blog/unpublish/route.ts:
import "@/lib/blog-platform";
import { unpublishHandler } from "@xexr/blog-platform";
export const POST = unpublishHandler;app/api/blog/version/route.ts:
import "@/lib/blog-platform";
import { versionHandler } from "@xexr/blog-platform";
export const GET = versionHandler;app/api/blog/slugs/[slug]/route.ts - Dry-run slug availability check used during validation:
import "@/lib/blog-platform";
import {
slugExistsHandler,
slugExistsOptionsHandler,
} from "@xexr/blog-platform";
export const GET = slugExistsHandler;
export const OPTIONS = slugExistsOptionsHandler;🛈 When this route is missing, Bloggy’s dry-run validation falls back to a warning and your platform logs will show
GET /api/blog/slugs/... 404. Adding the handler keeps the validation flow clean.
3. Configure Durable Storage (Drizzle + Turso)
By default, the package uses in-memory storage (not persistent). To persist published articles, plug in the Drizzle adapter.
- Install dependencies (host app):
pnpm add drizzle-orm @libsql/client- Create the tables (run once via your migrations):
// e.g., scripts/createBlogPlatformTables.ts or inside your migration
import { getDrizzleAdapterSql } from "@xexr/blog-platform";
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
const client = createClient({
url: process.env.BLOG_DATABASE_URL!,
authToken: process.env.BLOG_DATABASE_AUTH_TOKEN!,
});
const db = drizzle(client);
const { createArticles, createOperations } = getDrizzleAdapterSql();
await db.run(createArticles);
await db.run(createOperations);- Wire the adapter at app startup:
// app/(root)/layout.tsx or middleware.ts (server-only)
import { initBlogPlatform, DrizzleStorage, EnvAuth } from "@xexr/blog-platform";
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
const client = createClient({
url: process.env.BLOG_DATABASE_URL!,
authToken: process.env.BLOG_DATABASE_AUTH_TOKEN!,
});
const db = drizzle(client);
initBlogPlatform({
auth: new EnvAuth(), // Reads from BLOG_PLATFORM_API_KEYS
storage: new DrizzleStorage(db),
// optional: pass rateLimit config here
});Environment:
BLOG_DATABASE_URL=libsql://your-turso-url
BLOG_DATABASE_AUTH_TOKEN=your-turso-token4. Environment Variables
Add these to your .env.local:
# Required for rate limiting (if enabled)
UPSTASH_REDIS_REST_URL=your_redis_url
UPSTASH_REDIS_REST_TOKEN=your_redis_token
# Required for authentication (set either BLOG_PLATFORM_API_KEYS or BLOG_AUTH_TOKEN)
BLOG_PLATFORM_API_KEYS=your_secure_token
# BLOG_AUTH_TOKEN=your_secure_token
# Required for Drizzle adapter
BLOG_DATABASE_URL=libsql://your-turso-url
BLOG_DATABASE_AUTH_TOKEN=your-turso-token5. Image Handling with Cloudflare CDN
The blog platform uses Cloudflare-optimized images hosted on Bloggy's central domain. When Bloggy publishes articles, it provides fully-qualified CDN URLs that your blog platform displays.
How it works:
- Images are stored centrally on Bloggy's R2 storage
- Bloggy sends optimized CDN URLs (e.g.,
https://bloggy.poyzer.download/cdn-cgi/image/width=1200,height=630,fit=cover,quality=85,format=webp/sites/3/images/photo.webp) - Your blog displays these images using Next.js
<Image>component
Next.js Configuration (Required):
You must configure your next.config.js to allow images from the Bloggy domain:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'bloggy.poyzer.download', // Replace with your Bloggy instance domain
},
],
},
};
export default nextConfig;Important: Replace bloggy.poyzer.download with your actual Bloggy instance domain. Without this configuration, Next.js will throw an error about unconfigured hostnames.
6. Configure Sitemap (Optional)
Add blog content to your sitemap for better SEO:
app/sitemap.ts:
import { generateBlogSitemapEntries } from "@xexr/blog-platform";
import type { MetadataRoute } from "next";
import { getBlogData } from "@/lib/blog"; // Your blog data fetching
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// Your existing static routes
const staticRoutes: MetadataRoute.Sitemap = [
{
url: "https://yoursite.com",
lastModified: new Date(),
changeFrequency: "yearly",
priority: 1,
},
// ... other routes
];
// Generate blog entries
const { site, articles, authors } = await getBlogData();
const blogEntries = await generateBlogSitemapEntries(
site,
articles,
authors,
{
includeAuthorPages: true,
changeFrequency: { articles: "monthly" },
priority: { articles: 0.8 },
},
);
return [...staticRoutes, ...blogEntries];
}📖 For detailed sitemap configuration options, see SITEMAP-INTEGRATION.md
Publishing Workflow Guide
This guide explains how to publish articles from Bloggy to your blog using the @xexr/blog-platform package.
Prerequisites
Before you can publish articles, ensure you have:
- ✅ Installed and configured the
@xexr/blog-platformpackage (see Quick Start) - ✅ Set up your blog pages (
/blogand/blog/[slug]) - ✅ Configured API routes (
/api/blog/publish,/api/blog/version, etc.) - ✅ Set environment variables (
BLOG_PLATFORM_API_KEYS, database credentials) - ✅ Configured Tailwind CSS to process blog components
Step 1: Configure Publishing Platform in Bloggy
Navigate to Platform Settings:
- In Bloggy, go to Blogs → Select your blog → Edit → Publishing tab
Enter Platform Details:
- Blog Platform API URL: Your blog's API endpoint (e.g.,
https://yourblog.com/api/blog) - Public Blog URL: Your blog's public URL (e.g.,
https://yourblog.com/blog) - Publishing Token: The authentication token (matches
BLOG_PLATFORM_API_KEYSin your .env)
- Blog Platform API URL: Your blog's API endpoint (e.g.,
Test Connection:
- Click Test Connection to verify:
- Authentication is working
- Blog platform is accessible
- Versions are compatible (semantic versioning check)
- You should see: ✅ Compatible (green badge) or ⚠️ Minor warning (yellow badge)
- Click Test Connection to verify:
Save Configuration:
- Click Save Configuration
- Token is encrypted and stored securely
- Publishing is now enabled for all articles
Troubleshooting Connection Test:
| Issue | Solution |
|-------|----------|
| ❌ Invalid credentials | Check that BLOG_PLATFORM_API_KEYS in your .env matches the token you entered |
| ❌ Connection failed | Verify your Blog Platform API URL is correct and accessible |
| ❌ Version incompatible | Upgrade your blog platform package to match Bloggy's version |
Step 2: Publish an Article
Navigate to Article:
- Go to Blogs → Select your blog → Headlines
- Click on a headline to open the consolidated headline details page
- The article must be in Accepted status to publish
Initiate Publishing:
- In the top action bar, click the Publish button
- If no platform is configured, the button will be disabled with a helpful message
Configure Publication Settings: The publish modal opens with pre-filled fields:
URL Slug: Auto-generated from the title (lowercase, hyphens)
- Preview:
https://yourblog.com/blog/your-article-slug - Editable: Change to your preferred slug
- Validation: Must be lowercase letters, numbers, and hyphens only
- Preview:
Meta Description: Article excerpt (max 160 characters)
- Shows character count (e.g., "142/160 characters")
- Editable: Customize for search engines
Optional: Validate Before Publishing:
- Click Run Validation to perform a dry-run check
- System validates:
- Content meets requirements (min 50 words)
- Slug format is valid
- All required fields present
- Issues are displayed inline if found
Publish to Blog:
- Click Publish Article button
- Publishing typically completes in 2-5 seconds
- Success notification shows: "Article published successfully"
- Toast includes View Article link to the live blog
Post-Publish State:
- Status badge changes to Published (green)
- View Live link appears with publish date
- Article is accessible at:
https://yourblog.com/blog/your-article-slug
Step 3: Republish an Article (Updates)
Modify Article Content:
- Click Edit Article in the action bar
- Make your changes in the article editor
- Save the changes
Republish:
- Click the Publish button again
- A confirmation dialog appears:
⚠️ Republish Article
This will update the live article on your external blog.
Confirm Update:
- Click Confirm Republish
- Updated content pushes to blog (typically < 2 seconds)
- Blog article refreshes with new content
dateModifiedupdates in JSON-LD,datePublishedremains unchanged
What Gets Updated:
- ✅ Article content (title, body, images)
- ✅ Meta description
- ✅
dateModifiedtimestamp - ❌ URL slug (remains unchanged to preserve SEO)
- ❌
datePublished(original publish date preserved)
Step 4: Unpublish an Article
Initiate Unpublish:
- Click Unpublish button (visible when article is published)
- Confirmation modal explains consequences:
⚠️ Unpublish Article
This will remove the article from your blog. The URL will return a 404 error, and analytics data may be affected.
Confirm Removal:
- Click Confirm Unpublish
- Article removed from blog (returns 404)
- Status changes to Never Published
- Publish button becomes available again
Re-Publishing After Unpublish:
- You can republish at any time
- Same URL slug can be reused
- Treated as a new publish with new publish date
Publishing Status Reference
| Status | Description | Available Actions | |--------|-------------|-------------------| | Never Published | Article has never been published | ✅ Publish | | Published | Article is live on blog | ✅ Republish (update)✅ Unpublish | | Failed | Publishing failed due to error | ✅ Retry publish | | Publishing | Publish in progress | ⏳ Wait (typically < 5s) |
Error Handling & Troubleshooting
Common Publishing Errors
PUB003: Slug Already Exists
- Cause: Another article is already using this slug
- Solution: Choose a different slug (add date, number, or variation)
PUB005: Invalid Credentials
- Cause: Authentication token doesn't match
- Solution: Update token in platform settings and re-test connection
PUB006: Content Validation Failed
- Cause: Article doesn't meet publishing requirements
- Solution: Check error details (usually min 50 words required)
PUB007: Platform Unavailable
- Cause: Blog platform is down or unreachable
- Solution: Check your blog is running, then retry publish
PUB008: No Platform Configured
- Cause: Publishing platform not set up for this site
- Solution: Go to Blog → Edit → Publishing and configure platform
PUB009: Version Incompatible
- Cause: Bloggy version doesn't match blog platform version (major mismatch)
- Solution: Upgrade your
@xexr/blog-platformpackage
Retry Failed Publishes
If publishing fails:
- Error message shows specific error code (PUB001-PUB010)
- Toast includes Retry button for network/timeout errors
- Fix the underlying issue based on error code
- Click Retry or open publish modal again
- Idempotency protection prevents duplicate publishes
Author Handling
The blog platform displays author information based on Bloggy's author settings:
Author Resolution Order:
- Article Author: If article has an assigned author, use that author
- Site Default Author: If no article author, use site's default author
- No Author: If neither available, publish without author byline
Guest Author Display:
- Guest authors (marked with
isGuest: true) show Guest Author badge - Author bio card displays if
author.biois present - Author profile links are not included (post-MVP feature)
Example Byline Displays:
✅ With Author:
"by Alex Chen • Oct 4, 2025 • 4 min read • From Indie Web Growth"
✅ Guest Author:
"by Maya Patel (Guest Author) • Oct 4, 2025 • 3 min read • From Tech Insights"
✅ No Author:
"Oct 4, 2025 • 4 min read • From Indie Web Growth"SEO Best Practices
When publishing articles, the blog platform automatically handles:
Meta Tags:
<title>: Article title<meta name="description">: Your custom meta description<link rel="canonical">: Self-canonical URL
Structured Data (JSON-LD):
- BlogPosting schema with full article metadata
- Author Person schema (if author present)
- Organization schema for publisher
- BreadcrumbList for navigation
OpenGraph Tags:
og:title,og:description,og:urlog:type="article"- Featured image as
og:image(if available)
Image Optimization:
- Images served via Cloudflare CDN
- Automatic WebP conversion
- Responsive sizing
- Descriptive alt text
Version Compatibility
The blog platform uses semantic versioning to ensure compatibility:
Version Format: MAJOR.MINOR.PATCH
Compatibility Rules:
- ✅ Patch Differences (1.0.0 ↔ 1.0.3): Always compatible
- ⚠️ Minor Differences (1.0.0 ↔ 1.1.0): Compatible with warnings
- ❌ Major Differences (1.0.0 ↔ 2.0.0): Incompatible, blocks publishing
Checking Compatibility:
- Go to Blog → Edit → Publishing
- Click Test Connection
- View compatibility status in test results
- Upgrade blog platform if incompatible:
pnpm add @xexr/blog-platform@latest
Rate Limiting
Publishing API is rate-limited to 5 requests per minute per authentication token.
Rate Limit Behavior:
- Each publish/unpublish counts as 1 request
- Version check counts as 1 request
- Limit resets every 60 seconds
- Exceeded limits return HTTP 429 with
Retry-Afterheader
If Rate Limited:
- Wait for reset (shown in error message)
- System automatically retries after delay
- Avoid rapid republishing during testing
Security & Privacy
Credential Security:
- All tokens encrypted with AES-256-GCM before storage
- Tokens never displayed after save (shown as "••••")
- HTTPS required for all API communication
- Authentication required on all blog platform endpoints
Content Privacy:
- Only accepted articles can be published
- Draft articles remain private in Bloggy
- Unpublished articles return 404 on blog
- No content indexing until published
Best Practices
- Test Configuration First: Always test connection before publishing
- Use Descriptive Slugs: Make URLs readable and SEO-friendly
- Optimize Meta Descriptions: Keep under 160 characters, include keywords
- Assign Authors: Include author info for better engagement and SEO
- Preview Before Publishing: Review article in Bloggy's article tab
- Update Responsibly: Use republish for corrections, not complete rewrites
- Monitor Published URLs: Check "View Live" link after publishing
- Keep Versions Aligned: Update blog platform when Bloggy updates
API Endpoints
Publishing API
POST /api/blog/publish
Publishes an article to your blog.
Headers:
Authorization: Bearer your_blog_auth_token
Content-Type: application/json
X-Idempotency-Key: unique_request_idRequest Body:
{
"article": {
"id": "article-123",
"title": "Article Title",
"content": "# Article Content\n\nYour markdown content here...",
"slug": "article-title",
"excerpt": "Brief description...",
"publishedAt": "2024-01-01T00:00:00Z",
"featuredImage": {
"url": "https://example.com/image.jpg",
"alt": "Image description"
}
},
"author": {
"id": "author-123",
"name": "Author Name",
"bio": "Author biography...",
"image": "https://example.com/author.jpg"
},
"site": {
"id": "site-123",
"name": "Site Name",
"url": "https://example.com",
"description": "Site description..."
}
}Response:
{
"success": true,
"publishedUrl": "https://yoursite.com/blog/article-title",
"publishedAt": "2024-01-01T00:00:00Z"
}POST /api/blog/unpublish
Unpublishes an article from your blog.
Headers:
Authorization: Bearer your_blog_auth_token
Content-Type: application/json
X-Idempotency-Key: unique_request_idRequest Body:
{
"articleId": "article-123"
}Response:
{
"success": true,
"unpublishedAt": "2024-01-01T12:00:00Z"
}GET /api/blog/version
Returns version compatibility information.
Response:
{
"version": "0.1.0",
"compatible": true
}Rate Limiting
The publishing API is rate-limited to 5 requests per minute per authentication token using Upstash Redis.
Rate limit headers are included in responses:
X-RateLimit-Limit: Maximum requests per windowX-RateLimit-Remaining: Remaining requests in current windowX-RateLimit-Reset: Time when the rate limit resets
Error Handling
The API returns standardized error codes:
| Code | Description | HTTP Status | | ------ | ------------------------- | ----------- | | PUB001 | Missing required field | 400 | | PUB002 | Invalid slug format | 400 | | PUB003 | Slug already exists | 409 | | PUB004 | Network timeout | 408 | | PUB005 | Invalid credentials | 401 | | PUB006 | Content validation failed | 400 | | PUB007 | Platform unavailable | 503 | | PUB008 | No platform configured | 400 |
Type Definitions
The package exports TypeScript types for all data structures:
import type {
PublishedArticle,
PublishedAuthor,
PublishedSite,
PublishPayload,
PublishResult,
} from "@xexr/blog-platform/types";Customization & Styling
Theme Customization
The blog platform is designed to seamlessly blend with your existing design system. All components use semantic Tailwind classes that automatically inherit your site's theme.
No CSS Compilation Required: Unlike traditional component libraries, this package doesn't ship pre-compiled CSS. Instead, it uses Tailwind utility classes that your application's Tailwind configuration processes. This ensures perfect theme alignment with zero configuration.
What Gets Inherited Automatically:
- ✅ Colors: Primary, foreground, background, muted colors
- ✅ Typography: Font families, sizes, and weights
- ✅ Spacing: Consistent padding and margins
- ✅ Border Radius: Matches your design system
- ✅ Dark Mode: Automatic dark mode support via
dark:variants
Customization Points
1. Color Scheme
The blog uses these semantic color tokens from your theme:
/* Blog components automatically use: */
text-foreground /* Primary text color */
text-muted-foreground /* Secondary/muted text */
bg-card /* Card backgrounds */
bg-background /* Page backgrounds */
text-primary /* Brand color (links, CTAs) */
border-border /* Border colors */To customize: Simply adjust these colors in your Tailwind theme configuration:
/* globals.css - Tailwind v4 */
@theme inline {
--color-primary: oklch(0.6 0.2 250); /* Custom blue */
--color-foreground: oklch(0.2 0.02 250); /* Dark text */
/* ... other theme variables */
}2. Typography
The blog uses Tailwind's typography plugin (@tailwindcss/typography) with automatic theme inheritance:
/* Article content uses: */
<div className="prose dark:prose-invert">
{/* Inherits your prose styles */}
</div>To customize typography:
// tailwind.config.js
module.exports = {
theme: {
extend: {
typography: {
DEFAULT: {
css: {
maxWidth: '80ch', // Wider content
h1: {
fontSize: '3rem', // Larger headings
},
// ... custom prose styles
},
},
},
},
},
};3. Component Overrides
For advanced customization, you can override blog components:
Option A: Wrap and Extend
// app/blog/page.tsx
import { BlogHomePage } from "@xexr/blog-platform";
export default function CustomBlogPage() {
return (
<div className="my-custom-wrapper">
{/* Add custom header */}
<MyCustomHeader />
<BlogHomePage {...props} />
{/* Add custom footer */}
<MyCustomFooter />
</div>
);
}Option B: Component Replacement
// Create your own article page using the same data structure
import { getStorage } from "@xexr/blog-platform";
export default async function MyArticlePage({ params }) {
const storage = getStorage();
const article = await storage.getArticle(params.slug);
// Use your own components
return <MyCustomArticleLayout article={article} />;
}4. Layout Structure
The blog components provide clean, semantic HTML that's easy to style:
<!-- Blog Home Structure -->
<div className="container mx-auto px-4 py-8">
<header className="mb-12">
<h1 className="text-foreground">Site Name</h1>
<p className="text-muted-foreground">Tagline</p>
</header>
<article className="space-y-8">
<!-- Article cards -->
</article>
<footer className="mt-16">
<!-- Pagination -->
</footer>
</div>Customize via Tailwind:
// Adjust container width, padding, etc.
<BlogHomePage
articles={articles}
siteName="My Blog"
// Components respect Tailwind utility classes
/>5. Dark Mode
Dark mode is automatically supported via Tailwind's dark: variant:
/* Components include dark mode styles: */
<div className="bg-card dark:bg-card text-foreground dark:text-foreground">
{/* Automatically adapts to dark mode */}
</div>To enable dark mode in your Next.js app:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en" className="dark"> {/* Add 'dark' class */}
<body>{children}</body>
</html>
);
}Or use next-themes for dynamic toggling:
import { ThemeProvider } from "next-themes";
export default function RootLayout({ children }) {
return (
<html suppressHydrationWarning>
<body>
<ThemeProvider attribute="class" defaultTheme="system">
{children}
</ThemeProvider>
</body>
</html>
);
}Design Philosophy
The blog platform follows these design principles:
- Zero Configuration: Works out of the box with your existing theme
- Semantic Classes: Uses meaning-based tokens, not hardcoded colors
- Progressive Enhancement: Core functionality works without JavaScript
- Accessibility First: WCAG 2.1 AA compliant markup
- Performance Optimized: Minimal CSS footprint, no runtime styles
Common Customization Examples
Example 1: Custom Brand Colors
/* globals.css */
@theme inline {
--color-primary: oklch(0.6 0.25 140); /* Green primary */
--color-primary-foreground: oklch(1 0 0); /* White text on primary */
}Result: All blog links, CTAs, and accent colors become green.
Example 2: Wider Content Layout
// app/blog/[slug]/page.tsx
export default function ArticlePage({ params }) {
return (
<div className="container mx-auto max-w-5xl"> {/* Wider than default */}
<BlogArticlePage article={article} />
</div>
);
}Example 3: Custom Author Byline
// Create custom author component
import { AuthorByline as DefaultByline } from "@xexr/blog-platform";
function CustomAuthorByline({ author }) {
return (
<div className="flex items-center gap-4">
<img src={author.avatar} className="w-16 h-16 rounded-full" />
<div>
<DefaultByline author={author} />
<p className="text-sm text-muted-foreground">
@{author.twitter}
</p>
</div>
</div>
);
}SEO Features
- Meta tags: Automatic title, description, and Open Graph tags
- JSON-LD schema: Rich snippets for articles and authors
- Canonical URLs: Proper URL canonicalization
- Sitemap support: Built-in sitemap generation utilities
Security
- Authentication: Bearer token authentication required
- Rate limiting: Prevents abuse with Redis-based limiting
- CORS: Configurable CORS headers
- Input validation: Comprehensive input sanitization
Development
To work on this package locally:
# Install dependencies
pnpm install
# Build the package
pnpm build
# Run tests
pnpm test
# Type check
pnpm type
# Lint code
pnpm lintRecent Changes
v0.2.0 - Latest (2025-01-06)
- 🚀 Automated Versioning: Set up semantic versioning with changesets
- 🚀 GitHub Actions: Added automated publishing workflow
- 📦 NPM Package: Published to public NPM registry
- 📝 Documentation: Enhanced README with installation and setup guides
v0.1.2 (2025-01-06)
- 🔒 Security: Fail-closed authentication and comprehensive security headers
- 🎨 Enhancements: Theme inheritance, guest author badges, version compatibility
- 🐛 Fixes: Version endpoint, incompatible version handling, tone adjective removal
- 📚 Docs: Publishing workflow and styling documentation
Full Changelog: See CHANGELOG.md for complete version history.
License
MIT
Support
For issues and questions, please refer to the Bloggy documentation or create an issue in the project repository.
