@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.
Maintainers
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

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.tgzrxjs: ~7.8.0tslib: ^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-seoRequiere @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
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
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
@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.lastmodes 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
@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 registraseoExtension()al crear el parser, proveeSEO_CONFIG/SITE_NAMEuna vez enapp.config.ts(provideSeoConfig()/provideSeoSiteName()), y cada página inyectaPageSeoService/JsonLdServicepara aplicar su propio SEO. El script de buildfrugocorp_modules/parser-md-seo/scripts/generate-seo-files.mjsusa el entry point/sitemappara escribirpublic/sitemap.xml/robots.txtantes deng build, iterando elCONTENT_MANIFESTque ya generógenerate-content-manifest.mjsdeparser-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
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.
