@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-serverQuick Start
1. Set up environment variables
SUPABASE_URL=your-supabase-url
SUPABASE_SERVICE_KEY=your-supabase-service-key2. Run database migrations
npx blog-migrate3. 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 postsGET /blog/:slug- Get single postGET /sitemap.xml- Dynamic sitemapGET /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 categoryfeatured- Show only featured posts (true/false)search- Search in title and descriptionlanguage- 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- Paginationpublished- 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 keytitle- Post title (required)description- Short description (required)content- Full markdown content (required)slug- URL-friendly slug (unique, required)image- Featured image URLauthor- Author nameread_time- Estimated read time in minutespublished- Published status (boolean)featured- Featured status (boolean)published_at- Publication timestampcreated_at- Creation timestampupdated_at- Last update timestamp (auto-updated)tags- Array of tagscategory- Category nameviews- View count (auto-incremented)language- Language code (en/zh)original_post_id- Reference to original post for translationsuploaded_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
