Notxor tiene un blog

Defenestrando la vida

Cambiando el blog a org-static-blog

2020-10-02

Pues estoy de cambios en el blog y ya tengo algo visible que subir a la red para que se pueda ver. Me resistía al cambio, pues los sistemas son distintos y me arriesgaba a dejar por el camino muchos enlaces rotos, no sólo externos de otros blogs o redes sociales, sino también internos.

Después de pelearme con mis reticencias me puse al tajo e, incluso, me he dedicado a migrar también los diferentes artículos al formato de cabecera nuevo. De esta manera, se podrá consultar el contenido en el nuevo formato o en el antiguo... si al subir el contenido resulta todo como espero.

¿Por qué el cambio? Básicamente tengo estas razones:

¿Todo son ventajas? Pues no. Hay inconvenientes que exigen atención, como los enlaces rotos.

Enlaces rotos

A estas alturas, después de varios años escribiendo en el blog sobre mis cosas, resulta que hay quien lo lee y todo. No sólo eso, sino también que hay gente que lo enlaza y lo sigue por RSS.

El miedo al cambio viene, principalmente, de la cantidad de enlaces rotos que quedan siempre sueltos por el mundo. Incluso dentro del mismo blog o página. Por ello voy a intentar que esto no suceda o al menos que sea en el menor grado posible.

Podría haber optado por encogerme de hombros y decir: «sistema nuevo, adaptarse o morir». Pero no me ha parecido conveniente. Para pegar el cerrojazo siempre hay tiempo, me parece.

De momento, como he dicho antes, he convertido todos los artículos del sistema anterior, hecho con org-page, al nuevo sistema org-static-blog. Ha sido un trabajo relativamente sencillo pues sólo tenía que convertir algunos detalles de las cabeceras del fichero org: en lugar de la etiqueta tags de org-page, utiliza filetags, y también me encontré con alguna fecha que no cogía bien y me dediqué a añadir a mano los paréntesis angulares <..-..-..> y con eso está todo convertido... pero falta aún algo de trabajo.

Además, estoy escribiendo esto sin tener la certeza de que efectivamente no romperé ningún enlace... tengo confianza en que así sea, porque va a haber artículos duplicados para lo mismo. En las pruebas que he hecho en local, todo ha parecido funcionar correctamente... pero hasta que no lo pruebe subiéndolo todo al servidor, no puedo estar seguro. De todas formas me guardo una copia de seguridad del sitio viejo, por si tengo que replantear el tema.

El paquete lo puedes descargar por el procedimiento habitual, está en melpa y si tienes configurado ese repositorio de paquetes basta con el comando

M-x package-install <RET> org-static-blog <RET>

Yo soy algo más curioso y no lo he instalado: me he bajado el código del repositorio porque lo estoy cambiando un poco. De hecho no lo tengo donde esté accesible a Emacs, cuando lo uso tengo que cargarlo con load-file, pero aún no estoy en fase de darle cariño.

El código que he cambiado

El propio creador del sistema habla de que está pensado para que sea fácil de leer, comprender y modificar.

Above all, I tried to make org-static-blog as simple as possible. There are no magic tricks, and all of the source code is meant to be easy to read, understand and modify.

Después de haberlo mirado un poco, he de coincidir con el autor: ha sido fácil ajustar la funcionalidad a lo que quería conseguir sin complicarme mucho.

Una de las cosas que es ajustable es «ignorar» variables como org-export-with-toc o org-export-with-section-numbers y en su lugar, he incluido una línea #+options:. La ventaja es que puedo determinar si quiero números o índice en cada artículo, en lugar de limitarlo en todos. La función de crear un nuevo artículo me ha quedado así:

;;;###autoload
(defun org-static-blog-create-new-post (&optional draft)
  "Creates a new blog post.
Prompts for a title and proposes a file name. The file name is
only a suggestion; You can choose any other file name if you so
choose."
  (interactive)
  (let ((title (read-string (org-static-blog-gettext 'title))))
    (find-file (concat
                (if draft
                    org-static-blog-drafts-directory
                    org-static-blog-posts-directory)
                (read-string (org-static-blog-gettext 'filename)
                             (concat (format-time-string "%Y-%m-%d-" (current-time))
                                     (replace-regexp-in-string "\s" "-" (downcase title))
                                     ".org"))))
    (insert "#+title:    " title "\n"
            "#+date:     " (format-time-string "<%Y-%m-%d %H:%M>") "\n"
            "#+filetags: " "\n"
            "#+options:  H:3 num:nil toc:nil \\n:nil ::t |:t ^:nil -:nil f:t *:t <:t")))

Estructuras fijas de los ficheros

El código fundamental que falta en el org-static-blog son las cabeceras y pies de página. El sistema org-page lleva soporte para plantillas mustache que facilitan la vida en este sentido. Sin embargo, en este sistema va en el mismo código. Hay que cargar ese código en sus correspondientes variables:

  1. Para la cabecera del HTML:

    (defcustom org-static-blog-page-header
      "<meta name=\"author\" content=\"Notxor\">
      <meta name=\"referrer\" content=\"no-referrer\">
      <link href= \"/medios/css/estilos.css\" rel=\"stylesheet\" type=\"text/css\" />
      <link rel=\"icon\" href=\"/medios/img/favicon.png\">"
      "HTML to put in the <head> of each page."
      :type '(string)
      :safe t)
    

    Básicamente establece el autor, la dirección del fichero de estilos CSS y el favicon de la página. En mi caso, el autor lo pone fijo, pero no costaría mucho establecer una variable que cargue el contenido desde la cabecera, por ejemplo.

  2. Para el encabezamiento de cada página:

    (defcustom org-static-blog-page-preamble
      "<div>
          <header class=\"masthead\">
            <h1 class=\"masthead-title\"><a href=\"/\">Notxor tiene un blog</a></h1>
            <h2 class=\"masthead-subtitle\">Defenestrando la vida</h2>
            <img class=\"avatar\" src=\"/medios/img/abatar.png\" width=\"100px\"></img>
            <ul>
              <li><a href=\"/\">Blog</a></li>
              <li><a href=\"/tags/esperanto\">Esperanto</a></li>
              <li><a href=\"/tags/radio\">Radio</a></li>
              <li><a href=\"http://blog-antiguo.nueva-actitud.org\" target=\"_blank\">Blog antiguo</a></li>
              <li><a href=\"/etiquetas.html\">Etiquetas</a></li>
              <li><a href=\"/about/\">Acerca de...</a></li>
              <li><a href=\"/rss.xml\">RSS</a></li>
            </ul>
            <form method=\"get\" id=\"searchform\" action=\"//www.duckduckgo.com/\">
              <input type=\"text\" class=\"field\" name=\"q\" id=\"s\" placeholder=\"Buscar en DuckDuckGo\">
              <input type=\"hidden\" name=\"as_sites\" value=\"https://notxor.nueva-actitud.org\">
            </form>
          </header>
        </div>"
      "HTML to put before the content of each page."
      :type '(string)
      :safe t)
    

    Como se puede comprobar, la definición del «título» y el «subtítulo», el habitual menú de secciones con sus correspondientes enlaces y el formulario de buscar.

  3. Para el pie de página:

    (defcustom org-static-blog-page-postamble
       "<div>
        <script src=\"/medios/js/jquery-latest.min.js\"></script>
        <script src=\"https://polyfill.io/v3/polyfill.min.js?features=es6\"></script>
        <script id=\"MathJax-script\" async src=\"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js\"></script>
        <script src=\"/medios/js/main.js\"></script>
        <div class=\"footer\">
            <p>Generado con <code>org-static-blog</code> en <i>Emacs</i></p>
            <p>
                <img style=\"display: inline-block;\" src=\"/medios/img/cc-by-nc-sa.png\" align=\"middle\" />
                   <br />2012 - <span id=\"footerYear\"></span> <a href=\"mailto:notxor@nueva-actitud.org\">Notxor</a>
                &nbsp;&nbsp;-&nbsp;&nbsp;
                Powered by <a href=\"https://github.com/bastibe/org-static-blog\" target=\"_blank\">org-static-blog</a>
                <script type=\"text/javascript\">document.getElementById(\"footerYear\").innerHTML = (new Date()).getFullYear();</script>
            </p>
            <table>
                <tr>
                    <td>
                        <b>Donar por Paypal</b>
                    </td>
                    <td>
                        <b>Donar por Liberapay</b>
                    </td>
                </tr>
                <tr>
                    <td>
                        <!-- Inserción de botón pypal -->
                        <form action=\"https://www.paypal.com/cgi-bin/webscr\" method=\"post\" target=\"_top\">
                            <input type=\"hidden\" name=\"cmd\" value=\"_s-xclick\" />
                            <input type=\"hidden\" name=\"hosted_button_id\" value=\"29VXCW8SRNJ78\" />
                            <input type=\"image\" src=\"https://www.paypalobjects.com/es_ES/ES/i/btn/btn_donateCC_LG.gif\" border=\"0\" name=\"submit\" title=\"PayPal - Donar online de forma segura.\" alt=\"Botón Donar con PayPal\" />
                            <img alt=\"\" border=\"0\" src=\"https://www.paypal.com/es_ES/i/scr/pixel.gif\" width=\"1\" height=\"1\" />
                        </form>
                    </td>
                    <td>
                        <!-- Inserción de botón liberapay -->
                        <script src=\"https://liberapay.com/Notxor/widgets/button.js\"></script>
                        <noscript>
                            <a href=\"https://liberapay.com/Notxor/donate\">
                                <img alt=\"Donate using Liberapay\" src=\"https://liberapay.com/assets/widgets/donate.svg\"></a>
                        </noscript>
                    </td>
                </tr>
            </table>
        </div>
    </div>"
      "HTML to put after the content of each page."
      :type '(string)
      :safe t)
    

    Básicamente carga el poco javascript que utiliza el blog: coloreado de sintaxis, visualización de fórmulas matemáticas y el necesario para los botones de donación1. También está la información sobre la licencia y un enlace al código fuente que lo anima.

Los path

Una de las cosas que me encontré fue la manía que tiene el sistema de utilizar las URL de forma absoluta, comenzando por la dirección pública de la página. Eso coarta en gran medida las pruebas de visualización del sitio en local. Una costumbre que he adquirido tras escribir un artículo... monto un servidor local, bien con M-x httpd-start o desde una consola con python3 -m http.server 8080 luego lo abro con un navegador y lo pruebo. Veo si está todo correcto en su sitio, si las imágenes las he puesto bien y aparecen todas. Lo leo despacio por si encuentro algún error2.

Por todo ello, en muchos sitios he sustituido las llamadas a org-static-blog-get-absolute-url por una llamada a org-static-blog-get-local-url. Una función que la metí yo con un calzador para la tarea.

(defun org-static-blog-get-local-url (relative-url)
  "Esta es mía...Returns local URL based on the RELATIVE-URL passed to the function."
  (concat "/" (org-static-blog-get-relative-path relative-url)))

¿Qué consigo con esto? Pues básicamente que el blog completo sea navegable en local y no intente salir a Internet cada vez que pinches un enlace.

Además, para poner un poco de coherencia en los path del sitio, me empeñé en que las URL de los artículos se distribuyan por directorios con el formato año/mes/día y el título del mismo.

Para esto he tenido que hacer un par de cambios más. Uno de ellos viene recomendado en el mismo código:

(defun org-static-blog-generate-post-path (post-filename post-datetime)
  "Returns post public path based on POST-FILENAME and POST-DATETIME.

By default, this function returns post filepath unmodified, so script will
replicate file and directory structure of posts and drafts directories.

Override this function if you want to generate custom post URLs different
from how they are stored in posts and drafts directories.

For example, there is a post in posts directory with the
file path `hobby/charity-coding.org` and dated `<2019-08-20 Tue>`.

In this case, the function will receive following argument values:
- post-filename: 'hobby/charity-coding'
- post-datetime: datetime of <2019-08-20 Tue>

and by default will return 'hobby/charity-coding', so that the path
to HTML file in publish directory will be 'hobby/charity-coding.html'.

If this function is overriden with something like this:

(defun org-static-blog-generate-post-path (post-filename post-datetime)
  (concat (format-time-string \"%Y/%m/%d\" post-datetime)
          \"/\"
          (file-name-nondirectory post-filename)))

Then the output will be '2019/08/20/charity-coding' and this will be
the path to the HTML file in publish directory and the url for the post.

Por defecto es:
   post-filename)"
  (concat (format-time-string "%Y/%m/%d" post-datetime)
          "/"
          (file-name-nondirectory post-filename)))

Meter ese código ahí tal cual funciona como se espera, se genera la URL tal como dije antes y se crea el fichero html en un directorio dentro de su día, que está dentro de su mes y que a la vez está dentro de su año.

Lo que no avisa la documentación de la función es que luego tienes que sustituir, allí donde se busque el contenido de un artículo que no lo busque en el sitio por defecto con el formato por defecto:

/YYYY-mm-dd-nombre-del-artículo.html

En algunas funciones tenemos que sustituir también las formas que generan url's de ese estilo, para que busquen en el sitio correcto. Básicamente es sustituir la forma que añade la fecha al nombre del archivo por algo así:

(concat "/" (format-time-string "%Y/%m/%d" post-date)

No estoy seguro de haberlo hecho en todos los sitios oportunos, el código está disperso por distintas funciones que generan código html y es un poco enredoso encontrar los sitios adecuados. Lo he repasado varias veces y he visto que todos los enlaces que he probado funcionan. Sin embargo, hay tantos pichorros donde pinchar que no estoy seguro de haberlos probado todos. Si alguien encuentra algún enlace raro que no lleva a ningún lado le agradecería que me avisara.

Estructura de directorios

El sistema funciona de una manera bastante sencilla de entender: hay un directorio configurable donde escribir los artículos y otro donde escribir los borradores. Cuando generas el sitio, coge todo lo que haya en el directorio de referencia y genera los ficheros html. No los genera todos: sólo creará aquellos cuyo fichero html, si existe, sea más antiguo que su fichero org. O dicho de otro modo: sólo genera los modificados. En producción supongo que será mucho más sencillo rápido y efectivo... org-page empezaba a hacerse pesado la generación del sitio.

Otro aspecto sobre los directorios es que todo va al directorio raíz, por defecto, ya he contado que hay que modificar algo el código para conseguir que siga el formato de crear directorios (para fechas) y para otras cosas.

Además se pueden generar listados de artículos según las etiquetas, pero por defecto también las mete en el directorio raíz con el formato tag-etiqueta.html y contribuye al caos del raíz de forma considerable, sobre todo si te gustan la etiquetas como a mí. El caso es que me pareció muchos más manejable la forma de hacerlo de org-page que genera en un directorio tags/ una estructura de directorios, con el nombre de las etiquetas y dentro de ellos su correspondiente fichero index.html con el contenido. Para ello, modifiqué la función que genera esos ficheros de la siguiente manera:

(defun org-static-blog-assemble-tags ()
  "Render the tag archive and tag pages."
  (org-static-blog-assemble-tags-archive)
  (dolist (tag (org-static-blog-get-tag-tree))
    (org-static-blog-assemble-multipost-page
     (concat org-static-blog-publish-directory "/tags/" (downcase (car tag)) "/index.html")
     (cdr tag)
     (concat "<h1 class=\"title\">" (org-static-blog-gettext 'posts-tagged) " \"" (car tag) "\":</h1>"))))

Evidentemente, luego, al generar los enlaces que los visiten hay que modificar el código para que las encuentre con el mismo formato con código tal que:

(concat "/tags/" (downcase tag) "/index.html")

Además, hay que tener en cuenta los archivos rss.xml que quiero que se generen por etiqueta y hay que meterlos en algún sitio donde no se pisen unos a otros... pero lo vemos más despacio a continuación.

Fichero rss.xml por etiqueta

Otro problema que me planteaba con el sistema nuevo y que me resultaba una limitación es la generación de archivos RSS para cada etiqueta. De la gente que tengo constancia que sigue el blog, no accediendo al sitio, sino a través de lectores RSS, me constan, al menos tres de esos ficheros: el de la radio, el de Esperanto y el de Emacs. También sé que hay quien sigue el RSS general, no entiendo muy bien por qué.

El caso es que necesitaba generar los RSS por etiquetas y eso no era algo que contemplara el sistema, así que... ¿por qué no hacerlo yo? Pues manos a la obra: me puse a mirar el código y encontré una función que genera el RSS general (org-static-blog-assemble-rss). También encontré otra que genera un ítem para cada artículo. que se llama org-static-blog-get-rss-item y recibe como parámetro el =post-filename).

Partí del código de la primera pero añadiendo el parámetro tag:

(defun org-static-blog-rss-for-tag (tag)
  "Assemble the tag RSS feed.
The RSS-feed is an XML file that contains every blog post in a
machine-readable format."
  (let ((system-time-locale "en_US.utf-8") ; force dates to render as per RSS spec
        (rss-filename (concat org-static-blog-publish-directory "/tags/" (car tag) "/" org-static-blog-rss-file))
        (rss-items nil))
    (dolist (post-filename (cdr tag))
      (let ((rss-date (org-static-blog-get-date post-filename))
            (rss-text (org-static-blog-get-rss-item post-filename)))
        (add-to-list 'rss-items (cons rss-date rss-text)))
    (setq rss-items (sort rss-items (lambda (x y) (time-less-p (car y) (car x)))))
    (org-static-blog-with-find-file
     rss-filename
     (concat "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
             "<rss version=\"2.0\">\n"
             "<channel>\n"
             "<title><![CDATA[" org-static-blog-publish-title "]]></title>\n"
             "<description><![CDATA[" org-static-blog-publish-title "]]></description>\n"
             "<link>" org-static-blog-publish-url "</link>\n"
             "<lastBuildDate>" (format-time-string "%a, %d %b %Y %H:%M:%S %z" (current-time)) "</lastBuildDate>\n"
             org-static-blog-rss-extra
             (apply 'concat (mapcar 'cdr rss-items))
             "</channel>\n"
             "</rss>\n")))))

El parámetro tag llega como una lista de cadenas donde la primera es el nombre de la etiqueta y las siguientes son los ficheros que la llevan. Por tanto, monto el rss.xml en el mismo directorio que su listado de ficheros, para no sobreescribir un fichero con otro y el resto es idéntico a como se genera el RSS del blog.

Por un momento dudé si hacer la función interactiva o llamarla sólo para cada etiqueta. Al final, me decidí por hacerlo de una manera alternativa. Como no todas la etiquetas son relevantes, porque no todas tienen seguimiento, generar siempre los ficheros es un poco pérdida de tiempo. Así que lo solucioné de manera manual:

(defun org-static-blog-assemble-rss-by-tag ()
  "Assemble the RSS for tags."
  (interactive)
  (mapcar 'org-static-blog-rss-for-tag (org-static-blog-get-tag-tree)))

Es decir, cuando llamo a la función org-static-blog-assemble-rss-by-tag se generarán todos los ficheros rss.xml ─lo que lleva unos segundos─. ¿Por qué no generar sólo una etiqueta o unas pocas? Pues porque hacer unas etiquetas y no otras puede ser un poco confuso para los posibles lectores que lleguen al blog cuando encuentren que tienen referencias de un tema determinado y de otros no.

Como se ve, no es complicado adaptar un sistema tan sencillo: se pueden hacer muchas otras cosas y con muy pocas líneas de código.

El fichero CSS

Por otro lado, también ha habido un cambio de aspecto. He cambiado tipos de letras, colores y alguna cosilla más con la esperanza de hacerlo un poco más atractivo. Sin embargo, como podéis ver, tampoco es que mis capacidades de diseño sean sobresalientes. No ha quedado mal del todo.

Me había cansado del tema oscuro y aprovechando que iba a hacer cambios, me puse también manos a la obra con el aspecto. Aún me quedan algunas cosas que pulir.

El código completo

Sería muy largo poner aquí todo el código implicado. Se sale del tamaño razonable de un artículo del blog. Si alguien quiere acceder a él puede mirarlo en el siguiente repositorio:

https://codeberg.org/Notxor/org-static-blog

Conclusiones

Ha habido más cambios, por ejemplo en el código también he añadido la traducción al Esperanto de las cadenas del sistema. Al principio de comenzar a manejarlo pensé que debía utilizar dos blogs para cada una de las lenguas. Después he visto que me podía ajustar de otros modos.

Por supuesto el código que he subido al repositorio se puede utilizar como mejor veáis. Si hago cambios en el sistema para ajustarlo, para corregir algún bug o para dotarlo de alguna característica nueva, lo subiré al repositorio, pero tampoco tengo el ánimo de convertirlo en un proyecto que mantener... no me da de sí el tiempo como para comprometerme.

Al final, el sistema es tan sencillo que me parece hermoso, se entiende cómo funciona sin demasiado esfuerzo, es altamente personalizable y hasta un psicólogo como yo lo puede hacer sin ayuda.

Si estáis pensando en hacer un blog, queréis un sistema estático y sois usuarios de Emacs, es la alternativa estáis buscando... incluso le podéis automatizar la vista previa de forma muy sencilla:

(httpd-serve-directory org-static-blog-publish-directory)
(httpd-start)

Así no necesitas ni salir de Emacs para hacer las pruebas.

Nota al pie de página:

1

Que por cierto, a ver cuándo os estiráis y me puedo tomar mi primer café a vuestra salud :'(

2

Parece mentira, porque aún así se me cuelan siene' y siene'.

Categoría: blog emacs

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en Mastodon y en Diaspora con el nick de Notxor.

También se ha abierto un grupo en Telegram, para usuarios de esa red.

Disculpen las molestias.