Skip to main content

stacks-cms

Use when working with Stacks CMS posts, authors, block-document pages, revisions, menus, redirects, scheduled publishing, blog feeds, or public page serving. Covers @stacksjs/cms, site-scoped CMS models, routes, and actions.

Informations de source

Dépôt
stacksjs/stacks
Dernière activité de la source
11 octobre 2026 à 16:18
Langue détectée de SKILL.md
anglais
Étoiles
633
Forks
20

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
stacks-cms
description
Use when working with Stacks CMS posts, authors, block-document pages, revisions, menus, redirects, scheduled publishing, blog feeds, or public page serving. Covers @stacksjs/cms, site-scoped 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/app/Models/Content/` (Post, Author, Page) - Models: `storage/framework/defaults/app/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 ├── blog.ts # Markdown posts in content/blog: loadPosts, loadPost, renderPostBody, bylines ├── 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 Enable/install the CMS feature through its supported lifecycle, then generate and review migrations for changed models. The legacy posts/authors/pages namespace below coexists with the document-page exports described in [PAGES.md](PAGES.md). For a real page editor or public CMS fallback, start with the validated document APIs rather than raw `pages` column writes. ```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> ``` ### The `commentable` trait A model with `traits: { commentable: true }` (singular; `commentables` is inert) gains instance methods keyed to the record's own id: ```typescript await post.addComment({ body, author_name?, author_email?, user_id?, title? }, { status: 'pending' | 'approved' }) await post.comments() / post.approvedComments() / post.pendingComments() / post.rejectedComments() // oldest first await post.commentCount() ``` Only those five fields are written, so a validated form body can go straight in; status and owner come from the trait. `author_name` / `author_email` are for guest commenters (no account); never render the email. Moderate without a dashboard with `buddy comments:list [--status] [--type]`, `comments:approve <id>`, `comments:reject <id>`, `comments:delete <id>`. ## 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 | The site-wide `/sitemap.xml` merges the published posts in, so crawlers find them from the root sitemap too (see `stacks-server`, "Sitemap and robots.txt"). ## Markdown Blog (content/blog, `@stacksjs/cms/blog`) The simplest blog has no tables: each post is `content/blog/<slug>.md`, shipped with the commit that adds it. Add `resources/views/blog.stx` (index) and `resources/views/blog/[slug].stx` (post); once `blog.stx` exists the views server serves only `/blog/feed.xml` and `/blog/sitemap.xml` itself and leaves the pages to stx, and `/sitemap.xml` lists the posts. Pages, feed and sitemap all read posts through `@stacksjs/cms/blog`, so they agree on what is published. ```ts import { articleHead } from '@stacksjs/cms/article' import { bylineOf, createAuthorResolver, formatPostDate, loadPost, loadPosts, renderPostBody } from '@stacksjs/cms/blog' const posts = loadPosts() // published, featured first then newest; no bodies const post = loadPost(useRoute().params?.slug) // null for a draft, unknown or path-like slug -> notFound() const html = renderPostBody(post.body) // Bun markdown + renderPostHtml; {!! html !!} plus {!! articleHead() !!} // Bylines: create once at module scope (it caches lookups for ten minutes). export const resolveAuthors = createAuthorResolver({ lookup: async username => /* the app's profile */ ({ name, avatar, bio, url: `/${username}` }), }) const byline = bylineOf(await resolveAuthors(post)) // [{ name, initials, avatar, bio, url, joiner, avatarStyle }] ``` Frontmatter is one `key: value` per line: `title` and `date` (YYYY-MM-DD) are required, `draft: true` hides a post everywhere, `featured: true` lists it first, `description` is the excerpt and the feed text, `author` is the feed's plain-text byline, `image`/`imageAlt` the preview picture. Several authors: `authors: ada, grace` plus flat fallbacks `author.ada.name`, `author.ada.avatar`, `author.ada.bio` (a frontmatter bio wins over the profile's). One author may use `authorUsername`/`authorAvatar`/`authorBio`. Every key is also on `post.frontmatter`. Also exported: `parseFrontmatter`, `isPublished`, `authorsOf`, `readingMinutes` (`post.minutes`), `formatPostDate`, `joinNames`, `initialsOf`, `sortPosts`, `isPostSlug`, `blogContentDir`. ## Post Bodies: Images, Grids, Zoom and Embeds Blog post HTML goes through `renderPostHtml` from `@stacksjs/cms/article`, built on ts-medium-editor's article renderer. The BunPress blog (`core/actions/src/blog.ts`) does this already; an stx-native blog calls it from its post view: ```ts const { renderPostHtml, articleHead, articleScript } = require('@stacksjs/cms/article') post.html = renderPostHtml(markdownHtml) // then {!! articleHead() !!} in @push('head'), {!! articleScript() !!} in @push('scripts') ``` What authors get from plain markdown: - `![alt](/images/a.jpg "Caption")` alone in a paragraph becomes a figure with a caption. - Images in consecutive paragraphs become a grid: two side by side, three with the tallest on the left and two stacked on the right, four as 2x2, more in rows of three and two. Sizes are read from the files in `public/`, so columns are proportioned and nothing crops or shifts. - Lazy loading after the first image, click-to-zoom on every unlinked image. - A bare YouTube, Vimeo or `https://hq.training/a/<id>` link on its own line becomes an embed. Tune the look with custom properties (`--me-radius`, `--me-gap`, `--me-block-space`, `--me-caption-color`, `--me-zoom-backdrop`) on the post body. `imageSize(bytes)` and `publicImageResolver(dir)` are exported for other uses.
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub