Réglages du site, menus imbriqués modifiables et redirections gérées sans boucles ni open redirect
Installer
genpm add @core/siteCe que vous obtenez
- Source dans src/lib/site/, 10 fichiers. (19,3 ko)
- Règles IA dans src/lib/site/AGENTS.md, plus les fichiers de règles de l’IDE.
- Variables d’environnement ajoutées à .env.example : SITE_URL, SITE_REDIRECT_HOSTS.
- Résout @core/content, @core/contracts, @core/db pour vous.
README
Ce paquet n’a pas de README.
Voici exactement ce que lit votre IA quand elle travaille dans src/lib/site. Rien d’autre n’est ajouté à son contexte.
@core/site — rules for AI agents
Purpose
What every site has: settings (name, logo, contact, social profiles, legal page paths) as a @core/content singleton,
editable menus up to 3 levels as a content collection (slug = location: header, footer), and redirects managed
from the admin with loop detection, safe targets (site paths or https to allowed hosts) and hit counters.
No page content, no SEO tags (@core/seo reads these settings).
Map
index.ts— public API:getSiteSettings,getMenu,upsertRedirect(byfrom),updateRedirect(by id),resolveRedirect,redirectResponse,siteAdminResources.content.ts—siteSettings,menusand their schemas.redirects.ts— redirect logic.links.ts— link validation.adapters/hono.ts—redirectMiddleware.adapters/next.ts—redirectFor(req).
Integration
- Env:
SITE_URL(https://example.com), optionalSITE_REDIRECT_HOSTS(comma-separated external hosts allowed as redirect targets). Migrations as insrc/lib/db/AGENTS.md. - Import this module once at startup (it registers the
site_settingsandmenuscontent definitions). - Seed initial values with
seedEntry(@core/content): settings and theheader/footermenus, using the site's current ones. - Render:
const settings = await getSiteSettings({ locale }),const items = await getMenu('header', { locale }). - Redirects: Hono
app.use(redirectMiddleware); Next.jsmiddleware.tswithruntime: 'nodejs'callingredirectFor(req). - Add
...siteAdminResources()tosrc/genpm/admin.ts. - Verify: create a redirect
/old → /newand request/old(301 to/new).
Conventions
- Menu links are site paths,
https://,mailto:ortel:; open external links withrel="noopener". - Redirect sources are exact paths without query; the incoming query string is preserved.
- Permissions:
site_settings:*,menus:*,redirects:read|create|update|delete.
Don't
- Don't hardcode the site name, logo or menus in components once this module is installed.
- Don't build redirects from user input; only from the admin through
upsertRedirect/updateRedirect. Editing changes that row by id and fails withinvalid_path(422) if the newfrombelongs to another redirect. - Don't put legal text here; only the paths of the legal pages.
# @core/site — rules for AI agents
## Purpose
What every site has: settings (name, logo, contact, social profiles, legal page paths) as a @core/content singleton,
editable menus up to 3 levels as a content collection (slug = location: `header`, `footer`), and redirects managed
from the admin with loop detection, safe targets (site paths or https to allowed hosts) and hit counters.
No page content, no SEO tags (@core/seo reads these settings).
## Map
- `index.ts` — public API: `getSiteSettings`, `getMenu`, `upsertRedirect` (by `from`), `updateRedirect` (by id), `resolveRedirect`, `redirectResponse`, `siteAdminResources`.
- `content.ts` — `siteSettings`, `menus` and their schemas. `redirects.ts` — redirect logic. `links.ts` — link validation.
- `adapters/hono.ts` — `redirectMiddleware`. `adapters/next.ts` — `redirectFor(req)`.
## Integration
1. Env: `SITE_URL` (`https://example.com`), optional `SITE_REDIRECT_HOSTS` (comma-separated external hosts allowed as redirect targets). Migrations as in `src/lib/db/AGENTS.md`.
2. Import this module once at startup (it registers the `site_settings` and `menus` content definitions).
3. Seed initial values with `seedEntry` (@core/content): settings and the `header`/`footer` menus, using the site's current ones.
4. Render: `const settings = await getSiteSettings({ locale })`, `const items = await getMenu('header', { locale })`.
5. Redirects: Hono `app.use(redirectMiddleware)`; Next.js `middleware.ts` with `runtime: 'nodejs'` calling `redirectFor(req)`.
6. Add `...siteAdminResources()` to `src/genpm/admin.ts`.
7. Verify: create a redirect `/old → /new` and request `/old` (301 to `/new`).
## Conventions
- Menu links are site paths, `https://`, `mailto:` or `tel:`; open external links with `rel="noopener"`.
- Redirect sources are exact paths without query; the incoming query string is preserved.
- Permissions: `site_settings:*`, `menus:*`, `redirects:read|create|update|delete`.
## Don't
- Don't hardcode the site name, logo or menus in components once this module is installed.
- Don't build redirects from user input; only from the admin through `upsertRedirect` / `updateRedirect`. Editing changes that row by id and fails with `invalid_path` (422) if the new `from` belongs to another redirect.
- Don't put legal text here; only the paths of the legal pages.
L’arborescence exacte qui sera injectée, après .genpmignore. Épinglée à
// Ajustes del sitio (singleton) y menús (colección; el slug es la ubicación: header, footer…) sobre @core/content.
import { z } from 'zod';
import type { AdminField } from '../contracts/index.ts';
import { defineCollection, defineSingleton, getEntry, getSingleton } from '../content/index.ts';
import { isSafeLink } from './links.ts';
const Link = z.string().max(500).refine(isSafeLink, 'Use a site path (/about), an https:// URL, mailto: or tel:');
export const SocialPlatforms = ['facebook', 'instagram', 'x', 'tiktok', 'youtube', 'linkedin', 'pinterest', 'github', 'whatsapp', 'other'] as const;
export const SiteSettingsSchema = z.object({
name: z.string().min(1).max(120),
tagline: z.string().max(200).optional(),
/** Claves de @core/media o URLs (logo, favicon, imagen social por defecto). */
logo: z.string().max(500).optional(),
favicon: z.string().max(500).optional(),
ogImage: z.string().max(500).optional(),
contactEmail: z.email().optional(),
phone: z.string().max(40).optional(),
address: z.string().max(300).optional(),
social: z.array(z.object({ platform: z.enum(SocialPlatforms), url: Link })).max(20).default([]),
/** Rutas de las páginas legales (las plantillas las ponen los kits; aquí solo se asignan). */
legal: z
.object({ privacy: Link.optional(), terms: Link.optional(), cookies: Link.optional(), imprint: Link.optional(), returns: Link.optional() })
.default({}),
});
export type SiteSettings = z.infer<typeof SiteSettingsSchema>;
export type MenuItem = { label: string; href: string; newTab?: boolean; children?: MenuItem[] };
const item = (depth: number): z.ZodType<MenuItem> =>
z.object({
label: z.string().min(1).max(80),
href: Link,
newTab: z.boolean().optional(),
...(depth > 1 ? { children: z.array(z.lazy(() => item(depth - 1))).max(30).optional() } : {}),
}) as z.ZodType<MenuItem>;
/** Hasta 3 niveles de anidamiento. */
export const MenuSchema = z.object({ items: z.array(item(3)).max(30).default([]) });
const settingsFields: AdminField[] = [
{ name: 'name', label: 'Site name', type: 'text', required: true },
{ name: 'tagline', label: 'Tagline', type: 'text' },
{ name: 'logo', label: 'Logo', type: 'media' },
{ name: 'favicon', label: 'Favicon', type: 'media' },
{ name: 'ogImage', label: 'Default social image', type: 'media' },
{ name: 'contactEmail', label: 'Contact email', type: 'email' },
{ name: 'phone', label: 'Phone', type: 'text' },
{ name: 'address', label: 'Address', type: 'textarea' },
{ name: 'social', label: 'Social profiles', type: 'json' },
{ name: 'legal', label: 'Legal pages', type: 'json' },
];
export const siteSettings = defineSingleton('site_settings', {
schema: SiteSettingsSchema,
label: { singular: 'Site settings', plural: 'Site settings' },
fields: settingsFields,
titleField: 'name',
});
export const menus = defineCollection('menus', {
schema: MenuSchema,
label: { singular: 'Menu', plural: 'Menus' },
fields: [{ name: 'items', label: 'Items', type: 'json', required: true }],
});
/** Ajustes publicados (o null si aún no se han guardado). */
export async function getSiteSettings(opts: { locale?: string; draft?: boolean } = {}): Promise<SiteSettings | null> {
return (await getSingleton<SiteSettings>('site_settings', opts))?.data ?? null;
}
/** Elementos del menú de una ubicación (`header`, `footer`…); vacío si no existe. */
export async function getMenu(location: string, opts: { locale?: string; draft?: boolean } = {}): Promise<MenuItem[]> {
return (await getEntry<{ items: MenuItem[] }>('menus', location, opts))?.data.items ?? [];
}
Ce paquet ne déclare aucun serveur MCP.
| Version | Commit | Publié | Analyse |
|---|---|---|---|
| 1.1.0 | b7c13fa | il y a 3 heures | analyse réussie |
- npm
- zod ^4.0.0
- proposé
- GenPM propose la commande npm et ne l’exécute que si vous acceptez.
- Utilisé par (2)
- @core/kit-cms ^1.0.0@core/seo ^1.0.0
- analyse
- analyse réussie · 0 problème
- commit
- v1.1.0 → b7c13fa9b316c9880bceb62d510ee9661a76ae08 · vérifié après téléchargement
- scripts
- Aucun. GenPM n’exécute jamais le code des paquets.
- licence
- MIT
- Qualité
- 100/100
- Licence reconnuevalidé
- AGENTS.md explique son objectifvalidé
- AGENTS.md donne les étapes d’intégrationvalidé
- AGENTS.md liste conventions ou interditsvalidé
- Contient des testsvalidé
- Analyse de sécurité réussievalidé
- Publié au cours des 6 derniers moisvalidé
- Éditeur vérifiévalidé
- Résumé et mots-clésvalidé
- signalement
- Vous avez repéré un problème ?