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 適用後に組み込まれる正確なツリーです。固定先:
// Metadatos de página con valores por defecto del sitio. Devuelve un objeto compatible con `Metadata` de Next.js
// (sin importar `next`) y, para otros frameworks, `metaTags()` genera las etiquetas.
import { z } from 'zod';
import type { AdminField } from '../contracts/index.ts';
import { getSiteSettings, type SiteSettings } from '../site/index.ts';
import { absoluteUrl, siteUrl } from './url.ts';
/** Campos SEO que cualquier colección puede añadir a su esquema: `schema: Page.extend({ seo: SeoFieldsSchema })`. */
export const SeoFieldsSchema = z
.object({
title: z.string().max(70).optional(),
description: z.string().max(170).optional(),
image: z.string().max(500).optional(),
noindex: z.boolean().optional(),
})
.default({});
export const seoAdminFields: AdminField[] = [
{ name: 'seo.title', label: 'SEO title (≤ 70)', type: 'text' },
{ name: 'seo.description', label: 'SEO description (≤ 170)', type: 'textarea' },
{ name: 'seo.image', label: 'Social image (URL or path)', type: 'text' },
{ name: 'seo.noindex', label: 'Hide from search engines', type: 'boolean' },
];
export type PageSeo = {
title?: string;
description?: string;
/** Ruta canónica de la página (`/blog/x`). */
path: string;
/** Imagen social (URL o ruta). Por defecto la de los ajustes. */
image?: string;
locale?: string;
/** Versiones en otros idiomas: locale → ruta. Genera `hreflang` (incluye `x-default` con el idioma por defecto). */
alternates?: Record<string, string>;
defaultLocale?: string;
type?: 'website' | 'article' | 'product';
publishedTime?: Date;
modifiedTime?: Date;
noindex?: boolean;
};
export type SeoMetadata = {
title: string;
description?: string;
alternates: { canonical: string; languages?: Record<string, string> };
openGraph: {
title: string;
description?: string;
url: string;
siteName?: string;
locale?: string;
type: 'website' | 'article';
images?: Array<{ url: string }>;
publishedTime?: string;
modifiedTime?: string;
};
twitter: { card: 'summary' | 'summary_large_image'; title: string; description?: string; images?: string[] };
robots?: { index: boolean; follow: boolean };
};
/** Une la página con los ajustes del sitio: título `Página · Sitio`, imagen y descripción por defecto. */
export function buildMetadataWith(page: PageSeo, settings: SiteSettings | null, base: URL = siteUrl()): SeoMetadata {
const name = settings?.name;
const title = page.title ? (name && page.title !== name ? `${page.title} · ${name}` : page.title) : (name ?? '');
const description = page.description ?? settings?.tagline;
const canonical = absoluteUrl(page.path, base);
const image = page.image ?? settings?.ogImage;
const languages = page.alternates
? Object.fromEntries([
...Object.entries(page.alternates).map(([l, p]) => [l, absoluteUrl(p, base)]),
...(page.defaultLocale && page.alternates[page.defaultLocale] ? [['x-default', absoluteUrl(page.alternates[page.defaultLocale]!, base)]] : []),
])
: undefined;
const noindex = page.noindex || process.env.SEO_NOINDEX === '1';
return {
title,
...(description && { description }),
alternates: { canonical, ...(languages && { languages }) },
openGraph: {
title,
...(description && { description }),
url: canonical,
...(name && { siteName: name }),
...(page.locale && { locale: page.locale.replace('-', '_') }),
type: page.type === 'article' ? 'article' : 'website',
...(image && { images: [{ url: absoluteUrl(image, base) }] }),
...(page.publishedTime && { publishedTime: page.publishedTime.toISOString() }),
...(page.modifiedTime && { modifiedTime: page.modifiedTime.toISOString() }),
},
twitter: {
card: image ? 'summary_large_image' : 'summary',
title,
...(description && { description }),
...(image && { images: [absoluteUrl(image, base)] }),
},
...(noindex && { robots: { index: false, follow: true } }),
};
}
/** Como `buildMetadataWith`, leyendo los ajustes publicados de @core/site. */
export async function buildMetadata(page: PageSeo): Promise<SeoMetadata> {
return buildMetadataWith(page, await getSiteSettings({ locale: page.locale }));
}
const esc = (s: string) => s.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
/** Etiquetas `<title>`/`<meta>`/`<link>` en HTML para frameworks sin API de metadatos (Hono JSX, plantillas). */
export function metaTags(m: SeoMetadata): string {
const tags = [`<title>${esc(m.title)}</title>`, `<link rel="canonical" href="${esc(m.alternates.canonical)}">`];
const meta = (attr: 'name' | 'property', key: string, value?: string) => value && tags.push(`<meta ${attr}="${key}" content="${esc(value)}">`);
meta('name', 'description', m.description);
for (const [lang, href] of Object.entries(m.alternates.languages ?? {})) tags.push(`<link rel="alternate" hreflang="${esc(lang)}" href="${esc(href)}">`);
meta('property', 'og:title', m.openGraph.title);
meta('property', 'og:description', m.openGraph.description);
meta('property', 'og:url', m.openGraph.url);
meta('property', 'og:site_name', m.openGraph.siteName);
meta('property', 'og:locale', m.openGraph.locale);
meta('property', 'og:type', m.openGraph.type);
for (const i of m.openGraph.images ?? []) meta('property', 'og:image', i.url);
meta('name', 'twitter:card', m.twitter.card);
if (m.robots) meta('name', 'robots', `${m.robots.index ? 'index' : 'noindex'}, ${m.robots.follow ? 'follow' : 'nofollow'}`);
return tags.join('\n');
}
このパッケージは MCP サーバーを宣言していません。
| バージョン | コミット | 公開日 | スキャン |
|---|---|---|---|
| 1.0.0 | cf4e3c3 | 3 時間前 | スキャン合格 |
- 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 か月以内に公開達成
- 認証済みの公開者達成
- 概要とキーワード達成
- 報告
- 問題を見つけましたか?