npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ngx-docs-markdown-kit/parser-md-seo

v0.1.0

Published

Extension de @ngx-docs-markdown-kit/parser-md que extrae metadatos de SEO del frontmatter, y expone los servicios de Angular (meta tags, JSON-LD) + funciones (sitemap.xml/robots.txt) para aplicarlos.

Readme

@ngx-docs-markdown-kit/parser-md-seo

Extension de @ngx-docs-markdown-kit/parser-md para SEO completo: extrae description/keywords/etc del frontmatter de cada pagina Markdown, y trae los servicios Angular (PageSeoService/JsonLdService) que escriben meta tags/JSON-LD/favicons reales en el <head>, listos para que un crawler los indexe desde el primer render (SSR/prerender).

Uso

import { createParser } from '@ngx-docs-markdown-kit/parser-md';
import { seoExtension } from '@ngx-docs-markdown-kit/parser-md-seo';

const parser = createParser().use(seoExtension());

En app.config.ts, junto al resto de providers:

import { provideSeoSiteName, provideSeoConfig } from '@ngx-docs-markdown-kit/parser-md-seo';
import { SITE_NAME } from './core/site.config';
import { SEO_CONFIG } from '@frugocorp/parser-md-seo/config/seo.config';

providers: [
  provideSeoSiteName(SITE_NAME),
  provideSeoConfig(SEO_CONFIG),
];

Y en cada pagina (ver DocPage/HomePage de create-ngx-docs-site para el caso real):

private readonly pageSeo = inject(PageSeoService);
private readonly jsonLd = inject(JsonLdService);

this.pageSeo.apply({ title, description, slug, siteUrl: config.siteUrl, /* ... */ });
this.jsonLd.apply({ title, description, canonicalUrl, breadcrumb });

SeoConfig (provideSeoConfig())

Config SINCRONA del sitio -- ver el razonamiento completo en el comentario de seo-config.service.ts: nunca un JSON de public/ pedido por HTTP en runtime, siempre una constante TypeScript compilada, para que description/canonical/JSON-LD/favicons esten presentes en el HTML servido desde el primer render.

| Campo | Tipo | Notas | | --- | --- | --- | | description/keywords | string | Default cuando la pagina no trae el suyo en el frontmatter. | | ogImage | string \| null | Una sola imagen (legado/caso minimo) para og:image/twitter:image, sin width/height/type/alt. | | ogImages | OgImage[] | Set completo (banner + cuadrada, etc.), cada una con url/width?/height?/type?/alt? -- tiene PRIORIDAD sobre ogImage cuando no esta vacio. Produce un bloque og:image/og:image:url/:secure_url/:width/:height/:type/:alt por imagen (repetibles, a diferencia del resto de los tags). twitter:image siempre usa la PRIMERA. | | organizationLogo | string \| null | Logo para el JSON-LD de Organization -- separado de ogImage a proposito (Google recomienda cuadrado, Open Graph panoramico). | | siteUrl | string \| null | Dominio absoluto, sin / final. Sin esto, canonical/og:url/og:image/JSON-LD completo se omiten. | | author | string | meta[name="author"], fijo (no varia por pagina). | | robots | string | Default index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1, overridable por pagina (frontmatter robots:). | | themeColor | string | meta[name="theme-color"]. | | socialLinks | string[] | Alimenta sameAs del JSON-LD de Organization. | | contentLanguage | string \| null | meta[http-equiv="content-language"] (ej. "es-MX"). null = se omite. | | xUaCompatible | string \| null | meta[http-equiv="X-UA-Compatible"] (ej. "IE=edge") -- legado, inofensivo en navegadores modernos. null = se omite. | | twitterSite | string \| null | meta[name="twitter:site"] (ej. "@mihandle"). null = se omite. | | favicons | FaviconLink[] | Ver mas abajo. | | manifest | string \| null | link[rel="manifest"] (ej. "/site.webmanifest"). null = se omite. | | msapplicationTileColor | string \| null | meta[name="msapplication-TileColor"]. null = se omite. |

FaviconLink

{ rel: 'icon' | 'apple-touch-icon' | 'mask-icon'; href: string; type?: string; sizes?: string; color?: string }

color solo tiene efecto en rel: 'mask-icon' (Safari pinned tab, exige un SVG monocromo). Un sitio tipico declara varias entradas rel: 'icon' (distintos tamanos/formatos: .ico, PNG 16x16/32x32/96x96, SVG) + una apple-touch-icon (180x180) + opcionalmente una mask-icon. Ver local/project/meta-example.html (fuente del diseno) para un ejemplo completo, y frugocorp_modules/parser-md-seo/config/seo.config.ts (en la raiz de cualquier sitio generado) para el caso real ya armado.

Estos tags (favicons/manifest/content-language/X-UA-Compatible/twitter:site/msapplication-TileColor) se aplican UNA sola vez (en el constructor de PageSeoService, que es un singleton) -- a diferencia de description/og:title/etc, no cambian de una pagina a otra, asi que no hace falta (ni tiene sentido) pasarlos en cada .apply().

Estado

Implementado: seoExtension() (frontmatter), PageSeoService (meta tags + Open Graph + Twitter Card + favicons, ver arriba), JsonLdService (WebSite+Organization+WebPage+BreadcrumbList), provideSeoConfig()/provideSeoSiteName(), orden de colocacion en <head> + formato legible en dev (head-order.util.ts), generate-seo-files.mjs (sitemap.xml/robots.txt, entry point secundario sin dependencias de Angular). Ver el README raiz del repositorio para el diseno completo del monorepo.

parser-md-seo

parser-md-seo

version node npm

Dependencias:

  • @angular/animations: ^22.1.0
  • @angular/cdk: ^22.1.0
  • @angular/common: ^22.1.0
  • @angular/compiler: ^22.1.0
  • @angular/core: ^22.1.0
  • @angular/forms: ^22.1.0
  • @angular/material: ^22.1.0
  • @angular/platform-browser: ^22.1.0
  • @angular/platform-server: ^22.1.0
  • @angular/router: ^22.1.0
  • @angular/ssr: ^22.1.3
  • @ngx-docs-markdown-kit/parser-md: file:./vendor/parser-md/ngx-docs-markdown-kit-parser-md-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-seo: file:./vendor/parser-md-seo/ngx-docs-markdown-kit-parser-md-seo-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-code-block: file:./vendor/parser-md-code-block/ngx-docs-markdown-kit-parser-md-code-block-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-code-block-themes: file:./vendor/parser-md-code-block-themes/ngx-docs-markdown-kit-parser-md-code-block-themes-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-image: file:./vendor/parser-md-image/ngx-docs-markdown-kit-parser-md-image-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-card: file:./vendor/parser-md-card/ngx-docs-markdown-kit-parser-md-card-0.1.0.tgz
  • @ngx-docs-markdown-kit/parser-md-converter: file:./vendor/parser-md-converter/ngx-docs-markdown-kit-parser-md-converter-0.1.0.tgz
  • @ngx-docs-markdown-kit/ui: file:./vendor/ui/ngx-docs-markdown-kit-ui-0.1.0.tgz
  • rxjs: ~7.8.0
  • tslib: ^2.3.0

Apartados de parser-md-seo

  • Docs -- Documentación de @ngx-docs-markdown-kit/parser-md-seo -- meta tags, JSON-LD, sitemap.xml/robots.txt desde el frontmatter.
  • Parser MD SEO -- Meta tags dinamicos, JSON-LD y sitemap/robots build-time para sitios @ngx-docs-markdown-kit.

Docs de parser-md-seo

REGRESAR A APARTADOS DE parser-md-seo


Índice Docs de parser-md-seo

  • Primeros pasos -- Instalación y wiring en app.config.ts.
    • Instalación -- npm install + wiring de la extensión, el config y los providers.
  • Uso -- SeoConfig, los servicios (PageSeoService/JsonLdService/SiteMetaService) y el sitemap.
    • SeoConfig -- El config síncrono del sitio -- por qué nunca es un JSON pedido en runtime.
    • Servicios -- PageSeoService, JsonLdService y SiteMetaService -- qué hace cada uno y cuándo se aplica.
    • sitemap.xml y robots.txt -- buildSitemapXml/buildRobotsTxt -- entry point sin Angular, para scripts de build en Node.
  • Ecosistema -- Relación de @ngx-docs-markdown-kit/parser-md-seo con el resto del kit -- de qué depende y quién la consume.
    • Relación con el ecosistema -- De qué depende @ngx-docs-markdown-kit/parser-md-seo, quién la consume y con quién comparte el manifiesto de contenido.

Primeros pasos de parser-md-seo

< Índice Docs de parser-md-seo


Instalación de parser-md-seo

< Primeros pasos de parser-md-seo


npm install @ngx-docs-markdown-kit/parser-md-seo

Requiere @angular/common/@angular/core/@angular/platform-browser (^22.1.0) y @ngx-docs-markdown-kit/parser-md (^0.0.1) como peer dependencies.

1. Registrar la extensión en el parser

import { createParser } from '@ngx-docs-markdown-kit/parser-md';
import { seoExtension } from '@ngx-docs-markdown-kit/parser-md-seo';

const parser = createParser().use(seoExtension());

Extrae description/keywords/robots/etc. del frontmatter de cada página -- sin esto, meta nunca tiene esos campos disponibles para los servicios de abajo.

2. Proveer el config y el nombre del sitio

En app.config.ts, junto al resto de providers:

import { provideSeoSiteName, provideSeoConfig } from '@ngx-docs-markdown-kit/parser-md-seo';
import { SITE_NAME } from './core/site.config';
import { SEO_CONFIG } from '@frugocorp/parser-md-seo/config/seo.config';

providers: [
  provideSeoSiteName(SITE_NAME),
  provideSeoConfig(SEO_CONFIG),
];

provideSeoSiteName() inyecta el token interno SEO_SITE_NAME con el valor que le pases (acá, SITE_NAME) -- NO tiene default a propósito: a diferencia de otras librerías del ecosistema, un nombre de sitio no tiene ningún valor genérico razonable, tenés que proveerlo siempre. Ver SeoConfig para el resto del config.

3. Aplicar en cada página

private readonly pageSeo = inject(PageSeoService);
private readonly jsonLd = inject(JsonLdService);

this.pageSeo.apply({ title, description, slug, siteUrl: config.siteUrl /* ... */ });
this.jsonLd.apply({ title, description, canonicalUrl, breadcrumb });

Ver Servicios para el detalle completo de ambos.

Uso de parser-md-seo

< Índice Docs de parser-md-seo


SeoConfig de parser-md-seo

< Uso de parser-md-seo


Config SÍNCRONO del sitio -- nunca un JSON de public/ pedido por HTTP en runtime, siempre una constante TypeScript compilada. Con un fetch async, description/canonical/og:image/JSON-LD completo quedarían AUSENTES del HTML que ve un crawler (el fetch no resuelve a tiempo para el render que el SSR serializa) -- inaceptable para SEO real.

export const SEO_CONFIG: Partial<SeoConfig> = {
  description: 'Descripción por default del sitio.',
  keywords: 'palabra1, palabra2',
  siteUrl: 'https://mi-sitio.com',
  author: 'Mi Equipo',
  contentLanguage: 'es',
  favicons: [{ rel: 'icon', href: '/images/icono-128x128.png', type: 'image/png', sizes: '128x128' }],
};

Solo hace falta declarar las claves que querés personalizar -- el resto cae al DEFAULT_SEO_CONFIG de la librería.

| Campo | Tipo | Notas | | --- | --- | --- | | description/keywords | string | Default cuando la página no trae el suyo en el frontmatter. | | ogImage | string \| null | Una sola imagen (caso mínimo) para og:image/twitter:image, sin width/height/type/alt. | | ogImages | OgImage[] | Set completo (banner + cuadrada, etc.), cada una con url/width?/height?/type?/alt? -- tiene PRIORIDAD sobre ogImage cuando no está vacío. Produce un bloque og:image/og:image:url/:secure_url/:width/:height/:type/:alt por imagen (repetibles). twitter:image siempre usa la PRIMERA. | | organizationLogo | string \| null | Logo para el JSON-LD de Organization -- separado de ogImage a propósito (Google recomienda cuadrado, Open Graph panorámico). | | siteUrl | string \| null | Dominio absoluto, sin / final. Sin esto, canonical/og:url/og:image/JSON-LD completo se omiten -- mejor omitir que mandar una URL rota. | | author | string | meta[name="author"], fijo (no varía por página). | | robots | string | Default index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1, overridable por página (frontmatter robots:). | | themeColor | string | meta[name="theme-color"]. | | socialLinks | string[] | Alimenta sameAs del JSON-LD de Organization. | | contentLanguage | string \| null | meta[http-equiv="content-language"] (ej. "es-MX"). null = se omite. | | xUaCompatible | string \| null | meta[http-equiv="X-UA-Compatible"] (ej. "IE=edge"). null = se omite. | | twitterSite | string \| null | meta[name="twitter:site"] (ej. "@mihandle"). null = se omite. | | favicons | FaviconLink[] | Ver abajo. | | manifest | string \| null | link[rel="manifest"] (ej. "/site.webmanifest"). null = se omite. | | msapplicationTileColor | string \| null | meta[name="msapplication-TileColor"]. null = se omite. |

FaviconLink

{ rel: 'icon' | 'apple-touch-icon' | 'mask-icon'; href: string; type?: string; sizes?: string; color?: string }

color solo tiene efecto en rel: 'mask-icon' (Safari pinned tab, exige un SVG monocromo). Un sitio típico declara varias entradas rel: 'icon' (distintos tamaños/formatos) + una apple-touch-icon (180x180) + opcionalmente una mask-icon.

Se aplican una sola vez

favicons/manifest/contentLanguage/xUaCompatible/twitterSite/msapplicationTileColor los aplica SiteMetaService UNA sola vez (en su constructor, es un singleton) -- a diferencia de description/og:title/etc, no cambian de una página a otra, así que no hace falta (ni tiene sentido) pasarlos en cada .apply() de PageSeoService.

Servicios de parser-md-seo

< Uso de parser-md-seo


PageSeoService

apply(page: PageSeo): AppliedPageSeo -- escribe title/meta tags/Open Graph/Twitter Card/canonical para la página ACTUAL, se llama una vez por navegación (ej. desde DocPage):

interface PageSeo {
  readonly title: string;
  readonly description?: string;    // cae al default de SeoConfig si falta
  readonly keywords?: string;       // cae al default de SeoConfig si falta
  readonly image?: string | null;   // ruta relativa -- se resuelve a absoluta con siteUrl
  readonly ogTitle?: string;        // si falta, cae a "title"
  readonly ogDescription?: string;
  readonly twitterCard?: string;    // default "summary_large_image"
  readonly canonical?: string;      // si falta, se arma de siteUrl + slug
  readonly robots?: string;
  readonly siteUrl?: string | null;
  readonly slug: string;            // "index" = raíz del sitio
  readonly author?: string;
  readonly themeColor?: string;
}

Devuelve { canonicalUrl?, image? } -- undefined si no hay siteUrl configurado. Quien la llama reusa canonicalUrl para pasárselo a JsonLdService en vez de volver a derivarla.

Los tags que dependen de una URL absoluta (canonical/og:url/og:image/twitter:image) solo se escriben si hay una URL resuelta -- mejor omitirlos que mandar una URL rota.

JsonLdService

apply(page: JsonLdPage): void -- escribe el bloque <script type="application/ld+json"> con WebSite+Organization+WebPage+BreadcrumbList:

interface JsonLdBreadcrumbItem {
  readonly label: string;
  readonly url?: string; // el ultimo item del breadcrumb (la pagina actual) no lleva url
}

interface JsonLdPage {
  readonly title: string;
  readonly description?: string;
  readonly canonicalUrl?: string;   // reusar el que devolvio PageSeoService.apply()
  readonly breadcrumb: readonly JsonLdBreadcrumbItem[];
}

Sin siteUrl configurado, se omite entero -- mismo criterio que el resto del sistema (nunca un JSON-LD con URLs rotas).

SiteMetaService

Sin API propia que llamar -- es un singleton (providedIn: 'root') que aplica, en su CONSTRUCTOR (una sola vez por arranque de la app, incluido cada render SSR), los tags que nunca cambian de página en página: favicons, manifest, content-language, X-UA-Compatible, twitter:site, msapplication-TileColor. Alcanza con inyectarla (PageSeoService ya lo hace por vos) -- separada de PageSeoService a propósito: 2 ciclos de vida distintos (una vez vs. por página) son 2 responsabilidades distintas, aunque ambas estén bajo el paraguas de "SEO".

Cómo funciona por dentro: orden de las tags en <head>

Angular agrega las tags nuevas (vía Meta/Title) al FINAL de <head>, detrás de los <style> que el propio SSR inyecta por cada componente -- sin corrección, todo lo que escriben estos 3 servicios queda invisible al fondo del documento servido. Por eso los 3 (PageSeoService, JsonLdService, SiteMetaService) terminan su apply()/constructor llamando a reorderHeadTags() (head-order.util.ts) con la MISMA lista completa de selectores (SEO_TAG_ORDER) -- el resultado final es idéntico sin importar cuál de los 3 corrió último.

reorderHeadTags() usa querySelectorAll (no querySelector) a propósito: algunos selectores (favicons, los bloques repetidos de og:image) matchean varios elementos, y se mueven TODOS, como bloque contiguo, en el mismo orden relativo.

Las tags REPETIBLES (varios <link rel="icon">, varios <meta property="og:image">) no pasan por Meta/Title de Angular -- esas APIs solo saben hacer upsert de UN elemento por selector, no sirven para "N tags con la misma property". Las maneja marked-head-block.util.ts (appendMarkedElement/clearMarkedElements): cada elemento insertado se marca con el atributo data-ndmk-marker, y el bloque completo de la pasada anterior se borra antes de escribir uno nuevo -- evita que se acumulen en navegaciones sucesivas.

sitemap.xml y robots.txt de parser-md-seo

< Uso de parser-md-seo


@ngx-docs-markdown-kit/parser-md-seo/sitemap -- entry point APARTE, sin ninguna dependencia de Angular a propósito, para que un script de build en Node plano (ej. generate-seo-files.mjs de create-ngx-docs-site) lo importe sin arrastrar las clases @Injectable del entry point raíz (que exigen el linker de Angular para cargar fuera de un bundler Angular real).

import { buildSitemapXml, buildRobotsTxt } from '@ngx-docs-markdown-kit/parser-md-seo/sitemap';

const urls = [
  { loc: 'https://mi-sitio.com' },
  { loc: 'https://mi-sitio.com/docs/instalacion', lastmod: '2026-01-15' },
];

writeFileSync('public/sitemap.xml', buildSitemapXml(urls));
writeFileSync('public/robots.txt', buildRobotsTxt('https://mi-sitio.com'));

Ambas son funciones PURAS, sin acceso a filesystem/red -- quien las llama es responsable de recolectar la lista de URLs a partir de SU PROPIA estructura de navegación/contenido (ver buildContentManifest de parser-md); esta librería no conoce esos detalles.

  • buildSitemapXml(urls) -- protocolo sitemaps.org 0.9 válido. lastmod es opcional por URL -- se omite <lastmod> si no se especifica.
  • buildRobotsTxt(siteUrl) -- permite todo, apunta al sitemap. Si necesitás reglas más específicas (bloquear rutas puntuales, distinguir bots), escribí el archivo a mano en vez de usar esto -- es un default para el caso común, no un generador configurable.

Ecosistema de parser-md-seo

< Índice Docs de parser-md-seo


Relación con el ecosistema de parser-md-seo

< Ecosistema de parser-md-seo


@ngx-docs-markdown-kit/parser-md-seo es una extensión: depende de @ngx-docs-markdown-kit/parser-md como peer dependency y consume directamente su tipo ParserMdExtension (seoExtension() no es más que un objeto con extendMeta, registrado con .use() en el parser del consumidor). No reimplementa nada del parseo de frontmatter/Markdown en sí -- solo interpreta un puñado de campos ya extraídos por parser-md con significado especial para SEO.

Quién la usa hoy

  • create-ngx-docs-site (el CLI) es el único consumidor real hoy. Su plantilla registra seoExtension() al crear el parser, provee SEO_CONFIG/SITE_NAME una vez en app.config.ts (provideSeoConfig()/provideSeoSiteName()), y cada página inyecta PageSeoService/JsonLdService para aplicar su propio SEO. El script de build frugocorp_modules/parser-md-seo/scripts/generate-seo-files.mjs usa el entry point /sitemap para escribir public/sitemap.xml/robots.txt antes de ng build, iterando el CONTENT_MANIFEST que ya generó generate-content-manifest.mjs de parser-md.

Ninguna otra librería del ecosistema (parser-md-code-block, parser-md-code-block-themes, parser-md-image, parser-md-card, ui) depende de parser-md-seo -- SEO técnico es una preocupación de SITIO completo (un <head> por página, un solo SEO_CONFIG), no algo que una librería de contenido individual (un bloque de código, una imagen, una card) tenga motivo para conocer.

Una fuente de verdad compartida con parser-md-converter

@ngx-docs-markdown-kit/parser-md-converter no depende de parser-md-seo -- no hay ningún import entre ambos paquetes -- pero comparten la misma fuente de datos: el CONTENT_MANIFEST generado por parser-md. El script de build de generate-seo-files.mjs (esta librería) y el generate-readme.mjs de parser-md-converter leen exactamente el mismo árbol ya resuelto (mismas páginas, mismo lastmod, mismo orden) en vez de escanear content-parser-md/ cada uno por su cuenta. Consecuencia real: el sitemap y el README generado nunca pueden desincronizarse entre sí sobre qué páginas existen -- ambos parten del mismo manifiesto, escrito una sola vez por build.

Por qué el entry point /sitemap no depende de Angular

buildSitemapXml/buildRobotsTxt viven en @ngx-docs-markdown-kit/parser-md-seo/sitemap, un entry point de ng-packagr aparte del raíz, sin ninguna dependencia de @angular/core. La razón es práctica: generate-seo-files.mjs corre con node plano, ANTES de ng build -- si esas dos funciones vivieran en el entry point raíz (que sí exporta clases @Injectable), cargarlas fuera de un bundler Angular real exigiría pasar por el linker de Ivy, algo que un script de Node suelto no hace. Separar el entry point evita ese problema de raíz en vez de trabajarlo alrededor.

Parser MD SEO de parser-md-seo

REGRESAR A APARTADOS DE parser-md-seo


GitHub | Sitio | FrugoCorp

parser-md-seo extiende parser-md para que un sitio de documentacion sea indexable de verdad: extrae metadatos SEO del frontmatter de cada pagina y expone los servicios de Angular (meta tags, JSON-LD) mas las funciones de build-time (sitemap.xml, robots.txt) que los aplican.

Por que meta tags no alcanzan solos

Un crawler no siempre espera a que un fetch asincrono resuelva -- por eso PageSeoService y SiteMetaService aplican los tags ANTES del primer render (SSR/prerender), y JsonLdService arma WebSite+Organization+WebPage+BreadcrumbList desde la misma cadena de breadcrumbs real que usa la navegacion visual, nunca URLs inventadas a mano.

Que trae de base

PageSeoService (por pagina, title/description/canonical/OG/Twitter Card) separado de SiteMetaService (favicons/manifest, una sola vez) -- 2 ciclos de vida distintos. Generacion de sitemap.xml/robots.txt en build-time a partir del mismo manifiesto de contenido que arma parser-md, asi nunca se desincronizan entre si.