Criar temas

Tudo o que escreve no FolioPress — páginas, artigos, etiquetas — vive separado da forma como é mostrado. Essa camada de apresentação é um tema, e este guia desmonta um: a pasta, os templates, o ficheiro de definições e exatamente que dados cada template recebe. Se está à vontade com HTML e CSS, tem tudo o que precisa.

Um tema é uma pasta

Troque o tema e as mesmas páginas, artigos e menu são redesenhados — nada no conteúdo muda, só a roupa. Um tema é uma pasta com três coisas: um ficheiro de definições, templates e recursos estáticos.

Um tema é uma pasta obrigatório opcional my-theme/ theme.yml nome, cores, quantos artigos listar… templates/ base.html a moldura comum a todas as páginas home.html a página inicial page.html uma página (Sobre, Contacto…) post.html um artigo do blogue tag.html o arquivo de uma etiqueta search.html resultados da pesquisa 404.html · 500.html page-<slug>.html um layout próprio para uma página partials/ trechos incluídos noutros templates locales/pt-PT.json … static/ styles.css · scripts.js favicon.svg · apple-touch-icon.png
Quatro templates e o theme.yml chegam; tudo o resto é um extra opcional.

Quatro templates são obrigatórios — base.html, home.html, page.html e post.html — e um tema sem eles não carrega. Tudo o resto é opcional: um arquivo de etiquetas, uma página de pesquisa, páginas de erro personalizadas, um layout próprio para uma página específica e uma pasta locales/ para as palavras do próprio tema. Os temas podem ser editados diretamente na administração, em Temas, exportados como zip e importados noutro site.

De um URL a uma página

Ajuda ver o que o FolioPress faz com um pedido antes de o seu template sequer correr, porque explica por que razão os templates são tão curtos.

1 O visitante abre um URL /pt-pt/sobre 2 O FolioPress encontra o idioma e a página pt-PT · “Sobre” 3 Monta o contexto page, site_title, languages, navigation… 4 Escolhe um template page-sobre.html? senão page.html 5 Renderiza dentro de base.html HTML enviado ao navegador Cada valor chega já traduzido para o idioma do visitante; o template limita-se a imprimir o que recebe.
Um pedido do princípio ao fim. Os passos 3 e 4 são onde o tema faz o seu trabalho.

Duas coisas a reter. O idioma é lido do prefixo do URL, pelo que /pt-pt/sobre e /about são a mesma página em dois idiomas, e o contexto entregue ao template já vem no idioma certo — cada título, slug, rótulo de menu e conteúdo. E o template é escolhido por si: para uma página cujo slug no idioma principal é about, o FolioPress procura primeiro page-about.html e recorre a page.html se não existir. Não há nada para ligar.

Uma moldura, muitas páginas

Os templates são Jinja2 — HTML com dois tipos extra de parênteses. {{ page.title }} imprime um valor. {% if page.poster_image_url %} … {% endif %} é uma instrução: ciclos, condições e herança.

base.html a moldura comum a todas as páginas <head>, navegação, seletor de idioma {% block content %} o único espaço que cada template preenche rodapé, scripts Cada um destes preenche o espaço: home.html {% extends %} page.html {% extends %} post.html {% extends %} tag.html {% extends %} search.html {% extends %}
Cabeçalho e rodapé vivem num só sítio; os outros templates só escrevem o que vai no meio.

O base.html contém tudo o que as páginas partilham — o <head>, a navegação, o seletor de idioma, o rodapé, os scripts — com um único espaço vazio no meio. Todos os outros templates começam da mesma forma e preenchem esse espaço:

{% extends "base.html" %}

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

Tudo o que se repete entre templates — um cartão de artigo, uma barra de paginação — vai para partials/ e é incluído com {% include "partials/post-card.html" %}.

theme.yml: o painel de definições

É aqui que um tema se descreve e configura alguns comportamentos, sem tocar num template. Isto é o que os temas incluídos usam:

title: "Ink"
description: "Um tema limpo e minimalista"
version: "1.0.0"
author: "FolioPress"

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

settings:                    # livre; disponível nos templates como theme_settings
  primary_color: "#2563eb"

home:
  posts: 10                  # quantos artigos a página inicial recebe
  order_by: "-published_at"  # mais recentes primeiro
  has_pagination: false      # um corte, ou ligações de página reais?
  image_ratios:
    poster: "16:9"           # o que o recorte de imagens da administração oferece aqui
    gallery: "free"

page:  { posts: 10, order_by: "-published_at", has_pagination: false }
post:  { posts: 3,  order_by: "-published_at" }   # artigos relacionados sob um artigo
tag:   { posts: 12, order_by: "-published_at", has_pagination: true }

page-about:                  # substituição para uma página, pelo slug no idioma principal
  image_ratios: { poster: "1:1" }
post-tutorials:              # substituição para artigos cuja primeira etiqueta tem este slug
  image_ratios: { poster: "4:3" }
Chave O que controla
title, description, version, author Mostrados na página Temas da administração.
meta.supports_dark_mode, meta.responsive, meta.categories Descritivos; usados na galeria de temas.
meta.social_image Um ficheiro em static/ do tema, com 1200x630, usado como imagem de pré-visualização quando uma página não tem imagem de destaque própria. Sem ele, essa página é partilhada com o ícone de 180x180.
settings O que quiser. Entregue a todos os templates como theme_settings, para que uma cor ou um slogan possam viver aqui em vez de no CSS.
home, page, post, tag A lista de artigos que esse tipo de template recebe: posts (quantos), order_by, e se has_pagination acrescenta ligações de página reais ou apenas corta a lista.
order_by -published_at (mais recentes primeiro) ou published_at; o mesmo para created_at e updated_at, com ou sem o - inicial; ou random.
image_ratios O que o recorte de imagens da administração oferece ao editar esse tipo de conteúdo: uma proporção como 16:9, uma lista delas, ou free. Valores separados para poster e gallery.
page-<slug>, post-<tag-slug> As mesmas chaves, substituindo page/post para uma página (pelo slug no idioma principal) ou para os artigos cuja primeira etiqueta tem esse slug.

O que cada template consegue ver

Um template nunca vai buscar nada. Recebe um conjunto de valores prontos a usar — o FolioPress chama-lhe o contexto — e imprime o que precisa. Parte do conjunto é igual em todas as páginas; o resto depende do template que está a ser renderizado.

Todos os templates recebem 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() …e ainda, consoante o template: 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
O conjunto comum é igual em todas as páginas; os extras são o que distingue cada tipo de template.

Comum a todos os templates

Variável O que é
page A página ou artigo em exibição, já no idioma atual (ver abaixo). None no arquivo de etiquetas e na pesquisa.
site_title, site_description, footer Das Definições do site, no idioma atual. O footer já vem renderizado a partir de Markdown.
navigation_menu O menu configurado na administração: uma lista de {title, url, slug, page_id}, cada url pronto a usar neste idioma.
languages Os idiomas do site, indexados por código. Cada entrada: code, name, native_name, short_code, menu_label, flag.
current_language, current_language_code, default_language_code O idioma a ser renderizado (com a mesma forma de uma entrada de languages), o seu código e o idioma principal do site.
language_switch_urls Indexado por código de idioma: o URL desta mesma página nesse idioma, slug traduzido incluído.
home_url, url_language_code A página inicial no idioma atual (/ ou /pt-pt) e o prefixo em minúsculas a pôr à frente de qualquer URL que construa por si ('' para o idioma principal).
static_url, media_url De onde são servidos os ficheiros deste tema (/static/<tema>) e onde vivem as imagens carregadas.
page_title, meta_description, canonical_url, hreflang_urls, social_image_url, twitter_card_type, json_ld Tudo o que o <head> precisa: um título de separador sensato, URLs canónico e alternativos por idioma absolutos, uma imagem Open Graph e dados estruturados — tudo pré-calculado.
theme_name, theme_info, theme_settings O nome do tema ativo, o seu theme.yml completo e o bloco settings.
t O tradutor do próprio tema: {{ t('Read more') }} procura a frase em locales/<idioma>.json (ver abaixo).
request O pedido FastAPI, para o raro caso em que precisa do caminho ou de um parâmetro.

O objeto page

Tem a mesma forma quer seja uma página, um artigo ou uma entrada em qualquer lista de artigos:

Campo Notas
title, slug, url No idioma atual. O url é completo, incluindo o prefixo de idioma ou o domínio próprio do idioma.
content Já convertido de Markdown para HTML — imprima-o com {{ page.content | safe }}.
excerpt, meta_description Formas curtas, no idioma atual.
poster_image_url URL completo da imagem de capa, ou None.
gallery, content_gallery Listas de imagens: {id, url, title, width, height, original_filename}. Use image.url diretamente.
tags {slug, title, url} para cada etiqueta pública. As etiquetas privadas nunca são incluídas.
is_post, is_featured, published_at, created_at, updated_at Se é um artigo, se está em destaque, e as suas datas.
id O id na base de dados — útil para âncoras e chaves de cache.

Listas de artigos

Template Variável Conteúdo
home.html posts (alias recent_posts), pages, is_home Artigos que partilham uma etiqueta com a página inicial, na quantidade e ordem da secção home do theme.yml. Uma página inicial sem etiquetas não recebe artigos — etiquete-a para a preencher.
page.html posts (alias page_posts) Artigos que partilham uma etiqueta com esta página, da secção page. É assim que uma página “Blogue” lista os seus artigos.
post.html related_posts, is_post Outros artigos que partilham uma etiqueta com este, da secção post.
tag.html tag (title, slug), posts Todos os artigos publicados com esta etiqueta, da secção tag.
search.html search_query, search_results, results_count O termo pesquisado e as páginas e artigos correspondentes.

Cada lista de artigos vem com paginação na mesma forma: has_pagination, current_page, total_pages, has_previous, has_next, previous_page, next_page e pagination_urls com first, previous, next e last — None onde não há para onde ir. Com `has_pagination` desligado no `theme.yml`, a lista é simplesmente cortada em `posts` e os indicadores ficam a falso.

Idiomas e o seletor

Dois dos campos de idioma são calculados por si. short_code é só o código do idioma — EN — a menos que o site tenha também outra variante do mesmo idioma ativa, caso em que mantém a região: EN-US e EN-GB. menu_label segue a mesma regra com nomes: English, ou English (US) e English (UK). É exatamente a regra que o seletor de idioma da própria administração usa, pelo que o site e a administração nunca discordam de um rótulo. Um seletor completo é, por isso, curto:

{% 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 %}

O language_switch_urls já aponta para a mesma página em cada idioma — o slug traduzido, ou a página inicial se essa tradução ainda não existir —, pelo que um seletor nunca constrói um URL por si.

As palavras do próprio tema

O conteúdo é traduzido pelo autor na administração. As palavras do próprio tema — “Ler mais”, “Pesquisar”, “Todos os direitos reservados” — são traduzidas pelo tema, em templates/locales/<idioma>.json:

{
  "Read more": "Ler mais",
  "Search": "Pesquisar",
  "Change language": "Mudar idioma"
}

Envolva a frase em {{ t('Read more') }} e ela aparece no idioma do visitante. Uma frase em falta no ficheiro desse idioma recorre ao ficheiro do idioma principal, e depois à própria frase — pelo que um tema meio traduzido continua a funcionar, apenas em inglês onde ainda não há tradução. Os ficheiros viajam com o tema quando é exportado.

Uma página com o seu próprio layout

Uma página de apresentação, um formulário de contacto, uma grelha de portefólio — às vezes uma página merece um layout de que nenhuma outra precisa. Adicione um template chamado page-<slug>.html, em que <slug> é o slug dessa página no idioma principal, e o FolioPress usa-o para essa página em todos os idiomas. Apague o ficheiro e a página volta discretamente ao 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 %}

A página que um administrador escolhe como página do formulário de contacto recebe um pequeno extra: is_contact_page é verdadeiro, contact_form_action é para onde o formulário deve submeter, e contact_guard_token / contact_honeypot_field são dois campos ocultos que mantêm os robôs de fora. Depois de uma submissão, contact_status diz-lhe se deve mostrar um agradecimento ou um erro.

Comece por um tema que funciona

A forma mais rápida de construir um tema é abrir um dos incluídos — todos têm uma demonstração ao vivo — e lê-lo ao lado desta página. Duplique-o na administração, mude o CSS, depois os templates, e terá algo seu sem nunca partir de um ficheiro em branco.

Dois hábitos que poupam tempo. Imprima os valores que o contexto lhe dá em vez de os reconstruir — page.url, tag.url e language_switch_urls já sabem dos prefixos de idioma e dos domínios personalizados. E mantenha a moldura no base.html: se der por si a colar um cabeçalho num segundo template, ele pertence à base.