Criando um blog no Emacs

Um blog é nada mais do que uma porção de páginas html estáticas, no entanto escrever texto em html puro não é uma experiência agradável. Usuários de Emacs estão acostumados a utilizar o Org Mode para a criação de documentos e notas. O Org Mode suporta tudo o que é necessário para posts de um blog: texto com marcação, imagens e realce de sintaxe para blocos de código. Seria ótimo poder escrever os posts do seu blog no conforto do Emacs e é justamente isso que o sistema de publicação do Org Mode proporciona, publicar arquivos .org como páginas html.

O intuito deste post é demonstrar os passos necessários para a configuração do sistema de publicação do Org Mode visando a criação de um blog. Para facilitar o entendimento, algumas configurações serão suprimidas, você pode encontrar o exemplo completo do blog Vida em 8 Bits no meu GitHub.

O primeiro passo é carregar o sistema de publicação:

  (require 'ox-publish)

A configuração é toda realizada através da variável org-publish-project-alist:

  (setq org-publish-project-alist
      (list '("blog:main"
              :base-directory "./content"
              :base-extension "org"
              :publishing-directory "./public"
              :publishing-function org-html-publish-to-html)
            '("blog:assets"
              :base-directory "./assets"
              :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|woff2\\|ttf"
              :publishing-directory "./public"
              :recursive t
              :publishing-function org-publish-attachment)))

neste exemplo podemos ver a configuração de dois items - blog:main e blog:assets - que representam as páginas do blog e os assets - como imagens e arquivos CSS. Cada item realiza a configuração por meio de atributos:

  • :base-directory - Define o diretório de origem onde os arquivos estão localizados no seu sistema de arquivos.
  • :base-extension - Define a extensão dos arquivos do diretório de origem.
  • :publishing-directory - Define o diretório de publicação, isto é, onde os arquivos processados serão publicados.
  • :publishing-function - Define a função responsável pelo processamento dos arquivos.
  • :recursive - Define se sub-diretórios do diretório de origem devem ser considerados para processamento.

O exemplo define funções de publicação diferentes para cada item:

  • org-html-publish-to-html - Converte arquivos .org em páginas html.
  • org-publish-attachment - Copia arquivos sem qualquer modificação.

Por fim, basta uma única linha de código para realizar a publicação de todos os projetos configurados na variável org-publish-project-alist:

  (org-publish-all t)

De modo a facilitar a publicação é aconselhável colocar todo este código em um único arquivo .el e executá-lo a partir de um shell script:

  #!/bin/sh
  emacs -Q --script build-site.el

o parâmetro -Q executa o Emacs sem carregar arquivos de configuração, é mais rápido e garante que irá funcionar em qualquer computador.

Com isso, o blog está pronto. Agora basta escrever seus posts como documentos Org Mode. A página inicial deve ser nomeada index.org e links para outras páginas podem ser criados por meio de links no formato do Org-mode:

  [[file:other-page.org][Other Page]]

o mesmo vale para recursos como imagens:

  [[file:img/some-image.jpg]]

Para facilitar a adição de links, basta executar C-c c-l e interativamente informar o link e a descrição.

Adicionando cabeçalho e rodapé

Podemos querer modificar o estilo do blog como um todo para adicionar cabeçalho e rodapé em todas as páginas. Para isso podemos sobreescrever o exporter de html do Org Mode:

  (require 'cl-lib)
  (use-package esxml
    :pin "melpa-stable"
    :ensure t)

  (org-export-define-derived-backend 'site-html 'html
                                     :translate-alist
                                     '((template . org-html-template)))

  (defun org-html-template (contents info)
    (generate-page (org-export-data (plist-get info :title) info)
                   contents
                   info
                   :publish-date (org-export-data
                                  (org-export-get-date info "%B %e, %Y") info)))

  (cl-defun generate-page (title
                           content
                           info
                           &key
                           (publish-date))
    (concat
     "<!DOCTYPE html>"
     (sxml-to-xml
      `(html (@ (lang "en"))
             (head
              (meta (@ (charset "utf-8")))
              (meta (@ (author "Vida em 8 Bits - Maurício Mussatto Scopel")))
              (meta (@ (name "viewport")
                       (content "width=device-width, initial-scale=1, shrink-to-fit=no")))
              (link (@ (rel "icon") (type "image/png") (href "/img/favicon.png"))) 
              (link (@ (rel "stylesheet") (href ,(concat site-url "/css/code.css"))))
              (link (@ (rel "stylesheet") (href ,(concat site-url "/css/site.css"))))
              (title ,(concat title " - Vida em 8 Bits")))
             (body (site-header)
                   (div (@ (class "container"))
                        (h1 "Olá, leitores do Vida em 8 Bits!")
                        (div (@ (id "content"))
                             ,content))
                   (site-footer))))))

a função generate-page está super simplificada para dar o entendimento do essencial, contém alguns parâmetros não utilizados para demonstrar como dados podem ser extraídos para serem utilizados na geração do html. Além disso, demonstra como adicionar scripts ou arquivos CSS. As funções site-header e site-buffer foram omitidas por questões de brevidade, mas como seus nomes indicam, são funções para gerar o cabeçalho e o rodapé do blog, devem retornar templates dos elementos html no formato:

    (list `(element1)
          `(element2))

Melhorando o visual

Para um bom visual do seu blog, a instalação do pacote htmlize é essencial se você deseja utilizar blocos de código. O restante do html gerado pode ser customizado por meio de variáveis:

  (setq org-publish-use-timestamps-flag t
        org-export-with-section-numbers nil
        org-export-use-babel nil
        org-export-with-smart-quotes t
        org-export-with-sub-superscripts nil
        org-export-with-tags 'not-in-toc
        org-html-htmlize-output-type 'css
        org-html-prefer-user-labels t
        org-html-link-use-abs-url t
        org-html-link-org-files-as-html t
        org-html-html5-fancy t
        org-html-self-link-headlines t
        org-export-with-toc nil
        make-backup-files nil)

para compreender o objetivo de cada variável, execute M-x describe-variable ou use o atalho C-h v e em seguida digite o nome da variável.

Organizando o download de pacotes

Para evitar que pacotes obtidos na execução do script sejam instalados no seu diretório de configuração principal do Emacs, basta atualizar o valor da variável package-user-dir para apontar para um diretório oculto como por exemplo .packages:

  (require 'package)
  (setq package-user-dir (expand-file-name "./.packages"))

  (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/"))
  (add-to-list 'package-archives '("melpa-stable" . "https://stable.melpa.org/packages/"))

  ;; Initialize the package system
  (package-initialize)
  (unless package-archive-contents
    (package-refresh-contents))

  ;; Install use-package
  (unless (package-installed-p 'use-package)
    (package-install 'use-package))
  (require 'use-package)  

Acessando o blog

Para acessar seu blog localmente você pode utilizar o pacote simple-httpd para subir um servidor local, após a instalação do pacote basta executar M-x httpd-serve-directory, selecionar o diretório de publicação e então acessar o blog via http://localhost:8080.

Criando a listagem de posts

Todo blog tem uma página listando todos os posts pela data mais recente. Bem, isso é simples de resolver, basta automatizar a geração de uma página .org contendo a listagem dos posts, o sistema de publicação tomará conta do restante. O primeiro passo é adicionar uma propriedade de data aos seus posts:

  #+DATE: <2024-06-29>
  ...

Por fim, basta um simples script em Emacs Lisp para resolver o problema que:

  • Obtém todos os arquivos no diretório de posts do blog;
  • Para cada post:
    • Insere uma linha com o respectivo link no qual a descrição é o título extraído do documento .org;
    • Insere uma linha identificando o autor do blog e a data de publicação - obtida pelo atributo de data no arquivo.
  (require 'org)

  (defun parse-date (date)
    "Parse DATE to dd de mm, yyyy."
    (format-time-string "%d de %B, %Y" date))

  (defun org-get-date (file)
    "Extract the DATE property from an org mode FILE."
    (with-current-buffer (find-file-noselect file)
      (goto-char (point-min))
      (when (re-search-forward "^#\\+DATE: \\(.*\\)$" nil t)
        (substring-no-properties
         (match-string 1)))))

  (with-temp-file "content/blog.org"
    (let ((posts-folder "./content/posts/"))
      (seq-do
       (lambda (post)
         (insert (format "** [[../%s][%s]]\n\n"
                         (car (string-split post ".org"))
                         (org-get-title (concat posts-folder post))))
         (insert (format "%s por Maurício Mussatto Scopel\n"
                         (parse-date
                          (date-to-time
                           (org-get-date (concat posts-folder post)))))))
       (directory-files posts-folder nil ".org"))))

o leitor atento pode ter notado que este script não considera ordenação, é isso mesmo, ordenação não é uma preocupação enquanto o seu blog não tem mais de 1 post 😅.

Até o próximo post!