stacks-cms

$npx mdskill add stacksjs/stacks/stacks-cms

Manage CMS content in Stacks applications — posts, authors, pages, and more.

  • Fetch, store, update, and destroy posts, authors, pages, categories, and tags.
  • Depends on @stacksjs/cms package, models, and routes in a Stacks app.
  • Uses CMS models and actions to determine available operations.
  • Returns data or performs mutations via the CmsNamespace API.

SKILL.md

.github/skills/stacks-cmsView on GitHub ↗
---
name: stacks-cms
description: Use when working with the CMS in a Stacks application — posts, authors, pages, categories, tags, comments, blog configuration, RSS feeds, or sitemaps. Covers @stacksjs/cms, CMS models, routes, and actions.
license: MIT
compatibility: Bun >= 1.3.0, TypeScript
allowed-tools: Read Edit Write Bash Grep Glob
---

# Stacks CMS

## Key Paths
- Core package: `storage/framework/core/cms/src/`
- Models: `storage/framework/defaults/models/Content/` (Post, Author, Page)
- Models: `storage/framework/defaults/models/` (Comment, Tag)
- Actions: `storage/framework/defaults/app/Actions/Cms/`
- Routes: `routes/api.ts` (CMS and blog endpoints)
- Config: `config/blog.ts`

## Source Files
```
cms/src/
├── index.ts              # CmsNamespace — top-level API
├── posts/
│   ├── fetch.ts          # fetchById, fetchAll, fetchByStatus, fetchByCategory, fetchByAuthor
│   ├── store.ts          # store, attach, detach, sync (pivot tables)
│   ├── update.ts         # update
│   └── destroy.ts        # destroy, bulkDestroy
├── authors/
│   ├── fetch.ts          # fetchAll, fetchById, findByEmail, findByName, findByUserId
│   ├── store.ts          # store, findOrCreate
│   ├── update.ts         # update
│   └── destroy.ts        # destroy
├── pages/
│   ├── fetch.ts          # fetchById, fetchAll, fetchByTemplate, fetchByAuthor
│   └── store.ts          # store
├── categorizables/       # Categories with polymorphic pivot
├── taggables/            # Tags with polymorphic pivot
├── commentables/         # Comments with polymorphic support
└── build.ts              # Blog static site generation
```

## CMS Namespace

```typescript
interface CmsNamespace {
  posts: PostsModule
  postCategories: PostCategoriesModule
  tags: TagsModule
  comments: CommentsModule
  authors: AuthorsModule
  pages: PagesModule
}
```

## Models

### Post
- **Table**: `posts`
- **Relationships**: `belongsTo(['Author'])`
- **Traits**: UUID, Timestamps, Search, Seeder (20), Categorizable, Taggable, Commentable, API routes

| Field | Type | Validation | Default |
|-------|------|------------|---------|
| title | string | required, 3-255 chars | — |
| content | string | required, 10-1000 chars | — |
| excerpt | string | optional, 10-500 chars | — |
| poster | string | optional, valid URL | — |
| views | number | — | 0 |
| publishedAt | timestamp | optional | — |
| status | enum | published/draft/archived | draft |
| isFeatured | number | 0 or 1 | — |

### Author
- **Table**: `authors`, **Relationships**: `hasMany(['Post'])`, `belongsTo(['User'])`
- **Attributes**: `name` (5-255), `email` (unique, valid email)
- **Indexes**: Composite on `(email, name)`

### Page
- **Table**: `pages`, **Relationships**: `belongsTo(['Author'])`
- **Attributes**: `title`, `template` (enum: default/landing/blog/contact), `views`, `conversions`, `publishedAt`

### Comment
- **Table**: `comments`, **Relationships**: `belongsTo(['Post', 'User'])`
- **Attributes**: `authorName`, `authorEmail`, `content` (1-2000), `status` (pending/approved/spam/trash), `ipAddress`, `userAgent`, `isApproved`

### Tag
- **Table**: `tags`
- **Attributes**: `name` (unique, 2-50), `slug` (unique), `description`, `postCount`, `color`

## Posts API

```typescript
// Fetch
cms.posts.fetchById(id: number): Promise<PostJsonResponse | undefined>
cms.posts.fetchAll(): Promise<PostJsonResponse[]>
cms.posts.fetchByStatus(status: 'published' | 'draft' | 'archived'): Promise<PostJsonResponse[]>
cms.posts.fetchByCategory(category: string): Promise<PostJsonResponse[]>
cms.posts.fetchByAuthor(author: string): Promise<PostJsonResponse[]>
cms.posts.fetchByMinViews(minViews: number): Promise<PostJsonResponse[]>
cms.posts.fetchPublishedAfter(timestamp: number): Promise<PostJsonResponse[]>

// Store with pivot auto-attach
cms.posts.store(data: NewPost & { body?: string, category?: string }): Promise<PostJsonResponse>
cms.posts.attach(postId, tableName: 'categorizable_models' | 'taggable_models', ids: number[]): Promise<void>
cms.posts.detach(postId, tableName, ids?: number[]): Promise<void>
cms.posts.sync(postId, tableName, ids: number[]): Promise<void>

// Update & Destroy
cms.posts.update(id: number, data: Partial<PostUpdate>): Promise<PostJsonResponse>
cms.posts.destroy(id: number): Promise<boolean>
cms.posts.bulkDestroy(ids: number[]): Promise<number>
```

## Authors API

```typescript
cms.authors.findOrCreate(data: AuthorData): Promise<AuthorJsonResponse>
cms.authors.store(data: NewAuthor): Promise<AuthorJsonResponse>
cms.authors.findByEmail(email: string): Promise<AuthorJsonResponse | undefined>
cms.authors.findByName(name: string): Promise<AuthorJsonResponse | undefined>
cms.authors.findByUserId(userId: number): Promise<AuthorJsonResponse | undefined>
cms.authors.fetchAll(): Promise<AuthorJsonResponse[]>
cms.authors.fetchById(id: number): Promise<AuthorJsonResponse | undefined>
cms.authors.update(id, data): Promise<AuthorJsonResponse>
cms.authors.destroy(id): Promise<boolean>
```

## Tags API

```typescript
cms.tags.findOrCreateMany(names: string[], taggableType: string): Promise<number[]>
cms.tags.firstOrCreate(name: string, taggableType: string): Promise<TaggableTable>
cms.tags.fetchTags(): Promise<TaggableTable[]>
cms.tags.fetchTagById(id: number): Promise<TaggableTable | undefined>
cms.tags.countTaggedPosts(taggableType: string): Promise<number>
cms.tags.findMostUsedTag(taggableType?: string): Promise<{ name: string, count: number } | null>
cms.tags.fetchTagsWithPostCounts(): Promise<Array<{ name: string, postCount: number }>>
cms.tags.fetchTagDistribution(): Promise<Array<{ name: string, count: number, percentage: number }>>
```

## Comments API

```typescript
cms.comments.createComment(data: CreateCommentInput): Promise<CommentablesTable>
cms.comments.updateComment(id: number, input: UpdateCommentInput): Promise<CommentablesTable>
cms.comments.approveComment(id: number): Promise<CommentablesTable>
cms.comments.rejectComment(id: number): Promise<CommentablesTable>
cms.comments.deleteComment(id: number): Promise<void>
```

## Routes

### CMS Admin Routes
| Method | Path | Action |
|--------|------|--------|
| GET | `/cms/posts` | PostIndexAction |
| GET | `/cms/posts/{id}` | PostShowAction |
| POST | `/cms/posts` | PostStoreAction |
| PATCH | `/cms/posts/{id}` | PostUpdateAction |
| DELETE | `/cms/posts/{id}` | PostDestroyAction |
| PATCH | `/cms/posts/{id}/views` | PostViewsUpdateAction |
| CRUD | `/cms/authors/*` | Author CRUD |
| CRUD | `/cms/categories/*` | Categorizable CRUD |
| CRUD | `/cms/tags/*` | Taggable CRUD |
| CRUD | `/cms/comments/*` | Comment CRUD |
| CRUD | `/cms/pages/*` | Page CRUD |

### Public Blog Routes
| Method | Path | Description |
|--------|------|-------------|
| GET | `/blog/posts` | Public post listing |
| GET | `/blog/posts/{id}` | Public post detail |
| GET | `/blog/categories` | Public categories |
| GET | `/blog/tags` | Public tags |
| GET | `/blog/feed.xml` | RSS 2.0 feed (20 recent) |
| GET | `/blog/sitemap.xml` | XML sitemap for SEO |

## Blog Configuration (config/blog.ts)

```typescript
{
  subdomain: 'blog',
  title: 'Stacks Blog',
  description: 'The official Stacks.js blog',
  postsPerPage: 10,
  enableComments: true,
  enableRss: true,
  enableSitemap: true,
  enableSearch: true,
  social: { twitter: '@stacksjs', github: 'stacksjs/stacks' },
  theme: { primaryColor: '#3451b2', logo: '/images/logos/logo-transparent.svg' },
}
```

## Database Tables

| Table | Purpose |
|-------|---------|
| `posts` | Blog posts |
| `authors` | Content creators |
| `pages` | Static pages |
| `comments` | Comments (polymorphic) |
| `tags` | Tag definitions |
| `taggable_models` | Polymorphic pivot: tag ↔ model |
| `categorizables` | Category definitions |
| `categorizable_models` | Polymorphic pivot: category ↔ model |

## Gotchas
- **Polymorphic relations** — categories, tags, and comments use `*_type` fields to support multiple model types
- **Post `content` maps from `body`** — the model attribute is `content` but the DB column mapping comes from `body`
- **Author uses findOrCreate** — `PostStoreAction` auto-creates authors if they don't exist
- **Post store auto-attaches** — creating a post with category/tag data automatically calls `attach()` on pivot tables
- **Post update uses sync** — updating categories/tags uses `sync()` (detaches removed, attaches new)
- **Views increment is atomic** — `PATCH /cms/posts/{id}/views` increments by 1
- **RSS returns 20 items** — hardcoded to 20 most recent published posts
- **Sitemap priorities** — posts 0.8, categories 0.6, blog homepage 0.9
- **Comment status flow** — starts `pending`, can be approved, rejected (→ spam), or trashed

More from stacksjs/stacks

SkillDescription
stacks-actionsUse when working with Stacks server actions — creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 80+ default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/actions/.
stacks-aiUse when integrating AI capabilities into a Stacks application — using Anthropic/OpenAI/Ollama/AWS Bedrock drivers, image generation (DALL-E), vision analysis, RAG/vector search, embeddings, MCP (Model Context Protocol) clients, text summarization, sentiment analysis, content classification, personalization, or the buddy AI assistant. Covers @stacksjs/ai and config/ai.ts.
stacks-aliasUse when working with path aliases in a Stacks project — import resolution, module aliasing, or debugging import paths. Covers @stacksjs/alias which defines 260+ path mappings for the entire framework.
stacks-analyticsUse when adding analytics to a Stacks application — configuring Fathom or self-hosted analytics, generating tracking scripts, privacy-friendly analytics setup, or the analytics configuration. Covers @stacksjs/analytics and config/analytics.ts.
stacks-apiUse when building, modifying, or debugging API endpoints in a Stacks application — defining routes, handling requests, API middleware, working with the API server, HTTP client (fetcher), API resources, or OpenAPI generation. Covers both @stacksjs/api utilities and the stacks-api server implementation.
stacks-arraysUse when working with array utilities in Stacks — statistical operations (average, median, mode, standard deviation, z-score, percentile, covariance), array manipulation (unique, flatten, partition, shuffle, sample, move), containment checks, or the Arr facade. Covers @stacksjs/arrays.
stacks-authUse when implementing authentication, authorization, passkeys, TOTP/2FA, RBAC, gates, policies, session auth, token management, email verification, password resets, or rate limiting in a Stacks application. Covers the @stacksjs/auth package, config/auth.ts, app/Gates.ts, and app/Middleware/.
stacks-auto-importsUse when working with the Stacks auto-import system — understanding how browser and server auto-imports work, configuring auto-imported functions/models/composables, the auto-import manifests, type generation, or how globals are injected. Covers the auto-import pipeline at storage/framework/auto-imports/.
stacks-brandkitUse when defining or exploring a brand identity for a Stacks project and you want premium brand-kit REFERENCE IMAGES first - logo directions, palettes, typography specimens, identity boards, and application/mockup decks. Generates IMAGES ONLY, never code. Trained for minimalist, cinematic, editorial, dark-tech, luxury, cultural, security, gaming, developer-tool, and consumer-app brand systems with intentional logo concepting, sparse type, strong symbolic meaning, and disciplined palettes a coding step can build against.
stacks-browseUse for headless browser QA on Stacks applications — navigation, screenshots, responsive testing, console/network monitoring, and accessibility snapshots. Dependency-free: drives a system browser over the Chrome DevTools Protocol using only Bun (no Playwright/Puppeteer). Invoke with /stacks-browse.