Saltar al contenido

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).

El editor de configuración de la aplicación en modo visual: pestañas Visual y YAML, las colecciones Blog posts, Pages y Opening hours en una barra lateral, y el nombre, la etiqueta, la carpeta, la plantilla de nombre de archivo y la lista de campos de la colección elegida como formulario
El editor de configuración: el mismo archivo, editable de forma visual o como YAML validado.

Claves de primer nivel

ClaveDescripción
versionVersión del esquema de configuración. Por ahora siempre 1.
media.folderRuta del repositorio donde se hace commit de las imágenes subidas, por ejemplo public/uploads.
media.public_pathPrefijo de URL con el que el sitio publicado sirve esa carpeta, por ejemplo /uploads. Sirve para escribir enlaces de imagen correctos en tu Markdown.
collectionsLa lista de colecciones de contenido con las que trabajan los redactores. Cada colección tiene un nombre, una etiqueta, un tipo y campos.
componentsOpcional. 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.

ClaveDescripción
structureflat (por defecto) mantiene un archivo por entrada; bundle usa una carpeta por entrada (folder/slug/index.md).
mediasite (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.
extensionTipo 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).
formatFormato del frontmatter de las entradas nuevas: yaml (por defecto), toml o json. Las entradas existentes conservan su formato.
nestedtrue reúne también las entradas de subcarpetas (hasta cinco niveles), agrupadas por carpeta. Desactivado por defecto.
localesCódigos de idioma adicionales (por ejemplo [fr, en]) que hacen cada entrada editable por idioma, organizadas según la clave i18n.
i18nDó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.
slugPlantilla de nombre de archivo para entradas nuevas. Por defecto {{slug:title}}. Tokens: {{slug:field}} (campo convertido en slug), {{field}}, {{date}}, {{year}}, {{month}}, {{day}}.
sortCó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 it

Texto 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
TipoDescripciónEjemplo
stringUna sola línea de texto.{ name: title, type: string, required: true }
textTexto sin formato en varias líneas, mostrado como área de texto.{ name: summary, type: text }
markdownTexto con formato en el editor limpio, guardado como Markdown.{ name: body, type: markdown }
numberUn número entero o decimal.{ name: price, type: number }
booleanUn interruptor sí/no.{ name: featured, type: boolean, default: false }
dateUna fecha del calendario. default: now pone la fecha de hoy.{ name: date, type: date, default: now }
datetimeUna fecha con hora.{ name: published_at, type: datetime }
selectUna opción de una lista fija, mediante options.{ name: category, type: select, options: [news, recipes] }
multiselectVarias opciones de una lista fija, como casillas.{ name: categories, type: multiselect, options: [news, events] }
urlUna dirección web, validada como https://…{ name: website, type: url }
emailUna dirección de correo validada.{ name: contact, type: email }
colorUn selector de color, guardado como #rrggbb.{ name: accent, type: color }
imageUna imagen subida, con commit en la carpeta de medios.{ name: cover, type: image }
listUna lista repetible de elementos; of es la definición de campo del tipo de elemento.{ name: tags, type: list, of: { type: string } }
objectUn grupo de campos anidados, definidos con fields.{ name: cta, type: object, fields: [...] }
relationUna referencia a una entrada de otra colección folder (guarda su slug).{ name: author, type: relation, collection: team }
blocksSecciones apilables de distintas formas (cada elemento lleva una clave _type; cámbiala con typeKey).{ name: sections, type: blocks, blocks: [...] }
hiddenNo se muestra a los redactores; se escribe con un valor fijo (por ejemplo, una clave de plantilla).{ name: layout, type: hidden, default: post }
codeUn bloque de código en monoespaciado, con una pista de lenguaje opcional.{ name: snippet, type: code, language: js }
mapUna ubicación, editada como latitud/longitud (guardada como 'lat,lng').{ name: location, type: map }
computeUn valor de solo lectura derivado de una plantilla; cada {{token}} es el nombre de un campo hermano.{ name: ref, type: compute, template: '{{brand}}-{{sku}}' }
keyvaluePares clave/valor libres, por ejemplo metadatos adicionales.{ name: meta, type: keyvalue }
seoPanel 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 }
componentDespliega 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 work

Colecciones 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.

Esto complementa 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.