Crear un blog con Sphinx#

Por fin he dado el paso de retomar el blog, y con ello un proceso de migración que deseaba, generar el blog con Sphinx. Sobre este proceso les estaré contando en varias entradas, ya que la migración no la he concluido, y esta es la primera entrada al respecto, con lo más básico para iniciar y no fracasar en el intento.

¿Por qué Sphinx?#

Sphinx es un generador estático de documentación, casi el estándar para los proyectos desarrollados en Python, y para el cual se dispone de una buena cantidad de extensiones que nutren a este generador, entre las cuales, encontramos incluso una para convertir nuestro proyecto en un blog, y es Ablog.

Dado que mi lenguaje de programación principal es Python, es claro que por ello quiero una opción basada en Python. Pero antes yo usaba Nikola, que también está desarrollado en Python, pero Sphinx da el beneficio de una comunidad mayor y más activa (como usuarios y desarrolladores). Adicional a esto, el desarrollo de Sphinx y sus extensiones va más a la par de cambios en el ecosistema, y más alineado con las directivas de docutils. En los cambios del ecosistema, me parece interesante como el soporte de Jupyter Notebooks y Myst resulta muy natural, por lo cual entradas sobre código pueden tener un beneficio, y Myst expande las opciones de Markdown (y no es soportado adecuadamente en Nikola, ni en otros generadores que revisé).

También resulta que ya era más familiar con Sphinx, pero en el momento que pasé a un generador estático, no existía Ablog para ayudar en el proceso de usarlo para un blog.

Creando el blog#

Bueno, hora de poner manos a la obra. Para ello vamos a seguir el paso a paso.

Dependencias#

Usaremos Sphinx para la generación del contenido estático y Ablog nos permitirá incluir etiquetas para fechas, que son la diferencia esencial para distinguir una página de una publicación (y con ello, opciones de filtros para índices).

Respecto a la apariencia, podemos encontrar distintos temas, pero yo me he inclinado por PyData (que además usan varios blogs que sigo, así que he visto su resultado y tengo como indagar como usan algunas cosas). Adicional, conviene agregar Sphinx Design para añadir componentes adicionales como cuadrículas, pestañas, tarjetas y otros.

Sin duda (y ya lo usaba en Nikola), es necesario disponer de videos de Youtube, así que necesitamos Sphinx Contrib Youtube.

Aunque para compartir en las redes no necesitamos nada especial, poder contar con la generación de metadatos adecuada nos ayudará a una mejor indexación y una mejor previsualización. Para este fin usamos Sphinx Ext OpenGraph (que no solo incluye el protocolo OpenGraph, sino una etiqueta extra para Twitter Card). En este caso, tenemos un detalle extra, y es que para generar las imágenes que se comparten (ya que por defecto no usa la primera imagen de la publicación y no siempre hay una), es necesario instalar Matplotlib.

Y finalmente, para dar soporte de Markdown, usaremos Myst Parser. Algo interesante es que en extensión con Myst NB, tendremos el soporte para generar publicaciones en Notebook usando MystMd (así que instalamos también Jupyterlab). Esto me encanta, porque será natural publicar notas sobre código con sus resultados.

Sitemap genera el sitemap del sitio, aunque para usarlo con el esquema de internacionalización que deseo, tendré que cambiarlo en el futuro.

Copy button nos ayudará a crear la opción de copiar al portapapeles los bloques de código.

Con estos detalles, nuestro archivo requirements.txt lucirá de la siguiente forma:

# Generación estática blog y tema
sphinx
ablog
pydata-sphinx-theme

# Componentes
sphinx-design
sphinx-copybutton
sphinxcontrib-youtube

# Metadatos para compartir en redes
sphinxext-opengraph
matplotlib
sphinx-sitemap

# Soporte de Markdown y Notebook
myst-parser
jupyterlab
jupyterlab_myst
myst-nb
Opcionales

No todo se trata sobre la generación del contenido a nivel automático, también necesitamos apoyo mientras escribimos. Así que podemos instalar doc8, rstcheck y esbonio, para las validaciones de nuestros archivos .rst y Jupyterlab Myst, para ayudar en el renderizado en el Notebook mientras redactamos.

Siendo así, podemos tener nuestro archivo requirements-dev.txt así:

jupyterlab_myst
rstcheck
doc8
esbonio

Si usamos además VSCode, vale la pena las siguientes extensiones:

  • MyST-Markdown: Para editar MystMD

  • Esbonio: Para editar RST

  • reStructuredText (lexstudio): Para editar RST

  • Jupyter: Para manipular notebooks

  • Emoji: Para insertar emoji con la paleta de comandos 😀

  • Spell Right: Para corrección de ortografía.

  • Font Awesome Gallery: Para buscar la notación de los íconos

Otros

Bueno, aquí hay algunas extensiones que, posiblemente puedan ser útiles, pero para mí no lo fueron:

  • sphinxext-rediraffe: Con seguridad hay que tenerla a la mano. Es la extensión que más recomiendan para hacer redireccionamiento. En mi caso, no necesito todavía opciones avanzadas, así que me basta con la opción de las publicaciones de :redirect:.

  • sphinx-intl: Ayuda a la internacionalización. Sin embargo, no lo considero adecuado en proyectos como un blog, en el cual no necesariamente todas las entradas poseen traducción o podrían tener contextos que hagan que su traducción no sea completa. Me parece adecuado para documentación, no para un blog.

Configuración#

Puedes usar ablog start y responder las preguntas básicas de inicialización

> Root path for your project (path has to exist) [.]:
> Project name: Cosmoscalibur
> Author name(s): Edward Villegas-Pulgarin
> Base URL for your project: https://www.cosmoscalibur.com/

Dado que GitHub Pages solo puede usar el directorio docs/ para los sitios estáticos, conviene dejar como raíz del proyecto el directorio raíz del repositorio (si estás usando un repositorio git), para que el directorio de salida este en dicho nivel.

Respecto al nombre del proyecto, por defecto este incluirá la palabra blog al final del nombre que disponemos (en inglés estaría perfecto). Pero también encontraremos que el nombre HTML contiene la mención a documentation, que no es apropiado para nuestro blog. Ajustaremos esto en las siguientes variables:

project = 'Cosmoscalibur'
blog_title = 'Cosmoscalibur'
html_title = 'Cosmoscalibur'
html_short_title = 'Cosmoscalibur'

Respecto al tema, por defecto Ablog configura Alabaster, pero como les indiqué, usaremos el tema de PyData y podemos retirar alabaster del import.

html_theme = 'pydata_sphinx_theme'

Para incluir los metadatos de OpenGraph, añadiremos la dirección base y podemos añadir marcas personalizadas. Así, que aprovecharé a incluir la de creador para Twitter (ahora X), y la especificación de tipo viene por defecto en summary_large_image (esto no encontré como cambiarlo).

ogp_site_url = 'https://www.cosmoscalibur.com'
ogp_custom_meta_tags = [
    '<meta name="twitter:creator" content="@cosmoscalibur" />',
]

Para configurar el idioma por defecto, usamos la variable respectiva con el código ISO del lenguaje

language = 'es'

En mi caso, aunque es mi lengua nativa, espero publicar algunas veces en inglés. Por este motivo, pensando en la internacionalización del blog, dispondré de un patrón de rutas de la forma <lang>/blog/<year>/<post>, de tal forma que con cambiar directamente el segmento de <lang> se acceda a la versión del otro idioma.

Esto tiene impacto en la variable blog_path_pattern que permite definir el patrón de ruta para que se reconozcan automáticamente las publicaciones (no es necesario añadir la etiqueta). Adicional, es necesario definir la ruta para el archivo de publicaciones.

Definición de ruta de índice del blog y patrón de ruta de publicaciones.#
blog_path = 'blog'
blog_post_pattern = '*/blog/*/*'

Es necesario configurar también otros directorios y archivos. Para fines de facilidad en GitHub Pages, vamos a remover el guion bajo de los directorios destinados para los estáticos y las plantillas (por defecto son ignorados si comienzan de esta forma). Y añadiremos un directorio especial que nos permita agregar archivos directamente al raíz del despliegue (por ejemplo, para añadir el archivo CNAME).

Importante para la generación con GitHub Pages, el directorio de salida debe ser docs.

Finalmente, las variables de configuración en conf.py.

Definición de directorios de entradas y salidas para Sphinx y Ablog#
templates_path = ['templates']
html_static_path = ['static']
html_extra_path = ['files']
ablog_website = 'docs'

Ahora vamos a definir los archivos que no deben ser procesados. Esto es importante porque al estar el directorio de Sphinx al mismo nivel del directorio del repositorio, se ven todos los archivos. Adicional a esto, hay archivos generados por Sphinx que si no los borramos, en un despliegue posterior se intentarán procesar.

Definición de archivos y directorios a excluir de la compilación.#
exclude_patterns = [
    "_build",
    "***/.ipynb_checkpoints/*",
    'Pipfile',
    'LICENSE',
    'README.md',
    'requirements*.txt',
    '.vscode',
    '.venv',
    'docs',
    '.doctrees',
    '.gitignore',
]

Para habilitar las extensiones que vamos a usar, es necesario listarlas en extensions. Un detalle es que, aunque tenemos instalado Myst Parser, no lo añadimos explícitamente porque genera conflicto por ya estar definido dentro de Myst NB.

Algunos errores que se producen por esto son:

WARNING: mientras configura la extensión myst_nb: el rol 'sub-ref' ya está registrado, ese se reemplazará
WARNING: mientras configura la extensión myst_nb: la directiva 'figure-md' ya está registrada, esa se reemplazará
Extension error:
Valor de configuración 'myst_commonmark_only' ya presente
---
source_suffix '.md' is already registered

Así, entonces definimos

extensions = [
    'sphinx.ext.extlinks',
    'sphinx.ext.intersphinx',
    "myst_nb",
    "sphinx_design",
    "sphinxext.opengraph",
    "sphinxcontrib.youtube",
    'ablog',
    'sphinx_sitemap',
    'sphinx_copybutton',
]

Los dos primeros casos que habilitamos, son extensiones que vienen por defecto, y nos servirán para acortar la notación de URL y para enlazar cómodamente con otros proyectos de Sphinx (en futuras entradas lo revisaremos).

Ahora que tenemos habilitado Myst NB, podemos remover la línea de source_suffix porque esta es configurada por la extensión, y el valor por defecto evita que se compilen los archivos Markdown.

Respecto a las opciones de Myst, vamos a habilitar varias extensiones, que nos permitan usar más fácilmente las directivas (no usar el backtick), hacer sustituciones y habilitar el símbolo de dólar para las ecuaciones. adicional, vamos a crear referencias (targets) para los títulos hasta de tercer nivel (h1, h2 y h3). También podemos añadir etiquteas más fácilmente en bloques o líneas, sustituciones con Jinja2, bloques de definiciones, reemplazos o listas de tareas. Solo omití linkify, pues no le veo mucha utilidad.

myst_enable_extensions = [
    "amsmath",
    "attrs_inline",
    "colon_fence",
    "deflist",
    "dollarmath",
    "fieldlist",
    "html_admonition",
    "html_image",
    "replacements",
    "smartquotes",
    "strikethrough",
    "substitution",
    "tasklist",
]
myst_heading_anchors = 3

De la misma manera, espero que en la tabla de contenidos de la derecha, se muestren los títulos de tercer nivel. Para esto debemos ajustar las configuraciones del tema.

Nuestro identificador de Google Analytics también se dispone en esta misma parte (PyData soporta también Plausible).

También podemos agregar nuestros enlaces de Twitter y GitHub en la configuración del tema.

html_theme_options = {
    'show_toc_level': 2,
    'twitter_url': 'https://twitter.com/cosmoscalibur',
    'github_url': 'https://github.com/cosmoscalibur/',
}
# Después, esto servirá para separar local de desplegado con Action
html_theme_options['analytics'] = {'google_analytics_id': 'G-4YFQBC69LB'}

Hemos separado la línea de analytics con el fin de deshabilitarla fácilmente en pruebas, para que esto no afecte métricas (más adelante, el ideal es hacerlo con despliegue automático en GitHub Actions y así dependiente de una variable de entorno).

Respecto a los paneles laterales, haremos la siguiente configuración de momento

html_sidebars = {
    'index': [],
    "blog": ["ablog/categories.html", "ablog/archives.html"],
    "*/blog/**": ["ablog/postcard.html", "ablog/recentposts.html", "ablog/archives.html"],
}

No te preocupes, en otra entrada lo explicaré. De momento tomaré un por defecto, ya que estos paneles en caso de requerir personalización, necesitamos hacer HTML y quiero pensar bien que poner cuando no sean entradas de blog (en los cuales los casos dispuestos me parecen perfectos).

También podemos incluir los íconos de Font Awesome con

fontawesome_included = True

Debo decir que no me gusta la idea de habilitar que se muestre el código fuente de la publicación desde el sitio mismo, pues eso genera copia de los archivos en el directorio de salida duplicando estos. Si alguien lo desea ver, considero que para eso es el repositorio. Pero desafortunadamente, deshabilitarlo, igual hace el copiado, así que lo dejaré mostrando (si descubro como remover las copias, lo deshabilito).

html_show_sourcelink = True

Ahora, la configuración para las publicaciones. Necesitamos ahora definir el formato de la marca de tiempo, y podemos definir a nuestro gusto (pero debe ser consistente para esta opción en todas las publicaciones). También podemos indicar cuántos párrafos serán usados en la descripción y cuál imagen considerar para la previsualización. Finalmente, en caso de tener redireccionamiento, el tiempo en segundos para su ejecución.

post_date_format = '%Y-%m-%d'
post_date_format_short = '%Y-%m-%d'
post_auto_excerpt = 1
post_auto_image = 1
post_redirect_refresh = 0

Ahora vamos a habilitar que los feeds tengan texto completo para que puedan ser consumidos de forma completa por los lectores de este formato.

blog_feed_fulltext = True

Es importante configurar nuestra generación del sitemap para ayudar a los motores de búsqueda.

sitemap_url_scheme = '{link}'
html_baseurl = 'https://www.cosmoscalibur.com/'

Finalmente, y aunque no es lo último que pienso configurar (pero serán temas de otras entradas), los comentarios por Disqus (aunque estoy considerando cambiarlo, pero también será tema de otra entrada).

disqus_shortname = 'XXXXXXX'

Archivos y directorios extras#

Dentro de la configuración (conf.py), añadimos el directorio files. Este directorio nos permite añadir archivos directamente al directorio raíz del sitio generado. Esto es importante porque algunas validaciones requieren archivos en esta posición, como lo son:

  • CNAME: Para la validez de nuestro nombre de dominio.

  • .nokekyll: Para que GitHub ignore que es compilado por Jekyll. Si no hacemos esto, los directorios y archivos que inician por guion bajo se ignoran.

  • Archivo de Google Site verification: para demostrar propiedad de dominio ante Google.

  • Otros archivos de verificación de propiedad.

  • Archivo de robots.txt.

También si usamos un directorio a nivel de raíz del repositorio, este quedará disponible a este nivel. Este es mi caso con el directorio de images para las imágenes que uso.

Primera publicación#

Ablog nos ayudó generando en la inicialización, unos archivos de demostración. Puedes editarlos y disponerlos en la ubicación que consideres.

El caso de index.rst, es necesario disponerlo en la ubicación generada para la compilación del proyecto.

También nos crea una publicación por defecto. Esta podemos moverla a nuestro directorio es/blog/2024 (dado el año de elaboración) y allí editar.

Es importante tener presente que el título de primer nivel es el título de la publicación (en otros generadores, el título se añade como parte de una directiva). Como ya configuramos el patrón de ruta de las publicaciones, y la estamos cumpliendo, no es necesario seguir las directivas sino el front matter, ejemplo:

:redirect: blog/configurar-retroarch-en-steam
:date: 2021-12-14
:tags: steam, retroarch, libretro, gaming, linux, controles, videojuegos, emuladores
:category: tecnología/videojuegos
:author: Edward Villegas-Pulgarin
:language: es
---
date: 2024-05-14
tags: blog, sphinx, python, ablog, pydata
category: tecnología
author: Edward Villegas-Pulgarin
language: es
---

La sintaxis ya propia de MD y RST la puedes consultar. No es difícil.

En mi caso, soy el único autor del blog y en general, publicaré en español, así que vale la pena definir el autor y lenguaje por defecto en el conf.py.

blog_default_author = 'Edward'
blog_authors = {
    'Edward': ('Edward Villegas-Pulgarin', None),
}
blog_default_language = 'es'
blog_languages = {
    'es': ('Español', None),
    'en': ('English', None),
}

Generación#

Estamos listos, así que generemos el sitio.

ablog clean && ablog build && ablog serve

La parte de ablog clean es necesaria si queremos una compilación completa, ya que esto borra los temporales generados para evitar recompilar todo. Con ablog serve se abrirá el navegador y podremos explorar.

¿Y ahora qué?#

Pues te cuento que ahora sigue empezar a escribir en el blog. Pero también nos queda algo de exploración todavía.

De mi parte, algunos detalles que quiero próximamente

  • Barra de búsqueda basada en Google.

  • Soporte de internacionalización basado en directorio manual y no en pot.

  • Soporte de sitemap con internacionalización con el esquema anterior.

  • Vincular con otros external links, no solo github y twitter (ejemplo, mastodon).

Referencias#

Updated on 2024-05-25

  • Se agrega información extra sobre open graph para incluir marca de creador.

  • Se incluye información sobre todas las extensiones de Myst Parser a la fecha.

  • Se incluye extensión VSCode de Font Awesome Gallery.

  • Explicación extra de analytics para deshabilitar en pruebas

  • Se añaden más referencias.