diff --git a/README.md b/README.md
index 995e6dbaf..41ee3dc52 100644
--- a/README.md
+++ b/README.md
@@ -38,6 +38,7 @@ tabi has a perfect score on Google's Lighthouse audit:
 
 - [X] [Set any language as default](https://welpo.github.io/tabi/blog/faq-languages/#how-do-i-set-a-default-language-for-my-site). Set your base site to Chinese, Spanish, French, Hindi… or any [other supported language](/i18n). The theme's interface will be translated accordingly.
 - [X] [Integration with remote repositories](https://welpo.github.io/tabi/blog/mastering-tabi-settings/#git-repository-integration) on GitHub, GitLab, Gitea & Codeberg for commit history and showing the site source.
+- [X] [Series support](https://welpo.github.io/tabi/blog/series/) for creating sequential content like tutorials, courses, and multi-part stories.
 - [X] Dark and light themes. Defaults to the OS setting, with a switcher in the navigation bar.
 - [X] Thorough documentation. See [Mastering tabi Settings: A Comprehensive Guide](https://welpo.github.io/tabi/blog/mastering-tabi-settings/).
 - [X] Perfect Lighthouse score (Performance, Accessibility, Best Practices and SEO).
diff --git a/config.toml b/config.toml
index b39f191ee..de4bf8056 100644
--- a/config.toml
+++ b/config.toml
@@ -157,6 +157,16 @@ show_date = true
 # "both" - Show both the original date and the last updated date.
 post_listing_date = "date"
 
+# Show "Jump to posts" link next to series' title.
+# By default, the link appears automatically when a series description exceeds 2000 characters.
+# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
+# show_jump_to_posts = true
+
+# Determines if indexes should be increasing (false) or decreasing (true) in series' posts list.
+# It has only effect if the section uses indexes metadata (which is only the case for series as of now).
+# Can be set at section levels, following the hierarchy: section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
+post_listing_index_reversed = false  # Defaults to false.
+
 # DEPRECATED!
 # Use Zola's built-in `bottom_footnotes = true` in the [markdown] section instead. (Available since v0.19.0)
 # Adds backlinks to footnotes (loads ~500 bytes of JavaScripts).
diff --git a/content/blog/mastering-tabi-settings/img/jump_to_series_posts_dark.webp b/content/blog/mastering-tabi-settings/img/jump_to_series_posts_dark.webp
new file mode 100644
index 000000000..4790470c3
Binary files /dev/null and b/content/blog/mastering-tabi-settings/img/jump_to_series_posts_dark.webp differ
diff --git a/content/blog/mastering-tabi-settings/img/jump_to_series_posts_light.webp b/content/blog/mastering-tabi-settings/img/jump_to_series_posts_light.webp
new file mode 100644
index 000000000..4fee0d631
Binary files /dev/null and b/content/blog/mastering-tabi-settings/img/jump_to_series_posts_light.webp differ
diff --git a/content/blog/mastering-tabi-settings/index.ca.md b/content/blog/mastering-tabi-settings/index.ca.md
index 8963629e7..45eee50af 100644
--- a/content/blog/mastering-tabi-settings/index.ca.md
+++ b/content/blog/mastering-tabi-settings/index.ca.md
@@ -267,6 +267,34 @@ Si configures `tag_sorting = "frequency"`, s'ordenaran segons el nombre de publi
 
 ---
 
+### Sèries
+
+Per a una explicació detallada, consulta la [documentació de sèries](@/blog/series/index.ca.md).
+
+#### Enllaç per saltar a les publicacions
+
+| Pàgina | Secció | `config.toml` | Segueix la jerarquia | Requereix JavaScript |
+|:------:|:-------:|:-------------:|:------------------:|:-------------------:|
+|   ❌   |   ✅    |      ✅       |        ✅          |         ❌          |
+
+Per defecte, apareix automàticament un enllaç "Salta a les publicacions" al costat del títol de la sèrie quan una sèrie té un contingut de més de 2000 caràcters:
+
+{{ dual_theme_image(light_src="blog/series/img/jump_to_series_posts_light.webp", dark_src="blog/series/img/jump_to_series_posts_dark.webp" alt="enllaç per saltar a les publicacions de la sèrie", full_width=true) }}
+
+Estableix `show_jump_to_posts = true` per forçar l'activació de la funció i `show_jump_to_posts = false` per desactivar-la.
+
+#### Indexació de pàgines de sèries
+
+| Pàgina | Secció | `config.toml` | Segueix la jerarquia | Requereix JavaScript |
+|:------:|:-------:|:-------------:|:------------------:|:-------------------:|
+|   ❌   |   ✅    |      ✅       |        ✅          |         ❌          |
+
+Per defecte, les pàgines de sèries s'indexen (usant una indexació basada en 1) segons el `sort_by` de la secció de sèries.
+
+Estableix `post_listing_index_reversed = true` per invertir aquest índex.
+
+---
+
 ## Integració amb repositoris Git
 
 | Pàgina | Secció | `config.toml` | Segueix la jerarquia | Requereix JavaScript |
diff --git a/content/blog/mastering-tabi-settings/index.es.md b/content/blog/mastering-tabi-settings/index.es.md
index 5bf1e6b83..bb7a6100b 100644
--- a/content/blog/mastering-tabi-settings/index.es.md
+++ b/content/blog/mastering-tabi-settings/index.es.md
@@ -267,6 +267,34 @@ Si configuras `tag_sorting = "frequency"`, se ordenarán según el número de pu
 
 ---
 
+### Series
+
+Para una explicación detallada, consulta la [documentación de series](@/blog/series/index.es.md).
+
+#### Enlace para saltar a las publicaciones
+
+| Página | Sección | `config.toml` | Sigue jerarquía | Requiere JavaScript |
+|:------:|:-------:|:-------------:|:------------------:|:-------------------:|
+|   ❌   |   ✅    |      ✅       |        ✅          |         ❌          |
+
+Por defecto, aparece automáticamente un enlace "Saltar a publicaciones" junto al título de la serie cuando una serie tiene un contenido de más de 2000 caracteres:
+
+{{ dual_theme_image(light_src="blog/series/img/jump_to_series_posts_light.webp", dark_src="blog/series/img/jump_to_series_posts_dark.webp" alt="enlace para saltar a las publicaciones de la serie", full_width=true) }}
+
+Establece `show_jump_to_posts = true` para forzar la activación de la función y `show_jump_to_posts = false` para desactivarla.
+
+#### Indexación de páginas de series
+
+| Página | Sección | `config.toml` | Sigue la jerarquía | Requiere JavaScript |
+|:------:|:-------:|:-------------:|:------------------:|:-------------------:|
+|   ❌   |   ✅    |      ✅       |        ✅          |         ❌          |
+
+Por defecto, las páginas de series se indexan (usando una indexación basada en 1) según el `sort_by` de la sección de series.
+
+Establece `post_listing_index_reversed = true` para invertir el índice.
+
+---
+
 ## Integración con repositorios Git
 
 | Página | Sección | `config.toml` | Sigue la jerarquía | Requiere JavaScript |
diff --git a/content/blog/mastering-tabi-settings/index.md b/content/blog/mastering-tabi-settings/index.md
index ee880bc7a..341a3bc22 100644
--- a/content/blog/mastering-tabi-settings/index.md
+++ b/content/blog/mastering-tabi-settings/index.md
@@ -272,6 +272,34 @@ Setting `tag_sorting = "frequency"` will sort them by number-of-posts (descendin
 
 ---
 
+### Series
+
+For a detailed explanation of the series feature, see the [series documentation](@/blog/series/index.md).
+
+#### Jump to posts link
+
+| Page | Section | `config.toml` | Follows Hierarchy | Requires JavaScript |
+|:----:|:-------:|:-------------:|:-----------------:|:-------------------:|
+|  ❌  |     ✅  |      ✅       |           ✅      |         ❌          |
+
+By default, a "Jump to posts" link automatically appears next to the series title when a series has a content over 2000 characters:
+
+{{ dual_theme_image(light_src="blog/series/img/jump_to_series_posts_light.webp", dark_src="blog/series/img/jump_to_series_posts_dark.webp" alt="jump to series posts link", full_width=true) }}
+
+Set `show_jump_to_posts = true` to force the feature on and `show_jump_to_posts = false` to force it off.
+
+#### Series pages indexation
+
+| Page | Section | `config.toml` | Follows Hierarchy | Requires JavaScript |
+|:----:|:-------:|:-------------:|:-----------------:|:-------------------:|
+|  ❌  |     ✅  |      ✅       |           ✅      |         ❌          |
+
+By default, series page are indexed (using a 1-based indexing) as per the series section `sort_by`.
+
+Set `post_listing_index_reversed = true` to reverse this index.
+
+---
+
 ## Git Repository Integration
 
 | Page  | Section | `config.toml` | Follows Hierarchy | Requires JavaScript |
diff --git a/content/blog/series/img/jump_to_series_posts_dark.webp b/content/blog/series/img/jump_to_series_posts_dark.webp
new file mode 100644
index 000000000..d88a292b4
Binary files /dev/null and b/content/blog/series/img/jump_to_series_posts_dark.webp differ
diff --git a/content/blog/series/img/jump_to_series_posts_light.webp b/content/blog/series/img/jump_to_series_posts_light.webp
new file mode 100644
index 000000000..22f5e7886
Binary files /dev/null and b/content/blog/series/img/jump_to_series_posts_light.webp differ
diff --git a/content/blog/series/img/series_dark.webp b/content/blog/series/img/series_dark.webp
new file mode 100644
index 000000000..89223d015
Binary files /dev/null and b/content/blog/series/img/series_dark.webp differ
diff --git a/content/blog/series/img/series_light.webp b/content/blog/series/img/series_light.webp
new file mode 100644
index 000000000..780e2f26a
Binary files /dev/null and b/content/blog/series/img/series_light.webp differ
diff --git a/content/blog/series/img/series_reversed_dark.webp b/content/blog/series/img/series_reversed_dark.webp
new file mode 100644
index 000000000..112a85330
Binary files /dev/null and b/content/blog/series/img/series_reversed_dark.webp differ
diff --git a/content/blog/series/img/series_reversed_light.webp b/content/blog/series/img/series_reversed_light.webp
new file mode 100644
index 000000000..2a1dd0e23
Binary files /dev/null and b/content/blog/series/img/series_reversed_light.webp differ
diff --git a/content/blog/series/index.ca.md b/content/blog/series/index.ca.md
new file mode 100644
index 000000000..e56375f3f
--- /dev/null
+++ b/content/blog/series/index.ca.md
@@ -0,0 +1,424 @@
++++
+title = "Guia completa sobre sèries"
+date = 2024-11-08
+description = "Aprèn a organitzar les teves publicacions en sèries seqüencials, perfectes per a tutorials, cursos i històries de diverses parts."
+
+[taxonomies]
+tags = ["funcionalitat", "tutorial", "preguntes freqüents", "sèries"]
+
+[extra]
+quick_navigation_buttons = true
+toc = true
+mermaid = true
+social_media_card = "social_cards/ca_blog_series.jpg"
++++
+
+Una sèrie organitza publicacions relacionades en ordre seqüencial, similar als capítols d'un llibre. A diferència de les etiquetes, que simplement agrupen contingut relacionat, les sèries suggereixen un ordre específic de lectura de principi a fi.
+
+Les publicacions dins d'una sèrie no necessiten publicar-se de forma consecutiva; la funció de sèries reuneix publicacions temàticament vinculades en una seqüència coherent.
+
+El següent diagrama il·lustra com les publicacions de la sèrie (3, 5 i 8) existeixen dins del flux principal del blog mentre mantenen la seva pròpia seqüència ordenada dins de Sèrie 1.
+
+{% mermaid(full_width=true) %}
+flowchart
+    subgraph main[BLOG]
+        P1[Post 1]
+        P2[P2]
+        P3[P3]
+        P4[P4]
+        P5[P5]
+        P6[P6]
+        P7[P7]
+        P8[P8]
+        P9[P9]
+    end
+    subgraph series1[SÈRIE 1]
+        PS1["Post Sèrie 1 (=P3)"]
+        PS2["Post Sèrie 2 (=P5)"]
+        PS3["Post Sèrie 3 (=P8)"]
+    end
+    P3 o-.-o PS1
+    P5 o-.-o PS2
+    P8 o-.-o PS3
+{% end %}
+
+## Inici ràpid
+
+1. Crea un directori per a la teva sèrie
+2. Crea `_index.md` al directori de la sèrie
+3. Configura el front matter de `_index.md`:
+
+    {{ add_src_to_code_block(src="series/_index.md") }}
+
+    ```toml
+    title = "Aprenent Rust"
+    template = "series.html"
+    sort_by = "slug"
+    transparent = true
+
+    [extra]
+    series = true
+    ```
+
+4. Crea els teus articles de la sèrie en aquest directori
+
+Vols saber-ne més? Continua llegint!
+
+## Com funcionen les sèries?
+
+Una sèrie és simplement una secció que tabi gestiona de manera especial. Per a més detalls sobre seccions, consulta la [documentació de Zola](https://www.getzola.org/documentation/content/section/).
+
+Prenent l'exemple del diagrama anterior, l'estructura de directoris seria així:
+
+```txt
+content/
+    _index.md
+    blog/
+        _index.md
+        post1/
+            index.md
+        post2/
+            index.md
+        post4/
+            index.md
+        post6/
+            index.md
+        post7/
+            index.md
+        post9/
+            index.md
+        serie1/
+            _index.md
+            post3/
+                index.md
+            post5/
+                index.md
+            post8/
+                index.md
+```
+
+Per crear una sèrie, necessites:
+
+1. Utilitzar la plantilla `series.html`
+2. Establir `series = true` a la configuració `[extra]` de la secció
+3. Activar `transparent = true` per integrar les publicacions de la sèrie amb la secció del blog principal
+
+La pàgina principal de la sèrie mostra un resum seguit d'una llista de totes les publicacions a la sèrie:
+
+{{ dual_theme_image(light_src="blog/series/img/series_light.webp", dark_src="blog/series/img/series_dark.webp" alt="una sèrie", full_width=true) }}
+
+## Saltar a les publicacions
+
+Si el contingut d'una sèrie (el Markdown després del frontmatter a `_index.md`) supera els 2000 caràcters, apareix un enllaç "Salta a les publicacions" al costat del títol de la sèrie.
+
+{{ dual_theme_image(light_src="blog/series/img/jump_to_series_posts_light.webp", dark_src="blog/series/img/jump_to_series_posts_dark.webp" alt="enllaç per saltar a les publicacions de la sèrie", full_width=true) }}
+
+Per forçar l'activació o desactivació d'aquesta funció, configura `show_jump_to_posts` a la secció `[extra]` de la teva secció de sèries o a `config.toml`. Aquesta configuració segueix [la jerarquia](@/blog/mastering-tabi-settings/index.ca.md#jerarquia-de-configuracio).
+
+## Pàgines de sèries i ordre
+
+Totes les pàgines a la secció de sèries seran pàgines de sèrie. Les pàgines s'ordenaran segons el `sort_by` de la secció.
+
+Tot i que les sèries mantenen el seu propi ordre intern, romanen independents del flux cronològic de la secció principal (per exemple, `blog/`) gràcies a la configuració `transparent`.
+
+### Opcions d'ordre
+
+Tria entre aquests mètodes d'ordre, cadascun amb els seus avantatges:
+
+{% wide_container() %}
+
+`sort_by` | avantatges | desavantatges
+---------|------------|---------------
+`slug`    | L'ordre de les pàgines és explícit a la ruta (per exemple, `example.com/blog/series1/01-series-post-un`). | Cada pàgina de la sèrie ha de tenir el prefix corresponent.
+`weight`  | L'ordre de les pàgines és fàcil de configurar de forma transparent.<br>La primera publicació té pes `1`, la segona pes `2` i així successivament. | Cada pàgina de la sèrie ha de tenir el seu pes configurat.
+`date`    | L'ordre de les pàgines es pot configurar una sola vegada a la configuració de la secció. No cal fer res a cada pàgina. | L'ordre de les pàgines s'ha d'invertir perquè la primera pàgina sol ser la més antiga. Això només es pot aconseguir paginant la secció (`paginate_by = 9999`) i invertint el seu ordre (`paginate_reversed = true`).
+
+{% end %}
+
+{{ admonition(type="danger", title="Versió de Zola per ordenar per data", text="Per invertir correctament les dates, es requereix Zola v0.19.3+ (no publicada) perquè la informació de paginació estigui disponible a través de la funció `get_section`. En cas contrari, qualsevol cosa que depengui de l'ordre de les pàgines de la sèrie no serà correcta (per exemple, pàgina anterior/següent, llistes ordenades i no ordenades...) Vegeu [Zola PR #2653](https://github.com/getzola/zola/pull/2653).") }}
+
+### Indexació de pàgines
+
+Les pàgines en una sèrie s'indexen començant des d'1, seguint el seu ordre `sort_by`. Per invertir la indexació (fent que la primera pàgina tingui l'índex més alt), afegeix aquesta configuració a `_index.md` o `config.toml`:
+
+```toml
+[extra]
+post_listing_index_reversed = true  # Per defecte és false si no es configura
+```
+
+{{ dual_theme_image(light_src="blog/series/img/series_reversed_light.webp", dark_src="blog/series/img/series_reversed_dark.webp" alt="una sèrie amb índexs invertits", full_width=true) }}
+
+Aquesta configuració segueix [la jerarquia](@/blog/mastering-tabi-settings/index.ca.md#jerarquia-de-configuracio).
+
+## Plantilles d'introducció i conclusió
+
+Els articles d'una sèrie poden tenir seccions automàtiques d'introducció i conclusió. Aquestes es configuren al `_index.md` de la teva sèrie. Un exemple bàsic:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+default = "Aquest article és part de la sèrie $SERIES_HTML_LINK."
+
+[extra.series_outro_templates]
+default = "Gràcies per llegir la part $SERIES_PAGE_INDEX de $SERIES_HTML_LINK!"
+```
+
+Les seccions d'introducció i conclusió tenen les seves pròpies classes CSS (`series-page-intro` i `series-page-outro`), que et permeten personalitzar la seva aparença mitjançant [CSS personalitzat](@/blog/mastering-tabi-settings/index.ca.md#estils-css-personalitzats).
+
+### Tipus de plantilles
+
+El sistema de sèries utilitza diferents plantilles segons la posició de l'article a la sèrie:
+
+- `next_only` - Utilitzat per al primer article (té article següent però no anterior)
+- `middle` - Utilitzat per a articles amb articles anterior i següent
+- `prev_only` - Utilitzat per a l'últim article (té article anterior però no següent)
+- `default` - Plantilla per defecte utilitzada quan no existeix una plantilla específica per a la posició
+
+El sistema determina automàticament quina plantilla utilitzar segons la posició de l'article. Les plantilles es defineixen a la configuració de la sèrie (`_index.md`), com `extra.series_intro_templates` i `extra.series_outro_templates`:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+next_only = "Benvingut a la part 1! Següent: $NEXT_HTML_LINK"
+middle = "Anterior: $PREV_HTML_LINK | Següent: $NEXT_HTML_LINK"
+prev_only = "El capítol final! Anteriorment: $PREV_HTML_LINK"
+default = "Part $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER"
+```
+
+Totes les plantilles són opcionals. La selecció de plantilles segueix un sistema de prioritat:
+
+1. Si existeix una plantilla específica per a la posició (`next_only`, `middle`, o `prev_only`), s'utilitzarà aquesta
+2. Si no, s'utilitza la plantilla `default`
+3. Si no es defineix cap plantilla, no es mostrarà informació de la sèrie
+
+Mira l'[exemple de plantilla](#exemple-de-plantilla) per veure un exemple més elaborat.
+
+### Ubicació al contingut
+
+Per defecte:
+
+- Les introduccions de sèrie apareixen a l'inici del teu article
+- La conclusió apareix al final (abans de les notes al peu, si n'hi ha)
+
+Pots controlar exactament on apareixen utilitzant `<!-- series_intro -->` i `<!-- series_outro -->` al teu Markdown:
+
+```markdown
+Aquest paràgraf apareix abans de la introducció de la sèrie.
+
+<!-- series_intro -->
+
+Contingut principal de l'article.
+
+<!-- series_outro -->
+
+## Recursos d'aprenentatge
+
+Contingut addicional...
+
+[^1]: Les notes al peu sempre apareixeran al final.
+```
+
+## Variables
+
+Les plantilles de sèries utilitzen un sistema flexible de variables que et permet:
+
+1. Fer referència a informació de la sèrie (títol, enllaços)
+2. Afegir navegació entre articles
+3. Mostrar indicadors de progrés
+4. Incloure informació personalitzada utilitzant les teves pròpies variables
+
+Les variables són marcadors que comencen amb `$` i es reemplacen amb contingut real quan es construeix el teu lloc. Per exemple, `$SERIES_HTML_LINK` es converteix en un enllaç clicable a la pàgina índex de la teva sèrie.
+
+Hi ha tres tipus de variables:
+
+- [Variables bàsiques de sèrie](#variables-basiques-de-serie): Informació general sobre la sèrie
+- [Variables de navegació](#variables-de-navegacio): Enllaços a articles anterior/següent
+- [Variables personalitzades](#variables-personalitzades): Els teus propis marcadors per a informació addicional
+
+### Variables bàsiques de sèrie
+
+{% wide_container() %}
+
+| Variable | Disponibilitat | Retorna | Descripció | Exemple d'ús | Exemple de sortida |
+|----------|---------------|----------|------------|--------------|-------------------|
+| `$SERIES_TITLE` | Sempre | Text | Títol de la sèrie en text pla | `Part de $SERIES_TITLE` | Part d'Aprenent Rust |
+| `$SERIES_PERMALINK` | Sempre | Text | URL a l'índex de la sèrie | `[Veure totes les publicacions]($SERIES_PERMALINK)` | [Veure totes les publicacions](/series/learn-rust) |
+| `$SERIES_HTML_LINK` | Sempre | HTML | Enllaç llest per usar a la sèrie | `Benvingut a $SERIES_HTML_LINK!` | Benvingut a <a href="/series/learn-rust">Aprenent Rust</a>! |
+| `$SERIES_PAGES_NUMBER` | Sempre | Nombre | Total d'articles a la sèrie | `Una sèrie de $SERIES_PAGES_NUMBER parts` | Una sèrie de 5 parts |
+| `$SERIES_PAGE_INDEX` | Sempre | Nombre | Posició de l'article actual | `Part $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER` | Part 3 de 5 |
+| `$SERIES_PAGES_OLIST` | Sempre | HTML | Llista ordenada de tots els articles | `Articles a la sèrie: $SERIES_PAGES_OLIST` | Articles a la sèrie: <ol><li>Article actual</li><li><a href="...">Altres articles</a></li></ol> |
+| `$SERIES_PAGES_ULIST` | Sempre | HTML | Llista desordenada de tots els articles | `Articles a la sèrie: $SERIES_PAGES_ULIST` | Articles a la sèrie: <ul><li>Article actual</li><li><a href="...">Altres articles</a></li></ul> |
+
+{% end %}
+
+### Variables de navegació
+
+{% wide_container() %}
+
+| Variable | Disponibilitat | Retorna | Descripció | Exemple d'ús | Exemple de sortida |
+|----------|---------------|----------|------------|--------------|-------------------|
+| `$PREV_TITLE` | Existeix anterior | Text | Títol de l'article anterior | `Anteriorment: $PREV_TITLE` | Anteriorment: Configurant el teu entorn |
+| `$PREV_PERMALINK` | Existeix anterior | Text | URL a l'article anterior | `[← Enrere]($PREV_PERMALINK)` | [← Enrere](/series/learn-rust/setup) |
+| `$PREV_HTML_LINK` | Existeix anterior | HTML | Enllaç llest per usar a l'anterior | `Llegeix primer $PREV_HTML_LINK` | Llegeix primer <a href="/series/learn-rust/setup">Configurant el teu entorn</a> |
+| `$PREV_DESCRIPTION` | Existeix anterior | Text | Descripció de l'article anterior | `Resum: $PREV_DESCRIPTION` | Resum: Configurant Rust |
+| `$NEXT_TITLE` | Existeix següent | Text | Títol del següent article | `Següent: $NEXT_TITLE` | Següent: Patrons avançats |
+| `$NEXT_PERMALINK` | Existeix següent | Text | URL al següent article | `[Continuar →]($NEXT_PERMALINK)` | [Continuar →](/series/learn-rust/patterns) |
+| `$NEXT_HTML_LINK` | Existeix següent | HTML | Enllaç llest per usar al següent | `Continua amb $NEXT_HTML_LINK` | Continua amb <a href="/series/learn-rust/patterns">Patrons avançats</a> |
+| `$NEXT_DESCRIPTION` | Existeix següent | Text | Descripció del següent article | `Properament: $NEXT_DESCRIPTION` | Properament: Aprèn sobre les característiques avançades de pattern matching en Rust |
+
+{% end %}
+
+### Referència al primer article
+
+{% wide_container() %}
+
+| Variable | Disponibilitat | Retorna | Descripció | Exemple d'ús | Exemple de sortida |
+|----------|---------------|----------|------------|--------------|-------------------|
+| `$FIRST_TITLE` | Sempre | Text | Títol del primer article | `Comença amb $FIRST_TITLE` | Comença amb Introducció a Rust |
+| `$FIRST_HTML_LINK` | Sempre | HTML | Enllaç llest per usar al primer article | `Comença a $FIRST_HTML_LINK` | Comença a <a href="/series/learn-rust/intro">Introducció a Rust</a> |
+
+{% end %}
+
+### Exemple de plantilla
+
+{{ admonition(type="tip", title="Variables HTML vs text", text="Utilitza variables HTML (que acaben en `_HTML_LINK`) quan vulguis enllaços preparats per usar. Utilitza variables de text (que acaben en `_TITLE` o `_PERMALINK`) quan vulguis més control sobre el format.") }}
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+# Introducció
+[extra.series_intro_templates]
+next_only = """
+Benvingut a $SERIES_HTML_LINK! Aquesta sèrie de $SERIES_PAGES_NUMBER parts t'ensenyarà Rust des de zero.
+
+Següent: $NEXT_HTML_LINK - $NEXT_DESCRIPTION
+"""
+
+middle = """
+📚 Part $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER a $SERIES_HTML_LINK
+
+Anterior: $PREV_HTML_LINK
+Següent: $NEXT_HTML_LINK
+"""
+
+prev_only = """
+Benvingut a l'última part de $SERIES_HTML_LINK!
+Ets nou? Comença amb $FIRST_HTML_LINK per construir una base sòlida.
+
+Anterior: $PREV_HTML_LINK
+"""
+
+# Plantilla de respatller
+default = "Aquest article és part de la sèrie $SERIES_HTML_LINK."
+
+# Conclusió
+[extra.series_outro_templates]
+next_only = """
+Gràcies per llegir! 🙌
+
+Continua el teu viatge amb $NEXT_HTML_LINK, on $NEXT_DESCRIPTION
+O revisa l'esquema complet de la sèrie [$SERIES_TITLE]($SERIES_PERMALINK).
+"""
+
+middle = """
+---
+📝 Navegació de la sèrie
+
+- Anterior: $PREV_HTML_LINK
+- Següent: $NEXT_HTML_LINK
+- [Resum de la sèrie]($SERIES_PERMALINK)
+"""
+
+prev_only = """
+🎉 Felicitats! Has completat $SERIES_HTML_LINK.
+
+Vols repassar? Aquí vam començar: $FIRST_HTML_LINK
+O revisa el que acabem de veure a $PREV_HTML_LINK.
+"""
+
+# Respatller.
+default = """
+---
+Aquest article és la part $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER a $SERIES_HTML_LINK.
+"""
+```
+
+### Variables personalitzades
+
+Les plantilles de sèries admeten variables personalitzades per incloure informació addicional a tota la teva sèrie. El procés té dos passos:
+
+1. Primer, defineix els teus **marcadors** a la configuració de la teva sèrie (`_index.md`):
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra]
+series = true
+series_template_placeholders = ["$POSITION", "$TOPIC", "$DIFFICULTY"]
+```
+
+2. Després, a cada article de la sèrie, proporciona els valors per a aquests marcadors a `series_template_variables`:
+
+{{ add_src_to_code_block(src="series/article.md") }}
+
+```toml
+[extra.series_template_variables]
+position = "primer"
+topic = "Variables i tipus"
+difficulty = "Principiant"
+```
+
+### Ús de variables personalitzades
+
+Pots usar les teves variables personalitzades a qualsevol plantilla, juntament amb les variables integrades:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+default = """
+Aquest és l'article $POSITION a $SERIES_HTML_LINK.
+Tema d'avui: $TOPIC
+Nivell de dificultat: $DIFFICULTY
+"""
+```
+
+{{ admonition(type="warning", text="Encara que els marcadors es defineixen en majúscules (`$POSITION`), els noms de variables a `series_template_variables` han d'estar en minúscules (`position`).") }}
+
+### Exemple amb variables personalitzades
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+# A la configuració de la sèrie.
+[extra]
+series = true
+series_template_placeholders = ["$LEARNING_TIME", "$KEY_CONCEPTS"]
+
+series_intro_templates.default = """
+📚 Part $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER
+⏱️ Temps estimat: $LEARNING_TIME
+🔑 Conceptes clau: $KEY_CONCEPTS
+"""
+```
+
+{{ add_src_to_code_block(src="series/02-learning-rust/index.md") }}
+
+```toml
+# En un article de la sèrie.
+[extra.series_template_variables]
+learning_time = "30 minuts"
+key_concepts = "Funcions, gestió d'errors, coincidència de patrons"
+```
+
+Això generarà:
+
+```txt
+📚 Part 2 de 5
+⏱️ Temps estimat: 30 minuts
+🔑 Conceptes clau: Funcions, gestió d'errors, coincidència de patrons
+```
+
+{{ admonition(type="warning", title="Variables que falten", text="Si uses un marcador a les teves plantilles però no proporciones el seu valor a `series_template_variables`, la compilació fallarà amb un error que llista les variables que falten.") }}
diff --git a/content/blog/series/index.es.md b/content/blog/series/index.es.md
new file mode 100644
index 000000000..47664f046
--- /dev/null
+++ b/content/blog/series/index.es.md
@@ -0,0 +1,424 @@
++++
+title = "Guía completa sobre series"
+date = 2024-11-08
+description = "Aprende a organizar tus publicaciones en series secuenciales, perfectas para tutoriales, cursos e historias de varias partes."
+
+[taxonomies]
+tags = ["funcionalidad", "tutorial", "preguntas frecuentes", "series"]
+
+[extra]
+quick_navigation_buttons = true
+toc = true
+mermaid = true
+social_media_card = "social_cards/es_blog_series.jpg"
++++
+
+Una serie organiza publicaciones relacionadas en orden secuencial, similar a los capítulos de un libro. A diferencia de las etiquetas, que simplemente agrupan contenido relacionado, las series sugieren un orden específico de lectura de principio a fin.
+
+Las publicaciones dentro de una serie no necesitan publicarse de forma consecutiva; la función de series reúne publicaciones temáticamente vinculadas en una secuencia coherente.
+
+El siguiente diagrama ilustra cómo las publicaciones de la serie (3, 5 y 8) existen dentro del flujo principal del blog mientras mantienen su propia secuencia ordenada dentro de Serie 1.
+
+{% mermaid(full_width=true) %}
+flowchart
+    subgraph main[BLOG]
+        P1[Post 1]
+        P2[P2]
+        P3[P3]
+        P4[P4]
+        P5[P5]
+        P6[P6]
+        P7[P7]
+        P8[P8]
+        P9[P9]
+    end
+    subgraph series1[SERIE 1]
+        PS1["Post Serie 1 (=P3)"]
+        PS2["Post Serie 2 (=P5)"]
+        PS3["Post Serie 3 (=P8)"]
+    end
+    P3 o-.-o PS1
+    P5 o-.-o PS2
+    P8 o-.-o PS3
+{% end %}
+
+## Inicio rápido
+
+1. Crea un directorio para tu serie
+2. Crea `_index.md` en el directorio de la serie
+3. Configura el front matter de `_index.md`:
+
+    {{ add_src_to_code_block(src="series/_index.md") }}
+
+    ```toml
+    title = "Aprendiendo Rust"
+    template = "series.html"
+    sort_by = "slug"
+    transparent = true
+
+    [extra]
+    series = true
+    ```
+
+4. Crea tus artículos de la serie en este directorio
+
+¿Quieres saber más? ¡Sigue leyendo!
+
+## ¿Cómo funcionan las series?
+
+Una serie es simplemente una sección que tabi maneja de manera especial. Para más detalles sobre secciones, consulta la [documentación de Zola](https://www.getzola.org/documentation/content/section/).
+
+Tomando el ejemplo del diagrama anterior, la estructura de directorios sería así:
+
+```txt
+content/
+    _index.md
+    blog/
+        _index.md
+        post1/
+            index.md
+        post2/
+            index.md
+        post4/
+            index.md
+        post6/
+            index.md
+        post7/
+            index.md
+        post9/
+            index.md
+        serie1/
+            _index.md
+            post3/
+                index.md
+            post5/
+                index.md
+            post8/
+                index.md
+```
+
+Para crear una serie, necesitas:
+
+1. Usar la plantilla `series.html`
+2. Establecer `series = true` en la configuración `[extra]` de la sección
+3. Activar `transparent = true` para integrar las publicaciones de la serie con la sección del blog principal
+
+La página principal de la serie muestra un resumen seguido de una lista de todas las publicaciones en la serie:
+
+{{ dual_theme_image(light_src="blog/series/img/series_light.webp", dark_src="blog/series/img/series_dark.webp" alt="una serie", full_width=true) }}
+
+## Saltar a las publicaciones
+
+Si el contenido de una serie (el Markdown después del frontmatter en `_index.md`) supera los 2000 caracteres, aparece un enlace "Saltar a publicaciones" junto al título de la serie.
+
+{{ dual_theme_image(light_src="blog/series/img/jump_to_series_posts_light.webp", dark_src="blog/series/img/jump_to_series_posts_dark.webp" alt="enlace para saltar a las publicaciones de la serie", full_width=true) }}
+
+Para forzar la activación o desactivación de esta función, configura `show_jump_to_posts` en la sección `[extra]` de tu sección de series o en `config.toml`. Esta configuración sigue [la jerarquía](@/blog/mastering-tabi-settings/index.es.md#jerarquia-de-configuracion).
+
+## Páginas de series y orden
+
+Todas las páginas en la sección de series serán páginas de serie. Las páginas se ordenarán según el `sort_by` de la sección.
+
+Aunque las series mantienen su propio orden interno, permanecen independientes del flujo cronológico de la sección principal (por ejemplo, `blog/`) gracias a la configuración `transparent`.
+
+### Opciones de orden
+
+Elige entre estos métodos de orden, cada uno con sus ventajas:
+
+{% wide_container() %}
+
+`sort_by` | ventajas                                                                                                                                      | desventajas
+---------|-------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+`slug`    | El orden de las páginas es explícito en la ruta (por ejemplo, `example.com/blog/series1/01-series-post-uno`).                        | Cada página de la serie debe tener el prefijo correspondiente.
+`weight`  | El orden de las páginas es fácil de configurar de forma transparente.<br>La primera publicación tiene peso `1`, la segunda peso `2` y así sucesivamente. | Cada página de la serie debe tener su peso configurado.
+`date`    | El orden de las páginas se puede configurar una sola vez en la configuración de la sección. No hay que hacer nada en cada página.            | El orden de las páginas debe invertirse porque la primera página suele ser la más antigua. Esto solo se puede lograr paginando la sección (`paginate_by = 9999`) e invirtiendo su orden (`paginate_reversed = true`).
+
+{% end %}
+
+{{ admonition(type="danger", title="Versión de Zola para ordenar por fecha", text="Para invertir correctamente las fechas, se requiere Zola v0.19.3+ (no publicada) para que la información de paginación esté disponible a través de la función `get_section`. De lo contrario, cualquier cosa que dependa del orden de las páginas de la serie no será correcta (por ejemplo, página anterior/siguiente, listas ordenadas y no ordenadas...) Ver [Zola PR #2653](https://github.com/getzola/zola/pull/2653).") }}
+
+### Indexación de páginas
+
+Las páginas en una serie se indexan empezando desde 1, siguiendo su orden `sort_by`. Para invertir la indexación (haciendo que la primera página tenga el índice más alto), añade esta configuración a `_index.md` o `config.toml`:
+
+```toml
+[extra]
+post_listing_index_reversed = true  # Por defecto es false si no se configura
+```
+
+{{ dual_theme_image(light_src="blog/series/img/series_reversed_light.webp", dark_src="blog/series/img/series_reversed_dark.webp" alt="una serie con índices invertidos", full_width=true) }}
+
+Esta configuración sigue [la jerarquía](@/blog/mastering-tabi-settings/index.es.md#jerarquia-de-configuracion).
+
+## Plantillas de introducción y conclusión
+
+Los artículos de una serie pueden tener secciones automáticas de introducción y conclusión. Estas se configuran en el `_index.md` de tu serie. Un ejemplo básico:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+default = "Este artículo es parte de la serie $SERIES_HTML_LINK."
+
+[extra.series_outro_templates]
+default = "¡Gracias por leer la parte $SERIES_PAGE_INDEX de $SERIES_HTML_LINK!"
+```
+
+Las secciones de introducción y conclusión tienen sus propias clases CSS (`series-page-intro` y `series-page-outro`), lo que te permite personalizar su apariencia mediante [CSS personalizado](@/blog/mastering-tabi-settings/index.es.md#estilos-css-personalizados).
+
+### Tipos de plantillas
+
+El sistema de series usa diferentes plantillas según la posición del artículo en la serie:
+
+- `next_only` - Usado para el primer artículo (tiene artículo siguiente pero no anterior)
+- `middle` - Usado para artículos con artículos anterior y siguiente
+- `prev_only` - Usado para el último artículo (tiene artículo anterior pero no siguiente)
+- `default` - Plantilla por defecto usada cuando no existe una plantilla específica para la posición
+
+El sistema determina automáticamente qué plantilla usar según la posición del artículo. Las plantillas se definen en la configuración de la serie (`_index.md`), como `extra.series_intro_templates` y `extra.series_outro_templates`:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+next_only = "¡Bienvenido a la parte 1! Siguiente: $NEXT_HTML_LINK"
+middle = "Anterior: $PREV_HTML_LINK | Siguiente: $NEXT_HTML_LINK"
+prev_only = "¡El capítulo final! Anteriormente: $PREV_HTML_LINK"
+default = "Parte $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER"
+```
+
+Todas las plantillas son opcionales. La selección de plantillas sigue un sistema de prioridad:
+
+1. Si existe una plantilla específica para la posición (`next_only`, `middle`, o `prev_only`), se usará esa
+2. Si no, se usa la plantilla `default`
+3. Si no se define ninguna plantilla, no se mostrará información de la serie
+
+Mira el [ejemplo de plantilla](#ejemplo-de-plantilla) para ver un ejemplo más elaborado.
+
+### Ubicación en el contenido
+
+Por defecto:
+
+- Las introducciones de serie aparecen al inicio de tu artículo
+- La conclusión aparece al final (antes de las notas al pie, si las hay)
+
+Puedes controlar exactamente dónde aparecen usando `<!-- series_intro -->` y `<!-- series_outro -->` en tu Markdown:
+
+```markdown
+Este párrafo aparece antes de la introducción de la serie.
+
+<!-- series_intro -->
+
+Contenido principal del artículo.
+
+<!-- series_outro -->
+
+## Recursos de aprendizaje
+
+Contenido adicional...
+
+[^1]: Las notas al pie siempre aparecerán al final.
+```
+
+## Variables
+
+Las plantillas de series usan un sistema flexible de variables que te permite:
+
+1. Hacer referencia a información de la serie (título, enlaces)
+2. Añadir navegación entre artículos
+3. Mostrar indicadores de progreso
+4. Incluir información personalizada usando tus propias variables
+
+Las variables son marcadores que comienzan con `$` y se reemplazan con contenido real cuando se construye tu sitio. Por ejemplo, `$SERIES_HTML_LINK` se convierte en un enlace clicable a la página índice de tu serie.
+
+Hay tres tipos de variables:
+
+- [Variables básicas de serie](#variables-basicas-de-serie): Información general sobre la serie
+- [Variables de navegación](#variables-de-navegacion): Enlaces a artículos anterior/siguiente
+- [Variables personalizadas](#variables-personalizadas): Tus propios marcadores para información adicional
+
+### Variables básicas de serie
+
+{% wide_container() %}
+
+| Variable | Disponibilidad | Devuelve | Descripción | Ejemplo de uso | Ejemplo de salida |
+|----------|---------------|-----------|-------------|----------------|-------------------|
+| `$SERIES_TITLE` | Siempre | Texto | Título de la serie en texto plano | `Parte de $SERIES_TITLE` | Parte de Aprendiendo Rust |
+| `$SERIES_PERMALINK` | Siempre | Texto | URL al índice de la serie | `[Ver todas las publicaciones]($SERIES_PERMALINK)` | [Ver todas las publicaciones](/series/learn-rust) |
+| `$SERIES_HTML_LINK` | Siempre | HTML | Enlace listo para usar a la serie | `¡Bienvenido a $SERIES_HTML_LINK!` | ¡Bienvenido a <a href="/series/learn-rust">Aprendiendo Rust</a>! |
+| `$SERIES_PAGES_NUMBER` | Siempre | Número | Total de artículos en la serie | `Una serie de $SERIES_PAGES_NUMBER partes` | Una serie de 5 partes |
+| `$SERIES_PAGE_INDEX` | Siempre | Número | Posición del artículo actual | `Parte $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER` | Parte 3 de 5 |
+| `$SERIES_PAGES_OLIST` | Siempre | HTML | Lista ordenada de todos los artículos | `Artículos en la serie: $SERIES_PAGES_OLIST` | Artículos en la serie: <ol><li>Artículo actual</li><li><a href="...">Otros artículos</a></li></ol> |
+| `$SERIES_PAGES_ULIST` | Siempre | HTML | Lista desordenada de todos los artículos | `Artículos en la serie: $SERIES_PAGES_ULIST` | Artículos en la serie: <ul><li>Artículo actual</li><li><a href="...">Otros artículos</a></li></ul> |
+
+{% end %}
+
+### Variables de navegación
+
+{% wide_container() %}
+
+| Variable | Disponibilidad | Devuelve | Descripción | Ejemplo de uso | Ejemplo de salida |
+|----------|---------------|-----------|-------------|----------------|-------------------|
+| `$PREV_TITLE` | Existe anterior | Texto | Título del artículo anterior | `Anteriormente: $PREV_TITLE` | Anteriormente: Configurando tu entorno |
+| `$PREV_PERMALINK` | Existe anterior | Texto | URL al artículo anterior | `[← Atrás]($PREV_PERMALINK)` | [← Atrás](/series/learn-rust/setup) |
+| `$PREV_HTML_LINK` | Existe anterior | HTML | Enlace listo para usar al anterior | `Lee primero $PREV_HTML_LINK` | Lee primero <a href="/series/learn-rust/setup">Configurando tu entorno</a> |
+| `$PREV_DESCRIPTION` | Existe anterior | Texto | Descripción del artículo anterior | `Resumen: $PREV_DESCRIPTION` | Resumen: Configurando Rust |
+| `$NEXT_TITLE` | Existe siguiente | Texto | Título del siguiente artículo | `Siguiente: $NEXT_TITLE` | Siguiente: Patrones avanzados |
+| `$NEXT_PERMALINK` | Existe siguiente | Texto | URL al siguiente artículo | `[Continuar →]($NEXT_PERMALINK)` | [Continuar →](/series/learn-rust/patterns) |
+| `$NEXT_HTML_LINK` | Existe siguiente | HTML | Enlace listo para usar al siguiente | `Continúa con $NEXT_HTML_LINK` | Continúa con <a href="/series/learn-rust/patterns">Patrones avanzados</a> |
+| `$NEXT_DESCRIPTION` | Existe siguiente | Texto | Descripción del siguiente artículo | `Próximamente: $NEXT_DESCRIPTION` | Próximamente: Aprende sobre las características avanzadas de pattern matching en Rust |
+
+{% end %}
+
+### Referencia al primer artículo
+
+{% wide_container() %}
+
+| Variable | Disponibilidad | Devuelve | Descripción | Ejemplo de uso | Ejemplo de salida |
+|----------|---------------|-----------|-------------|----------------|-------------------|
+| `$FIRST_TITLE` | Siempre | Texto | Título del primer artículo | `Comienza con $FIRST_TITLE` | Comienza con Introducción a Rust |
+| `$FIRST_HTML_LINK` | Siempre | HTML | Enlace listo para usar al primer artículo | `Empieza en $FIRST_HTML_LINK` | Empieza en <a href="/series/learn-rust/intro">Introducción a Rust</a> |
+
+{% end %}
+
+### Ejemplo de plantilla
+
+{{ admonition(type="tip", title="Variables HTML vs texto", text="Usa variables HTML (que terminan en `_HTML_LINK`) cuando quieras enlaces listos para usar. Usa variables de texto (que terminan en `_TITLE` o `_PERMALINK`) cuando quieras más control sobre el formato.") }}
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+# Introducción.
+[extra.series_intro_templates]
+next_only = """
+¡Bienvenido a $SERIES_HTML_LINK! Esta serie de $SERIES_PAGES_NUMBER partes te enseñará Rust desde cero.
+
+Siguiente: $NEXT_HTML_LINK - $NEXT_DESCRIPTION
+"""
+
+middle = """
+📚 Parte $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER en $SERIES_HTML_LINK
+
+Anterior: $PREV_HTML_LINK
+Siguiente: $NEXT_HTML_LINK
+"""
+
+prev_only = """
+¡Bienvenido a la última parte de $SERIES_HTML_LINK!
+¿Eres nuevo? Comienza con $FIRST_HTML_LINK para construir una base sólida.
+
+Anterior: $PREV_HTML_LINK
+"""
+
+# Plantilla de respaldo.
+default = "Este artículo es parte de la serie $SERIES_HTML_LINK."
+
+# Conclusión.
+[extra.series_outro_templates]
+next_only = """
+¡Gracias por leer! 🙌
+
+Continúa tu viaje con $NEXT_HTML_LINK, donde $NEXT_DESCRIPTION
+O revisa el esquema completo de la serie [$SERIES_TITLE]($SERIES_PERMALINK).
+"""
+
+middle = """
+---
+📝 Navegación de la serie
+
+- Anterior: $PREV_HTML_LINK
+- Siguiente: $NEXT_HTML_LINK
+- [Resumen de la serie]($SERIES_PERMALINK)
+"""
+
+prev_only = """
+🎉 ¡Felicidades! Has completado $SERIES_HTML_LINK.
+
+¿Quieres repasar? Aquí comenzamos: $FIRST_HTML_LINK
+O revisa lo que acabamos de ver en $PREV_HTML_LINK.
+"""
+
+# Respaldo.
+default = """
+---
+Este artículo es la parte $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER en $SERIES_HTML_LINK.
+"""
+```
+
+### Variables personalizadas
+
+Las plantillas de series admiten variables personalizadas para incluir información adicional en toda tu serie. El proceso tiene dos pasos:
+
+1. Primero, define tus **marcadores** en la configuración de tu serie (`_index.md`):
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra]
+series = true
+series_template_placeholders = ["$POSITION", "$TOPIC", "$DIFFICULTY"]
+```
+
+2. Luego, en cada artículo de la serie, proporciona los valores para estos marcadores en `series_template_variables`:
+
+{{ add_src_to_code_block(src="series/article.md") }}
+
+```toml
+[extra.series_template_variables]
+position = "primero"
+topic = "Variables y tipos"
+difficulty = "Principiante"
+```
+
+### Uso de variables personalizadas
+
+Puedes usar tus variables personalizadas en cualquier plantilla, junto con las variables integradas:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+default = """
+Este es el artículo $POSITION en $SERIES_HTML_LINK.
+Tema de hoy: $TOPIC
+Nivel de dificultad: $DIFFICULTY
+"""
+```
+
+{{ admonition(type="warning", text="Aunque los marcadores se definen en mayúsculas (`$POSITION`), los nombres de variables en `series_template_variables` deben estar en minúsculas (`position`).") }}
+
+### Ejemplo con variables personalizadas
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+# En la configuración de la serie.
+[extra]
+series = true
+series_template_placeholders = ["$LEARNING_TIME", "$KEY_CONCEPTS"]
+
+series_intro_templates.default = """
+📚 Parte $SERIES_PAGE_INDEX de $SERIES_PAGES_NUMBER
+⏱️ Tiempo estimado: $LEARNING_TIME
+🔑 Conceptos clave: $KEY_CONCEPTS
+"""
+```
+
+{{ add_src_to_code_block(src="series/02-learning-rust/index.md") }}
+
+```toml
+# En un artículo de la serie.
+[extra.series_template_variables]
+learning_time = "30 minutos"
+key_concepts = "Funciones, manejo de errores, coincidencia de patrones"
+```
+
+Esto generará:
+
+```txt
+📚 Parte 2 de 5
+⏱️ Tiempo estimado: 30 minutos
+🔑 Conceptos clave: Funciones, manejo de errores, coincidencia de patrones
+```
+
+{{ admonition(type="warning", title="Variables faltantes", text="Si usas un marcador en tus plantillas pero no proporcionas su valor en `series_template_variables`, la compilación fallará con un error que lista las variables faltantes.") }}
diff --git a/content/blog/series/index.md b/content/blog/series/index.md
new file mode 100644
index 000000000..bbb464805
--- /dev/null
+++ b/content/blog/series/index.md
@@ -0,0 +1,424 @@
++++
+title = "A Complete Guide to Series"
+date = 2024-11-08
+description = "Learn how to organize your posts into sequential series, perfect for tutorials, courses, and multi-part stories."
+
+[taxonomies]
+tags = ["showcase", "tutorial", "FAQ", "series"]
+
+[extra]
+quick_navigation_buttons = true
+toc = true
+mermaid = true
+social_media_card = "social_cards/es_blog_series.jpg"
++++
+
+A series organizes related posts in a sequential order, similar to chapters in a book. Unlike tags, which simply group related content, series suggest a specific reading order from start to finish.
+
+Posts within a series do not need to be published consecutively; the series feature brings together thematically linked posts in a coherent sequence.
+
+The diagram below illustrates how series posts (3, 5, and 8) exist within the main blog flow while maintaining their own ordered sequence within Series 1.
+
+{% mermaid(full_width=true) %}
+flowchart
+    subgraph main[BLOG]
+        P1[Post 1]
+        P2[P2]
+        P3[P3]
+        P4[P4]
+        P5[P5]
+        P6[P6]
+        P7[P7]
+        P8[P8]
+        P9[P9]
+    end
+    subgraph series1[SERIES 1]
+        PS1["Series Post 1 (=P3)"]
+        PS2["Series Post 2 (=P5)"]
+        PS3["Series Post 3 (=P8)"]
+    end
+    P3 o-.-o PS1
+    P5 o-.-o PS2
+    P8 o-.-o PS3
+{% end %}
+
+## Quick Start
+
+1. Create a directory for your series.
+2. Create `_index.md` in the series directory.
+3. Set up the `_index.md` front matter:
+
+    {{ add_src_to_code_block(src="series/_index.md") }}
+
+    ```toml
+    title = "Learning Rust"
+    template = "series.html"
+    sort_by = "slug"
+    transparent = true
+
+    [extra]
+    series = true
+    ```
+
+4. Create your series articles in this directory.
+
+Want more? Keep reading!
+
+## How Do Series Work?
+
+A series is just a section which is handled in a special way by tabi. For more details on sections, see the [Zola documentation](https://www.getzola.org/documentation/content/section/).
+
+Taking the example from the diagram above, the directory structure would be as follow:
+
+```txt
+content/
+    _index.md
+    blog/
+        _index.md
+        post1/
+            index.md
+        post2/
+            index.md
+        post4/
+            index.md
+        post6/
+            index.md
+        post7/
+            index.md
+        post9/
+            index.md
+        series1/
+            _index.md
+            post3/
+                index.md
+            post5/
+                index.md
+            post8/
+                index.md
+```
+
+To create a series, you need to:
+
+1. Use the `series.html` template
+2. Set `series = true` in the section's `[extra]` configuration
+3. Enable `transparent = true` to integrate series posts with the parent blog section
+
+The series main page displays an overview followed by a list of all posts in the series:
+
+{{ dual_theme_image(light_src="blog/series/img/series_light.webp", dark_src="blog/series/img/series_dark.webp" alt="a series", full_width=true) }}
+
+## Jump to Posts
+
+If the content of a series (the Markdown after the front matter in `_index.md`) is over 2000 characters, a "Jump to posts" link appears next to the series title.
+
+{{ dual_theme_image(light_src="blog/series/img/jump_to_series_posts_light.webp", dark_src="blog/series/img/jump_to_series_posts_dark.webp" alt="jump to series posts link", full_width=true) }}
+
+To force the feature on or off, set `show_jump_to_posts` in the `[extra]` section of your series section or in `config.toml`. This setting follows [the hierarchy](@/blog/mastering-tabi-settings/index.md#settings-hierarchy).
+
+## Series Pages and Order
+
+All pages in the series section will be a series page. The series pages will be ordered as per the series section `sort_by`.
+
+While series maintain their own internal order, they remain independent from the main section's (e.g. `blog/`) chronological flow thanks to the `transparent` setting.
+
+### Sorting Options
+
+Choose from these sorting methods, each with its own advantages:
+
+{% wide_container() %}
+
+`sort_by` | pros                                                                                                                                      | cons
+---------|-------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ `slug`    | The series pages order is made explicit in the path (e.g. `example.com/blog/series1/01-series-post-one`).                        | Each series page must be prefixed accordingly.
+ `weight`  | The series pages order is easy to set up transparently.<br>First series post has weight `1`, second series post has weight `2` and so on. | Each series page must have its weight set accordingly.
+ `date`    | The series pages order can be configured once in the series section configuration. No need to do anything on each series page.            | The series pages order has to be reversed because the first page is usually the oldest. This can only be achieved by paginating the series section (`paginate_by = 9999`) and reversing its order (`paginate_reversed = true`).
+
+{% end %}
+
+{{ admonition(type="danger", title="Zola version to sort by date", text="In order to properly reverse dates, Zola v0.19.3+ (unreleased) is required so that pagination information is available through the `get_section` function. Anything relying on the series pages order won't be correct in a series page otherwise (e.g. previous/next series page, ordered and unordered list…) See [Zola PR #2653](https://github.com/getzola/zola/pull/2653).") }}
+
+### Page Indexing
+
+Pages in a series are indexed starting from 1, following their `sort_by` order. To reverse the indexing (making the first page have the highest index instead), add this setting to `_index.md` or `config.toml`:
+
+```toml
+[extra]
+post_listing_index_reversed = true  # Defaults to false if unset.
+```
+
+{{ dual_theme_image(light_src="blog/series/img/series_reversed_light.webp", dark_src="blog/series/img/series_reversed_dark.webp" alt="a series with indexes reversed", full_width=true) }}
+
+This setting follows [the hierarchy](@/blog/mastering-tabi-settings/index.md#settings-hierarchy).
+
+## Intro and Outro Templates
+
+Series articles can have automatic introduction and conclusion sections. These are configured in your series' `_index.md`. A basic example:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+default = "This article is part of the $SERIES_HTML_LINK series."
+
+[extra.series_outro_templates]
+default = "Thanks for reading part $SERIES_PAGE_INDEX of $SERIES_HTML_LINK!"
+```
+
+The intro and outro sections each have their own CSS classes (`series-page-intro` and `series-page-outro`), allowing you to customize their appearance through [custom CSS](@/blog/mastering-tabi-settings/index.md#custom-css).
+
+### Template Types
+
+The series system uses different templates based on an article's position in the series:
+
+- `next_only` - Used for the first article (has next article but no previous)
+- `middle` - Used for articles with both previous and next articles
+- `prev_only` - Used for the last article (has previous article but no next)
+- `default` - Fallback template used when a specific position template isn't defined
+
+The system automatically determines which template to use based on the article's position. The templates are defined in the series configuration (`_index.md`), as `extra.series_intro_templates` and `extra.series_outro_templates`.:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+next_only = "Welcome to part 1! Next up: $NEXT_HTML_LINK"
+middle = "Previous: $PREV_HTML_LINK | Next: $NEXT_HTML_LINK"
+prev_only = "The final chapter! Previously: $PREV_HTML_LINK"
+default = "Part $SERIES_PAGE_INDEX of $SERIES_PAGES_NUMBER"
+```
+
+All templates are optional. Template selection follows a priority system:
+
+1. If a position-specific template exists (`next_only`, `middle`, or `prev_only`), it will be used
+2. Otherwise, the `default` template is used
+3. If no templates are defined at all, no series information will be displayed
+
+See the [template example](#template-example) for a more elaborate example.
+
+### Placement in Content
+
+By default:
+
+- Series introductions appear at the start of your article
+- Series outro appears at the end (before footnotes, if any)
+
+You can control exactly where these appear using `<!-- series_intro -->` and `<!-- series_outro -->` in your Markdown:
+
+```markdown
+This paragraph appears before the series introduction.
+
+<!-- series_intro -->
+
+Main content of the article.
+
+<!-- series_outro -->
+
+## Learning Resources
+
+Extra content…
+
+[^1]: Footnotes will always appear at the end.
+```
+
+## Variables
+
+Series templates use a flexible variable system that lets you:
+
+1. Reference series information (title, links)
+2. Add navigation between articles
+3. Show progress indicators
+4. Include custom information using your own variables
+
+Variables are placeholders starting with `$` that get replaced with actual content when your site builds. For example, `$SERIES_HTML_LINK` becomes a clickable link to your series index page.
+
+There are three types of variables:
+
+- [**Basic Series Variables**](#basic-series-variables): General information about the series
+- [**Navigation Variables**](#navigation-variables): Links to previous/next articles
+- [**Custom Variables**](#custom-variables): Your own placeholders for additional information
+
+### Basic Series Variables
+
+{% wide_container() %}
+
+| Variable | Availability | Returns | Description | Example Usage | Example Output |
+|----------|-------------|---------|-------------|---------------|----------------|
+| `$SERIES_TITLE` | Always | Text | Plain text title of the series | `Part of $SERIES_TITLE` | Part of Learn Rust |
+| `$SERIES_PERMALINK` | Always | Text | URL to series index | `[See all posts]($SERIES_PERMALINK)` | [See all posts](/series/learn-rust) |
+| `$SERIES_HTML_LINK` | Always | HTML | Ready-to-use link to series | `Welcome to $SERIES_HTML_LINK!` | Welcome to <a href="/series/learn-rust">Learn Rust</a>! |
+| `$SERIES_PAGES_NUMBER` | Always | Number | Total articles in series | `A $SERIES_PAGES_NUMBER part series` | A 5 part series |
+| `$SERIES_PAGE_INDEX` | Always | Number | Current article's position | `Part $SERIES_PAGE_INDEX of $SERIES_PAGES_NUMBER` | Part 3 of 5 |
+| `$SERIES_PAGES_OLIST` | Always | HTML | Ordered list of all articles | `Articles in series: $SERIES_PAGES_OLIST` | Articles in series: <ol><li>Current article</li><li><a href="...">Other articles</a></li></ol> |
+| `$SERIES_PAGES_ULIST` | Always | HTML | Unordered list of all articles | `Articles in series: $SERIES_PAGES_ULIST` | Articles in series: <ul><li>Current article</li><li><a href="...">Other articles</a></li></ul> |
+
+{% end %}
+
+### Navigation Variables
+
+{% wide_container() %}
+
+| Variable | Availability | Returns | Description | Example Usage | Example Output |
+|----------|-------------|---------|-------------|---------------|----------------|
+| `$PREV_TITLE` | Previous exists | Text | Previous article's title | `Previously: $PREV_TITLE` | Previously: Setting Up Your Environment |
+| `$PREV_PERMALINK` | Previous exists | Text | URL to previous article | `[← Back]($PREV_PERMALINK)` | [← Back](/series/learn-rust/setup) |
+| `$PREV_HTML_LINK` | Previous exists | HTML | Ready-to-use link to previous | `Read $PREV_HTML_LINK first` | Read <a href="/series/learn-rust/setup">Setting Up Your Environment</a> first |
+| `$PREV_DESCRIPTION` | Previous exists | Text | Description of previous article | `Recap: $PREV_DESCRIPTION` | Recap: Setting up Rust |
+| `$NEXT_TITLE` | Next exists | Text | Next article's title | `Next up: $NEXT_TITLE` | Next up: Advanced Patterns |
+| `$NEXT_PERMALINK` | Next exists | Text | URL to next article | `[Continue →]($NEXT_PERMALINK)` | [Continue →](/series/learn-rust/patterns) |
+| `$NEXT_HTML_LINK` | Next exists | HTML | Ready-to-use link to next | `Continue with $NEXT_HTML_LINK` | Continue with <a href="/series/learn-rust/patterns">Advanced Patterns</a> |
+| `$NEXT_DESCRIPTION` | Next exists | Text | Description of next article | `Coming up: $NEXT_DESCRIPTION` | Coming up: Learn about Rust's advanced pattern matching features |
+
+{% end %}
+
+### First Article Reference
+
+{% wide_container() %}
+
+| Variable | Availability | Returns | Description | Example Usage | Example Output |
+|----------|-------------|---------|-------------|---------------|----------------|
+| `$FIRST_TITLE` | Always | Text | First article's title | `Start with $FIRST_TITLE` | Start with Introduction to Rust |
+| `$FIRST_HTML_LINK` | Always | HTML | Ready-to-use link to first article | `Begin at $FIRST_HTML_LINK` | Begin at <a href="/series/learn-rust/intro">Introduction to Rust</a> |
+
+{% end %}
+
+### Template Example
+
+{{ admonition(type="tip", title="HTML vs text variables", text="Use HTML variables (ending in `_HTML_LINK`) when you want ready-made links. Use text variables (ending in `_TITLE` or `_PERMALINK`) when you want more control over the formatting.") }}
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+# Introduction.
+[extra.series_intro_templates]
+next_only = """
+Welcome to $SERIES_HTML_LINK! This $SERIES_PAGES_NUMBER-part series will teach you Rust from scratch.
+
+Up next: $NEXT_HTML_LINK - $NEXT_DESCRIPTION
+"""
+
+middle = """
+📚 Part $SERIES_PAGE_INDEX of $SERIES_PAGES_NUMBER in $SERIES_HTML_LINK
+
+Previously: $PREV_HTML_LINK
+Next up: $NEXT_HTML_LINK
+"""
+
+prev_only = """
+Welcome to the final part of $SERIES_HTML_LINK!
+New here? Start with $FIRST_HTML_LINK to build a strong foundation.
+
+Previously: $PREV_HTML_LINK
+"""
+
+# Fallback template.
+default = "This article is part of the $SERIES_HTML_LINK series."
+
+# Outro.
+[extra.series_outro_templates]
+next_only = """
+Thanks for reading! 🙌
+
+Continue your journey with $NEXT_HTML_LINK, where $NEXT_DESCRIPTION
+Or check out the complete [$SERIES_TITLE]($SERIES_PERMALINK) series outline.
+"""
+
+middle = """
+---
+📝 Series Navigation
+
+- Previous: $PREV_HTML_LINK
+- Next: $NEXT_HTML_LINK
+- [Series Overview]($SERIES_PERMALINK)
+"""
+
+prev_only = """
+🎉 Congratulations! You've completed $SERIES_HTML_LINK.
+
+Want to review? Here's where we started: $FIRST_HTML_LINK
+Or check what we just covered in $PREV_HTML_LINK.
+"""
+
+# Fallback.
+default = """
+---
+This article is part $SERIES_PAGE_INDEX of $SERIES_PAGES_NUMBER in $SERIES_HTML_LINK.
+"""
+```
+
+### Custom Variables
+
+Series templates support custom variables for additional information you want to include across your series. The process takes two steps:
+
+1. First, define your **placeholders** in your series configuration (`_index.md`):
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra]
+series = true
+series_template_placeholders = ["$POSITION", "$TOPIC", "$DIFFICULTY"]
+```
+
+2. Then, in each series article, provide the values for these placeholders in `series_template_variables`:
+
+{{ add_src_to_code_block(src="series/article.md") }}
+
+```toml
+[extra.series_template_variables]
+position = "first"
+topic = "Variables and Types"
+difficulty = "Beginner"
+```
+
+### Using Custom Variables
+
+You can use your custom variables in any template, alongside the built-in variables:
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+[extra.series_intro_templates]
+default = """
+This is the $POSITION article in $SERIES_HTML_LINK.
+Today's topic: $TOPIC
+Difficulty level: $DIFFICULTY
+"""
+```
+
+{{ admonition(type="warning", text="While placeholders are defined with uppercase (`$POSITION`), the variable names in `series_template_variables` must be lowercase (`position`).") }}
+
+### Example with Custom Variables
+
+{{ add_src_to_code_block(src="series/_index.md") }}
+
+```toml
+# In the series configuration.
+[extra]
+series = true
+series_template_placeholders = ["$LEARNING_TIME", "$KEY_CONCEPTS"]
+
+series_intro_templates.default = """
+📚 Part $SERIES_PAGE_INDEX of $SERIES_PAGES_NUMBER
+⏱️ Estimated time: $LEARNING_TIME
+🔑 Key concepts: $KEY_CONCEPTS
+"""
+```
+
+{{ add_src_to_code_block(src="series/02-learning-rust/index.md") }}
+
+```toml
+# In an article of the series.
+[extra.series_template_variables]
+learning_time = "30 minutes"
+key_concepts = "Functions, Error Handling, Pattern Matching"
+```
+
+This will output:
+
+```txt
+📚 Part 2 of 5
+⏱️ Estimated time: 30 minutes
+🔑 Key concepts: Functions, Error Handling, Pattern Matching
+```
+
+{{ admonition(type="warning", title="Missing Variables", text="If you use a placeholder in your templates but don't provide its value in `series_template_variables`, the build will fail with an error listing the missing variables.") }}
diff --git a/content/blog/series/social_cards/blog_series.jpg b/content/blog/series/social_cards/blog_series.jpg
new file mode 100644
index 000000000..0de682864
Binary files /dev/null and b/content/blog/series/social_cards/blog_series.jpg differ
diff --git a/content/blog/series/social_cards/ca_blog_series.jpg b/content/blog/series/social_cards/ca_blog_series.jpg
new file mode 100644
index 000000000..12ccc6cc4
Binary files /dev/null and b/content/blog/series/social_cards/ca_blog_series.jpg differ
diff --git a/content/blog/series/social_cards/es_blog_series.jpg b/content/blog/series/social_cards/es_blog_series.jpg
new file mode 100644
index 000000000..3a24a961d
Binary files /dev/null and b/content/blog/series/social_cards/es_blog_series.jpg differ
diff --git a/content/projects/tabi/index.ca.md b/content/projects/tabi/index.ca.md
index 01f353d60..47c2b9bda 100644
--- a/content/projects/tabi/index.ca.md
+++ b/content/projects/tabi/index.ca.md
@@ -19,8 +19,9 @@ social_media_card = "social_cards/ca_projects_tabi.jpg"
 
 - [Estableix qualsevol idioma com a predeterminat](https://welpo.github.io/tabi/ca/blog/faq-languages/#com-estableixo-la-llengua-predeterminada-del-meu-lloc). Configura el teu lloc en xinès, espanyol, francès, hindi… o qualsevol [altre idioma compatible](https://welpo.github.io/tabi/ca/blog/faq-languages/#quines-llengues-admet-tabi). La interfície del tema es traduirà en conseqüència.
 - [Integració amb repositoris remots](https://welpo.github.io/tabi/ca/blog/mastering-tabi-settings#integracio-amb-repositoris-git) a GitHub, GitLab, Gitea i Codeberg per a l'historial de commits i mostrar el codi font del lloc.
-- Temes clars i foscos. S'adapta a la configuració del sistema operatiu, amb un interruptor a la barra de navegació.
+- Tema clar i fosc. S'adapta a la configuració del sistema operatiu, amb un interruptor a la barra de navegació.
 - [Suport multilingüe complet](https://welpo.github.io/tabi/ca/blog/faq-languages/#com-gestiona-tabi-el-suport-multilingue). Afegeix tants idiomes com vulguis i deixa que els teus usuaris triin amb el selector d'idioma.
+- [Suport per a sèries](https://welpo.github.io/tabi/ca/blog/series/) per crear contingut seqüencial com tutorials, cursos i històries multipart.
 - Puntuació perfecta en Lighthouse (Rendiment, Accessibilitat, Millors Pràctiques i SEO).
 - Suport per a [diagrames de Mermaid](https://welpo.github.io/tabi/ca/blog/shortcodes/#diagrames-de-mermaid) per a crear diagrames i gràfics amb text.
 - Ressaltat de sintaxi de codi amb colors basats en [Catppuccin](https://github.com/catppuccin/catppuccin) Frappé.
diff --git a/content/projects/tabi/index.es.md b/content/projects/tabi/index.es.md
index abb091b4e..2ed16e58e 100644
--- a/content/projects/tabi/index.es.md
+++ b/content/projects/tabi/index.es.md
@@ -20,7 +20,8 @@ social_media_card = "social_cards/es_projects_tabi.jpg"
 - [Establece cualquier idioma como predeterminado](https://welpo.github.io/tabi/es/blog/faq-languages/#como-establezco-el-idioma-predeterminado-de-mi-sitio). Configura tu sitio en chino, español, francés, hindi… o cualquier [otro idioma compatible](https://welpo.github.io/tabi/es/blog/faq-languages/#que-idiomas-admite-tabi). La interfaz del tema se traducirá en consecuencia.
 - [Integración con repositorios remotos](https://welpo.github.io/tabi/es/blog/mastering-tabi-settings/#integracion-con-repositorios-git) en GitHub, GitLab, Gitea y Codeberg para el historial de commits y mostrar el código fuente del sitio.
 - [Soporte multilingüe completo](https://welpo.github.io/tabi/es/blog/faq-languages/#como-gestiona-tabi-el-soporte-multilingue). Añade tantos idiomas como desees y deja que tus usuarios elijan con un selector de idioma.
-- Temas claros y oscuros. Se adapta a la configuración del sistema operativo, con un interruptor en la barra de navegación.
+- Tema claro y oscuro. Se adapta a la configuración del sistema operativo, con un interruptor en la barra de navegación.
+- [Soporte para series](https://welpo.github.io/tabi/es/blog/series/) para crear contenido secuencial como tutoriales, cursos e historias en varias partes.
 - Puntuación perfecta en Lighthouse (Rendimiento, Accesibilidad, Mejores Prácticas y SEO).
 - Soporte para [diagramas de Mermaid](https://welpo.github.io/tabi/es/blog/shortcodes/#diagramas-de-mermaid) para crear diagramas y gráficos con texto.
 - Resaltado de sintaxis de código con colores basados en [Catppuccin](https://github.com/catppuccin/catppuccin) Frappé.
diff --git a/content/projects/tabi/index.md b/content/projects/tabi/index.md
index a3fa4d611..8b7ac54fc 100644
--- a/content/projects/tabi/index.md
+++ b/content/projects/tabi/index.md
@@ -18,6 +18,7 @@ social_media_card = "social_cards/projects_tabi.jpg"
 
 - [Set any language as default](https://welpo.github.io/tabi/blog/faq-languages/#how-do-i-set-a-default-language-for-my-site). Set your base site to Chinese, Spanish, French, Hindi… or any [other supported language](https://welpo.github.io/tabi/blog/faq-languages/#what-languages-does-tabi-support). The theme's interface will be translated accordingly.
 - [Integration with remote repositories](https://welpo.github.io/tabi/blog/mastering-tabi-settings/#git-repository-integration) on GitHub, GitLab, Gitea & Codeberg for commit history and showing the site source.
+- [Series support](https://welpo.github.io/tabi/blog/series/) for creating sequential content like tutorials, courses, and multi-part stories.
 - Dark and light themes. Defaults to the OS setting, with a switcher in the navigation bar.
 - Thorough documentation. See [Mastering tabi Settings: A Comprehensive Guide](https://welpo.github.io/tabi/blog/mastering-tabi-settings/).
 - Perfect Lighthouse score (Performance, Accessibility, Best Practices and SEO).
diff --git a/i18n/ar.toml b/i18n/ar.toml
index ccc36a972..55bc5e556 100644
--- a/i18n/ar.toml
+++ b/i18n/ar.toml
@@ -30,6 +30,7 @@ few_results = "تم العثور على $NUMBER نتائج"  # for 3 to 10 searc
 many_results = "تم العثور على $NUMBER نتيجة"  # 11 or more search results.
 
 # Navigation.
+jump_to_posts = "الإنتقال إلى التدوينات"
 read_more = "إقرأ المزيد"
 one_posts = "تدوينة واحدة"  #One blog post.
 two_posts = "تدوينتين"  #Two blog posts.
diff --git a/i18n/ca.toml b/i18n/ca.toml
index 2c8ccfa84..148417899 100644
--- a/i18n/ca.toml
+++ b/i18n/ca.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER resultat"  # "1 result"
 many_results = "$NUMBER resultats"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Saltar als articles"
 read_more = "Llegir més"
 one_posts = "$NUMBER entrada"
 many_posts = "$NUMBER entrades"
diff --git a/i18n/de.toml b/i18n/de.toml
index 3671fd046..fdafcad98 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -27,6 +27,7 @@ one_results = "$NUMBER Ergebnis"  # "1 result"
 many_results = "$NUMBER Ergebnisse"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Zu den Beiträgen springen"
 read_more = "Weiterlesen"
 one_posts = "$NUMBER Beitrag"
 many_posts = "$NUMBER Beiträge"
diff --git a/i18n/en.toml b/i18n/en.toml
index d081e9cb2..745335a91 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER result"  # "1 result"
 many_results = "$NUMBER results"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Jump to posts"
 read_more = "Read more"
 one_posts = "$NUMBER post"
 many_posts = "$NUMBER posts"  # "3 posts"
diff --git a/i18n/es.toml b/i18n/es.toml
index fb40061f4..36fdd945c 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER resultado"
 many_results = "$NUMBER resultados"
 
 # Navigation.
+jump_to_posts = "Saltar a las entradas"
 read_more = "Leer más"
 one_posts = "$NUMBER entrada"
 many_posts = "$NUMBER entradas"
diff --git a/i18n/et.toml b/i18n/et.toml
index 9ebc0f472..77a9a19e2 100644
--- a/i18n/et.toml
+++ b/i18n/et.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER tulemus"  # "1 result"
 many_results = "$NUMBER tulemust"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Hüppa postitusteni"
 read_more = "Loe edasi"
 one_posts = "$NUMBER postitus"
 many_posts = "$NUMBER postitust"  # "3 posts"
diff --git a/i18n/fa.toml b/i18n/fa.toml
index 5b76e7cc2..13fdd58fa 100644
--- a/i18n/fa.toml
+++ b/i18n/fa.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER نتیجه"   # "1 result"
 many_results = "$NUMBER نتیجه"  # "3 results"
 
 # Navigation.
+jump_to_posts = "پرش به نوشته‌ها"
 read_more = "ادامه مطلب"
 one_posts = "$NUMBER مطلب"
 many_posts = "$NUMBER مطلب"  # "3 posts"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index d27b1eeea..88469e127 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER résultat"  # "1 result"
 many_results = "$NUMBER résultats"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Aller aux articles"
 read_more = "Lire plus"
 one_posts = "$NUMBER article"
 many_posts = "$NUMBER articles"
diff --git a/i18n/hi.toml b/i18n/hi.toml
index 6de5c006d..eabfe77d2 100644
--- a/i18n/hi.toml
+++ b/i18n/hi.toml
@@ -25,6 +25,7 @@ one_results = "$NUMBER परिणाम"  # "1 result"
 many_results = "$NUMBER परिणाम"  # "3 results"
 
 # Navigation.
+jump_to_posts = "पोस्ट पर जाएं"
 read_more = "और पढ़ें"
 one_posts = "$NUMBER पोस्ट"
 many_posts = "$NUMBER पोस्ट्स"
diff --git a/i18n/it.toml b/i18n/it.toml
index 5e7fd6662..a6db4243a 100644
--- a/i18n/it.toml
+++ b/i18n/it.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER risultato"
 many_results = "$NUMBER risultati"
 
 # Navigation.
+jump_to_posts = "Vai ai post"
 read_more = "Leggi di più"
 one_posts = "$NUMBER post"
 many_posts = "$NUMBER post"
diff --git a/i18n/ja.toml b/i18n/ja.toml
index a28ca5362..5969bf3d7 100644
--- a/i18n/ja.toml
+++ b/i18n/ja.toml
@@ -27,6 +27,7 @@ one_results = "$NUMBER 結果"  # "1 result"
 many_results = "$NUMBER 結果"  # "3 results"
 
 # Navigation.
+jump_to_posts = "投稿へジャンプ"
 read_more = "続きを読む"
 one_posts = "$NUMBER 投稿"
 many_posts = "$NUMBER 投稿"
diff --git a/i18n/ko.toml b/i18n/ko.toml
index 945d7b517..9172796bd 100644
--- a/i18n/ko.toml
+++ b/i18n/ko.toml
@@ -27,6 +27,7 @@ one_results = "$NUMBER 결과"  # "1 result"
 many_results = "$NUMBER 결과"  # "3 results"
 
 # Navigation.
+jump_to_posts = "게시물로 이동"
 read_more = "더 읽기"
 one_posts = "$NUMBER 게시물"
 many_posts = "$NUMBER 게시물"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index 9471c3777..772e81295 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER resultaat"  # "1 result"
 many_results = "$NUMBER resultaten"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Naar berichten springen"
 read_more = "Lees meer"
 one_posts = "$NUMBER bericht" # "1 post"
 many_posts = "$NUMBER berichten"  # "3 posts"
diff --git a/i18n/or.toml b/i18n/or.toml
index e689be8fb..1cb535efb 100644
--- a/i18n/or.toml
+++ b/i18n/or.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER ପରିଣାମ"  # "1 result"
 many_results = "$NUMBER ପରିଣାମଗୁଡ଼ିକ"  # "3 results"
 
 # Navigation.
+jump_to_posts = "ପୋଷ୍ଟକୁ ଯାଆନ୍ତୁ"
 read_more = "ଆହୁରି ପଢ଼ନ୍ତୁ"
 one_posts = "$NUMBER ପୋଷ୍ଟ"
 many_posts = "$NUMBER ପୋଷ୍ଟଗୁଡ଼ିକ"  # "3 posts"
diff --git a/i18n/pt-PT.toml b/i18n/pt-PT.toml
index a05e99a95..164ef49a9 100644
--- a/i18n/pt-PT.toml
+++ b/i18n/pt-PT.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER resultado"  # "1 result"
 many_results = "$NUMBER resultados"  # "3 results"
 
 # Navigation.
+jump_to_posts = "Ir para as publicações"
 read_more = "Ler mais"
 one_posts = "$NUMBER publicação"
 many_posts = "$NUMBER publicações"
diff --git a/i18n/ru.toml b/i18n/ru.toml
index aad5d630f..e8a3ad3fe 100644
--- a/i18n/ru.toml
+++ b/i18n/ru.toml
@@ -28,6 +28,7 @@ few_results = "$NUMBER результата"  # 2, 3, 4 but not 12-14
 many_results = "$NUMBER результатов"  # 5-9, 0, 11-14, and others
 
 # Navigation.
+jump_to_posts = "Перейти к записям"
 read_more = "Читать далее"
 post = "пост"
 one_posts = "$NUMBER пост"
diff --git a/i18n/uk.toml b/i18n/uk.toml
index 95a0f51c6..fada833d1 100644
--- a/i18n/uk.toml
+++ b/i18n/uk.toml
@@ -33,6 +33,7 @@ few_results = "$NUMBER результати"
 many_results = "$NUMBER результатів"
 
 # Navigation.
+jump_to_posts = "Перейти до дописів"
 read_more = "Читати далі"
 one_posts = "$NUMBER пост"
 few_posts = "$NUMBER пости"  # 2, 3, 4 but not 12-14
diff --git a/i18n/zh-Hans.toml b/i18n/zh-Hans.toml
index c44557fcc..29d707721 100644
--- a/i18n/zh-Hans.toml
+++ b/i18n/zh-Hans.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER 个结果"
 many_results = "$NUMBER 个结果"
 
 # Navigation.
+jump_to_posts = "跳转到文章"
 read_more = "阅读全文"
 one_posts = "$NUMBER 篇文章"
 many_posts = "$NUMBER 篇文章"
diff --git a/i18n/zh-Hant.toml b/i18n/zh-Hant.toml
index 14866c513..7c41eaa5b 100644
--- a/i18n/zh-Hant.toml
+++ b/i18n/zh-Hant.toml
@@ -23,6 +23,7 @@ one_results = "$NUMBER 個結果"
 many_results = "$NUMBER 個結果"
 
 # Navigation.
+jump_to_posts = "跳轉到文章"
 read_more = "閱讀全文"
 one_posts = "$NUMBER 篇文章"
 many_posts = "$NUMBER 篇文章"
diff --git a/sass/parts/_misc.scss b/sass/parts/_misc.scss
index b68a1059d..e066a34bf 100644
--- a/sass/parts/_misc.scss
+++ b/sass/parts/_misc.scss
@@ -253,3 +253,24 @@ details summary {
 [data-force-text-direction="rtl"] * {
     direction: inherit;
 }
+
+.title-with-jump {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.title-with-jump h1 {
+    flex: 1;
+}
+
+.jump-link {
+    flex-shrink: 0;
+    font-size: 0.9rem;
+}
+
+@media (max-width: 500px) {
+    .title-with-jump {
+        flex-direction: column;
+    }
+}
diff --git a/sass/parts/_posts_list.scss b/sass/parts/_posts_list.scss
index ce38a83fd..92cfe4674 100644
--- a/sass/parts/_posts_list.scss
+++ b/sass/parts/_posts_list.scss
@@ -1,24 +1,28 @@
 .bloglist-container {
     display: grid;
-    grid-template-columns: 1fr;
+    grid-template-columns: 1fr 8fr;
 }
 
-.bloglist-row {
+.bloglist-meta {
     display: flex;
     align-items: flex-start;
     background-color: var(--navbar-color);
     padding-block: 2.5rem;
 
-    .bloglist-meta {
+    ul {
         margin-inline-end: 0.7rem;
         padding: 0;
-        width: 13.5rem;
         color: var(--meta-color);
         font-weight: 300;
         font-size: 0.9rem;
 
         li {
             list-style-type: none;
+            white-space: nowrap;
+        }
+
+        li.date {
+            width: 13.5rem;
         }
 
         li.draft-label {
@@ -26,23 +30,17 @@
             line-height: 1.2rem;
         }
     }
+}
 
-    .bloglist-tags {
-        margin-top: 0.1rem;
-
-        .tag {
-            display: inline-block;
-            margin-inline-end: 0.7rem;
-            font-weight: 400;
-            font-size: 0.75rem;
-            text-transform: uppercase;
-        }
-    }
+.bloglist-content {
+    display: flex;
+    align-items: flex-start;
+    background-color: var(--navbar-color);
+    padding: 2.5rem 0;
 
-    .bloglist-content {
+    div {
         flex: 1;
 
-
         .bloglist-title {
             margin: 0;
             font-weight: bold;
@@ -58,6 +56,18 @@
             }
         }
 
+        .bloglist-tags {
+            margin-top: 0.1rem;
+
+            .tag {
+                display: inline-block;
+                margin-inline-end: 0.7rem;
+                font-weight: 400;
+                font-size: 0.75rem;
+                text-transform: uppercase;
+            }
+        }
+
         .description p {
             margin: 0.5rem 0 1rem;
             color: var(--text-color);
@@ -86,12 +96,15 @@
 }
 
 @media only screen and (max-width: 1100px) {
-    .bloglist-row {
-        flex-direction: column;
-        align-items: flex-start;
+    .bloglist-container {
+        grid-template-columns: 1fr;
+    }
+
+    .bloglist-meta {
         padding-block: 2rem;
+        border-bottom: 0;
 
-        .bloglist-meta {
+        ul {
             margin-block-end: 0;
             width: 100%;
 
@@ -100,8 +113,15 @@
                 margin-inline-end: 0.3rem;
             }
         }
+    }
+
+    .bloglist-content {
+        flex-direction: column;
+        align-items: flex-start;
+        padding: 0;
+        padding-bottom: 2rem;
 
-        .bloglist-content {
+        div {
             width: 100%;
         }
     }
diff --git a/static/feed_style.xsl b/static/feed_style.xsl
index 5fa7160f0..f4f5e9897 100644
--- a/static/feed_style.xsl
+++ b/static/feed_style.xsl
@@ -60,8 +60,8 @@
             <xsl:variable name="post_listing_date" select="/atom:feed/tabi:metadata/tabi:post_listing_date"/>
             <div class="bloglist-container">
               <xsl:for-each select="/atom:feed/atom:entry">
-                <section class="bloglist-row bottom-divider">
-                  <ul class="bloglist-meta">
+                <section class="bloglist-meta bottom-divider">
+                  <ul>
                     <xsl:variable name="show_date" select="$post_listing_date = 'date' or $post_listing_date = 'both'"/>
                     <xsl:variable name="show_updated" select="$post_listing_date = 'updated' or $post_listing_date = 'both'"/>
 
@@ -87,7 +87,9 @@
                       </li>
                     </xsl:if>
                   </ul>
-                  <div class="bloglist-content">
+                </section>
+                <section class="bloglist-content bottom-divider">
+                  <div>
                     <div class="bloglist-title">
                       <a>
                         <xsl:attribute name="href">
diff --git a/templates/base.html b/templates/base.html
index 6db8bfd79..d3d0cc141 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -5,6 +5,7 @@
 {% import "macros/settings.html" as macros_settings %}
 {% import "macros/table_of_contents.html" as macros_toc %}
 {% import "macros/translate.html" as macros_translate %}
+{% import "macros/series_page.html" as macros_series_page %}
 
 {# Load the internationalisation data for the current language from
 the .toml files in the user's '/i18n' folder, falling back to the theme's.
diff --git a/templates/macros/list_posts.html b/templates/macros/list_posts.html
index 790e1b794..6fd7be04e 100644
--- a/templates/macros/list_posts.html
+++ b/templates/macros/list_posts.html
@@ -1,40 +1,63 @@
-{% macro list_posts(posts, max, language_strings="", section_path="blog") %}
+{# `metadata` can be "dates", "indexes" or both (e.g. "dates indexes" or "indexes dates"). #}
+{# If both, the order doesn't matter and indexes will always be displayed before dates. #}
+{# It would also work with arrays (e.g. ["dates"] or ["indexes"] or even ["indexes","dates"]). #}
+{# Nevertheless, arrays cannot be used as a default value for a macro parameter in Tera (see https://github.com/Keats/tera/issues/710). #}
+{# `paginator` is only used to compute indexes metadata and can be let empty otherwise. #}
+{% macro list_posts(posts, max, metadata="dates", language_strings="", section_path="blog", paginator="") %}
 
 {%- set separator = config.extra.separator | default(value="•") -%}
 
 <div class="bloglist-container">
     {% for post in posts %}
         {% if loop.index <= max %}
-            {% if loop.index == max %}
-                <section class="bloglist-row">
-            {% elif loop.last %}
-                <section class="bloglist-row">
+            {% if loop.index == max or loop.last %}
+                {% set bottom_divider = false %}
             {% else %}
-                <section class="bloglist-row bottom-divider">
+                {% set bottom_divider = true %}
             {% endif %}
 
-            <ul class="bloglist-meta">
-                {%- set allowed_post_listing_dates = ["date", "updated", "both"] -%}
-                {%- set post_listing_date = config.extra.post_listing_date | default(value="date") -%}
-                {%- if post_listing_date not in allowed_post_listing_dates -%}
-                    {{ throw(message="ERROR: Invalid value for config.extra.post_listing_date. Allowed values are 'date', 'updated', or 'both'.") }}
+        <section class="bloglist-meta {% if bottom_divider -%}bottom-divider{%- endif -%}">
+            <ul>
+                {%- if "indexes" in metadata -%}
+                    {%- set post_index = loop.index -%}
+                    {%- set nb_posts = posts | length -%}
+                    {# in case we have a pager, the index has been computed for the current page. #}
+                    {%- if paginator -%}
+                        {%- set nb_posts = paginator.total_pages -%}
+                        {%- set number_of_other_pages = paginator.current_index - 1 -%}
+                        {%- set posts_per_page = paginator.paginate_by -%}
+                        {%- set posts_in_other_pages = number_of_other_pages * posts_per_page -%}
+                        {%- set post_index = posts_in_other_pages + post_index -%}
+                    {%- endif -%}
+                    {%- if macros_settings::evaluate_setting_priority(setting="post_listing_index_reversed", page=section, default_global_value=false) == "true" -%}
+                        {# index starts at 1 instead of 0 #}
+                        {%- set post_index = nb_posts + 1 - post_index -%}
+                    {%- endif -%}
+                    <li class="index-label">{{ post_index }}</li>
                 {%- endif -%}
+                {%- if "dates" in metadata -%}
+                    {%- set allowed_post_listing_dates = ["date", "updated", "both"] -%}
+                    {%- set post_listing_date = config.extra.post_listing_date | default(value="date") -%}
+                    {%- if post_listing_date not in allowed_post_listing_dates -%}
+                        {{ throw(message="ERROR: Invalid value for config.extra.post_listing_date. Allowed values are 'date', 'updated', or 'both'.") }}
+                    {%- endif -%}
 
-                {%- set show_date = post.date and post_listing_date == "date" or post.date and post_listing_date == "both" or post.date and post_listing_date == "updated" and not post.updated -%}
-                {%- set show_updated = post.updated and post_listing_date == "updated" or post.updated and post_listing_date == "both" -%}
+                    {%- set show_date = post.date and post_listing_date == "date" or post.date and post_listing_date == "both" or post.date and post_listing_date == "updated" and not post.updated -%}
+                    {%- set show_updated = post.updated and post_listing_date == "updated" or post.updated and post_listing_date == "both" -%}
 
-                {%- if show_date or show_updated -%}
-                    {%- if show_date -%}
-                        <li class="date">{{- macros_format_date::format_date(date=post.date, short=false, language_strings=language_strings) -}}</li>
-                    {%- endif -%}
-                    {%- if show_date and show_updated -%}
-                        <li class="mobile-only">{{- separator -}}</li>
-                    {%- endif -%}
-                    {%- if show_updated -%}
-                        {%- set last_updated_str = macros_translate::translate(key="last_updated_on", default="Updated on $DATE", language_strings=language_strings) -%}
-                        {%- set formatted_date = macros_format_date::format_date(date=post.updated, short=true, language_strings=language_strings) -%}
-                        {%- set updated_str = last_updated_str | replace(from="$DATE", to=formatted_date) -%}
-                        <li class="date">{{ updated_str }}</li>
+                    {%- if show_date or show_updated -%}
+                        {%- if show_date -%}
+                            <li class="date">{{- macros_format_date::format_date(date=post.date, short=false, language_strings=language_strings) -}}</li>
+                        {%- endif -%}
+                        {%- if show_date and show_updated -%}
+                            <li class="mobile-only">{{- separator -}}</li>
+                        {%- endif -%}
+                        {%- if show_updated -%}
+                            {%- set last_updated_str = macros_translate::translate(key="last_updated_on", default="Updated on $DATE", language_strings=language_strings) -%}
+                            {%- set formatted_date = macros_format_date::format_date(date=post.updated, short=true, language_strings=language_strings) -%}
+                            {%- set updated_str = last_updated_str | replace(from="$DATE", to=formatted_date) -%}
+                            <li class="date">{{ updated_str }}</li>
+                        {%- endif -%}
                     {%- endif -%}
                 {%- endif -%}
 
@@ -42,7 +65,9 @@
                 <li class="draft-label">{{ macros_translate::translate(key="draft", default="DRAFT", language_strings=language_strings) }}</li>
                 {% endif %}
             </ul>
-            <div class="bloglist-content">
+        </section>
+        <section class="bloglist-content {% if bottom_divider -%}bottom-divider{%- endif -%}">
+            <div>
                 <h2 class="bloglist-title">
                     <a href="{{ post.permalink }}">{{ post.title }}</a>
                 </h2>
diff --git a/templates/macros/series_page.html b/templates/macros/series_page.html
new file mode 100644
index 000000000..d5704a15a
--- /dev/null
+++ b/templates/macros/series_page.html
@@ -0,0 +1,162 @@
+{#
+Those macros deal with introduction and navigation for series pages.
+Using macros have been prefered over partial inclusion or inline code to make sure series_ordered_pages is forced to be used.
+A section's pages natural order is invalid in case of reversed pagination which would lead to invalid series' pages order.
+To prevent this, pages are ordered correctly in a separate variable which must be used instead of the series section pages.
+#}
+
+{#
+Computes the introduction of a series's page.
+
+Parameters:
+    - `page`: The page object being part of the series.
+		- `series_section`: The series' section the page belongs to.
+		- `series_ordered_pages`: The series' pages properly ordered (see at the top of this file for an explanation).
+		- `language_strings`: A dictionary containing the translation strings.
+#}
+{% macro process_series_template(template_type, page, series_section, series_ordered_pages, language_strings) %}
+    {%- if "series" in series_section.extra and series_section.extra.series -%}
+        {# Prepare variables for substitution #}
+        {%- set series_title = series_section.title -%}
+        {%- set series_permalink = series_section.permalink -%}
+        {%- set series_html_link = '<a href="' ~ series_section.permalink ~ '" aria_label="' ~ series_section.title ~ '">' ~ series_section.title ~ '</a>' -%}
+		{# Build series pages list #}
+		{%- set series_pages_list = [] -%}
+		{%- for series_page in series_ordered_pages -%}
+			{%- if series_page.relative_path == page.relative_path -%}
+				{%- set series_pages_list_item = '<li>' ~ series_page.title ~ '</li>' -%}
+			{%- else -%}
+				{%- set series_pages_list_item = '<li><a href="' ~ series_page.permalink ~ '" aria_label="' ~ series_page.title ~ '">' ~ series_page.title ~ '</a></li>' -%}
+			{%- endif -%}
+			{%- set_global series_pages_list = series_pages_list | concat(with=series_pages_list_item) -%}
+		{%- endfor -%}
+		{%- set series_pages_list = series_pages_list | join(sep="") -%}
+		{%- if macros_settings::evaluate_setting_priority(setting="post_listing_index_reversed", page=series_section, default_global_value=false) == "true" -%}
+			{%- set series_pages_ordered_list = '<ol reversed>' ~ series_pages_list ~ '</ol>' -%}
+		{%- else -%}
+			{%- set series_pages_ordered_list = '<ol>' ~ series_pages_list ~ '</ol>' -%}
+		{%- endif -%}
+		{%- set series_pages_unordered_list = '<ul>' ~ series_pages_list ~ '</ul>' -%}
+
+        {# Get page position and navigation info #}
+        {%- set series_pages_number = 0 -%}
+        {%- set series_page_index = 0 -%}
+        {%- set first_page = series_ordered_pages | first -%}
+        {%- set is_found = false -%}
+
+        {%- for series_page in series_ordered_pages -%}
+            {%- set_global series_pages_number = series_pages_number + 1 -%}
+            {%- if series_page.relative_path == page.relative_path -%}
+                {%- set_global series_page_index = series_pages_number -%}
+                {%- set_global is_found = true -%}
+            {%- else -%}
+                {%- if not is_found -%}
+                    {%- set_global prev_page = series_page -%}
+                {%- elif not next_page is defined -%}
+                    {%- set_global next_page = series_page -%}
+                {%- endif -%}
+            {%- endif -%}
+        {%- endfor -%}
+
+		{# Determine template to use based on available navigation #}
+		{%- set position = "middle" -%}
+		{%- if prev_page is defined and not next_page is defined -%}
+			{%- set_global position = "prev_only" -%}
+		{%- elif next_page is defined and not prev_page is defined -%}
+			{%- set_global position = "next_only" -%}
+		{%- endif -%}
+
+        {# Get template from config #}
+        {%- set templates_key = "series_" ~ template_type ~ "_templates" -%}
+        {%- set template = "" -%}
+        {%- if series_section.extra[templates_key] is defined -%}
+            {%- if series_section.extra[templates_key][position] is defined -%}
+                {%- set_global template = series_section.extra[templates_key][position] -%}
+            {%- elif series_section.extra[templates_key].default is defined -%}
+                {%- set_global template = series_section.extra[templates_key].default -%}
+            {%- endif -%}
+        {%- endif -%}
+
+		{# Prepare navigation variables #}
+		{%- if prev_page is defined -%}
+			{%- set prev_title = prev_page.title -%}
+			{%- set prev_permalink = prev_page.permalink -%}
+			{%- set prev_html_link = '<a href="' ~ prev_page.permalink ~ '" aria_label="' ~ prev_page.title ~ '">' ~ prev_page.title ~ '</a>' -%}
+			{%- set prev_description = prev_page.description | default(value="") -%}
+		{%- endif -%}
+		{%- if next_page is defined -%}
+			{%- set next_title = next_page.title -%}
+			{%- set next_permalink = next_page.permalink -%}
+			{%- set next_html_link = '<a href="' ~ next_page.permalink ~ '" aria_label="' ~ next_page.title ~ '">' ~ next_page.title ~ '</a>' -%}
+			{%- set next_description = next_page.description | default(value="") -%}
+		{%- endif -%}
+
+		{# Replace standard variables #}
+		{%- set template = template
+			| replace(from="$SERIES_TITLE", to=series_title)
+			| replace(from="$SERIES_PERMALINK", to=series_permalink)
+			| replace(from="$SERIES_HTML_LINK", to=series_html_link)
+			| replace(from="$FIRST_TITLE", to=first_page.title)
+			| replace(from="$FIRST_HTML_LINK", to='<a href="' ~ first_page.permalink ~ '">' ~ first_page.title ~ '</a>')
+			| replace(from="$SERIES_PAGES_NUMBER", to=series_pages_number | as_str)
+			| replace(from="$SERIES_PAGE_INDEX", to=series_page_index | as_str)
+			| replace(from="$SERIES_PAGES_OLIST", to=series_pages_ordered_list)
+			| replace(from="$SERIES_PAGES_ULIST", to=series_pages_unordered_list)
+		-%}
+
+        {# Replace navigation variables if they exist #}
+		{%- if prev_page is defined -%}
+		{%- set template = template
+			| replace(from="$PREV_TITLE", to=prev_title)
+			| replace(from="$PREV_PERMALINK", to=prev_permalink)
+			| replace(from="$PREV_HTML_LINK", to=prev_html_link)
+			| replace(from="$PREV_DESCRIPTION", to=prev_description)
+		-%}
+		{%- endif -%}
+
+		{%- if next_page is defined -%}
+		{%- set template = template
+			| replace(from="$NEXT_TITLE", to=next_title)
+			| replace(from="$NEXT_PERMALINK", to=next_permalink)
+			| replace(from="$NEXT_HTML_LINK", to=next_html_link)
+			| replace(from="$NEXT_DESCRIPTION", to=next_description)
+		-%}
+		{%- endif -%}
+
+		{# Custom placeholders #}
+		{%- if series_section.extra.series_template_placeholders is defined -%}
+			{%- set missing_vars = [] -%}
+			{%- for placeholder in series_section.extra.series_template_placeholders -%}
+				{%- if placeholder in template -%}
+					{%- set var_name = placeholder | replace(from="$", to="") | lower -%}
+					{%- if page.extra.series_template_variables is defined and page.extra.series_template_variables[var_name] is defined -%}
+						{%- set_global template = template | replace(from=placeholder, to=page.extra.series_template_variables[var_name]) -%}
+					{%- else -%}
+						{%- set_global missing_vars = missing_vars | concat(with=var_name) -%}
+					{%- endif -%}
+				{%- endif -%}
+			{%- endfor -%}
+			{%- if missing_vars | length > 0 -%}
+				{%- set missing_vars_str = missing_vars | join(sep=", ") -%}
+				{{ throw(message="ERROR: The following variables are included in this page's series templates (`series_template_placeholders`) but have not been set in the `series_template_variables` of this page: " ~ missing_vars_str) }}
+			{%- endif -%}
+		{%- endif -%}
+
+        {# Output the processed template if not empty #}
+        {%- if template | length > 0 -%}
+            <section class="series-page-{{ template_type }}">
+                {{ template | markdown | safe }}
+            </section>
+        {%- endif -%}
+    {%- endif -%}
+{% endmacro %}
+
+{# Macro for series introduction #}
+{% macro get_introduction(page, series_section, series_ordered_pages, language_strings) %}
+    {{ macros_series_page::process_series_template(template_type="intro", page=page, series_section=series_section, series_ordered_pages=series_ordered_pages, language_strings=language_strings) }}
+{% endmacro %}
+
+{# Macro for series outro #}
+{% macro get_outro(page, series_section, series_ordered_pages, language_strings) %}
+    {{ macros_series_page::process_series_template(template_type="outro", page=page, series_section=series_section, series_ordered_pages=series_ordered_pages, language_strings=language_strings) }}
+{% endmacro %}
diff --git a/templates/page.html b/templates/page.html
index 969baa0fc..c269531ad 100644
--- a/templates/page.html
+++ b/templates/page.html
@@ -180,6 +180,33 @@ <h1 class="article-title">
             {% endif %}
         </ul>
 
+        {#- A page is part of a series if one of the sections above (whether it is transparent or not) has the `extra.series` parameter set to true. -#}
+        {#- As a series might be a transparent section in order to mix up its articles with those of the section just above or the root, -#}
+        {#- there is no other way but to compute the potential path of each ancestor section related to the page and look for the first one being a series. -#}
+        {#- Using the `ancestors` field of a section is not possible because transparent sections are not present in this field. -#}
+        {%- set current_path = [] -%}
+        {%- set section_paths = [] -%}
+        {%- for path in page.relative_path | split(pat="/") | slice(end=-1) -%}
+            {%- set_global current_path = current_path | concat(with=path) -%}
+            {%- set section_path = current_path | concat(with="_index.md") | join(sep="/") -%}
+            {%- set_global section_paths = section_paths | concat(with=section_path) -%}
+        {%- endfor -%}
+        {#- The series the page is part of is the closest section flagged as a series, if any -#}
+        {%- for section_path in section_paths | reverse -%}
+            {%- set section_file_exists = load_data(path=section_path, required=false) -%}
+            {%- if section_file_exists -%}
+                {%- set loaded_section = get_section(path=section_path,lang=lang) -%}
+                {%- if "series" in loaded_section.extra and loaded_section.extra.series -%}
+                    {%- set_global series_section = loaded_section -%}
+                    {%- set_global series_ordered_pages = loaded_section.pages -%}
+                    {%- if loaded_section.paginated | default(value=0) > 0 and loaded_section.paginate_reversed -%}
+                        {%- set_global series_ordered_pages = loaded_section.pages | reverse -%}
+                    {%- endif -%}
+                    {%- break -%}
+                {%- endif -%}
+            {%- endif -%}
+        {%- endfor -%}
+
         {% if page.extra.tldr %}
             <div class="admonition note">
                 <div class="admonition-icon admonition-icon-note"></div>
@@ -192,18 +219,48 @@ <h1 class="article-title">
             </div>
         {% endif %}
 
-        {# Optional table of contents below the header #}
+        {#- Optional table of contents below the header -#}
         {% if page.toc and macros_settings::evaluate_setting_priority(setting="toc", page=page, default_global_value=false) == "true" %}
             {{ macros_toc::toc(page=page, header=true, language_strings=language_strings) }}
         {% endif %}
 
         <section class="body">
-            {# The replace pattern is used to enable arbitrary locations for the Table of Contents #}
-            {# This is Philipp Oppermann's workaround: https://github.com/getzola/zola/issues/584#issuecomment-474329637 #}
-            {{ page.content | replace(from="<!-- toc -->", to=macros_toc::toc(page=page, header=false, language_strings=language_strings)) | safe }}
+            {#- Replace series_intro placeholder -#}
+            {%- set content_with_intro = page.content -%}
+            {%- if "<!-- series_intro -->" in page.content -%}
+                {%- set series_intro_html = macros_series_page::get_introduction(page=page, series_section=series_section, series_ordered_pages=series_ordered_pages, language_strings=language_strings) -%}
+                {%- set content_with_intro = content_with_intro | replace(from="<!-- series_intro -->", to=series_intro_html) -%}
+            {%- elif series_section -%}
+                {%- set series_intro_html = macros_series_page::get_introduction(page=page, series_section=series_section, series_ordered_pages=series_ordered_pages, language_strings=language_strings) -%}
+                {%- set content_with_intro = series_intro_html ~ content_with_intro -%}
+            {%- endif -%}
+
+            {#- Handle series_outro placeholder -#}
+            {%- set processed_content = content_with_intro -%}
+            {%- if "<!-- series_outro -->" in content_with_intro -%}
+                {%- set series_outro_html = macros_series_page::get_outro(page=page, series_section=series_section, series_ordered_pages=series_ordered_pages, language_strings=language_strings) -%}
+                {%- set processed_content = processed_content | replace(from="<!-- series_outro -->", to=series_outro_html) -%}
+            {%- elif series_section -%}
+                {%- set series_outro_html = macros_series_page::get_outro(page=page, series_section=series_section, series_ordered_pages=series_ordered_pages, language_strings=language_strings) -%}
+                {#- We want the outro at the end of the article, but before footnote definitions -#}
+                {%- set footnotes_marker = '<hr><ol class="footnotes-list">' -%}
+                {%- if footnotes_marker in content_with_intro -%}
+                    {%- set content_sections = processed_content | split(pat=footnotes_marker) -%}
+                    {%- set main_content = content_sections | first -%}
+                    {%- set footnotes_content = content_sections | slice(start=1) | join(sep=footnotes_marker) -%}
+                    {%- set processed_content = main_content ~ series_outro_html ~ footnotes_marker ~ footnotes_content -%}
+                {%- else -%}
+                    {%- set processed_content = processed_content ~ series_outro_html -%}
+                {%- endif -%}
+            {%- endif -%}
+
+            {#- Replace TOC and render final content -#}
+            {#- The replace pattern is used to enable arbitrary locations for the Table of Contents -#}
+            {#- This is Philipp Oppermann's workaround: https://github.com/getzola/zola/issues/584#issuecomment-474329637 -#}
+            {{ processed_content | replace(from="<!-- toc -->", to=macros_toc::toc(page=page, header=false, language_strings=language_strings)) | safe }}
         </section>
 
-        {# Check if comments are enabled, checking that they are not disabled on the specific page #}
+        {#- Check if comments are enabled, checking that they are not disabled on the specific page -#}
         {% set systems = ["giscus", "utterances", "hyvortalk", "isso"] %}
         {% set enabled_systems = 0 %}
         {% set comment_system = "" %}
@@ -219,11 +276,6 @@ <h1 class="article-title">
             {% endif %}
         {% endfor %}
 
-        {# Ensure only one comment system is enabled #}
-        {% if enabled_systems > 1 %}
-            {{ throw(message="ERROR: Multiple comment systems have been enabled for the same page. Check your config.toml and individual page settings to ensure only one comment system is activated at a time.") }}
-        {% endif %}
-
         {% if macros_settings::evaluate_setting_priority(setting="show_previous_next_article_links", page=page, default_global_value=true) == "true" %}
             {%- if page.lower or page.higher -%}
                 {% set next_label = macros_translate::translate(key="next", default="Next", language_strings=language_strings) %}
@@ -271,7 +323,11 @@ <h1 class="article-title">
             {%- endif -%}
         {%- endif -%}
 
-        {# Comments #}
+        {#- Comments -#}
+        {#- Ensure only one comment system is enabled -#}
+        {% if enabled_systems > 1 %}
+            {{ throw(message="ERROR: Multiple comment systems have been enabled for the same page. Check your config.toml and individual page settings to ensure only one comment system is activated at a time.") }}
+        {% endif %}
         {% if comment_system %}
             {% include "partials/comments.html" %}
         {% endif %}
diff --git a/templates/partials/extra_features.html b/templates/partials/extra_features.html
index ad0080da4..46a10dffd 100644
--- a/templates/partials/extra_features.html
+++ b/templates/partials/extra_features.html
@@ -1,8 +1,9 @@
+{%- set page_or_section = page | default(value=section) -%}
 {# Quick navigation buttons #}
-{% if macros_settings::evaluate_setting_priority(setting="quick_navigation_buttons", page=page, default_global_value=false) == "true" %}
+{% if macros_settings::evaluate_setting_priority(setting="quick_navigation_buttons", page=page_or_section, default_global_value=false) == "true" %}
     <div id="button-container">
         {# Button to go show a floating Table of Contents #}
-        {% if page.toc %}
+        {% if page_or_section.toc %}
             <div id="toc-floating-container">
                 <input type="checkbox" id="toc-toggle" class="toggle"/>
                 <label for="toc-toggle" class="overlay"></label>
@@ -10,7 +11,7 @@
                     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="M414.82-193.094q-18.044 0-30.497-12.32-12.453-12.319-12.453-30.036t12.453-30.086q12.453-12.37 30.497-12.37h392.767q17.237 0 29.927 12.487 12.69 12.486 12.69 30.203 0 17.716-12.69 29.919t-29.927 12.203H414.82Zm0-244.833q-18.044 0-30.497-12.487Q371.87-462.9 371.87-480.45t12.453-29.92q12.453-12.369 30.497-12.369h392.767q17.237 0 29.927 12.511 12.69 12.512 12.69 29.845 0 17.716-12.69 30.086-12.69 12.37-29.927 12.37H414.82Zm0-245.167q-18.044 0-30.497-12.32t-12.453-30.037q0-17.716 12.453-30.086 12.453-12.369 30.497-12.369h392.767q17.237 0 29.927 12.486 12.69 12.487 12.69 30.203 0 17.717-12.69 29.92-12.69 12.203-29.927 12.203H414.82ZM189.379-156.681q-32.652 0-55.878-22.829t-23.226-55.731q0-32.549 23.15-55.647 23.151-23.097 55.95-23.097 32.799 0 55.313 23.484 22.515 23.484 22.515 56.246 0 32.212-22.861 54.893-22.861 22.681-54.963 22.681Zm0-245.167q-32.652 0-55.878-23.134-23.226-23.135-23.226-55.623 0-32.487 23.467-55.517t56.12-23.03q32.102 0 54.721 23.288 22.62 23.288 22.62 55.775 0 32.488-22.861 55.364-22.861 22.877-54.963 22.877Zm-.82-244.833q-32.224 0-55.254-23.288-23.03-23.289-23.03-55.623 0-32.333 23.271-55.364 23.272-23.03 55.495-23.03 32.224 0 55.193 23.288 22.969 23.289 22.969 55.622 0 32.334-23.21 55.364-23.21 23.031-55.434 23.031Z"/></svg>
                 </label>
                 <div class="toc-content">
-                    {{ macros_toc::toc(page=page, header=false, language_strings=language_strings) }}
+                    {{ macros_toc::toc(page=page_or_section, header=false, language_strings=language_strings) }}
                 </div>
             </div>
         {% endif %}
@@ -30,13 +31,13 @@
 {% endif %}
 
 {# Add KaTeX functionality #}
-{%- if macros_settings::evaluate_setting_priority(setting="katex", page=page, default_global_value=false) == "true" -%}
+{%- if macros_settings::evaluate_setting_priority(setting="katex", page=page_or_section, default_global_value=false) == "true" -%}
     <link rel="stylesheet" href="{{ get_url(path='katex.min.css', trailing_slash=false) | safe }}">
     <script defer src="{{ get_url(path='js/katex.min.js', trailing_slash=false) | safe }}"></script>
 {%- endif -%}
 
 {# Load mermaid.js #}
-{%- if macros_settings::evaluate_setting_priority(setting="mermaid", page=page, default_global_value=false) == "true" -%}
+{%- if macros_settings::evaluate_setting_priority(setting="mermaid", page=page_or_section, default_global_value=false) == "true" -%}
     {%- if config.extra.serve_local_mermaid | default(value=true) -%}
         <script defer src="{{ get_url(path='js/mermaid.min.js', trailing_slash=false) | safe }}"></script>
     {%- else -%}
@@ -45,7 +46,7 @@
 {%- endif -%}
 
 {# Add copy button to code blocks #}
-{%- if macros_settings::evaluate_setting_priority(setting="copy_button", page=page, default_global_value=true) == "true" -%}
+{%- if macros_settings::evaluate_setting_priority(setting="copy_button", page=page_or_section, default_global_value=true) == "true" -%}
     {#- Add hidden HTML elements with the translated strings for the button's interactions -#}
     <span id="copy-success" class="hidden">
         {{ macros_translate::translate(key="copied", default="Copied!", language_strings=language_strings) }}
@@ -57,11 +58,11 @@
 {%- endif -%}
 
 {# JavaScript to use the "Show source or path" on code blocks shortcode: https://welpo.github.io/tabi/blog/shortcodes/#show-source-or-path #}
-{%- if macros_settings::evaluate_setting_priority(setting="add_src_to_code_block", page=page, default_global_value=false) == "true" -%}
+{%- if macros_settings::evaluate_setting_priority(setting="add_src_to_code_block", page=page_or_section, default_global_value=false) == "true" -%}
     <script defer src="{{ get_url(path='js/addSrcToCodeBlock.min.js', trailing_slash=false) | safe }}"></script>
 {%- endif -%}
 
 {# Add backlinks to footnotes #}
-{%- if macros_settings::evaluate_setting_priority(setting="footnote_backlinks", page=page, default_global_value=false) == "true" -%}
+{%- if macros_settings::evaluate_setting_priority(setting="footnote_backlinks", page=page_or_section, default_global_value=false) == "true" -%}
     <script defer src="{{ get_url(path='js/footnoteBacklinks.min.js', trailing_slash=false | safe )}}"></script>
 {%- endif -%}
diff --git a/templates/partials/paginate.html b/templates/partials/paginate.html
index 1833dca5b..6502af77e 100644
--- a/templates/partials/paginate.html
+++ b/templates/partials/paginate.html
@@ -1,4 +1,4 @@
-{% if paginator %}
+{% if paginator and paginator.number_pagers > 1 %}
     <ul class="pagination">
         {% if paginator.previous %}
             <li class="page-item page-prev">
diff --git a/templates/series.html b/templates/series.html
new file mode 100644
index 000000000..5a4cc0c3c
--- /dev/null
+++ b/templates/series.html
@@ -0,0 +1,62 @@
+{% extends "base.html" %}
+
+{% block main_content %}
+
+{# Throw an error if the section is not flagged as a series. #}
+{# This page would be displayed properly but it would become impossible for the series' child pages to reference their series. #}
+{%- if "series" not in section.extra or not section.extra.series -%}
+    {{ throw(message="Section is not flagged as a series. Set `section.extra.series` to `true` if you want to use `series.html` template.") }}
+{%- endif -%}
+
+<main>
+{%- if section.extra.header %}
+    {%- include "partials/home_banner.html" -%}
+{% endif -%}
+
+    {%- set show_jump = false -%}
+    {%- set show_jump_hierarchy = macros_settings::evaluate_setting_priority(setting="show_jump_to_posts", page=section) -%}
+    {%- if show_jump_hierarchy == "true" -%}
+        {%- set show_jump = true -%}
+    {%- elif show_jump_hierarchy != "false" -%}
+        {#- Default to true if the content is long and var is unset #}
+        {%- if section.content | length > 2000 -%}
+            {%- set show_jump = true -%}
+        {%- endif -%}
+    {%- endif -%}
+
+    {%- if show_jump -%}
+    <div class="title-with-jump bottom-divider">
+        <h1 class="title-container section-title">{{ section.title }}</h1>
+        <a href="#posts-list" class="jump-link">{{ macros_translate::translate(key="jump_to_posts", default="Jump to posts", language_strings=language_strings) }} ↓</a>
+    </div>
+    {%- else -%}
+        {{ macros_page_header::page_header(title=section.title) }}
+    {%- endif -%}
+
+    <section class="body">
+        {{ section.content | safe }}
+    </section>
+
+    <div id="posts-list">
+        <h2 class="bottom-divider">
+            {{ macros_translate::translate(key="all_posts", default="All posts", language_strings=language_strings) }}
+        </h2>
+        {%- if paginator %}
+            {%- set pages = paginator.pages -%}
+        {% else %}
+            {%- set pages = section.pages -%}
+        {% endif -%}
+
+        {% set max_posts = section.extra.max_posts | default(value=999999) %}
+        {{ macros_list_posts::list_posts(posts=pages, max=max_posts, metadata="indexes", language_strings=language_strings, section_path=section.path, paginator=paginator | default(value="")) }}
+    </div>
+
+    {% if paginator %}
+        {%- include "partials/paginate.html" -%}
+    {% endif %}
+
+</main>
+
+{%- include "partials/extra_features.html" -%}
+
+{% endblock main_content %}
diff --git a/theme.toml b/theme.toml
index 3a8bca202..9affeb083 100644
--- a/theme.toml
+++ b/theme.toml
@@ -114,6 +114,16 @@ show_date = true
 # "both" - Show both the original date and the last updated date.
 post_listing_date = "date"
 
+# Show "Jump to posts" link next to series' title.
+# By default, the link appears automatically when a series description exceeds 2000 characters.
+# Can be set at page or section levels, following the hierarchy: page > section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
+# show_jump_to_posts = true
+
+# Determines if indexes should be increasing (false) or decreasing (true) in series' posts list.
+# It has only effect if the section uses indexes metadata (which is only the case for series as of now).
+# Can be set at section levels, following the hierarchy: section > config. See: https://welpo.github.io/tabi/blog/mastering-tabi-settings/#settings-hierarchy
+post_listing_index_reversed = false  # Defaults to false.
+
 # DEPRECATED!
 # Use Zola's built-in `bottom_footnotes = true` in the [markdown] section instead. (Available since v0.19.0)
 # Adds backlinks to footnotes (loads ~500 bytes of JavaScripts).