npm package: @jjjjjia11/content-server
Description: Simple blog server with API routes, sitemap generation, and database migrations
Author: Shumbird
Latest version: 1.0.1

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

```bash
npm install @jjjjjia11/content-server
```

## Quick Start

### 1. Set up environment variables

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

### 2. Run database migrations

```bash
npx blog-migrate
```

### 3. Add routes to your Express app

```typescript
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

```typescript
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:**
```json
{
  "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:**
```json
{
  "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:**
```json
{
  "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

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

## Robots.txt Configuration

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

## Authentication Middleware

To enable admin routes, provide an auth middleware:

```typescript
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

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

## License

MIT