Referencia de configuración
Todo lo que entiende pullpress.config.yml: el único archivo que convierte tu repositorio en un CMS.
El archivo de configuración está en la raíz de tu repositorio y tiene control de versiones como todo lo demás. A diferencia de los cambios de contenido (que abren una solicitud de cambio para revisar), guardar la configuración es una acción de administrador que hace commit directamente en tu rama por defecto, así que un nuevo modelo de contenido se aplica al momento (y se valida antes de escribirse). Tiene unas pocas claves de primer nivel, una lista de colecciones y un pequeño conjunto de tipos de campo.
¿Prefieres editar en la aplicación? El editor de configuración integrado tiene un modo visual cuyo selector de tipos de campo agrupa cada tipo con una descripción de una línea, y su modo YAML valida antes de guardar: una errata de sintaxis se señala en su línea y columna exactas, y los problemas de estructura se listan por ruta de campo. El archivo se llama pullpress.config.yml (la extensión .yaml también sirve).


Claves de primer nivel
| Clave | Descripción |
|---|---|
version | Versión del esquema de configuración. Por ahora siempre 1. |
media.folder | Ruta del repositorio donde se hace commit de las imágenes subidas, por ejemplo public/uploads. |
media.public_path | Prefijo de URL con el que el sitio publicado sirve esa carpeta, por ejemplo /uploads. Sirve para escribir enlaces de imagen correctos en tu Markdown. |
collections | La lista de colecciones de contenido con las que trabajan los redactores. Cada colección tiene un nombre, una etiqueta, un tipo y campos. |
components | Opcional. Un mapa de grupos de campos con nombre y reutilizables, a los que remiten los campos con type: component. Define un grupo una vez y reutilízalo en varias colecciones. |
Tipos de colección
Una colección describe un tipo de contenido. Su type decide cómo se corresponden las entradas con los archivos del repositorio.
folder: un archivo por entrada
Cada entrada es su propio archivo Markdown dentro de la carpeta configurada. Los redactores pueden crear, editar y eliminar entradas: ideal para entradas de blog, noticias o miembros del equipo.
collections:
- name: blog
label: Blog posts
type: folder
folder: content/blog
fields:
- { name: title, label: Title, type: string, required: true }
- { name: date, label: Date, type: date, default: now }
- { name: body, label: Body, type: markdown }Formatos de frontmatter. Los tres formatos se conservan: YAML entre líneas ---, TOML entre líneas +++ (el predeterminado de Hugo) y un objeto JSON suelto al principio del archivo. Las entradas existentes conservan siempre su formato, el orden de sus claves y las claves que PullPress no gestiona. El frontmatter YAML conserva además sus # comments, anclas y cadenas de bloque |/> al guardar: editar un campo produce un diff que solo toca ese campo. (Los frontmatter TOML y JSON conservan los valores y el orden de las claves, pero las líneas de comentario de TOML no se conservan.) Pon format: toml (o json) en una colección para crear las entradas nuevas en ese formato; la detección lo rellena por ti cuando tu contenido existente lo usa.
Los sitios que usan una carpeta por entrada (content collections de Astro, leaf bundles de Hugo: content/my-post/index.md) ponen structure: bundle. Añade media: colocated para guardar las imágenes de una entrada dentro de su propia carpeta, referenciadas de forma relativa (./photo.jpg), para que el procesamiento de imágenes de tu generador las recoja. La detección del repositorio te propone todo esto: reconoce las content collections de Astro, los leaf y branch bundles de Hugo (más contentDir y los archivos de data/), las colecciones de Jekyll de _config.yml, las carpetas de contenido de Next.js y los bundles anidados por fecha, y escribe las etiquetas generadas en el idioma de tu interfaz:
collections:
- name: projects
label: Projects
type: folder
folder: redesign/content
structure: bundle # one folder per entry: content/my-post/index.md
media: colocated # images live inside the entry folder (./photo.jpg)
slug: "{{date}}-{{slug:title}}"
fields:
- { name: title, label: Title, type: string, required: true }
- { name: date, label: Date, type: date, default: now }
- { name: body, label: Body, type: markdown }files: páginas fijas
Un conjunto fijo de archivos que listas de forma explícita. Los redactores pueden editarlos, pero no añadir ni quitar ninguno: lo adecuado para páginas únicas como Quiénes somos o Contacto. Cada archivo lleva sus propios fields (anidados bajo ese archivo, no compartidos a nivel de colección), así que cada página puede tener una forma distinta.
collections:
- name: pages
label: Pages
type: files
files:
- name: about
label: About page
file: content/about.md
fields:
- { name: title, label: Title, type: string, required: true }
- { name: body, label: Body, type: markdown }
- name: contact
label: Contact page
file: content/contact.md
fields:
- { name: heading, label: Heading, type: string, required: true }
- { name: body, label: Body, type: markdown }La detección lo usa también para las páginas de sección: el _index.md de un branch bundle de Hugo (la página de inicio de una sección) no es una entrada de la colección folder, así que la detección los reúne todos en una colección files generada “Section pages”, editable como cualquier otra página, sin cableado manual.
data: un único archivo de datos estructurados
Un archivo YAML, JSON o TOML que se edita por completo mediante un formulario, sin cuerpo Markdown. Úsalo para datos estructurados que tus plantillas leen directamente: horarios de apertura, menús de navegación, ajustes del sitio. Las claves del archivo que no están en la configuración de campos sobreviven a cada guardado, en los tres formatos. Los archivos YAML conservan también sus comentarios; los TOML y JSON conservan sus claves y su orden, pero se reformatean.
collections:
- name: opening_hours
label: Opening hours
type: data
file: data/opening-hours.yml
fields:
- name: hours
label: Weekly hours
type: list
of:
type: object
fields:
- { name: day, label: Day, type: select, options: [mon, tue, wed, thu, fri, sat, sun] }
- { name: open, label: Opens, type: string }
- { name: close, label: Closes, type: string }
- { name: closed, label: Closed all day, type: boolean, default: false }Algunos archivos de datos no son un mapa sino una gran lista: un array al principio del archivo, como el que lee el loader file() de Astro (src/data/authors.json con [ …, … ]), o los archivos de datos en lista de Hugo/Jekyll/Eleventy. Pon root: list y declara exactamente un campo de tipo list: sus elementos son el array de primer nivel del archivo, editado como un formulario repetible. Los elementos que no tocas se reescriben tal cual (se conservan el orden de sus claves y las claves adicionales); TOML no puede representar un array de primer nivel, así que esto funciona con archivos .json, .yaml y .yml. La detección propone esta forma automáticamente.
Los archivos CSV y TSV (Jekyll _data/*.csv, exportaciones de hojas de cálculo) funcionan igual: siempre son tablas, así que apunta una colección data con root: list al archivo y lista las columnas como campos string del único campo list. La fila de cabecera y el orden de las columnas se conservan al guardar; cada valor va y vuelve como texto.
collections:
- name: authors
label: Authors
type: data
file: src/data/authors.json # the file's root is [ … ], not { … }
root: list
fields: # exactly one field, of type list
- name: items
label: Authors
type: list
of:
type: object
fields:
- { name: name, label: Name, type: string, required: true }
- { name: avatar, label: Avatar, type: image }folder de entradas de datos: sin cuerpo Markdown
A veces cada entrada son solo datos estructurados sin cuerpo de artículo: autores, miembros del equipo, productos, listas de enlaces. Da a una colección folder una extension de datos (json, yaml, yml o toml) y cada entrada se convierte en un único archivo de datos que los redactores rellenan como un formulario: el mismo crear, editar, eliminar, revisar y el mismo historial que con Markdown, solo que sin cuerpo. Así funcionan exactamente las colecciones de Astro con type: 'data', y la detección te las propone.
collections:
- name: authors
label: Authors
type: folder
folder: src/content/authors
extension: json # each entry is a .json data file (no body)
slug: "{{slug:name}}"
fields:
- { name: name, label: Name, type: string, required: true }
- { name: bio, label: Bio, type: text }
- { name: avatar, label: Avatar, type: image }El archivo entero son los datos (su formato sigue la extensión) y, como en una colección data, el orden de las claves y las claves que no están en tu lista de campos se conservan en cada guardado. No hay campo de cuerpo, así que no añadas uno; las relaciones también pueden apuntar a estas colecciones.
Opciones de colección
Además de name, label, folder y fields, una colección folder admite estas claves opcionales. La detección rellena la mayoría por ti.
| Clave | Descripción |
|---|---|
structure | flat (por defecto) mantiene un archivo por entrada; bundle usa una carpeta por entrada (folder/slug/index.md). |
media | site (por defecto) envía las subidas a la carpeta de medios; colocated las guarda dentro de la carpeta bundle de la propia entrada, referenciadas como ./photo.jpg. colocated requiere structure: bundle. |
extension | Tipo de archivo de las entradas: md (por defecto), markdown o mdx para páginas Markdown; html, adoc u org para contenido que no es Markdown (el frontmatter se conserva; declara el cuerpo como type: code para editarlo como texto sin formato); o json, yaml, yml o toml para que cada entrada sea un archivo de datos sin cuerpo (una carpeta de entradas de datos, como las data collections de Astro). |
format | Formato del frontmatter de las entradas nuevas: yaml (por defecto), toml o json. Las entradas existentes conservan su formato. |
nested | true reúne también las entradas de subcarpetas (hasta cinco niveles), agrupadas por carpeta. Desactivado por defecto. |
locales | Códigos de idioma adicionales (por ejemplo [fr, en]) que hacen cada entrada editable por idioma, organizadas según la clave i18n. |
i18n | Dónde están las traducciones: suffix (por defecto) usa archivos hermanos (post.fr.md); subfolder usa una carpeta por idioma (Hugo content/<lang>/, Astro/Starlight docs/<lang>/). Con subfolder, apunta folder a la carpeta del idioma por defecto y nombra ese segmento en defaultLocale; por ejemplo, folder: content/en, defaultLocale: en, locales: [fr, de] coloca las entradas en francés en content/fr. |
slug | Plantilla de nombre de archivo para entradas nuevas. Por defecto {{slug:title}}. Tokens: {{slug:field}} (campo convertido en slug), {{field}}, {{date}}, {{year}}, {{month}}, {{day}}. |
sort | Cómo se ordena la lista de entradas: { field, order } con order asc o desc. Por defecto, el campo de fecha, la más reciente primero. |
Una colección files, en cambio, lista cada archivo con sus propios fields (y un format opcional por archivo); una colección data apunta a un único file y comparte un solo conjunto de fields.
Tipos de campo
Cada campo de una colección se convierte en un control de formulario en el editor. Las opciones comunes label, required y default funcionan en todos los tipos. Otras dos ayudan a tus redactores: un texto help muestra una pista bajo el campo, y un placeholder muestra texto de ejemplo dentro de un campo vacío. Los campos obligatorios llevan un asterisco rojo y, al enviar, un resumen amable de lo que falta.
Campos de solo lectura. Añade readOnly: true para mostrar un campo con su valor guardado sin que los redactores puedan cambiarlo: el nombre de un documento de ajustes, un slug bloqueado, un id externo. Está bloqueado tanto en el editor web como en la app de iOS, y el servidor restaura el valor guardado en cada guardado, así que un campo de solo lectura nunca puede desviarse, llegue como llegue un cambio.
- name: title
type: string
label: Name
readOnly: true # shown, but editors can't change itTexto por idioma. La interfaz se adapta al idioma elegido por cada redactor, y tú puedes hacer lo mismo con tus propios textos de label, help y placeholder. La clave sin sufijo la ve todo el mundo; añade un sufijo _<language> (uno de en, nl, fr, de, es) para sustituirla para los redactores que usan ese idioma. No se traduce nada automáticamente: escribes cada variante.
- name: title
type: string
label: Titel # shown to everyone
help: De titel van je pagina
help_en: Your page title # only for editors using English| Tipo | Descripción | Ejemplo |
|---|---|---|
string | Una sola línea de texto. | { name: title, type: string, required: true } |
text | Texto sin formato en varias líneas, mostrado como área de texto. | { name: summary, type: text } |
markdown | Texto con formato en el editor limpio, guardado como Markdown. | { name: body, type: markdown } |
number | Un número entero o decimal. | { name: price, type: number } |
boolean | Un interruptor sí/no. | { name: featured, type: boolean, default: false } |
date | Una fecha del calendario. default: now pone la fecha de hoy. | { name: date, type: date, default: now } |
datetime | Una fecha con hora. | { name: published_at, type: datetime } |
select | Una opción de una lista fija, mediante options. | { name: category, type: select, options: [news, recipes] } |
multiselect | Varias opciones de una lista fija, como casillas. | { name: categories, type: multiselect, options: [news, events] } |
url | Una dirección web, validada como https://… | { name: website, type: url } |
email | Una dirección de correo validada. | { name: contact, type: email } |
color | Un selector de color, guardado como #rrggbb. | { name: accent, type: color } |
image | Una imagen subida, con commit en la carpeta de medios. | { name: cover, type: image } |
list | Una lista repetible de elementos; of es la definición de campo del tipo de elemento. | { name: tags, type: list, of: { type: string } } |
object | Un grupo de campos anidados, definidos con fields. | { name: cta, type: object, fields: [...] } |
relation | Una referencia a una entrada de otra colección folder (guarda su slug). | { name: author, type: relation, collection: team } |
blocks | Secciones apilables de distintas formas (cada elemento lleva una clave _type; cámbiala con typeKey). | { name: sections, type: blocks, blocks: [...] } |
hidden | No se muestra a los redactores; se escribe con un valor fijo (por ejemplo, una clave de plantilla). | { name: layout, type: hidden, default: post } |
code | Un bloque de código en monoespaciado, con una pista de lenguaje opcional. | { name: snippet, type: code, language: js } |
map | Una ubicación, editada como latitud/longitud (guardada como 'lat,lng'). | { name: location, type: map } |
compute | Un valor de solo lectura derivado de una plantilla; cada {{token}} es el nombre de un campo hermano. | { name: ref, type: compute, template: '{{brand}}-{{sku}}' } |
keyvalue | Pares clave/valor libres, por ejemplo metadatos adicionales. | { name: meta, type: keyvalue } |
seo | Panel SEO: título + descripción (con pistas de longitud), URL canónica, imagen OG y noindex, con una vista previa del resultado de búsqueda y del JSON-LD. | { name: seo, type: seo } |
component | Despliega un grupo de campos reutilizable del mapa components de primer nivel. | { name: cta, type: component, component: callToAction } |
list y object se combinan: una list de object te da grupos de campos repetibles, como el horario semanal del ejemplo anterior.
Reglas de validación
Además de required, un campo puede reflejar exactamente las restricciones que impone el esquema de tu generador, para que los errores se detecten en el editor con un mensaje amable y no con un build fallido. Los campos numéricos admiten min, max e integer: true; los campos de texto (string, text, markdown, code) admiten minLength, maxLength y una expresión regular en pattern; list y multiselect admiten minItems y maxItems. Las infracciones aparecen junto al campo mientras escribes (un campo opcional puede quedar vacío; sus reglas se aplican en cuanto se escribe algo), y las mismas reglas se ejecutan en el servidor en cada guardado.
- { name: title, type: string, required: true, maxLength: 70 }
- { name: rating, type: number, min: 1, max: 5, integer: true }
- { name: sku, type: string, pattern: "^[A-Z]{2}-\\d{4}$" }
- { name: tags, type: list, of: { type: string }, minItems: 1, maxItems: 8 }Definir blocks
Un campo blocks permite a los redactores apilar secciones de formas distintas (un hero, una cita destacada, una galería…). Lista los tipos de sección bajo blocks; cada uno es un grupo con nombre de fields (con una label opcional), igual que un object:
- name: sections
label: Page sections
type: blocks
blocks:
- name: hero
label: Hero
fields:
- { name: heading, type: string, required: true }
- { name: image, type: image }
- name: quote
label: Pull quote
fields:
- { name: text, type: text, required: true }
- { name: source, type: string }Cada elemento que añade un redactor registra qué bloque es bajo una clave _type (hero, quote, …), para que tu generador sepa cómo mostrar cada sección. Si tu esquema espera otra clave (el z.discriminatedUnion('type', …) de Astro, o contenido importado de Decap CMS, que la guarda como type), pon typeKey en el campo (typeKey: type) y PullPress lee, escribe y valida esa clave en su lugar. El importador de Decap lo configura automáticamente, así que las entradas importadas siguen funcionando sin cambios.
Campos condicionales (showIf)
Cualquier campo puede declarar showIf para aparecer solo cuando un campo hermano coincide. Los campos ocultos se vacían (así nunca llegan al frontmatter), y la regla required de un campo condicional solo se aplica mientras es visible.
fields:
- { name: hasCta, label: Add a call to action?, type: boolean }
- name: ctaUrl
label: Button URL
type: url
required: true
showIf: { field: hasCta, truthy: true } # equals / oneOf also workColecciones anidadas
Pon nested: true en una colección folder para reunir también las entradas de subcarpetas (hasta cinco niveles) y mostrarlas agrupadas por carpeta: práctico para árboles de documentación. La búsqueda y los selectores de relaciones recogen las entradas anidadas automáticamente.
collections:
- name: docs
label: Documentation
type: folder
folder: content/docs
nested: true # gather entries from subfolders too
fields:
- { name: title, type: string, required: true }
- { name: body, type: markdown }Componentes de campo reutilizables
Define una vez un grupo de campos con nombre bajo el mapa components de primer nivel y colócalo en cualquier colección con un campo component. Al cargarse se despliega como un grupo de objeto normal, así que un cambio en el grupo actualiza todas las colecciones que lo usan.
components:
callToAction:
- { name: label, type: string }
- { name: url, type: url }
collections:
- name: pages
type: folder
folder: content/pages
fields:
- { name: title, type: string, required: true }
- { name: cta, type: component, component: callToAction }Dónde viven los medios (y por qué no en S3)
Las subidas se guardan con commit en tu repositorio, junto al contenido que las usa. Es una decisión deliberada, no una función que falta: el repositorio sigue siendo la única fuente de verdad, las imágenes viajan con sus entradas por ramas, revisión e historial, y clonar el repositorio te da el sitio completo, sin un segundo sistema que respaldar o pagar.
¿Necesitas una CDN o redimensionar imágenes? Deja que tu generador o tu hosting lo hagan en el build (el procesamiento de imágenes de Astro y Next.js, la CDN de imágenes de Netlify, las URL fetch de Cloudinary). PullPress guarda el original en Git; tu build lo hace rápido. Las subidas se reducen a tamaño web en el navegador antes de enviarse, incluso una foto de móvil de 20–50 MB, lo que en la práctica mantiene los repositorios bastante pequeños. Un almacenamiento de objetos externo (S3 y similares) rompería la garantía “tu repositorio lo es todo”, así que no lo tenemos previsto.
Descubribilidad por IA (llms.txt)
PullPress puede generar un llms.txt en la raíz de tu repositorio: un índice en texto plano de tu sitio (su nombre, su URL y, por cada colección folder, una lista de títulos de entradas) que los motores de respuesta de IA leen para resumir y citar tu sitio correctamente. Se construye de forma determinista a partir de tu modelo de contenido (sin llamadas a IA, nada sale de tu repositorio).
Los administradores lo encuentran en el panel Ajustes → Descubribilidad por IA (GEO) de un sitio, que también muestra una puntuación de preparación: si la URL de tu sitio está configurada, si existe llms.txt, si tienes contenido publicado y campos de descripción/SEO y, si la URL de tu sitio es accesible, si la portada expone una meta description, etiquetas Open Graph y JSON-LD. Pulsa Actualizar llms.txt para (re)generar el archivo; se hace commit como de cualquier otro cambio: pasando por revisión, o directamente en tu rama por defecto en sitios con publicación directa.
brandvoice.md, más abajo: la voz de marca guía cómo escribe la IA para ti, mientras que llms.txt ayuda a la IA a encontrar y citar lo que ya has publicado. No gestiona robots.txt ni tu sitemap: déjaselos a tu generador.Voz de marca para agentes de IA
Coloca un brandvoice.md opcional en la raíz de tu repositorio para describir el tono del sitio, las palabras preferidas y prohibidas y el nivel de redacción. Los agentes de IA conectados por MCP pueden leerlo (con la herramienta get_style_guide) y seguirlo al redactar. Como es un archivo normal de tu repositorio, tiene control de versiones y se revisa como cualquier otro contenido, y es único para cada cliente.