Na partes anteriores desta série de artigos já contei o que motivou a migração do meu blog antigo para este novo, como foi o processo de escolha da nova plataforma e como criar um ambiente de desenvolvimento que considero ideal para, dentre outras coisas, o trabalho que vamos executar a seguir: customizar um tema pré-existente do Pelican. Como não dá para falar disso en passant, vou dedicar este artigo somente a este tema para ter o espaço necessário para tratar mais detalhadamente sobre a customização de temas no Pelican.

Cai dentro!

Customização de um tema

Esta parte do texto não é um tutorial para iniciantes! Se você não ler a documentação do Pelican e do Jinja2 e não souber ao menos o básico de Python, não há o que aprender aqui.

Em geral, todo mundo que resolve ter um blog começa com a escolha do tema que mais agrada. Seja no WordPress, MovableType, Blogger, whatever, o cidadão/cidadã sai em busca de um tema que contém ao menos os recursos de navegação e estilo que se já não são exatamente o que ele busca, pelo menos está perto disso. Se está perto e o cidadão/cidadã dispõe de algum conhecimento de programação para a web, ele vai lá e customiza. Com o Pelican não é diferente.

Para o MexApi eu parti de um tema chamado Gum. Visualmente falando, ele é quase isso que você vê agora. O Gum usa um framework de apresentação que eu não conhecia, mas que é muito poderoso e fácil de usar, o Gumby Framework. Mas testando as páginas, notei que era capenga em um outro tanto de coisas: arquitetura de informação (PDF) inconsistente, semântica e SEO zero, páginas internas pouco informativas, dentre outras coisas que me incomodavam. Como o 'look'n feel' era o que mais me agradava entre os temas disponíveis, decidi ir com o Gum como base para daí modificar as coisas que eu gostaria para o meu jeito minucioso de fazer as coisas.

Jinja2

Graças a um DSL para o Python chamado Jinja2, o meu trabalho foi bastante simplificado, embora algumas poucas vezes bastante complicado (falo disso adiante). O Jinja2 é o cara responsável por pegar um arquivo de texto com marcação reStructuredText ou Markdown e transformá-lo no documento HTML que você está lendo agora. Aplique um CSS (no caso o Gumby) em cima deste HTML e você tem um site bonitinho.

A estrutura de temas do Pelican é bastante simples:

$ ls
LICENSE  README.md  screenshot.png  static/  templates/  typography.png

Os arquivos na raiz do tema são firula. O que importa está nos diretórios. Em static estão os CSSs, JavaScripts, fontes e tudo o mais que transformam um HTML puro em alguma coisa visual e funcionalmente atraente. Já templates guarda os arquivos HTML que efetivamente constroem o site. São nesses arquivos que a diversão reside.

Vamos pegar um exemplo.

{% extends "base.html" %}
{% block title %}{{ SITENAME }}: Tags{% endblock %}
{% block description %}Everything on {{ SITENAME }}, organized by tags.{% endblock %}
{% block tctitle %}{{ SITENAME }}: Tags{% endblock %}
{% block tcdesc %}Everything on {{ SITENAME }}, organized by tags.{% endblock %}
{% block ogtitle %}{{ SITENAME }}: Tags{% endblock %}
{% block ogdesc %}Everything on {{ SITENAME }}, organized by tags.{% endblock %}
{% block ogperma %}{{ SITEURL }}/tags.html{% endblock %}
{% block content %}
        <nav id="tag-list">
          <h2>Everything here, organized by tags</h2>
          <ul class="item-list">
{% for tag, articles in tags|sort %}
            <li class="item"><i class="icon-tag muted"></i><a href="{{ SITEURL }}/{{ tag.url }}">{{ tag }} <span class="muted">[{{ articles|count }}]</span></a></li>
{% endfor %}
          </ul><!-- /.item-list -->
        </nav><!-- /#tag-list -->
{% endblock %}

Este é o código da página de tags. Vamos dissecá-lo um pouco para entender melhor como o Jinja2 atua.

  1. {% extends "base.html" %}: isso diz ao Jinja2 que este template estende as funcionalidades do template base.html.
  2. {% block title %}{{ SITENAME }}: Tags{% endblock %}: procura em base.html o bloco chamado title e altera o seu conteúdo para o que está declarado na variavel de configuração {{ SITENAME }} mais o texto : Tags. Note que as variáveis aparecem entre {{ ... }} e o texto que vai sair do jeito que está simplesmente é escrito como se fosse um HTML normal.
  3. {% block content %}...{% endblock %}: altera o bloco chamado content.
  4. {% for tag, articles in tags|sort %}...{% endfor %}: executa um loop usando o conteúdo das variáveis para preencher o HTML, sendo que a estas variáveis é aplicado o filtro interno do Jinja2 chamado sort.
  5. {{ SITEURL }}/{{ tag.url }}: {{ SITEURL }} é uma variável de configuração, enquanto {{ tag.url }} é uma variável do contexto do Pelican.

Regrinha:

  • Variáveis em {{ MAIÚSCULO }} vem do arquivo de configuração do Pelican (pelicanconf.py ou publishconf.py), enquanto as em {{ minúsculo }} vem do contexto de tempo de execução do Pelican (variáveis internas).

Com esse jeitão aí e depois de alguma leitura na documentação do Pelican e do Jinja2 (RTFM), você conseguirá se virar muito bem na customização dos templates. Eu que não sou lá dos mais espertos levei algumas poucas horas para pegar o espírito da coisa.

Semântica, SEO, structured data

Depois de pegar a manha com os templates, saí customizando um monte de coisas. Fiz um extenso trabalho de semântica em HTML5, SEO para ter bons resultados nos mecanismos de pesquisa, adicionei dados estruturados, incluí ícones bonitinhos, melhorei a apresentação em dispositivos móveis, criei uma boa apresentação de imagens e vídeos e por aí vai.

No fim, apesar do visual ser muito semelhante ao tema original, as entranhas mudaram radicalmente. Tanto que eu criei o meu próprio tema para desacoplar do Gum original: o MexApi. Se você quiser ter uma idéia do tamanho da mudança, eu criei e mantenho1 um patch do tema MexApi em relação ao tema original. No momento em que escrevo este artigo, são nada menos do que 863 linhas de código diferentes!

Top Tags

Se você for um cidadão/cidadã detalhista como eu as coisas podem e irão se complicar. Vou contar os dois eventos mais complexos que me ocorreram no trabalho de customização e como cheguei a uma solução em ambos. Dica: quem tem amigo e cara-de-pau, tem tudo.

O primeiro foi relativo à lista de tags (top tags aí do lado) que aparece na home e em algumas páginas internas, como esta. No tema original, ela é implementada como um 'tag cloud', onde o código gera algumas classes especiais que, via CSS, vc pode ajustar o tamanho de cada texto da tag. Eu não gosto desta implementação e prefiro listar as tags que contenham mais artigos, bem como de mostrar a quantidade de artigos em cada tag. E isso me gerou um problema: não encontrei meios de fazer o Pelican ou o Jinja2 ordenar as tags por quantidade de artigos em cada uma. E aí entrou o amigo.

Eu tenho o prazer e orgulho de ser amigo pessoal do Elcio Ferreira, da Visie, um dos consultores, gerentes de projeto e desenvolvedores mais competentes que eu já vi por aí, por lá e acolá, além de ser um ser humano de primeira classe. Como eu sei que ele é um especialista em Python, pedi ajuda. Ele se prontificou a me ajudar e até clonou o meu repositório do blog no GitHub localmente para entender melhor o problema. Para resolver a questão ele explorou, com a elegância de sempre, um recurso bem jóia do Jinja2, os filtros. No mesmo dia em que pedi pinico, ele surge com esta solução que é ao mesmo tempo simples e engenhosa:

def tagsort(tags):
  return sorted(tags,lambda a,b:len(b[1])-len(a[1]))

Com este filtro, devidamente habilitado no pelicanconf.py pela variável JINJA_FILTERS, o template que faz o que eu quero ficou assim:

<nav id="tags">
  <h4>Top Tags</h4>
{% if tags %}
  <ul>
{% for tag, articles in tags|tagsort %}
{% if loop.index0 == TAG_CLOUD_LENGTH %}{% break %}{% endif %}
    <li class="tag"><i class="icon-tag muted"></i><a href="{{ SITEURL }}/{{ tag.url }}">{{ tag }}</a> <span class="muted">[{{ articles|count }}]</span></li>
{% endfor %}
    <li class="tag-1"><i class="icon-tag muted"></i><a href="{{ SITEURL }}/tags.html">All tags</a>  <span class="muted">[{{ tags|count }}]</span></li>
  </ul>
</nav><!-- /#tags -->
{% endif %}

Valeu por essa, Elcio! Quando quiser, é só marcar aquele churrasco, por minha conta. ;)

Archive Page

Boa parte do que há de legal (ao menos para mim) neste blog no que diz respeito a apresentação e entranhas veio da inspiração de outro blog: o de um britânico2 gente boa chamado Duncan Lock. O blog dele é fantástico! Além de ter bons artigos sobre vários temas da tecnologia (o cara é usuário de Ubuntu, XFCE, Thunar, um excelente ilustrador e um grande dev também), foi o blog dele que me motivou a deixar de lado a idéia inicial do Octopress e cair dentro do Pelican. Recomendo muito a visita ao blog do Dunc.

A página de archive dele é linda! Completamente diferente da original do Gum, muito bem estruturada e fácil de navegar. Eu queria uma igual e peguei o código dele para fazer a minha. Quando fui implementar no meu tema acabei descobrindo um bug no código dele que fazia com que os posts de todos os anos fossem listados sob a separação por ano, e não apenas os posts do ano em questão.

O código original do Dunc que gera a página de arquivo é:

{% for year, date_year in dates|groupby( 'date.year' )|sort(reverse=True) %}
  <h2 class="archive-year">{{ year }}</h2>
  {% for month, date_month in dates|groupby( 'date.month' )|sort(reverse=True) %}
  <h3>{{ month|month_name }}</h3>

Neste loop, note que primeiro é gerado um cabeçalho com o ano, e depois há um outro loop dentro do primeiro que preenche com os artigos, que deveriam ser o daquele ano apenas. Mas, como a lógica está incorreta, todos os artigos, de qualquer ano, aparecem dentro do todos os anos. Como o primeiro post do Duncan foi em abril deste ano, ele não havia percebido esse bug, já que a condição para ele se manifestar ainda não existe.

Quebrei a cabeça com isso por várias horas! Muitas mesmo. Não conseguia ver uma luz para resolver esse problema e estava ficando frustrado com isso. Já havia abusado do Elcio anteriormente e não queria pedir novamente a ele para me ajudar. Meu conhecimento em Python e Jinja2 havia batido no limite. O que eu fiz? Fui atrás do email do Duncan e, na maior cara dura, enviei uma mensagem a ele. Disse a ele que havia descoberto um bug na página de arquivos de seu blog, que eu estava querendo implementar igual, e pedi ajuda para resolver.

Ele respondeu horas depois, no mesmo dia. Muito cordialmente, agradeceu por eu apontar o bug, que ele havia reconhecido o problema, mas que como estava em férias, poderia não olhar isso tão cedo. Eu respondi agradecendo e dizendo que se encontrasse uma solução, enviaria para ele em retribuição à inspiração. Voltei a buscar uma solução para o problema, mas sem conseguir avançar. Alguns minutos depois chega outro email dele com uma solução. Acho que o Duncan é muito parecido comigo neste aspecto: se sei que há um problema com algo que eu cuido, não paro de pensar nisso até o problema estar resolvido.

A sugestão dele foi essa, elegantérrima:

{% for year, date_year in dates|groupby( 'date.year' )|sort(reverse=True) %}
  <div class="tab-content {% if loop.first %}active{% endif %}">
{% for month, date_month in date_year|groupby( 'date.month' )|sort(reverse=True) %}
    <h3>{{ month|month_name }}</h3>

Note que o primeiro loop é idêntico ao anterior. Já o segundo não. No segundo, ao invés de buscar os meses no objeto dates, ele busca em dates_year. Matou a charada com cinco caracteres!

O meu template da página de artigos é implementado de modo diferente ao que o Duncan optou, uma vez que eu aproveitei o Gumby Framework e usei os tabs que ele possui. Mas fora a apresentação, a lógica é idêntica.

Agradeço a Duncan Lock por essa! Thank you very much, Dunc!

pelicanconf.py

Há uma série de customizações aqui no MexApi que dependem de filtros customizados do Jinja2 e de varáveis que criei, mas que não existem no pelicanconf.py original. Por isso, sugiro a você dar uma olhada no meu para ter uma idéia do que precisa ser feito na sua customização. Se você quiser saber onde uma variável ou filtro do Jinja2 é usado no código, rode o seguinte comando na raiz do seu Pelican:

$ grep -r "SUMMARY_ARCHIVE_LENGTH"

Isso faz o grep procurar recursivamente pela string SUMMARY_ARCHIVE_LENGTH3. Daí você vai saber em qual arquivo a variável ou filtro está sendo usado, o que é particularmente útil na hora de depurar erros no make regenerate do Pelican.

Patch no Pelican

O Duncan também implementou uma coisa bem legal no Pelican dele que eu também queria: hora de atualização dos artigos baseado na hora de modificação do arquivo no filesystem. Só que isso requer um patch no próprio Pelican para criar um objeto novo, que eu chamei de mdate. Veja como eu uso:

<time class="updated hidden" itemprop="dateModified" datetime="{{ article.mdate.strftime(DATE_ISO) }}">{{ article.mdate.strftime(DATE_ISO) }}</time>

Esse objeto, mdate, não existe no Pelican e se você fizer referência a ele obterá um erro e seu blog não será gerado na hora do make. Para obter esse recurso, a melhor coisa é você ir direto na fonte e aprender com quem o criou. Se você está fazendo o que eu disse no segundo artigo e usando o virtualenv, o generators.py ao qual ele se refere estará no seguinte caminho:

~/.virtualenvs/meusite/lib64/python2.7/site-packages/pelican/generators.py

Note que o Duncan chama a variável de last_modified_date. Eu preferi abreviar e chamar a minha apenas de mdate. Eu e meu minimalismo.

Mais um artigo longo. Acredito que por uma boa razão e desejo que seja proveitoso para alguém. Se tiver dúvidas ou quiser corrigir algo, os comentários estão aí para isso.


  1. Eu não mantenho mais esse patch. O MexApi v3 mudou tanto que não era mais possível fazer isso. O link para o patch foi alterado para o último commit nele. Após isso, ele foi abandonado, coitado. 

  2. Eu havia dito que o Duncan Lock era canadense. Ele me esclareceu por email que é de fato britânico e atualmente vive no Canadá. 

  3. Ele também apontou que havia um erro de grafia na variável: estava _LENGHT, o correto é _LENGTH. Já corrigi aqui e no código disponível no GitHub. Tks again, Duncan!