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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@jjjjjia11/content-server

v1.0.1

Published

Simple blog server with API routes, sitemap generation, and database migrations

Readme

@jjjjjia11/content-server

Simple, powerful blog server with Express API routes, dynamic sitemap.xml/robots.txt generation, and Supabase database migrations.

Features

  • Blog API: RESTful routes for blog post management
  • SEO Ready: Dynamic sitemap.xml and robots.txt generation
  • Public & Admin: Public read access + authenticated admin routes
  • Database Migrations: One-command database setup
  • Multi-language: Built-in English/Chinese support
  • View Tracking: Automatic view counting
  • Slug-based URLs: SEO-friendly routing

Installation

npm install @jjjjjia11/content-server

Quick Start

1. Set up environment variables

SUPABASE_URL=your-supabase-url
SUPABASE_SERVICE_KEY=your-supabase-service-key

2. Run database migrations

npx blog-migrate

3. Add routes to your Express app

import express from 'express';
import { createBlogServer } from '@jjjjjia11/content-server';

const app = express();

const { blogRouter, sitemapRouter } = createBlogServer({
  supabaseUrl: process.env.SUPABASE_URL!,
  supabaseServiceKey: process.env.SUPABASE_SERVICE_KEY!,
  baseUrl: 'https://yoursite.com', // Required for sitemap
});

// Mount routers
app.use('/blog', blogRouter);
app.use('/', sitemapRouter); // Serves /sitemap.xml and /robots.txt

app.listen(3000);

That's it! You now have:

  • GET /blog - List blog posts
  • GET /blog/:slug - Get single post
  • GET /sitemap.xml - Dynamic sitemap
  • GET /robots.txt - Dynamic robots.txt

Configuration

createBlogServer({
  // Required
  supabaseUrl: string;
  supabaseServiceKey: string;
  baseUrl: string; // e.g., 'https://example.com'

  // Optional
  authMiddleware?: (req, res, next) => void; // Enables admin routes
  sitemap?: {
    additionalUrls?: SitemapEntry[]; // Extra URLs to include
    exclude?: string[]; // URL patterns to exclude
  };
  robots?: {
    allowAll?: boolean; // Default: true
    disallowPaths?: string[]; // Paths to disallow
    additionalRules?: string[]; // Custom rules
  };
});

API Routes

Public Routes

GET /blog

List published blog posts with pagination.

Query Parameters:

  • page - Page number (default: 1)
  • limit - Posts per page (default: 10, max: 100)
  • category - Filter by category
  • featured - Show only featured posts (true/false)
  • search - Search in title and description
  • language - Filter by language (en/zh)

Response:

{
  "posts": [
    {
      "id": 1,
      "title": "My Blog Post",
      "description": "Post description",
      "slug": "my-blog-post",
      "image": "https://...",
      "author": "John Doe",
      "readTime": 5,
      "publishedAt": "2024-01-01T00:00:00Z",
      "category": "Tech",
      "tags": ["javascript", "react"],
      "views": 123
    }
  ],
  "pagination": {
    "total": 50,
    "page": 1,
    "limit": 10,
    "totalPages": 5
  }
}

GET /blog/:slug

Get a single published blog post by slug (automatically increments view count).

Response:

{
  "post": {
    "id": 1,
    "title": "My Blog Post",
    "description": "Post description",
    "content": "Full markdown content...",
    "slug": "my-blog-post",
    ...
  }
}

Admin Routes (requires authentication)

Enable by providing authMiddleware in config.

GET /blog/admin/all

List all posts including unpublished.

Query Parameters:

  • page, limit - Pagination
  • published - Filter by published status (true/false)
  • language - Filter by language

GET /blog/admin/:id

Get single post by ID (including unpublished).

POST /blog

Create a new blog post.

Request Body:

{
  "title": "My Post",
  "description": "Description",
  "content": "Markdown content",
  "slug": "my-post",
  "published": false,
  "featured": false,
  "image": "https://...",
  "author": "John Doe",
  "readTime": 5,
  "category": "Tech",
  "tags": ["javascript"],
  "language": "en"
}

PUT /blog/:id

Update an existing blog post.

DELETE /blog/:id

Delete a blog post.

Sitemap & Robots Routes

GET /sitemap.xml

Dynamically generated sitemap with:

  • All published blog posts
  • Homepage
  • Blog index
  • Custom URLs from config

Caching: Cached for 1 hour, auto-invalidated on post create/update/delete.

POST /sitemap/invalidate-cache

Manually invalidate sitemap cache (automatically called on CRUD operations).

GET /robots.txt

Dynamically generated robots.txt with:

  • Allow/disallow rules
  • Custom paths
  • Sitemap reference

Sitemap Configuration

createBlogServer({
  ...
  sitemap: {
    additionalUrls: [
      {
        url: 'https://example.com/about',
        lastmod: '2024-01-01',
        changefreq: 'monthly',
        priority: 0.7
      }
    ],
    exclude: ['/admin', '/private']
  }
});

Robots.txt Configuration

createBlogServer({
  ...
  robots: {
    allowAll: true,
    disallowPaths: ['/admin/*', '/api/*'],
    additionalRules: [
      'User-agent: GPTBot',
      'Disallow: /'
    ]
  }
});

Authentication Middleware

To enable admin routes, provide an auth middleware:

const authMiddleware = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) {
    return res.status(401).json({ error: 'No authorization header' });
  }

  const token = authHeader.replace('Bearer ', '');
  const userSupabase = createClient(supabaseUrl, supabaseAnonKey, {
    global: { headers: { Authorization: `Bearer ${token}` } }
  });

  const { data: { user }, error } = await userSupabase.auth.getUser();
  if (error || !user) {
    return res.status(401).json({ error: 'Invalid token' });
  }

  // User must have role 'editor' or 'admin' in user_metadata
  req.user = user;
  req.userSupabase = userSupabase;
  next();
};

const { blogRouter } = createBlogServer({
  ...
  authMiddleware
});

Database Schema

The migration creates a blog_posts table with:

  • id - Serial primary key
  • title - Post title (required)
  • description - Short description (required)
  • content - Full markdown content (required)
  • slug - URL-friendly slug (unique, required)
  • image - Featured image URL
  • author - Author name
  • read_time - Estimated read time in minutes
  • published - Published status (boolean)
  • featured - Featured status (boolean)
  • published_at - Publication timestamp
  • created_at - Creation timestamp
  • updated_at - Last update timestamp (auto-updated)
  • tags - Array of tags
  • category - Category name
  • views - View count (auto-incremented)
  • language - Language code (en/zh)
  • original_post_id - Reference to original post for translations
  • uploaded_assets - Array of asset URLs

Indexes created for performance on: published, slug, language, created_at, category, featured, published_at

Row Level Security (RLS) enabled with policies:

  • Public can read published posts
  • Editors/admins can manage all posts

TypeScript Types

import type {
  BlogPost,
  Database,
  SitemapEntry,
  ContentServerConfig
} from '@jjjjjia11/content-server';

License

MIT