Agora com o poder de datafiles eu posso fazer algo que está me incomodando: dar aliases a tags.

Por que isso me incomoda?

No meu processo de criação de post, a primeira coisa que eu faço é chamar o rake, como visto em Rakefile, parte 2 - criando rascunho. Nessa parte, eu me faço algumas perguntas:

  • qual o título do post?
  • quais as tags?

E por hora são só essas. E nisso de perguntar quais tags as opções são abertas. Isso permite que eu coloque as tags que eu quiser. E isso é bom, até que acontecem coisas… assim…

javacript e js como tags distintas

e assim…

markdown e md como tags distintas

E, bem… essa flexibilidade ao mesmo tempo que é uma coisa boa ao mesmo tempo me é uma maldição por conta de como a criatividade funciona…

Então, como posso resolver isso? Posso resolver da maneira mais simples, que seria interceptando no processo de rake, mas também posso resolver isso com um plugin no Jekyll!

Ideia geral

Existem alguns cantos que uso tags, mas os mais proeminentes são:

Para o caso do feed.xml e outros cantos que itera em cima das tags do post via liquid, a solução é pra ser via liquid puramente. Algo como

{{ tags | normalize_tags | uniq }}

onde normalize_tags é uma função liquid que eu mesmo irei criar, que usa o datafile de aliases de tags, e uniq já existe.

Para o caso de código Ruby, a solução vai ser via Ruby mesmo. Além disso, vou tentar aproveitar e adicionar a lista de aliases na página da tag, só por pura diversão mesmo.

Fazendo testes

Bem, vamos fazer testes de adicionar filtros liquid antes de por em para valer nos lugares que deveriam de fato ser usados. Vamos primeiro definir o conteúdo do datafile?

Os dados usados no teste são esses, eventualmente eles serão alterados após a escrita dessa seção do artigo:

- tag: javascript
  alias: [ js, javaescripto ]
  description: "gambiarra web"
- tag: typescript
  alias: [ ts ]
  description: "gambiarra web, porém tipada"
- tag: markdown
  alias: [ md ]
  description: "linguagem de marcação mais bonita que html"

Dito isso, hora dos testes…

Acessando os dados para uma tabela simples

Vamos fazer uma escrita simples? No primeiro nível de itemização, a tag em si. No segundo nível, seus aliases.

{%- for tag_data in site.data.tag_alias %}
- {{ tag_data.tag }}
{% for tag_alias in tag_data.alias %}
  - {{ tag_alias }}
{% endfor %}
{% endfor %}

E isso renderiza:

- javascript

  - js

  - javaescripto


- typescript

  - ts


- markdown

  - md


Que gera isso daqui:

  • javascript

    • js

    • javaescripto

  • typescript

    • ts
  • markdown

    • md

Até aqui.

Hmm, esqueci de controlar os espaços… Depois de um pouco de tentativa e erro consegui isso:

{%- for tag_data in site.data.tag_alias %}
- {{ tag_data.tag }}
{%- for tag_alias in tag_data.alias %}
  - {{ tag_alias }}{% endfor %}{% endfor %}

Que renderiza

- javascript
  - js
  - javaescripto
- typescript
  - ts
- markdown
  - md

Com resultado final daqui:

  • javascript
    • js
    • javaescripto
  • typescript
    • ts
  • markdown
    • md

Até aqui. Bem mais satisfatório.

Alterando a lista

Será que eu consigo mapear os elementos da lista? Digamos que eu queira ir diretamente para as descrições das tags, sem passar pelas tags. Se eu fizer um {% assign %} eu sei que consigo. E diretamente no {% for %}?

{% for description in site.data.tag_alias | map: "description" %}
<!-- mensagem de erro:
  Liquid Warning: Liquid syntax error (line 181): Expected end_of_string but found pipe in "description in site.data.tag_alias | map: "description""
  -->
{% for description in site.data.tag_alias map: "description" %}
<!-- mensagem de erro:
  Liquid Warning: Liquid syntax error (line 181): Invalid attribute in for loop. Valid attributes are limit and offset in "description in site.data.tag_alias map: "description""
  -->

É, não deu. Preciso do passo anterior para fazer {% assign %}.

{%- assign tag_descriptions = site.data.tag_alias | map: "description" -%}
{%- for description in tag_descriptions %}
- {{ description }}{% endfor %}

Que gerou:

  • gambiarra web
  • gambiarra web, porém tipada
  • linguagem de marcação mais bonita que html

Criando uma lista

O modo mais fácil que eu vejo de testar a tag liquid para fazer o processamento de normalizar as tags das publicações é criar uma variável de vetor e pedir pra normalizar. Algo que seria mais ou menos assim:

[ "js", "md", "javaescripto" ]

e que depois de fazer o mapeamento ficasse assim:

[ "javascript", "markdown", "javascript" ]

Então, como criar uma variável de array via liquid? A resposta simples: não é diretamente. O mais fáicl é fazer uma lista DSV e pedir para explodir a lista em cima do divisor:

{%- assign arr = "js,md,javaescripto" | split: "," -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

E voi là:

  • js
  • md
  • javaescripto

Conforme esperado. Agora, após normalizar, esperaria encontrar algo assim:

  • javascript
  • markdown
  • javascript

Tudo em campo, vamos lá! Estudar como fazer o primeiro filtro em liquid!

A partir desse momento, vou ter uma variável doravante denominada teste_array com o conteúdo acima, já com o valor, pronta para uso.

Isso significa que não preciso me preocupar com nenhum detalhe sobre instanciar essa variável novamente, só usar.

Mais um plugin em liquid?

Sim, mais um plugin em liquid. Aqui as docs do Jekyll. Vamos explorar?

Basicamente eu declaro um módulo e exporto sua função em Liquid::Template.register_filter. Vamos fazer um teste. Vou aproveitar o meu plugin e adicionar a função de normalizar tag. Para o primeiro instante, vou retornar batata, a string, só para confirmar o funcionamento. Ficou assim:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input)
            "batata"
        end
    end
end

Liquid::Template.register_filter(Computaria::TagNormalizer)

Bora testar?

{%- assign arr = teste_array | normalize_tags -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

E com isso obtive….

- batata

Por quê? Bem, porque o filter se aplica ao input, e no caso o input era o array em si, não os elementos do array. Bora corrigir isso então? O chato é que para testar no Computaria em si vou precisar ficar derrubando e levantando de novo o servidor. Eu poderia testar de modo mais otimizado? Obviamente. Irei? Não, vou sofrer mesmo.

A correção seria algo assim:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input)
            input.map do |element|
                "batata"
            end
        end
    end
end

Liquid::Template.register_filter(Computaria::TagNormalizer)

Vamos testar?

{%- assign arr = teste_array | normalize_tags -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

E com isso obtive…

- batata
- batata
- batata

Pegando contexto

Bem, aparentemente a única coisa que o filtro tem acesso é aquilo que é passado explicitamente para ele. Então vou precisar alterar um pouco a API de uso do liquid: vou precisar passar os aliases. Mas, como que eu crio um filtro com parâmetro? Bem, descobri que não vi isso nas documentações consultados…

Em compensação, achei no repositório os filtros padrões!

https://github.com/Shopify/liquid/blob/main/lib/liquid/standardfilters.rb

E aqui tem um exemplo:

    def map(input, property)
      InputIterator.new(input, context).map do |e|
        e = e.call if e.is_a?(Proc)

        if property == "to_liquid"
          e
        elsif e.respond_to?(:[])
          r = fetch_property(e, property)
          r.is_a?(Proc) ? r.call : r
        end
      end
    rescue TypeError
      raise_property_error(property)
    end

Isso permite que eu passe um parâmetro posicional para o filtro. Vamos testar?

Primeiro por experimento só ver o que acontece sem alterar o código do meu filtro (não deve ter nenhuma alteração no resultado final se o liquid aceitar isso):

{%- assign arr = teste_array | normalize_tags "ostra" -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

Como esperado, o liquid não alterou o resultado, nas mostrou que tem problemas:

Liquid Warning: Liquid syntax error (line 387): Expected end_of_string but found string in “{{teste_array | normalize_tags “ostra” }}”

Ok, hora de permitir passar essa string!

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input, renomeio)
            input.map do |element|
                renomeio
            end
        end
    end
end

Liquid::Template.register_filter(Computaria::TagNormalizer)

E…

    Liquid Warning: Liquid syntax error (line 387): Expected end_of_string but found string in "{{teste_array | normalize_tags "ostra" }}" in ~/computaria/blog/_drafts/tags-alias.md
  Liquid Exception: Liquid error (line 387): wrong number of arguments (given 1, expected 2) in ~/computaria/blog/_drafts/tags-alias.md
rake aborted!
Liquid::ArgumentError: Liquid error (line 387): wrong number of arguments (given 1, expected 2) (Liquid::ArgumentError)
~/computaria/blog/_plugins/tags.rb:89:in `normalize_tags'
~/computaria/blog/Rakefile:11:in `block in <top (required)>'

Caused by:
ArgumentError: wrong number of arguments (given 1, expected 2) (ArgumentError)
~/computaria/blog/_plugins/tags.rb:89:in `normalize_tags'
~/computaria/blog/Rakefile:11:in `block in <top (required)>'
Tasks: TOP => default => run
(See full trace by running task with --trace)

Nem sobe… mas por que será?

Olhando o exemplo do map:

{%- assign tag_descriptions = site.data.tag_alias | map: "description" -%}

Puts, esqueci o :! Foi isso? Primeiro vou testar com a versão sem o renomeio, só pra ver se dá o mesmo erro. Então voltemos o filtro pra versão anterior:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input)
            input.map do |element|
                "batata"
            end
        end
    end
end

Liquid::Template.register_filter(Computaria::TagNormalizer)

E o teste:

{%- assign arr = teste_array | normalize_tags: "ostra" -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

O liquid recusou! Agora não foi warning, foi erro mesmo:

Liquid Exception: Liquid error (line 454): wrong number of arguments (given 2, expected 1)

Muito bem, retornando a versão com renomeio e voltamos a testar:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input, renomeio)
            input.map do |element|
                renomeio
            end
        end
    end
end

Liquid::Template.register_filter(Computaria::TagNormalizer)
{%- assign arr = teste_array | normalize_tags: "ostra" -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

Perfeito, renomeou tudo para ostra agora!

- ostra
- ostra
- ostra

Muito bem, vamos passar o argumento site.data.tag_alias… vou voltar a imprimir batata, mas agora vou dar a quantidade de elementos passados no argumento:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input, tag_alias)
            input.map do |element|
                "batata #{tag_alias.size}"
            end
        end
    end
end

Liquid::Template.register_filter(Computaria::TagNormalizer)

E o teste:

{%- assign arr = teste_array | normalize_tags: site.data.tag_alias -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

Resultado:

  • batata 3
  • batata 3
  • batata 3

Perfeito, está funcionando! Agora vou fazer um mapa reverso (do alias para a tag principal) e então bater nos itens para ver se aparece alguma coisa interessante. Se aparecer, usar o que encontrou. Caso contrário, usar o item literal.

Primeiro, verificar se eu fiz o código correto do mapa reverso:

def normalize_tags(input, tag_alias)
    reverse_alias = { }
    for tag in tag_alias do
        for single_alias in tag.alias do
            reverse_alias[single_alias] = tag
        end
    end
    puts reverse_alias
    input.map do |element|
        "batata #{tag_alias.size}"
    end
end

O resultado gerado não deve mudar, mas deve imprimir no console só para eu constatar se tá funcionando ou não…

E, bem, esqueci que mapa não é objeto…

NoMethodError: undefined method `alias' for {"tag"=>"javascript", "alias"=>["js", "javaescripto"], "description"=>"gambiarra web"}:Hash (NoMethodError)

                for single_alias in tag.alias do
                                       ^^^^^^

Ok, vamos acessar como um mapa mesmo:

def normalize_tags(input, tag_alias)
    reverse_alias = { }
    for tag in tag_alias do
        for single_alias in tag["alias"] do
            reverse_alias[single_alias] = tag
        end
    end
    puts reverse_alias
    input.map do |element|
        "batata #{tag_alias.size}"
    end
end

E, bem, deu certo?

{
    "js"=>{"tag"=>"javascript", "alias"=>["js", "javaescripto"], "description"=>"gambiarra web"},
    "javaescripto"=>{"tag"=>"javascript", "alias"=>["js", "javaescripto"], "description"=>"gambiarra web"},
    
    "ts"=>{"tag"=>"typescript", "alias"=>["ts"], "description"=>"gambiarra web, porém tipada"},
    
    "md"=>{"tag"=>"markdown", "alias"=>["md"], "description"=>"linguagem de marcação mais bonita que html"}}

Eu posso pegar qualquer informação da tag. Beleza, vamos botar no teste agora. Se tiver, pega reverse_alias[el].tag, caso contrário pega el:

def normalize_tags(input, tag_alias)
    reverse_alias = { }
    for tag in tag_alias do
        for single_alias in tag["alias"] do
            reverse_alias[single_alias] = tag
        end
    end
    input.map do |element|
        unless reverse_alias[element].nil?
            reverse_alias[element]["tag"]
        else
            element
        end
    end
end
{%- assign arr = teste_array | normalize_tags: site.data.tag_alias -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

E o resultado foi:

- javascript
- markdown
- javascript

Perfeito! Como eu queria! Mas… e se eu errei na hora de pegar o default? Vou passar o filtr duas vezes, só pra garantir (com um sort no meio pra ter certeza que tá duplo filtrando):

{%- assign arr = teste_array | normalize_tags: site.data.tag_alias | sort | normalize_tags: site.data.tag_alias -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

E como esperado, deu certíssimo:

- javascript
- javascript
- markdown

Um reverse antes do segundo filtro por via das dúvidas?

{%- assign arr = teste_array | normalize_tags: site.data.tag_alias | sort | reverse | normalize_tags: site.data.tag_alias -%}
{%- for el in arr %}
- {{ el }}{% endfor %}
- markdown
- javascript
- javascript

Ok, minhas desconfianças são infundadas.

Pegando realmente contexto

Sabe aquela conversa de que o filtro pega apenas os argumentos? Aparentemente? Então… eu esqueci de ler algo na documentação do próprio Jekyll…

ProTip™: Access the site object using Liquid

Jekyll lets you access the site object through the @context.registers feature of Liquid at @context.registers[:site]. For example, you can access the global configuration file _config.yml using @context.registers[:site].config

Oops… falha minha… Mas deve facilitar bastante! Minha API deve voltar ao que era esperado!

def normalize_tags(input)
    tag_alias = @context.registers[:site].data["tag_alias"]
    reverse_alias = { }
    for tag in tag_alias do
        for single_alias in tag["alias"] do
            reverse_alias[single_alias] = tag
        end
    end
    input.map do |element|
        unless reverse_alias[element].nil?
            reverse_alias[element]["tag"]
        else
            element
        end
    end
end
{%- assign arr = teste_array | normalize_tags -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

Com resultado:

- javascript
- markdown
- javascript

Bem, pelo menos o sofrimento anterior serviu para eu aprender a passar parâmetros posicionais aos filtros.

Antes de sair, deixa só eu deixar seguro caso não tenha os aliases…

def normalize_tags(input)
    tag_alias = @context.registers[:site].data["tag_alias"]
    return input if tag_alias.nil?
    reverse_alias = { }
    for tag in tag_alias do
        for single_alias in tag["alias"] do
            reverse_alias[single_alias] = tag
        end
    end
    input.map do |element|
        unless reverse_alias[element].nil?
            reverse_alias[element]["tag"]
        else
            element
        end
    end
end

Reverse alias no contexto

Não quero ficar constantemente fazendo o mapeamento reverso dos aliases, até porque posso esperar executar isso constantemente durante o ciclo de compilação. Então, já que existe o contexto, por que não usá-lo?

Vou fazer aqui para guardar e já imprimir alguns testes no console:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input)
            reverse_alias = Computaria::reverse_alias_tag @context
            return input if reverse_alias.nil?
            input.map do |element|
                unless reverse_alias[element].nil?
                    reverse_alias[element]["tag"]
                else
                    element
                end
            end
        end
    end

    private

    def self.reverse_alias_tag(context)
        reverse_alias = context.registers[:reverse_alias]
        tag_alias = context.registers[:site].data["tag_alias"]
        return nil if tag_alias.nil?

        unless reverse_alias.nil?
            old_tag_alias = context.registers[:old_tag_alias]

            if old_tag_alias == tag_alias
                puts "old e atual são iguais"
            else
                puts old_tag_alias
                puts tag_alias
            end
            return reverse_alias
        end

        reverse_alias = { }
        for tag in tag_alias do
            for single_alias in tag["alias"] do
                reverse_alias[single_alias] = tag
            end
        end
        puts "computou e guardou"
        context.registers[:reverse_alias] = reverse_alias
        context.registers[:old_tag_alias] = tag_alias
        return reverse_alias
    end
end

# ... registros ...

Fiz o seguinte teste:

{%- assign arr = teste_array | normalize_tags -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

{%- assign arr = teste_array | normalize_tags -%}
{%- for el in arr %}
- {{ el }}{% endfor %}

{%- assign arr = teste_array | normalize_tags -%}
{%- for el in arr %}
- {{ el }}{% endfor %}
- javascript
- markdown
- javaescripto
- javascript
- markdown
- javaescripto
- javascript
- markdown
- javaescripto

E ele imprimiu:

      Generating... 
computou e guardou
old e atual são iguais
old e atual são iguais
                    done in 2.724 seconds.

Ok, sucesso. E incrementalmente, como ele se comporta? Salvar aqui o arquivo do post e…

                    _drafts/tags-alias.md
computou e guardou
old e atual são iguais
old e atual são iguais
                    ...done in 6.681899 seconds.

Ok, ok. E alterações no datafile?

                    _data/tag_alias.yaml
computou e guardou
old e atual são iguais
old e atual são iguais
                    ...done in 2.245662 seconds.

Muito bom. Isso para mim significa que o objeto de contexto é efêmero, sendo criado um novo a cada vez que o Jekyll precisa regerar o blog. Portanto, se eu já memoizei o meu reverse_alias, posso confiar nisso doravante: não será alterado.

A versão final fica mais assim:

module Computaria

    # ... coisas sobre paginação e tags ...

    module TagNormalizer
        def normalize_tags(input)
            reverse_alias = Computaria::reverse_alias_tag @context
            return input if reverse_alias.nil?
            input.map do |element|
                unless reverse_alias[element].nil?
                    reverse_alias[element]["tag"]
                else
                    element
                end
            end
        end
    end

    private

    def self.reverse_alias_tag(context)
        reverse_alias = context.registers[:reverse_alias]
        return reverse_alias unless reverse_alias.nil?

        tag_alias = context.registers[:site].data["tag_alias"]
        return nil if tag_alias.nil?

        reverse_alias = { }
        for tag in tag_alias do
            for single_alias in tag["alias"] do
                reverse_alias[single_alias] = tag
            end
        end
        context.registers[:reverse_alias] = reverse_alias
        return reverse_alias
    end
end

# ... registros ...

Alterando o feed

+        {% assign tags = post.tags | normalize_tags | uniq %}
-        {% for tag in post.tags %}
+        {% for tag in tags %}
         <category>{{ tag | xml_escape }}</category>
         {% endfor %}

Só essa mudança e tudo mágico. Para testar, localizei onde ficava este post no RSS e brinquei com as tags dele. Exemplos de valores que usei:

  • meta jekyll ruby liquid js
  • meta jekyll ruby liquid js javascript
  • meta javascript jekyll ruby liquid js

Tudo funcionou como esperado.

Alterando a paginação

Hmmm, aqui eu não tenho acesso ao context que eu tinha no liquid. Até tentei procurar alguma maneira de recuperar isso… mas sem sucesso.

O que eu tenho, confirmado? site.data funciona igual ao context.registers[:site].data (na real o context.registers[:site] é exatamente site, porém armazenado no contexto do liquid). Pelo menos ao rodar o generator ele só irá executar mais uma vez a reversão de tags, né? Eu até poderia adicionar no site.data["reverse_tag"]… mas não. Vou jogar seguro por hora.

Extraí a parte que faz o grafo reverso da parte que lida com o contexto:

def self.reverse_alias_tag(context)
    reverse_alias = context.registers[:reverse_alias]
    return reverse_alias unless reverse_alias.nil?

    tag_alias = context.registers[:site].data["tag_alias"]
    return nil if tag_alias.nil?

    reverse_alias = reverse_alias_tag_pure_data tag_alias

    context.registers[:reverse_alias] = reverse_alias
    return reverse_alias
end

def self.reverse_alias_tag_pure_data(tag_alias)
    return nil if tag_alias.nil?

    reverse_alias = { }
    for tag in tag_alias do
        for single_alias in tag["alias"] do
            reverse_alias[single_alias] = tag
        end
    end
    return reverse_alias
end

E agora que vem a parte divertida…

O código original da separação por 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

Mas esse código carrega algumas hidden assuptions. Que são BEM RAZOÁVEIS na verdade.

A primeira é que posts_local estará ordenada do mais novo ao mais antigo. E isso de fato acontece nesse caso.

A segunda é que cada tag é única. Porém, ao adicionar aliases, eu posso vir a ter tags sinânimas repetidas. Ou seja: eu adiciono o mesmo post múltiplas vezes na tag real, depois de resolver os aliases todos. Fiz um teste adicionando o post atual com as tags meta jekyll ruby liquid js javascript, ele apareceu no começo da listagem porém também apareceu posteriormente!

Então, como resolver essa bagunça? Fazendo por partes.

Primeiro, vamos separar a questão de aglutinar os posts da tag de gerar o TagPage. Com isso, podemos mexer melhor neles:

normalized_tags_posts = { }
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    normalized_tags_posts[tag] = posts_local
end
normalized_tags_posts.each do |tag, posts_local|
    tagPage = TagPage.new(site, tag, posts_local)
    site.categories["tags"] << tagPage
    site.pages << tagPage
end

Ok, com isso, agora eu posso me preocupar com as coisas distintas: separar nos buckets de tags e de fato gerar as páginas de tags:

normalized_tags_posts = { }

# buckets
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    normalized_tags_posts[tag] = posts_local
end

# gerar TagPage
normalized_tags_posts.each do |tag, posts_local|
    tagPage = TagPage.new(site, tag, posts_local)
    site.categories["tags"] << tagPage
    site.pages << tagPage
end

Bem, agora precisamos remover o alias das tags. No momento vamos trabalhar só com a parte “buckets”.

Para começar, preciso ter meu reverse_alias:

reverse_alias = Computaria::reverse_alias_tag_pure_data site.data["tag_alias"]

Ótimo! Agora, preciso trabalhar, no bucket, com a versão canônica, sem o alias. Vou chamar de unaliased_tag. Aqui vou só preparando o terreno, ainda sem maiores resoluções de alias reverso:

reverse_alias = Computaria::reverse_alias_tag_pure_data site.data["tag_alias"]
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    unalised_tag = tag
    normalized_tags_posts[unalised_tag] = posts_local
end

Ok, hora do alias reverso. Porém, algumas coisas podem acontecer:

  • o reverse_alias ser nulo
  • o reverse_alias não tem referência reversa à tag específica

Então, a não ser que reverse_alias.nil? ou que reverse_alias[tag].nil?, e pego o valor de reverse_alias[tag]. Caso contrário eu pego simplesmente tag:

reverse_alias = Computaria::reverse_alias_tag_pure_data site.data["tag_alias"]
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    unaliased_tag = unless reverse_alias.nil? or reverse_alias[tag].nil?
        reverse_alias[tag]["tag"]
    else
        tag
    end
    normalized_tags_posts[unalised_tag] = posts_local
end

Agora eu preciso concatenar o posts_local e associar a normalized_tags_posts[unalised_tag]! Para eu poder fazer isso, primeiramente preciso garantir a inicialização de normalized_tags_posts[unalised_tag]:

reverse_alias = Computaria::reverse_alias_tag_pure_data site.data["tag_alias"]
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    unaliased_tag = unless reverse_alias.nil? or reverse_alias[tag].nil?
        reverse_alias[tag]["tag"]
    else
        tag
    end
    if normalized_tags_posts[unaliased_tag].nil?
        normalized_tags_posts[unaliased_tag] = []
    end
    normalized_tags_posts[unalised_tag] += posts_local
end

O oprtador += com arrays fará com que eu tenho agora um array que seja a concatenação do array anterior com o novo. Mas, eu posso ser melhor do que isso, não posso? Claro! Se é a primeira vez, e já que o posts_local não é reutilizado adiante, eu posso colocar ele diretamente no mapa. E para os outros casos? += mesmo.

reverse_alias = Computaria::reverse_alias_tag_pure_data site.data["tag_alias"]
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    unaliased_tag = unless reverse_alias.nil? or reverse_alias[tag].nil?
        reverse_alias[tag]["tag"]
    else
        tag
    end
    if normalized_tags_posts[unaliased_tag].nil?
        normalized_tags_posts[unaliased_tag] = []
    else
        normalized_tags_posts[unalised_tag] += posts_local
    end
end

Mas por uma questão de depuração… vou imprimir os posts que advém do alias:

reverse_alias = Computaria::reverse_alias_tag_pure_data site.data["tag_alias"]
site.tags.each do |tag, posts|
    posts_local = posts.select do |p|
        p.data["draft"] != 'true'
    end
    next if posts_local.empty?
    unaliased_tag = unless reverse_alias.nil? or reverse_alias[tag].nil?
        reverse_alias[tag]["tag"]
    else
        tag
    end
    if normalized_tags_posts[unaliased_tag].nil?
        normalized_tags_posts[unaliased_tag] = []
    else
        for post in posts_local do
            puts "adicionando post #{post.data["title"]} na tag real <#{unaliased_tag}> advindo de <#{tag}>" if unaliased_tag == 'javascript'
        end
        normalized_tags_posts[unalised_tag] += posts_local
    end
end

E pronto, estou satisfeito. Agora, vamos garantir a geração correta do TagPage. Começando da base:

normalized_tags_posts.each do |tag, posts_local|
    tagPage = TagPage.new(site, tag, posts_local)
    site.categories["tags"] << tagPage
    site.pages << tagPage
end

A primeira coisa que eu quero é ordenar por data:

normalized_tags_posts.each do |tag, posts_local|
    tagPage = TagPage.new(site, tag, posts_local.sort_by do |post| post.date end)
    site.categories["tags"] << tagPage
    site.pages << tagPage
end

Hmm, tá duplicando alguns resultados (os que forçadamente tem js e javascript). Ok, resolve-se isso com uniq:

normalized_tags_posts.each do |tag, posts_local|
    tagPage = TagPage.new(site, tag, posts_local.sort_by do |post| post.date end.uniq)
    site.categories["tags"] << tagPage
    site.pages << tagPage
end

E finalmente… está invertida a ordem…

normalized_tags_posts.each do |tag, posts_local|
    tagPage = TagPage.new(site, tag, posts_local.sort_by do |post| post.date end.uniq.reverse)
    site.categories["tags"] << tagPage
    site.pages << tagPage
end

Com isso, tenho o site funcionando com plenitude.

Notas finais

Por hora, não tenho muito o que eu desejo falar sobre cada tag individualmente. Então no datafile não vou colocar descrição. Por hora, ficou assim:

- tag: javascript
  alias: [ js ]
- tag: typescript
  alias: [ ts ]
- tag: markdown
  alias: [ md ]
- tag: bash
  alias: [ shell, shell-script ]

Para acompanhar novas mudanças, só acessar o arquivo no repositório.

Também aproveitei e removi o layout hardcoded que estava gerando a página de tags, e coloquei em um layout apropriado: tags.html.

Para isso:

     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",
+                "layout" => "tags",
                 "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'>
-    
-</ul>
-</div>
-          
-            "
         end
     end

Como o layout base era o default, mantive isso no frontmatter do tags.html. O conteúdo é idêntico a o que tinha antes, mas agora posso delegar completamente, não preciso injetar mais nada a nível de ruby. Fica até mais… idiomático?… o uso da ferramenta assim.