@core/content 기반 블로그: 글, 작성자, 카테고리, 태그, 시리즈, 관련 글, 아카이브, RSS/Atom/JSON 피드
설치
genpm add @core/blog포함 내용
- src/lib/blog/에 소스 코드, 파일 7개. (20kB)
- src/lib/blog/AGENTS.md에 AI 규칙, 그리고 IDE 규칙 파일.
- .env.example에 추가되는 환경 변수: SITE_URL.
- @core/content, @core/contracts, @core/db, @core/rich-text을(를) 자동으로 해결합니다.
README
이 패키지에는 README가 없습니다.
이것이 AI가 src/lib/blog에서 작업할 때 읽는 내용 그대로입니다. 그 외에는 컨텍스트에 아무것도 추가되지 않습니다.
@core/blog — rules for AI agents
Purpose
Blog domain built only from @core/content collections (posts, authors, categories, tags, series; no tables of
its own): filtered and paginated listings, a post with its relations resolved, related posts, monthly archive, series
order, RSS 2.0 / Atom / JSON Feed, sitemap and search sources and admin resources. No routes or templates (those
come from the app or @core/kit-blog), no comments or newsletter (separate modules).
Map
index.ts— public API:listPosts,getPost,relatedPosts,archive,postsInMonth,rssFeed,atomFeed,jsonFeed,blogSitemapSource,blogSearchSource,blogAdminResources.collections.ts— schemas (PostSchemausesRichTextSchema).posts.ts— reads.feeds.ts— feeds.sources.ts— sitemap/search/admin.
Integration
- Import this module at startup (it registers the collections) and set
SITE_URL. - Routes (Next.js example):
/blog→listPosts({ page, locale });/blog/[slug]→getPost(slug, { locale, draft })(404 when null) +relatedPosts(post);/blog/category/[slug]→listPosts({ category }); same fortag,author,series. - Feeds:
/blog/feed.xml→rssFeed({ title, description })(application/rss+xml),/blog/atom.xml,/blog/feed.json. - Register
blogSitemapSource()insrc/genpm/seo.ts,blogSearchSource()insrc/genpm/search.tsand...blogAdminResources()insrc/genpm/admin.ts. - Render the body with
<RichText doc={post.data.body} />(@core/rich-text) and addjsonLd.article(...)(@core/seo). - Verify: publish a post, it appears in
/blog, the feed and the sitemap; a scheduled post does not.
Conventions
- Relations are slugs: create the author/category/tag entries with the same slug.
membersOnlyposts: the page must hide the body unless the user is entitled; feeds and search only expose the excerpt.- Paths default to
/blog/<slug>; pass your ownpath/postPathif the blog lives elsewhere.
Don't
- Don't query
content_entriesdirectly for posts; uselistPosts(it respects publication and locale fallback). - Don't put the full body of members-only posts in feeds, search or meta descriptions.
# @core/blog — rules for AI agents
## Purpose
Blog domain built only from @core/content collections (`posts`, `authors`, `categories`, `tags`, `series`; no tables of
its own): filtered and paginated listings, a post with its relations resolved, related posts, monthly archive, series
order, RSS 2.0 / Atom / JSON Feed, sitemap and search sources and admin resources. No routes or templates (those
come from the app or @core/kit-blog), no comments or newsletter (separate modules).
## Map
- `index.ts` — public API: `listPosts`, `getPost`, `relatedPosts`, `archive`, `postsInMonth`, `rssFeed`, `atomFeed`, `jsonFeed`, `blogSitemapSource`, `blogSearchSource`, `blogAdminResources`.
- `collections.ts` — schemas (`PostSchema` uses `RichTextSchema`). `posts.ts` — reads. `feeds.ts` — feeds. `sources.ts` — sitemap/search/admin.
## Integration
1. Import this module at startup (it registers the collections) and set `SITE_URL`.
2. Routes (Next.js example): `/blog` → `listPosts({ page, locale })`; `/blog/[slug]` → `getPost(slug, { locale, draft })`
(404 when null) + `relatedPosts(post)`; `/blog/category/[slug]` → `listPosts({ category })`; same for `tag`, `author`, `series`.
3. Feeds: `/blog/feed.xml` → `rssFeed({ title, description })` (`application/rss+xml`), `/blog/atom.xml`, `/blog/feed.json`.
4. Register `blogSitemapSource()` in `src/genpm/seo.ts`, `blogSearchSource()` in `src/genpm/search.ts` and `...blogAdminResources()` in `src/genpm/admin.ts`.
5. Render the body with `<RichText doc={post.data.body} />` (@core/rich-text) and add `jsonLd.article(...)` (@core/seo).
6. Verify: publish a post, it appears in `/blog`, the feed and the sitemap; a scheduled post does not.
## Conventions
- Relations are slugs: create the author/category/tag entries with the same slug.
- `membersOnly` posts: the page must hide the body unless the user is entitled; feeds and search only expose the excerpt.
- Paths default to `/blog/<slug>`; pass your own `path`/`postPath` if the blog lives elsewhere.
## Don't
- Don't query `content_entries` directly for posts; use `listPosts` (it respects publication and locale fallback).
- Don't put the full body of members-only posts in feeds, search or meta descriptions.
.genpmignore 적용 후 주입될 정확한 트리입니다. 고정 대상:
// Feeds RSS 2.0, Atom 1.0 y JSON Feed 1.1 con los últimos posts publicados (nunca borradores ni programados).
import { getEntry, listEntries } from '../content/index.ts';
import { type RenderOptions, toHtml } from '../rich-text/index.ts';
import type { AuthorData, PostData } from './collections.ts';
import { summarize } from './posts.ts';
export type FeedOptions = {
/** Origen https del sitio (por defecto SITE_URL). */
siteUrl?: string;
title: string;
description?: string;
/** Ruta del blog y de cada post. */
blogPath?: string;
postPath?: (slug: string) => string;
locale?: string;
limit?: number;
/** Incluir el contenido completo (si no, solo el extracto). Los posts `membersOnly` siempre van solo con extracto. */
fullContent?: boolean;
render?: RenderOptions;
};
const xml = (s: string) => s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
async function items(o: FeedOptions) {
const base = new URL(o.siteUrl ?? process.env.SITE_URL ?? '');
const path = o.postPath ?? ((s: string) => `${o.blogPath ?? '/blog'}/${s}`);
const entries = await listEntries<PostData>('posts', { locale: o.locale, orderBy: '-publishedAt', limit: Math.min(o.limit ?? 20, 100) });
const out = [];
for (const e of entries) {
const s = summarize(e);
const names: string[] = [];
for (const a of s.authors) names.push((await getEntry<AuthorData>('authors', a))?.data.name ?? a);
out.push({
...s,
url: new URL(path(s.slug), base).toString(),
html: o.fullContent && !s.membersOnly ? toHtml(e.data.body, o.render) : `<p>${xml(s.excerpt)}</p>`,
authorNames: names,
});
}
return { base, items: out, home: new URL(o.blogPath ?? '/blog', base).toString() };
}
export async function rssFeed(o: FeedOptions): Promise<string> {
const { items: list, home } = await items(o);
const body = list
.map(
(i) =>
`<item><title>${xml(i.title)}</title><link>${xml(i.url)}</link><guid isPermaLink="true">${xml(i.url)}</guid>${i.publishedAt ? `<pubDate>${i.publishedAt.toUTCString()}</pubDate>` : ''}<description>${xml(i.excerpt)}</description>${o.fullContent ? `<content:encoded>${xml(i.html)}</content:encoded>` : ''}${i.categories.map((c) => `<category>${xml(c)}</category>`).join('')}</item>`,
)
.join('');
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>${xml(o.title)}</title><link>${xml(home)}</link><description>${xml(o.description ?? o.title)}</description>${o.locale ? `<language>${xml(o.locale)}</language>` : ''}<atom:link href="${xml(`${home}/feed.xml`)}" rel="self" type="application/rss+xml"/>${body}</channel></rss>`;
}
export async function atomFeed(o: FeedOptions): Promise<string> {
const { items: list, home } = await items(o);
const updated = (list[0]?.updatedAt ?? new Date(0)).toISOString();
const body = list
.map(
(i) =>
`<entry><title>${xml(i.title)}</title><link href="${xml(i.url)}"/><id>${xml(i.url)}</id>${i.publishedAt ? `<published>${i.publishedAt.toISOString()}</published>` : ''}<updated>${i.updatedAt.toISOString()}</updated>${i.authorNames.map((n) => `<author><name>${xml(n)}</name></author>`).join('')}<summary>${xml(i.excerpt)}</summary>${o.fullContent ? `<content type="html">${xml(i.html)}</content>` : ''}</entry>`,
)
.join('');
return `<?xml version="1.0" encoding="UTF-8"?>\n<feed xmlns="http://www.w3.org/2005/Atom"${o.locale ? ` xml:lang="${xml(o.locale)}"` : ''}><title>${xml(o.title)}</title><link href="${xml(home)}"/><link rel="self" href="${xml(`${home}/atom.xml`)}"/><id>${xml(home)}</id><updated>${updated}</updated>${body}</feed>`;
}
export async function jsonFeed(o: FeedOptions): Promise<string> {
const { items: list, home } = await items(o);
return JSON.stringify({
version: 'https://jsonfeed.org/version/1.1',
title: o.title,
home_page_url: home,
feed_url: `${home}/feed.json`,
...(o.description && { description: o.description }),
...(o.locale && { language: o.locale }),
items: list.map((i) => ({
id: i.url,
url: i.url,
title: i.title,
summary: i.excerpt,
...(o.fullContent ? { content_html: i.html } : {}),
...(i.publishedAt && { date_published: i.publishedAt.toISOString() }),
date_modified: i.updatedAt.toISOString(),
...(i.authorNames.length && { authors: i.authorNames.map((name) => ({ name })) }),
tags: i.tags,
})),
});
}
이 패키지는 MCP 서버를 선언하지 않습니다.
| 버전 | 커밋 | 게시일 | 검사 |
|---|---|---|---|
| 1.0.1 | 8aaf3ee | 3시간 전 | 검사 통과 |
- npm
- zod ^4.0.0
- 제안됨
- GenPM은 npm 명령을 제안하고, 동의한 경우에만 실행합니다.
- 사용하는 패키지 (1)
- @core/kit-blog ^1.0.0
- 검사
- 검사 통과 · 문제 0건
- 커밋
- v1.0.1 → 8aaf3ee41bc0e822456e2ca2de743f11eee88f20 · 가져온 뒤 검증됨
- 스크립트
- 없음. GenPM은 패키지 코드를 절대 실행하지 않습니다.
- 라이선스
- MIT
- 품질
- 100/100
- 인정된 라이선스충족
- AGENTS.md에 목적 설명충족
- AGENTS.md에 통합 단계충족
- AGENTS.md에 규칙 또는 금지 사항충족
- 테스트 포함충족
- 보안 검사 통과충족
- 최근 6개월 내 게시충족
- 인증된 게시자충족
- 요약과 키워드충족
- 신고
- 문제가 있나요?