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