Crear temas

Todo lo que escribes en FolioPress — páginas, entradas, etiquetas — vive aparte de cómo se muestra. Esa capa de presentación es un tema, y esta guía desmonta uno: la carpeta, las plantillas, el archivo de ajustes y exactamente qué datos recibe cada plantilla. Si te manejas con HTML y CSS, tienes todo lo que necesitas.

Un tema es una carpeta

Cambia el tema y las mismas páginas, entradas y menú se redibujan — nada del contenido cambia, solo su ropa. Un tema es una carpeta con tres cosas: un archivo de ajustes, plantillas y recursos estáticos.

Un tema es una carpeta obligatorio opcional my-theme/ theme.yml nombre, colores, cuántas entradas listar… templates/ base.html el armazón común a todas las páginas home.html la página de inicio page.html una página (Acerca de, Contacto…) post.html una entrada del blog tag.html el archivo de una etiqueta search.html resultados de búsqueda 404.html · 500.html page-<slug>.html un diseño propio para una página partials/ fragmentos incluidos desde otras plantillas locales/pt-PT.json … static/ styles.css · scripts.js favicon.svg · apple-touch-icon.png
Cuatro plantillas y theme.yml bastan; todo lo demás es un extra opcional.

Cuatro plantillas son obligatorias — base.html, home.html, page.html y post.html — y un tema sin ellas no carga. Todo lo demás es opcional: un archivo de etiquetas, una página de búsqueda, páginas de error personalizadas, un diseño propio para una página concreta y una carpeta locales/ para las palabras del propio tema. Los temas se pueden editar directamente en la administración, en Temas, exportar como zip e importar en otro sitio.

De una URL a una página

Ayuda ver qué hace FolioPress con una petición antes de que tu plantilla llegue a ejecutarse, porque explica por qué las plantillas son tan cortas.

1 El visitante abre una URL /es-es/acerca-de 2 FolioPress encuentra el idioma y la página es-ES · “Acerca de” 3 Construye el contexto page, site_title, languages, navigation… 4 Elige una plantilla page-acerca-de.html? si no, page.html 5 Se renderiza dentro de base.html HTML enviado al navegador Cada valor llega ya traducido al idioma del visitante; la plantilla solo imprime lo que recibe.
Una petición de principio a fin. Los pasos 3 y 4 son donde el tema hace su trabajo.

Dos cosas a tener en cuenta. El idioma se lee del prefijo de la URL, así que /es-es/acerca-de y /about son la misma página en dos idiomas, y el contexto que recibe tu plantilla ya está en el idioma correcto — cada título, slug, etiqueta de menú y contenido. Y la plantilla se elige por ti: para una página cuyo slug en el idioma principal es about, FolioPress busca primero page-about.html y recurre a page.html si no existe. No hay nada que conectar.

Un armazón, muchas páginas

Las plantillas son Jinja2 — HTML con dos tipos extra de llaves. {{ page.title }} imprime un valor. {% if page.poster_image_url %} … {% endif %} es una instrucción: bucles, condiciones y herencia.

base.html el armazón común a todas las páginas <head>, navegación, selector de idioma {% block content %} el único hueco que cada plantilla rellena pie de página, scripts Cada una de estas rellena el hueco: home.html {% extends %} page.html {% extends %} post.html {% extends %} tag.html {% extends %} search.html {% extends %}
Cabecera y pie viven en un solo sitio; el resto de plantillas solo escribe lo que va en medio.

base.html contiene todo lo que comparten las páginas — el <head>, la navegación, el selector de idioma, el pie, los scripts — con un único hueco en medio. Todas las demás plantillas empiezan igual y rellenan ese hueco:

{% extends "base.html" %}

{% block content %}
  <h1>{{ page.title }}</h1>
  {{ page.content | safe }}
{% endblock %}

Todo lo que se repite entre plantillas — una tarjeta de entrada, una barra de paginación — va en partials/ y se incluye con {% include "partials/post-card.html" %}.

theme.yml: el panel de ajustes

Aquí es donde un tema se describe y configura unos cuantos comportamientos, sin tocar ninguna plantilla. Esto es lo que usan los temas incluidos:

title: "Ink"
description: "Un tema limpio y minimalista"
version: "1.0.0"
author: "FolioPress"

meta:
  supports_dark_mode: true
  responsive: true
  categories: [minimal, portfolio]

settings:                    # libre; disponible en las plantillas como theme_settings
  primary_color: "#2563eb"

home:
  posts: 10                  # cuántas entradas recibe la página de inicio
  order_by: "-published_at"  # las más recientes primero
  has_pagination: false      # ¿un corte, o enlaces de página reales?
  image_ratios:
    poster: "16:9"           # lo que ofrece el recortador de imágenes de la administración aquí
    gallery: "free"

page:  { posts: 10, order_by: "-published_at", has_pagination: false }
post:  { posts: 3,  order_by: "-published_at" }   # entradas relacionadas bajo una entrada
tag:   { posts: 12, order_by: "-published_at", has_pagination: true }

page-about:                  # anulación para una página, por su slug en el idioma principal
  image_ratios: { poster: "1:1" }
post-tutorials:              # anulación para las entradas cuya primera etiqueta tiene este slug
  image_ratios: { poster: "4:3" }
Clave Qué controla
title, description, version, author Se muestran en la página Temas de la administración.
meta.supports_dark_mode, meta.responsive, meta.categories Descriptivos; se usan en la galería de temas.
meta.social_image Un archivo en static/ del tema, de 1200x630, usado como imagen de vista previa cuando una página no tiene imagen de portada propia. Sin él, esa página se comparte con el icono de 180x180.
settings Lo que quieras. Se entrega a todas las plantillas como theme_settings, para que un color o un lema puedan vivir aquí en lugar de en el CSS.
home, page, post, tag La lista de entradas que recibe ese tipo de plantilla: posts (cuántas), order_by, y si has_pagination añade enlaces de página reales o simplemente corta la lista.
order_by -published_at (las más recientes primero) o published_at; igual con created_at y updated_at, con o sin el - inicial; o random.
image_ratios Lo que ofrece el recortador de imágenes de la administración al editar ese tipo de contenido: una proporción como 16:9, una lista de ellas, o free. Valores separados para poster y gallery.
page-<slug>, post-<tag-slug> Las mismas claves, anulando page/post para una página (por su slug en el idioma principal) o para las entradas cuya primera etiqueta tiene ese slug.

Qué puede ver cada plantilla

Una plantilla nunca va a buscar nada. Recibe un conjunto de valores listos para usar — FolioPress lo llama el contexto — e imprime lo que necesita. Parte del conjunto es igual en todas las páginas; el resto depende de qué plantilla se está renderizando.

Todas las plantillas reciben este conjunto page site_title site_description navigation_menu languages current_language language_switch_urls footer static_url media_url home_url canonical_url hreflang_urls t() …y además, según la plantilla: home.html posts pages is_home page.html posts is_contact_page post.html related_posts is_post tag.html tag posts search.html search_query search_results results_count
El conjunto común es el mismo en todas las páginas; los extras son lo que distingue a cada tipo de plantilla.

Común a todas las plantillas

Variable Qué es
page La página o entrada que se está viendo, ya en el idioma actual (ver más abajo). None en el archivo de etiquetas y en la búsqueda.
site_title, site_description, footer De los Ajustes del sitio, en el idioma actual. footer ya viene renderizado desde Markdown.
navigation_menu El menú configurado en la administración: una lista de {title, url, slug, page_id}, cada url lista para usar en este idioma.
languages Los idiomas del sitio, indexados por código. Cada entrada: code, name, native_name, short_code, menu_label, flag.
current_language, current_language_code, default_language_code El idioma que se está renderizando (con la misma forma que una entrada de languages), su código y el idioma principal del sitio.
language_switch_urls Indexado por código de idioma: la URL de esta misma página en ese idioma, slug traducido incluido.
home_url, url_language_code La página de inicio en el idioma actual (/ o /pt-pt) y el prefijo en minúsculas que debes poner delante de cualquier URL que construyas tú ('' para el idioma principal).
static_url, media_url Desde dónde se sirven los archivos de este tema (/static/<tema>) y dónde viven las imágenes subidas.
page_title, meta_description, canonical_url, hreflang_urls, social_image_url, twitter_card_type, json_ld Todo lo que necesita el <head>: un título de pestaña sensato, URLs canónica y alternativas por idioma absolutas, una imagen Open Graph y datos estructurados — todo precalculado.
theme_name, theme_info, theme_settings El nombre del tema activo, su theme.yml completo y su bloque settings.
t El traductor del propio tema: {{ t('Read more') }} busca la frase en locales/<idioma>.json (ver más abajo).
request La petición FastAPI, para el raro caso en que necesites la ruta o un parámetro de consulta.

El objeto page

Tiene la misma forma tanto si es una página, una entrada o un elemento de cualquier lista de entradas:

Campo Notas
title, slug, url En el idioma actual. url es completa, incluido el prefijo de idioma o el dominio propio del idioma.
content Ya convertido de Markdown a HTML — imprímelo con {{ page.content | safe }}.
excerpt, meta_description Formas cortas, en el idioma actual.
poster_image_url URL completa de la imagen de portada, o None.
gallery, content_gallery Listas de imágenes: {id, url, title, width, height, original_filename}. Usa image.url directamente.
tags {slug, title, url} por cada etiqueta pública. Las etiquetas privadas nunca se incluyen.
is_post, is_featured, published_at, created_at, updated_at Si es una entrada, si está destacada, y sus fechas.
id El id en la base de datos — útil para anclas y claves de caché.

Listas de entradas

Plantilla Variable Contenido
home.html posts (alias recent_posts), pages, is_home Entradas que comparten una etiqueta con la página de inicio, con el tamaño y orden de la sección home del theme.yml. Una página de inicio sin etiquetas no recibe entradas — etiquétala para llenarla.
page.html posts (alias page_posts) Entradas que comparten una etiqueta con esta página, de la sección page. Así es como una página “Blog” lista sus entradas.
post.html related_posts, is_post Otras entradas que comparten una etiqueta con esta, de la sección post.
tag.html tag (title, slug), posts Todas las entradas publicadas con esta etiqueta, de la sección tag.
search.html search_query, search_results, results_count El término buscado y las páginas y entradas coincidentes.

Cada lista de entradas viene con paginación de la misma forma: has_pagination, current_page, total_pages, has_previous, has_next, previous_page, next_page y pagination_urls con first, previous, next y last — None donde no hay a dónde ir. Con `has_pagination` desactivado en `theme.yml`, la lista simplemente se corta en `posts` y los indicadores quedan en falso.

Idiomas y el selector

Dos de los campos de idioma se calculan por ti. short_code es solo el código del idioma — EN — salvo que el sitio tenga activada también otra variante del mismo idioma, en cuyo caso conserva la región: EN-US y EN-GB. menu_label sigue la misma regla con nombres: English, o English (US) y English (UK). Es exactamente la regla que usa el selector de idioma de la propia administración, así que el sitio y la administración nunca discrepan en una etiqueta. Un selector completo es, por tanto, corto:

{% if languages | length > 1 %}
<button type="button" aria-label="{{ t('Change language') }}">
  {{ current_language.short_code }}
</button>
<ul>
  {% for lang in languages.values() | sort(attribute='menu_label') %}
  <li>
    {% if lang.code == current_language.code %}
      <span aria-current="true">{{ lang.menu_label }}</span>
    {% else %}
      <a href="{{ language_switch_urls[lang.code] }}" hreflang="{{ lang.code }}">{{ lang.menu_label }}</a>
    {% endif %}
  </li>
  {% endfor %}
</ul>
{% endif %}

language_switch_urls ya apunta a la misma página en cada idioma — el slug traducido, o la página de inicio si esa traducción aún no existe —, así que un selector nunca construye una URL por sí mismo.

Las palabras del propio tema

El contenido lo traduce el autor en la administración. Las palabras del propio tema — “Leer más”, “Buscar”, “Todos los derechos reservados” — las traduce el tema, en templates/locales/<idioma>.json:

{
  "Read more": "Leer más",
  "Search": "Buscar",
  "Change language": "Cambiar de idioma"
}

Envuelve la frase en {{ t('Read more') }} y aparecerá en el idioma del visitante. Una frase que falte en el archivo de ese idioma recurre al archivo del idioma principal, y después a la propia frase — así que un tema a medio traducir sigue funcionando, solo que en inglés donde aún no hay traducción. Los archivos viajan con el tema cuando se exporta.

Una página con su propio diseño

Una página de aterrizaje, un formulario de contacto, una cuadrícula de portfolio — a veces una página merece un diseño que ninguna otra necesita. Añade una plantilla llamada page-<slug>.html, donde <slug> es el slug de esa página en el idioma principal, y FolioPress la usará para esa página en todos los idiomas. Borra el archivo y la página vuelve discretamente a page.html.

{# templates/page-about.html #}
{% extends "base.html" %}
{% block content %}
<section class="about">
  <img src="{{ page.poster_image_url }}" alt="">
  <h1>{{ page.title }}</h1>
  {{ page.content | safe }}
</section>
{% endblock %}

La página que un administrador elige como página del formulario de contacto recibe un pequeño extra: is_contact_page es verdadero, contact_form_action es adonde debe enviarse el formulario, y contact_guard_token / contact_honeypot_field son dos campos ocultos que mantienen fuera a los bots. Tras un envío, contact_status te dice si mostrar un agradecimiento o un error.

Empieza por un tema que funciona

La forma más rápida de construir un tema es abrir uno de los incluidos — todos tienen una demostración en vivo — y leerlo junto a esta página. Duplícalo en la administración, cambia el CSS, luego las plantillas, y tendrás algo tuyo sin haber partido nunca de un archivo en blanco.

Dos hábitos que ahorran tiempo. Imprime los valores que te da el contexto en lugar de reconstruirlos — page.url, tag.url y language_switch_urls ya saben de prefijos de idioma y dominios personalizados. Y mantén el armazón en base.html: si te descubres pegando una cabecera en una segunda plantilla, pertenece a la base.