npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-platform

Tailwind 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 color
  • text-muted-foreground - Uses your site's muted text color
  • bg-card - Uses your site's card background color
  • text-primary - Uses your site's primary brand color
  • dark: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 navigation prop is optional. You can omit previousArticle and nextArticle if 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.

  1. Install dependencies (host app):
pnpm add drizzle-orm @libsql/client
  1. 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);
  1. 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-token

4. 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-token

5. 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:

  1. Images are stored centrally on Bloggy's R2 storage
  2. 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)
  3. 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:

  1. ✅ Installed and configured the @xexr/blog-platform package (see Quick Start)
  2. ✅ Set up your blog pages (/blog and /blog/[slug])
  3. ✅ Configured API routes (/api/blog/publish, /api/blog/version, etc.)
  4. ✅ Set environment variables (BLOG_PLATFORM_API_KEYS, database credentials)
  5. ✅ Configured Tailwind CSS to process blog components

Step 1: Configure Publishing Platform in Bloggy

  1. Navigate to Platform Settings:

    • In Bloggy, go to Blogs → Select your blog → EditPublishing tab
  2. 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_KEYS in your .env)
  3. 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)
  4. 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

  1. 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
  2. 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
  3. 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
    • Meta Description: Article excerpt (max 160 characters)

      • Shows character count (e.g., "142/160 characters")
      • Editable: Customize for search engines
  4. 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
  5. 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
  6. 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)

  1. Modify Article Content:

    • Click Edit Article in the action bar
    • Make your changes in the article editor
    • Save the changes
  2. Republish:

    • Click the Publish button again
    • A confirmation dialog appears:

      ⚠️ Republish Article

      This will update the live article on your external blog.

  3. Confirm Update:

    • Click Confirm Republish
    • Updated content pushes to blog (typically < 2 seconds)
    • Blog article refreshes with new content
    • dateModified updates in JSON-LD, datePublished remains unchanged

What Gets Updated:

  • ✅ Article content (title, body, images)
  • ✅ Meta description
  • dateModified timestamp
  • ❌ URL slug (remains unchanged to preserve SEO)
  • datePublished (original publish date preserved)

Step 4: Unpublish an Article

  1. 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.

  2. Confirm Removal:

    • Click Confirm Unpublish
    • Article removed from blog (returns 404)
    • Status changes to Never Published
    • Publish button becomes available again
  3. 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-platform package

Retry Failed Publishes

If publishing fails:

  1. Error message shows specific error code (PUB001-PUB010)
  2. Toast includes Retry button for network/timeout errors
  3. Fix the underlying issue based on error code
  4. Click Retry or open publish modal again
  5. Idempotency protection prevents duplicate publishes

Author Handling

The blog platform displays author information based on Bloggy's author settings:

Author Resolution Order:

  1. Article Author: If article has an assigned author, use that author
  2. Site Default Author: If no article author, use site's default author
  3. 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.bio is 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:url
  • og: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:

  1. Go to Blog → Edit → Publishing
  2. Click Test Connection
  3. View compatibility status in test results
  4. 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-After header

If Rate Limited:

  1. Wait for reset (shown in error message)
  2. System automatically retries after delay
  3. 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

  1. Test Configuration First: Always test connection before publishing
  2. Use Descriptive Slugs: Make URLs readable and SEO-friendly
  3. Optimize Meta Descriptions: Keep under 160 characters, include keywords
  4. Assign Authors: Include author info for better engagement and SEO
  5. Preview Before Publishing: Review article in Bloggy's article tab
  6. Update Responsibly: Use republish for corrections, not complete rewrites
  7. Monitor Published URLs: Check "View Live" link after publishing
  8. 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_id

Request 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_id

Request 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 window
  • X-RateLimit-Remaining: Remaining requests in current window
  • X-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:

  1. Zero Configuration: Works out of the box with your existing theme
  2. Semantic Classes: Uses meaning-based tokens, not hardcoded colors
  3. Progressive Enhancement: Core functionality works without JavaScript
  4. Accessibility First: WCAG 2.1 AA compliant markup
  5. 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 lint

Recent 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.