@vunojs/content
| Install | |
|---|---|
npm install @vunojs/content |
|
| Latest Version: | 0.0.29 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 25, 2026 |
| Links: | npm |
@vunojs/content
Markdown, colecciones con frontmatter tipado y renderizado compatible con SSR para aplicaciones Vuno.
El módulo lee archivos .md de content/, los compila durante vuno prepare, desarrollo y build, y registra loadContent, getContentMeta, listContent, VContentRenderer y VMarkdown. Los Markdown no crean rutas de la aplicación por sí solos: una página Vue de app/pages/ decide qué URL muestra cada documento.
Instalación
@vunojs/content se publica en npm desde la 0.0.10, con la misma versión que el resto de paquetes de Vuno. En una aplicación que ya tiene Vuno instalado y configurado:
pnpm add -D @vunojs/content
Para usar el adaptador visual opcional de Vuno UI, añade también @vunojs/ui:
pnpm add -D @vunojs/ui
Dentro de este monorepo, apps/vuno ya lo tiene como dependencia workspace:*; otra aplicación del workspace lo añade con pnpm --filter my-app add -D '@vunojs/content@workspace:*'.
| Import | Cuándo usarlo |
|---|---|
@vunojs/content |
Configurar vunoContent(), colecciones y campos del schema. |
@vunojs/content/client |
Renderizar Content desde el runtime de la app, tipar su árbol y definir un adaptador visual. |
@vunojs/content/components |
Importar VContentRenderer o VMarkdown explícitamente. |
@vunojs/content/editor |
Tipos de app/.vuno/content/editor-manifest.json para herramientas de editor, como la extensión de VS Code. |
#vuno/content |
Módulo generado por cada app: loadContent, getContentMeta, listContent y tipos de sus colecciones. |
#vuno/content no es una subruta del paquete instalada en node_modules; se genera en la aplicación al preparar Vuno. El paquete runtime no depende de @vunojs/ui: el adaptador visual es opcional.
@vunojs/content/internal también existe, pero no es para aplicaciones: es el contrato entre paquetes de Vuno que usa el setup generado bajo app/.vuno. Sus nombres pueden cambiar en cualquier versión sin garantía de semver.
Registra el módulo en vuno.config.ts:
import { contentField, defineContentCollection, vunoContent } from '@vunojs/content'
import { defineConfig } from '@vunojs/core/config'
export default defineConfig({
modules: [
vunoContent({
collections: {
docs: defineContentCollection({
include: 'docs/**/*.md',
schema: {
description: contentField.string(),
status: contentField.enum(['draft', 'published'] as const, { default: 'draft' }),
title: contentField.string({ required: true }),
},
}),
},
}),
],
})
collections es explícito: crear una carpeta bajo content/ no declara automáticamente una colección. Cada include es un glob relativo a content/. Si omites collections, el módulo crea una colección predeterminada llamada content para **/*.md, sin schema propio. directory permite cambiar la carpeta base, por ejemplo vunoContent({ directory: 'articles' }).
Después de configurar el módulo, ejecuta la preparación del proyecto para generar los tipos del editor:
pnpm exec vuno prepare
El comando vuno dev también prepara el proyecto. Si acabas de instalar el módulo y el editor sigue mostrando tipos anteriores, ejecuta vuno prepare y reinicia el servidor de TypeScript del editor.
Crear documentos y entender sus rutas
Con la configuración anterior, content/docs/getting-started/introduction.md puede contener:
---
title: Introducción
description: Primeros pasos con la documentación
status: published
---
# Introducción
Este contenido se renderiza desde una página Vue.
Su path de Content es /docs/getting-started/introduction. La ruta sale de la posición del archivo respecto de content/, no del nombre de la colección ni del valor de include. content/docs/index.md corresponde a /docs y content/index.md a /. Un path desconocido hace que loadContent devuelva undefined.
Un .md vacío o que solo contiene espacios se ignora mientras lo estás creando; empieza a aparecer en el manifiesto cuando tiene contenido. Si ya tiene contenido pero incumple el schema (por ejemplo, falta un title obligatorio), Vuno muestra el error de validación en desarrollo y conserva la última versión válida sin cerrar el servidor. La preparación y el build siguen rechazando un documento no vacío inválido.
La URL pública /docs/getting-started/introduction solo funciona cuando existe una página de Vuno que la atiende. Hay dos formas habituales:
- Una página fija, como
app/pages/docs/getting-started/introduction.vue, para un documento concreto. - Una página dinámica, como
app/pages/docs/[...path].vue, para varios documentos bajo/docs/.
No confundas los dos árboles: content/ contiene datos y app/pages/ contiene rutas ejecutables.
Cargar y renderizar un documento
Para una página fija:
<!-- app/pages/docs/getting-started/introduction.vue -->
<script setup lang="ts">
const document = await loadContent('/docs/getting-started/introduction', 'docs')
</script>
<template>
<VContentRenderer
v-if="document"
:value="document"
/>
</template>
loadContent es un autoimport tipado. Con una colección explícita devuelve Promise<ProjectContentDocument<'docs'> | undefined>; sin ella devuelve un documento de cualquiera de las colecciones o undefined. Comprueba siempre el caso undefined antes de renderizar. document.meta contiene el frontmatter validado, document.title el título detectado, document.path la ruta de Content, document.body el árbol serializable del Markdown y document.toc los h2 y h3 en orden ({ id, label, level }), con el mismo id que lleva cada encabezado renderizado. También puedes importar explícitamente loadContent y el tipo ProjectContentDocument desde #vuno/content.
El segundo argumento es el nombre de la colección, no una parte de la ruta. Si el documento existe pero pertenece a otra colección, el resultado es undefined sin cargar el módulo de ese documento. Esto es útil cuando el componente receptor exige un schema concreto. TypeScript rechaza un nombre de colección que no esté declarado en vuno.config.ts; aun así debes manejar undefined porque un Markdown puede faltar o moverse.
Para varias páginas que coinciden con los archivos bajo content/docs/:
<!-- app/pages/docs/[...path].vue -->
<script setup lang="ts">
const route = useRoute()
const document = shallowRef(await loadContent(route.path, 'docs'))
setResponseStatus(document.value === undefined ? 404 : 200)
onBeforeRouteUpdate(async (to) => {
document.value = await loadContent(to.path, 'docs')
})
</script>
<template>
<VContentRenderer
v-if="document"
:value="document"
/>
<p v-else>Esta página no existe.</p>
</template>
Aquí route.path sirve directamente porque la URL y el path de Content coinciden. El catch-all anidado se considera una ruta normal; la página responde 404 cuando no encuentra el documento. setResponseStatus afecta a la respuesta SSR con búfer; una navegación ya abierta en el navegador actualiza el contenido, pero no vuelve a emitir un estado HTTP para el documento HTML.
Si una página acepta varias colecciones, omite el segundo argumento y discrimina por collection antes de utilizar campos propios de cada schema:
const document = await loadContent(route.path)
if (document?.collection === 'docs') {
console.log(document.meta.title)
} else if (document?.collection === 'news') {
console.log(document.meta.category)
}
El tipo de meta se estrecha junto con collection: no necesitas un guard propio ni un cast. En este ejemplo, news debe estar declarada con un campo category en su schema; cambia los nombres por los de tu aplicación.
Si la URL y la ubicación del Markdown difieren, traduce la ruta de forma explícita. Por ejemplo, si la URL es /docs/getting-started/introduction pero el archivo está en content/getting-started/introduction.md:
const contentPath = route.path.replace(/^\/docs(?=\/|$)/, '')
const document = await loadContent(contentPath)
// loadContent('/getting-started/introduction')
Aplica la misma conversión dentro de onBeforeRouteUpdate. Dejar un path fijo como loadContent('/getting-started/assets') hará que todas las URLs muestren ese mismo documento.
Listar documentos
listContent devuelve metadatos sin body ni toc; no descarga el Markdown de cada entrada:
<script setup lang="ts">
const pages = listContent('docs')
const publishedPages = pages.filter((page) => page.meta.status === 'published')
</script>
<template>
<nav aria-label="Documentación">
<a
v-for="page in publishedPages"
:key="page.id"
:href="https://github.com/vunojs/vuno/blob/HEAD/page.path"
>
{{ page.meta.title }}
</a>
</nav>
</template>
El nombre 'docs' debe existir en collections. Su schema determina el tipo de page.meta; por ejemplo, status será 'draft' | 'published' y title será string. Sin argumento, listContent() devuelve las entradas de todas las colecciones. Los metadatos son visibles en el manifiesto generado, así que un estado draft no protege contenido: filtra o controla el acceso en la aplicación si no debe publicarse.
Si conoces la ruta y solo necesitas su frontmatter, usa getContentMeta sin cargar el documento completo:
const meta = getContentMeta('/docs/getting-started/introduction', 'docs')
if (meta) console.log(meta.title, meta.description)
Devuelve el meta validado o undefined si la ruta no existe o pertenece a otra colección. Acepta una barra final en la ruta, igual que loadContent. La colección es opcional, pero pasar 'docs' permite que TypeScript conozca el schema exacto; sin ella, el resultado es la unión de los metadatos de todas las colecciones. También puedes importarlo explícitamente desde #vuno/content.
Renderizar Markdown de una cadena
VMarkdown sirve para texto que ya tienes en memoria y no necesita un archivo ni una colección:
<script setup lang="ts">
const source = '# Hola\n\nMarkdown **dinámico**.'
</script>
<template>
<VMarkdown :source="source" />
</template>
VContentRenderer acepta un documento devuelto por loadContent o su body. Ambos componentes usan article como elemento raíz y aceptan as="div" o as="section".
Integración opcional con Vuno UI
El runtime de Content no depende de Vuno UI. Si quieres VProse, VAlert y bloques VCode, registra ambos módulos y el adaptador:
import { vunoContent } from '@vunojs/content'
import { defineConfig } from '@vunojs/core/config'
import vunoUiModule from '@vunojs/ui'
import { vunoUiContent } from '@vunojs/ui/content'
export default defineConfig({
modules: [vunoUiModule(), vunoContent({ renderer: vunoUiContent({ code: 'interactive' }) })],
})
code: 'semantic' conserva <pre><code> estándar. code: 'interactive' usa el componente visual VCode. El adaptador registra la directiva Alert, con el nombre del componente que pinta; los nombres distinguen mayúsculas y :::alert hace fallar la generación. Por ejemplo:
:::Alert{color="info" title="Consejo"}
El contenido del aviso se renderiza con VAlert.
:::
Para una directiva propia, declara su nombre y sus props en vuno.config.ts, y proporciona un componente global registrado por la aplicación:
vunoContent({
directives: {
callout: {
component: 'AppCallout',
kind: 'block',
props: {
tone: contentField.enum(['info', 'warning'] as const),
},
},
},
})
Después, un documento puede usar :::callout{tone="warning"}. Una directiva desconocida o una prop inválida detiene la generación con un error. Las directivas solo aceptan datos serializables; el HTML crudo permanece deshabilitado.
Tipos y archivos generados
vuno prepare escribe los módulos, el manifiesto del editor y las declaraciones bajo app/.vuno/. La declaración de #vuno/content contiene los nombres y schemas de las colecciones de ese proyecto. Los autoimports loadContent, getContentMeta y listContent apuntan a ese módulo tipado; no es necesario declarar variables globales a mano ni modificar archivos generados.
Si el editor muestra loadContent: any, verifica que vunoContent() esté registrado, ejecuta pnpm exec vuno prepare desde la aplicación y comprueba que app/.vuno/content.d.ts exista. Si una URL devuelve 404, comprueba por separado que exista la página en app/pages/, que el archivo .md coincida con un include y que el argumento de loadContent sea el path generado desde content/.
Desarrollo del paquete en el monorepo
pnpm --filter @vunojs/content test ejecuta sus pruebas unitarias y pnpm --filter @vunojs/content build actualiza el dist que consume el resto del workspace. Para validar tipos del consumidor después de cambiar la API generada, ejecuta pnpm typecheck desde la raíz: pasa por el grafo de Turbo y comprueba apps/vuno, además del paquete. Una prueba aislada del paquete no demuestra que el tipo #vuno/content generado por la app siga siendo compatible.
Related Packages
A Nuxt theme for showing blog posts and snippet notes with flexible layouts and...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.0.29 | ^3.5.44 | MIT | |
| 0.0.28 | ^3.5.44 | MIT | |
| 0.0.27 | ^3.5.44 | MIT | |
| 0.0.26 | ^3.5.44 | MIT | |
| 0.0.25 | ^3.5.44 | MIT | |
| 0.0.24 | ^3.5.44 | MIT | |
| 0.0.23 | ^3.5.44 | MIT | |
| 0.0.22 | ^3.5.44 | MIT | |
| 0.0.21 | ^3.5.44 | MIT | |
| 0.0.20 | ^3.5.44 | MIT | |
| 0.0.19 | ^3.5.43 | MIT | |
| 0.0.18 | ^3.5.43 | MIT | |
| 0.0.17 | ^3.5.43 | MIT | |
| 0.0.16 | ^3.5.43 | MIT | |
| 0.0.15 | ^3.5.43 | MIT | |
| 0.0.14 | ^3.5.43 | MIT | |
| 0.0.13 | ^3.5.43 | MIT | |
| 0.0.12 | ^3.5.43 | MIT | |
| 0.0.11 | ^3.5.43 | MIT | |
| 0.0.10 | ^3.5.43 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |