Usando as tags - Parte 1: página de tags
Eu sempre coloco tags nos posts com intenção de fazer linkagem entre as tags. Mas, que tal, fazer uso dessa metadado e transformar em algo útil?
Esse é o primeiro post em uma série de 3 posts sobre colocar tags no Computaria. Os outros são:
- Página de tags
- Tags nos posts, visível
- Tags no índice
Plugins do Jekyll
Para alterar o comportamento do Jekyll para algo além do que ele foi configurado para fazer, você precisa enxertar código nele. No caso, como Jekyll roda em Ruby, se enxerta código Ruby. E o jeito que o Jekyll prevê de se fazer isso é usando o que ele chama de “plugins”.
Os plugins se situam (localmente) na pasta /_plugins/,
com referência da raiz padrão do Jekyll, onde está o
_config.yml. Localmente, não precisa fazer nenhuma
alteração no arquivo de configuração, o que é diferente
de quando se importa uma gem que você precisa especificar
em no campo plugins o que você deseja usar, como aqui
no computaria se usa o jekyll-katex.
Generators do Jekyll
Especificamente, para gerar uma página, as referências apontam
para se usar um tal de
generator.
No primeiro momento, não entendi o que se dizia da documentação,
mas uma coisa me chamou muito a atenção:
Method:
generate. Description: Generates content as a side-effect.
Tradução livre da descrição:
Gera conteúdo como efeito-colateral.
O que fazer com isso? Bem, por hora, guardar na cabeça. Vamos
a classe foco daqui: Jekyll::Generator. Ela tem um método de
atenção: generate(site). Como brincar com isso? Que tal
interceptar isso?
Coloca a dependência do irb, bundle install para pegar
a atualização disso em específico, e no arquivo que está o
Generator, dentro do generate(site): IRB.start(__FILE__).
Ok, naïve demais. Colocar o campo site como acessível
ao irb seria bem melhor:
require "jekyll"
require 'irb'
module Computaria
def self.s(site)
@@site = site
end
def self.site
@@site
end
class Generator < Jekyll::Generator
def generate(site)
@@site = site
Computaria.s(site)
IRB.start(__FILE__)
end
end
end
E dentro do irb bastaria um Computaria.site para inspecionar os
seus atributos:
rb(main):001> Computaria.site
=> #<Jekyll::Site @source=/path/to/computaria/blog>
Lá dentro, conheci o seguinte:
site.categories: um mapa de nome da categoria para lista de páginas daquilosite.posts: a lista de postagenssite.pages: a lista de “páginas”site.tags: a “lista” de tagssite.static_files: a lista de arquivos estáticos
Sobre site.pages descobri uma coisa bem legal, que fez bastante sentido
agora quando percebi problemas em
Oops, quebrei o about, e agora?
e Criando páginas discretas:
na listagem de site.pages aparecia .css, aparecia .js, até .xml.
Porque, ao ser um arquivo a ser processado com Liquid, ele era colocado
em site.pages antes de ser processado pelo Liquid. Logo, aquilo que
eventualmente seria processado pelo Liquid era considerado a priori
como uma “page” e, por isso, estava ali. Como no
Deixando a pipeline visível para acompanhar deploy do blog
foi usado o Liquid para (por exemplo) o javascript de carregar
a página de pipeline, ele acabou indo parar no about e
causando a quebra mencionado no
Oops, quebrei o about, e agora?.
Para adicionar uma página, preciso apenas fazer um site.pages << my_page,
considerando que my_page seja um objeto do tipo Jekyll::Page.
Criando uma página
Primeiro, experimentar criar uma página de exemplo. Examinando uma das páginas
geradas, vi que o conteúdo era gerado dentro do campo @content. Então, vamos
criar um conteúdo com parte markdown? E parte Liquid?
Coloquei no generator uma coisa bem simples:
site.pages << Marmota.new(site)
E agora fica a questão de definir Marmota. Para interpretar markdown, precisei
identificar a extensão (campo @ext) como sendo .md. No primeiro momento eu
fiz assim:
class Marmota < Jekyll::Page
def initialize(site)
@site = site
@ext = ".md"
@content = "
# Título
Alguma coisa
> {{ site.url }}
"
end
end
E falhou miseravelmente:
Liquid Exception: undefined method `[]' for nil:NilClass in <...>/computaria/blog/_layouts/default.html
rake aborted!
NoMethodError: undefined method `[]' for nil:NilClass (NoMethodError)
@excerpt = data["excerpt"] ? data["excerpt"].to_s : nil
^^^^^^^^^^^
<...>/computaria/blog/Rakefile:11:in `block in <top (required)>'
Mas por quê? Porque basicamente tudo no Liquid é gerado lendo o campo data. Próxima iteração,
adicionar um @data vazio:
class Marmota < Jekyll::Page
def initialize(site)
@site = site
@ext = ".md"
@data = {
}
@content = "
# Título
Alguma coisa
> {{ site.url }}
"
end
end
Ok, agora gerou. Mas teve um conflito:
Conflict: The following destination is shared by multiple files.
The written file may end up with unexpected contents.
<...>/computaria/blog/_site/index.html
- index.html
-
Por que isso? Porque o Jekyll usa algumas dicas para gerar a URL
da página. E no caso na ausência de dicas ele assume index.html
do local onde ele aponta. No caso, não aponta pra nada, então
é um conflito com a raiz do blog. Nada bom. Cutucando as propriedades
descobri que era @basename, e assim funcionou:
class Marmota < Jekyll::Page
def initialize(site)
@site = site
@ext = ".md"
@data = {
}
@basename = "marmota"
@content = "
# Título
Alguma coisa
> {{ site.url }}
"
end
end
E ficou assim o renderizado:
<html><head></head><body><h1 id="título">Título</h1>
<p>Alguma coisa</p>
<blockquote>
<p>https://computaria.gitlab.io</p>
</blockquote>
</body></html>
Para colocar esse
<iframe>bonitinho eu precisei limpar o background. Para tal, poderia ter feito inline umstyle="background-color: white". Mas ao examinar como o resto do blog estava lidando com a cor de fundo, cheguei a conclusão que talvez não fosse a melhor escolha deixar inline, pois se dependia de um valor$background-colornoscss. Para manter isso, criei a classeclean-backgroundassim:.clean-background { background-color: $background-color; }e adicionei as classes da tag
<iframe class="clean-background">
Mas, falando sério? Ficou feio pra caramba. Porque não pegou o layout básico.
Como o layout é algo que vai ser no frigir de tudo manipulado pelo Liquid,
ele reside dentro do campo data:
class Marmota < Jekyll::Page
def initialize(site)
@site = site
@ext = ".md"
@data = {
}
@basename = "marmota"
@content = "
# Título
Alguma coisa
> {{ site.url }}
"
end
end
Vale a pena criar subir localmente e acessar baseado no nome do arquivo
(no caso, /marmota).
Mais alguns campos que podem vir a ser úteis, por questão de completude:
class Marmota < Jekyll::Page
def initialize(site)
@site = site
@ext = ".md"
@data = {
}
@basename = "marmota"
@base = "#{site.source}/marmota"
@name = "marmota.md"
@content = "
# Título
Alguma coisa
> {{ site.url }}
"
end
end
Criando uma página de tags
Ok, agora que sei o básico de se criar uma página dinamicamente, vamos focar no alvo principal: criar a página das tags.
Vou pegar a tag frontend para motivar, porque ela tem diversos
posts (5 publicados, 1 rascunho local). A priori, quero fazer ela
tal qual é a index.html.
Esse vai ser um layout a ser usado pelas diversas listagens de tags,
portanto vou aproveitar e criar logo o layout
tag-list.html.
Só que, no caso, no lugar de resgatar as informações através da
variável site.posts, vou resgatar de page.posts. Ficou assim
a primeira iteração:
---
layout: default
---
<div class="home">
<h1 class="page-heading">Posts</h1>
<ul class="post-list">
{% for post in page.posts %}
{% unless post.draft == 'true' %}
<li>
<span class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</span>
<h2>
<a class="post-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a>
</h2>
</li>
{% endunless %}
{% endfor %}
</ul>
<p class="rss-subscribe">subscribe <a href="{{ "/feed.xml" | prepend: site.baseurl }}">via RSS</a></p>
</div>
Hmmmm, o “subscribe” ali ficou fora de canto, melhor remover. E também não está indicando qual a tag, então melhor colocar a tag em ênfase. Próxima iteração:
---
layout: default
---
<div class="home">
<h1 class="page-heading">Posts de {{ page.tag }}</h1>
<ul class="post-list">
{% for post in page.posts %}
{% unless post.draft == 'true' %}
<li>
<span class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</span>
<h2>
<a class="post-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a>
</h2>
</li>
{% endunless %}
{% endfor %}
</ul>
</div>
Ok, melhorou. Agora preciso me lembrar de, na hora de criar a página,
além de povoar o @data["posts"] com os posts também preencher o
@data["tag"] com o nome da tag.
Bem, tudo tranquilo. Certo? Vamos inserir na nossa página Marmota
para ver como fica, depois vamos lidar com outras questões como
botar no canto certo:
class Marmota < Jekyll::Page
def initialize(site)
@site = site
@ext = ".html"
@data = {
"layout" => "tag-list",
"tag" => "frontend",
"posts" => site.tags["frontend"]
}
@basename = "marmota"
end
end
Ok, deu bom. Mas tem um caso estranho… e se por acaso todas
as publicações forem com
posts.draft == "true"?
Isso significa gerar uma página apenas com o nome da tag sem post algum.
Para eventualmente contornar isso, vou assumir um protocolo:
só irei criar páginas de tags caso tenho no mínimo uma
postagem sem ser draft. E digo mais: já irei passar,
garantidamente, os posts filtrados sem nenhum draft.
Bem, já que estou seguindo esse protocolo, não tem mais porque filtrar para exibir apenas os posts não rascunhos, né? Nesse caso, vamos remover o condicional do layout:
---
layout: default
---
<div class="home">
<h1 class="page-heading">Posts de {{ page.tag }}</h1>
<ul class="post-list">
{% for post in page.posts %}
<li>
<span class="post-meta">{{ post.date | date: "%b %-d, %Y" }}</span>
<h2>
<a class="post-link" href="{{ post.url | prepend: site.baseurl }}">{{ post.title }}</a>
</h2>
</li>
{% endfor %}
</ul>
</div>
Como vou chamar desse jeito, vamos adaptar aqui a chamada de Marmota
para já construir em cima dos posts filtrados:
posts = site.tags["frontend"]
posts_local = posts.select do |p|
p.data["draft"] != 'true'
end
site.pages << Marmota.new(site, posts_local) unless posts_local.empty?
# definição da classe Marmota
class Marmota < Jekyll::Page
def initialize(site, posts)
@site = site
@ext = ".html"
@data = {
"layout" => "tag-list",
"tag" => "frontend",
"posts" => posts
}
@basename = "marmota"
end
end
E assim temos o layout
tag-list.html.
Iterando para gerar as páginas de tags
Já temos quase tudo que precisamos para a página de tags.
Podemos deixar ainda mais adequado passando os posts filtrados
e, também, a tag em si. Desse modo, não precisamos mais nos
preocupar com a tag hard-codada. Aproveitar também e aposentar
a classe Marmota, vamos chamar de TabPage.
A iteração vai ser feita em cima de site.tags. Para iterar
em cima de um dicionário em Ruby, pedimos para dict.each e
passamos um bloco de código que aceita duas variáveis: a
chave do dicionário e o seu valor. A filtragem de valor
é usando array.select, passando um bloco que, ao retornar
true, mantém o elemento e ao retornar false ele não é
inserido na coleção resultante.
site.tags.each do |tag, posts|
posts_local = posts.select do |p|
p.data["draft"] != 'true'
end
site.pages << TagPage.new(site, tag, posts_local) unless posts_local.empty?
end
Só isso infelizmente gerou o conflito de nomes. Todas as tags foram geradas
para o mesmo endereço. Para resolver, basta colocar que o basename dela
dependa também da tag passada:
class TagPage < Jekyll::Page
attr_reader :tag, :posts, :data
def initialize(site, tag, posts)
@site = site # the current site instance.
@base = "#{site.source}/#{tag}" # path to the source directory.
@dir = "tags/#{tag}" # the directory the page will reside in.
@tag = tag
@posts = posts
# All pages have the same filename, so define attributes straight away.
@basename = 'index' # filename without the extension.
@ext = '.html' # the extension.
@name = 'index.html' # basically @basename + @ext.
# Initialize data hash with a key pointing to all posts under current category.
# This allows accessing the list in a template via `page.linked_docs`.
@data = {
"layout" => "tag-list",
"tag" => tag,
"posts" => @posts,
"title" => tag
}
end
end
Para ter um permalink mais adequado, coloquei as páginas geradas dentro
da categoria tags. Isso significou, neste instante, que preciso criar
o objeto de TabPage e adicionar simultaneamente a site.pages
e também a site.categories["tags"].
Para isso, vou usar o continue adequado dentro do bloco: o next.
Assim, ao chegar em um valor que não deve ser manipulado (todos os posts
da tag serem rascunhos), ele irá ignorar a execução daquele bloco
específico e partir pra próxima iteração:
site.categories["tags"] = []
site.tags.each do |tag, posts|
posts_local = posts.select do |p|
p.data["draft"] != 'true'
end
next if posts_local.empty?
tagPage = TagPage.new(site, tag, posts_local)
site.categories["tags"] << tagPage
site.pages << tagPage
end
Listando todas as tags
Até agora, tudo bom, mas essas páginas estão inalcançáveis. Precisaria ter uma página central com a listagem de todas as tags. Vamos criar uma página central que recebe todas as páginas de tags criadas? Já tenho mesmo um vetor só pra isso através das categorias. A ideia é chamar uma classe de página e isto bastar:
central = CentralTag.new(site, site.categories["tags"])
site.categories["tags"] << central
site.pages << central
Vamos seguir o modelo de páginas do índice de posts e também do índice de posts por tag. Só que, agora, como o post individual não está sendo levado em consideração na iteração, preciso de outra coisa para o metadado. Que tal a quantidade de postagens? Algo assim:
<div class='home'>
<h1 class='page-heading'>Posts por tag</h1>
<ul class='post-list'>
{% for tag in page.sitetags %}
<li>
<span class='post-meta'>{{ tag.posts.size }} posts</span>
<h2>
<a class='post-link' href='{{ tag.url | prepend: site.baseurl }}'>{{ tag.tag }}</a>
</h2>
</li>
{% endfor %}
</ul>
</div>
Do jeito que está a ordem das tags parece bem aletória. Então, será que podemos
organizar por nome? Claro. O array fornece o método sort_by:
tags.sort_by do |element|
element.tag
end
esse método permite dizer o que deve ser levado em consideração na hora de
ordenar os elementos. No caso, estou querando usar a string tag, que é
um atributo de TagPage.
Hmmm, algumas coisas não ficaram legais. Para garantir uma bela ordenação,
resolvi que deveria comparar com “lowercase”. Depois percebi que o
acento em álgebra estava atrapalhando. Daí foi mal fácil resolver esse
problema de imediato com o á trocando-o por a:
tags.sort_by do |element|
element.tag.downcase.gsub("á", "a")
end
Ficando assim o todo:
class CentralTag < Jekyll::Page
def initialize(site, tags)
@site = site # the current site instance.
@base = site.source # path to the source directory.
@dir = "tags" # the directory the page will reside in.
# All pages have the same filename, so define attributes straight away.
@basename = 'index' # filename without the extension.
@ext = '.html' # the extension.
@name = 'index.html' # basically @basename + @ext.
# Initialize data hash with a key pointing to all posts under current category.
# This allows accessing the list in a template via `page.linked_docs`.
@data = {
"layout" => "default",
"sitetags" => tags.sort_by do |element| element.tag.downcase.gsub("á", "a") end,
"show" => true,
"title" => "Tags"
}
@content = "
<div class='home'>
<h1 class='page-heading'>Posts por tag</h1>
<ul class='post-list'>
{% for tag in page.sitetags %}
<li>
<span class='post-meta'>{{ tag.posts.size }} posts</span>
<h2>
<a class='post-link' href='{{ tag.url | prepend: site.baseurl }}'>{{ tag.tag }}</a>
</h2>
</li>
{% endfor %}
</ul>
</div>
"
end
end