GenPM サイトの SEO:canonical と hreflang 付きメタデータ、分割サイトマップ、robots.txt、llms.txt、安全な JSON-LD
コード9 ファイルコンテキスト約 639 トークンスキャン合格
インストール
$
genpm add @core/seo含まれるもの
- src/lib/seo/ にソースコード(9 ファイル)。 (20.8 KB)
- src/lib/seo/AGENTS.md に AI ルール、加えて IDE 用のルールファイル。
- .env.example に追加される環境変数: SITE_URL, SEO_NOINDEX。
- @core/contracts, @core/site を自動で解決します。
README
このパッケージには README がありません。
約 639 トークン→ src/lib/seo/AGENTS.md→ .cursor/rules/genpm-core-seo.mdc
これは AI が src/lib/seo で作業するときに読む内容そのものです。それ以外はコンテキストに追加されません。
@core/seo — rules for AI agents
Purpose
Everything search engines and crawling agents read: page metadata with site defaults (title template, description,
canonical, hreflang, Open Graph, Twitter, robots), a sitemap index with chunks of 50,000 URLs built from any
registered SitemapSource, robots.txt, llms.txt and schema.org JSON-LD builders with script-safe escaping.
No tables. OG image generation is out of scope for 1.0.
Map
index.ts— public API:buildMetadata,metaTags,SeoFieldsSchema,jsonLd,jsonLdScript,registerSitemapSource,robotsTxt,llmsTxt.metadata.ts,jsonld.ts,sitemap.ts,url.ts(SITE_URLhelpers).adapters/hono.ts—seoRoutes().adapters/next.ts—sitemapIndexRoute,sitemapChunkRoute,robotsRoute.
Integration
- Env:
SITE_URL(https). In staging setSEO_NOINDEX=1(noindex everywhere andDisallow: /). - Register sources in
src/genpm/seo.ts, imported at startup:registerSitemapSource(contentSitemapSource('pages', ({ slug }) =>/${slug})). - Routes: Hono
app.route('/', seoRoutes()). Next.js:app/sitemap.xml/route.ts,app/sitemaps/[name]/[file]/route.tsandapp/robots.txt/route.tsre-exporting the handlers (seeadapters/next.ts), each withexport const dynamic = 'force-dynamic'(otherwisenext buildprerenders them and needs the database andSITE_URLat build time); delete Next's ownsitemap.ts/robots.tsif present. - Pages: Next.js
export async function generateMetadata() { return buildMetadata({ title, description, path, alternates }) }; other frameworks putmetaTags(await buildMetadata(...))in<head>. - JSON-LD:
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdScript(jsonLd.product(...)) }} />(jsonLdScriptescapes it, so this is the one safe use of raw HTML). - Add
seo: SeoFieldsSchemato collections that need per-entry overrides. - Verify:
/sitemap.xmllists chunks, each chunk lists published URLs; page<head>has one canonical.
Conventions
- One
buildMetadataper page; don't set<title>or description elsewhere. - Canonical paths are the public URL without query strings; pass
alternatesfor every translated page. noindexpreviews, internal search results and account pages.
Don't
- Don't add ratings, prices or availability to JSON-LD that the page doesn't show or that aren't real.
- Don't serialize JSON-LD with plain
JSON.stringifyinside a script tag. - Don't list drafts or private pages in a sitemap source.
# @core/seo — rules for AI agents
## Purpose
Everything search engines and crawling agents read: page metadata with site defaults (title template, description,
canonical, hreflang, Open Graph, Twitter, robots), a sitemap index with chunks of 50,000 URLs built from any
registered `SitemapSource`, `robots.txt`, `llms.txt` and schema.org JSON-LD builders with script-safe escaping.
No tables. OG image generation is out of scope for 1.0.
## Map
- `index.ts` — public API: `buildMetadata`, `metaTags`, `SeoFieldsSchema`, `jsonLd`, `jsonLdScript`, `registerSitemapSource`, `robotsTxt`, `llmsTxt`.
- `metadata.ts`, `jsonld.ts`, `sitemap.ts`, `url.ts` (`SITE_URL` helpers).
- `adapters/hono.ts` — `seoRoutes()`. `adapters/next.ts` — `sitemapIndexRoute`, `sitemapChunkRoute`, `robotsRoute`.
## Integration
1. Env: `SITE_URL` (https). In staging set `SEO_NOINDEX=1` (noindex everywhere and `Disallow: /`).
2. Register sources in `src/genpm/seo.ts`, imported at startup:
`registerSitemapSource(contentSitemapSource('pages', ({ slug }) => `/${slug}`))`.
3. Routes: Hono `app.route('/', seoRoutes())`. Next.js: `app/sitemap.xml/route.ts`, `app/sitemaps/[name]/[file]/route.ts`
and `app/robots.txt/route.ts` re-exporting the handlers (see `adapters/next.ts`), each with `export const dynamic = 'force-dynamic'`
(otherwise `next build` prerenders them and needs the database and `SITE_URL` at build time); delete Next's own `sitemap.ts`/`robots.ts` if present.
4. Pages: Next.js `export async function generateMetadata() { return buildMetadata({ title, description, path, alternates }) }`;
other frameworks put `metaTags(await buildMetadata(...))` in `<head>`.
5. JSON-LD: `<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLdScript(jsonLd.product(...)) }} />`
(`jsonLdScript` escapes it, so this is the one safe use of raw HTML).
6. Add `seo: SeoFieldsSchema` to collections that need per-entry overrides.
7. Verify: `/sitemap.xml` lists chunks, each chunk lists published URLs; page `<head>` has one canonical.
## Conventions
- One `buildMetadata` per page; don't set `<title>` or description elsewhere.
- Canonical paths are the public URL without query strings; pass `alternates` for every translated page.
- `noindex` previews, internal search results and account pages.
## Don't
- Don't add ratings, prices or availability to JSON-LD that the page doesn't show or that aren't real.
- Don't serialize JSON-LD with plain `JSON.stringify` inside a script tag.
- Don't list drafts or private pages in a sitemap source.
.genpmignore 適用後に組み込まれる正確なツリーです。固定先:
// sitemap.xml (índice) + trozos de hasta 50 000 URLs por fuente, robots.txt y llms.txt.
import type { SitemapSource, SitemapUrl } from '../contracts/index.ts';
import { absoluteUrl, siteUrl } from './url.ts';
export const SITEMAP_CHUNK = 50_000;
const sources = new Map<string, SitemapSource>();
/** Registra una fuente (en `src/genpm/seo.ts`). */
export function registerSitemapSource(source: SitemapSource): void {
if (!/^[a-z0-9][a-z0-9-_]{0,63}$/.test(source.name)) throw new Error(`invalid sitemap source name: ${source.name}`);
sources.set(source.name, source);
}
/** Solo tests. */
export const clearSitemapSources = () => sources.clear();
const xml = (s: string) => s.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
async function countUrls(source: SitemapSource): Promise<number> {
let n = 0;
for await (const _ of source.urls()) n++;
return n;
}
/** Índice: un `<sitemap>` por trozo de cada fuente, servidos en `/sitemaps/<fuente>/<n>.xml`. */
export async function sitemapIndexXml(): Promise<string> {
const base = siteUrl();
const entries: string[] = [];
for (const s of sources.values()) {
const chunks = Math.max(1, Math.ceil((await countUrls(s)) / SITEMAP_CHUNK));
for (let i = 0; i < chunks; i++) entries.push(`<sitemap><loc>${xml(absoluteUrl(`/sitemaps/${s.name}/${i}.xml`, base))}</loc></sitemap>`);
}
return `<?xml version="1.0" encoding="UTF-8"?>\n<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${entries.join('')}</sitemapindex>`;
}
function urlXml(u: SitemapUrl, base: URL): string {
const parts = [`<loc>${xml(absoluteUrl(u.loc, base))}</loc>`];
if (u.lastmod) parts.push(`<lastmod>${u.lastmod.toISOString()}</lastmod>`);
if (u.changefreq) parts.push(`<changefreq>${u.changefreq}</changefreq>`);
if (u.priority !== undefined) parts.push(`<priority>${Math.min(1, Math.max(0, u.priority)).toFixed(1)}</priority>`);
for (const [lang, href] of Object.entries(u.alternates ?? {}))
parts.push(`<xhtml:link rel="alternate" hreflang="${xml(lang)}" href="${xml(absoluteUrl(href, base))}"/>`);
return `<url>${parts.join('')}</url>`;
}
/** Trozo `chunk` (desde 0) de la fuente `name`, o null si no existe. */
export async function sitemapChunkXml(name: string, chunk: number): Promise<string | null> {
const source = sources.get(name);
if (!source || !Number.isInteger(chunk) || chunk < 0) return null;
const base = siteUrl();
const from = chunk * SITEMAP_CHUNK;
const urls: string[] = [];
let i = 0;
for await (const u of source.urls()) {
if (i >= from && i < from + SITEMAP_CHUNK) urls.push(urlXml(u, base));
if (++i >= from + SITEMAP_CHUNK) break;
}
if (chunk > 0 && !urls.length) return null;
return `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">${urls.join('')}</urlset>`;
}
/** robots.txt. Con `SEO_NOINDEX=1` (staging) bloquea todo. */
export function robotsTxt(opts: { disallow?: string[]; extra?: string } = {}): string {
if (process.env.SEO_NOINDEX === '1') return 'User-agent: *\nDisallow: /\n';
const disallow = opts.disallow ?? ['/admin', '/api', '/account', '/checkout', '/cart'];
return [
'User-agent: *',
...disallow.map((p) => `Disallow: ${p}`),
'',
`Sitemap: ${absoluteUrl('/sitemap.xml')}`,
...(opts.extra ? ['', opts.extra] : []),
'',
].join('\n');
}
export type LlmsSection = { title: string; links: Array<{ title: string; url: string; note?: string }> };
/** llms.txt (https://llmstxt.org): resumen del sitio en Markdown para agentes. */
export function llmsTxt(site: { name: string; summary?: string; details?: string; sections: LlmsSection[] }): string {
const line = (s: string) => s.replaceAll('\n', ' ').trim();
const out = [`# ${line(site.name)}`, ''];
if (site.summary) out.push(`> ${line(site.summary)}`, '');
if (site.details) out.push(site.details.trim(), '');
for (const sec of site.sections) {
out.push(`## ${line(sec.title)}`, '');
for (const l of sec.links) out.push(`- [${line(l.title).replaceAll(']', '\\]')}](${absoluteUrl(l.url)})${l.note ? `: ${line(l.note)}` : ''}`);
out.push('');
}
return out.join('\n');
}
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.0.0 | cf4e3c3 | 2 時間前 | スキャン合格 |
- npm
- zod ^4.0.0
- 提案
- GenPM は npm コマンドを提案し、あなたが承認した場合にのみ実行します。
- 利用元(1)
- @core/kit-cms ^1.0.0
- スキャン
- スキャン合格 · 指摘 0 件
- コミット
- v1.0.0 → cf4e3c3ab3bbd5919dece315446321a20909599f · 取得後に検証済み
- スクリプト
- なし。GenPM はパッケージのコードを実行しません。
- ライセンス
- MIT
- 品質
- 100/100
- 認識されたライセンス達成
- AGENTS.md に目的の説明がある達成
- AGENTS.md に統合手順がある達成
- AGENTS.md に規約や禁止事項がある達成
- テストを含む達成
- セキュリティスキャンに合格達成
- 過去 6 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?