diff --git a/_ext/__init__.py b/_ext/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/_ext/rss.py b/_ext/rss.py
new file mode 100644
index 000000000..153bec939
--- /dev/null
+++ b/_ext/rss.py
@@ -0,0 +1,101 @@
+"""
+Create an RSS feed of tutorials
+
+Cribbed from: https://github.com/python/peps/blob/main/pep_sphinx_extensions/generate_rss.py
+"""
+
+from dataclasses import dataclass, asdict
+from datetime import datetime, UTC
+from email.utils import format_datetime
+from html import escape
+from pprint import pformat
+from typing import TYPE_CHECKING
+from urllib.parse import urljoin
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+
+
+def _format_rfc_2822(dt: datetime) -> str:
+ datetime = dt.replace(tzinfo=UTC)
+ return format_datetime(datetime, usegmt=True)
+
+
+@dataclass
+class RSSItem:
+ title: str
+ date: datetime
+ description: str
+ url: str
+ author: str = "pyOpenSci"
+
+ @classmethod
+ def from_meta(cls, page_name: str, meta: dict, app: "Sphinx") -> "RSSItem":
+ """Create from a page's metadata"""
+ url = urljoin(app.config.html_baseurl, app.builder.get_target_uri(page_name))
+ # purposely don't use `get` here because we want to error if these fields are absent
+ return RSSItem(
+ title=meta[":og:title"],
+ description=meta[":og:description"],
+ date=datetime.fromisoformat(meta["date"]),
+ author=meta.get(":og:author", "pyOpenSci"),
+ url=url,
+ )
+
+ def render(self) -> str:
+ return f"""\
+
+ {escape(self.title, quote=False)}
+ {escape(self.url, quote=False)}
+ {escape(self.description, quote=False)}
+ {escape(self.author, quote=False)}
+ {self.url}
+ {_format_rfc_2822(self.date)}
+"""
+
+
+@dataclass
+class RSSFeed:
+ items: list[RSSItem]
+ last_build_date: datetime = datetime.now()
+ title: str = "pyOpenSci Tutorials"
+ link: str = "https://www.pyopensci.org/python-package-guide/tutorials/intro.html"
+ self_link: str = "https://www.pyopensci.org/python-package-guide/tutorials.rss"
+ description: str = "Tutorials for learning python i guess!!!"
+ language: str = "en"
+
+ def render(self) -> str:
+ items = sorted(self.items, key=lambda i: i.date, reverse=True)
+ items = "\n".join([item.render() for item in items])
+ return f"""\
+
+
+
+ {self.title}
+ {self.link}
+
+ {self.description}
+ {self.language}
+ {_format_rfc_2822(self.last_build_date)}
+{items}
+
+
+ """
+
+
+def generate_tutorials_feed(app: "Sphinx"):
+ from sphinx.util import logging
+
+ logger = logging.getLogger("_ext.rss")
+ logger.info("Generating RSS feed for tutorials")
+ metadata = app.builder.env.metadata
+ tutorials = [t for t in metadata if t.startswith("tutorials/")]
+ feed_items = [RSSItem.from_meta(t, metadata[t], app) for t in tutorials]
+ feed = RSSFeed(items=feed_items)
+ with open(app.outdir / "tutorials.rss", "w") as f:
+ f.write(feed.render())
+
+ logger.info(
+ f"Generated RSS feed for tutorials, wrote to {app.outdir / 'tutorials.rss'}"
+ )
+ logger.debug(f"feed items: \n{pformat([asdict(item) for item in feed_items])}")
diff --git a/conf.py b/conf.py
index f7099152e..de9920210 100644
--- a/conf.py
+++ b/conf.py
@@ -10,12 +10,17 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
+import os
+import sys
+sys.path.insert(0, os.path.abspath('.'))
from datetime import datetime
import subprocess
import os
+from typing import TYPE_CHECKING
+from _ext import rss
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
current_year = datetime.now().year
organization_name = "pyOpenSci"
@@ -199,3 +204,14 @@
bibtex_bibfiles = ["bibliography.bib"]
# myst complains about bibtex footnotes because of render order
suppress_warnings = ["myst.footnote"]
+
+
+def _post_build(app: "Sphinx", exception: Exception | None) -> None:
+ rss.generate_tutorials_feed(app)
+
+
+def setup(app: "Sphinx"):
+ app.connect("build-finished", _post_build)
+
+ # Parallel safety: https://www.sphinx-doc.org/en/master/extdev/index.html#extension-metadata
+ return {"parallel_read_safe": True, "parallel_write_safe": True}
diff --git a/tutorials/add-license-coc.md b/tutorials/add-license-coc.md
index a0d5c5020..6cabd8d97 100644
--- a/tutorials/add-license-coc.md
+++ b/tutorials/add-license-coc.md
@@ -1,3 +1,9 @@
+---
+:og:description: Learn how to add a LICENSE and CODE_OF_CONDUCT file to your Python package. This lesson covers choosing a permissive license, placing key files for visibility on GitHub and PyPI, and adopting the Contributor Covenant to support an inclusive community.
+:og:title: Add a License and Code of Conduct to your python package
+date: 1970-01-02
+---
+
# Add a `LICENSE` & `CODE_OF_CONDUCT` to your Python package
In the [previous lesson](add-readme) you:
diff --git a/tutorials/add-readme.md b/tutorials/add-readme.md
index 2d7ef5c6e..56bd342c6 100644
--- a/tutorials/add-readme.md
+++ b/tutorials/add-readme.md
@@ -1,3 +1,9 @@
+---
+:og:description: Learn how to create a clear, effective README file for your Python package. This lesson covers what to include, why each section matters, and how a well-structured README improves usability and discoverability on GitHub and PyPI.
+:og:title: Add a README file to your Python package
+date: 1970-01-03
+---
+
# Add a README file to your Python package
In the previous lessons you learned:
diff --git a/tutorials/command-line-reference.md b/tutorials/command-line-reference.md
index 2d7788fbb..a77e9488d 100644
--- a/tutorials/command-line-reference.md
+++ b/tutorials/command-line-reference.md
@@ -1,3 +1,9 @@
+---
+:og:description: Learn how to add a command-line interface (CLI) to your Python package using the argparse library. This lesson walks you through creating a CLI entry point so users can run your package directly from the terminal.
+:og:title: Command Line Reference Guide
+date: 1970-01-04
+---
+
# Command Line Reference Guide
```{important}
diff --git a/tutorials/get-to-know-hatch.md b/tutorials/get-to-know-hatch.md
index 55f4d693f..e67edc8f3 100644
--- a/tutorials/get-to-know-hatch.md
+++ b/tutorials/get-to-know-hatch.md
@@ -1,3 +1,9 @@
+---
+:og:description: Get started with Hatch, a modern Python packaging tool. This lesson introduces Hatch’s features and shows how it simplifies environment management, project scaffolding, and building your package.
+:og:title: Get to Know Hatch
+date: 1970-01-05
+---
+
# Get to Know Hatch
Our Python packaging tutorials use the tool
diff --git a/tutorials/installable-code.md b/tutorials/installable-code.md
index 8aabdd35e..30ce585ef 100644
--- a/tutorials/installable-code.md
+++ b/tutorials/installable-code.md
@@ -1,6 +1,7 @@
---
-:og:description: Learn how to make your Python code installable so you can use it across projects.
+:og:description: Learn how to make your code installable as a Python package using Hatch. This lesson walks you through structuring your code and configuring pyproject.toml so others can easily install and use your package.
:og:title: Make your Python code installable so it can be used across projects
+date: 1970-01-01
---
# Make your Python code installable
diff --git a/tutorials/intro.md b/tutorials/intro.md
index ddc151538..1feb7f0c7 100644
--- a/tutorials/intro.md
+++ b/tutorials/intro.md
@@ -1,3 +1,9 @@
+---
+:og:description: This page outlines the key steps to create, document, and share a high-quality scientific Python package. Here you will also get an overview of the pyOpenSci packaging guide and what you’ll learn.
+:og:title: Python packaging 101
+date: 1970-01-05
+---
+
(packaging-101)=
# Python packaging 101
diff --git a/tutorials/publish-conda-forge.md b/tutorials/publish-conda-forge.md
index a41780269..bda10f9ac 100644
--- a/tutorials/publish-conda-forge.md
+++ b/tutorials/publish-conda-forge.md
@@ -1,3 +1,9 @@
+---
+:og:description: Learn how to publish your Python package on conda-forge to make it easily installable with conda. This lesson covers the submission process, metadata requirements, and maintaining your feedstock.
+:og:title: Publish your Python package that is on PyPI to conda-forge
+date: 1970-01-06
+---
+
# Publish your Python package that is on PyPI to conda-forge
In the previous lessons, you've learned:
diff --git a/tutorials/publish-pypi.md b/tutorials/publish-pypi.md
index b19938343..7c6e8b281 100644
--- a/tutorials/publish-pypi.md
+++ b/tutorials/publish-pypi.md
@@ -1,3 +1,9 @@
+---
+:og:description: Learn how to publish your Python package on PyPI so others can install it using pip. This lesson covers building your package, creating a PyPI account, and uploading your distribution files.
+:og:title: Publish your Python package to PyPI
+date: 1970-01-07
+---
+
# Publish your Python package to PyPI
:::{todo}
diff --git a/tutorials/pyproject-toml.md b/tutorials/pyproject-toml.md
index 56728764b..1c0b75dde 100644
--- a/tutorials/pyproject-toml.md
+++ b/tutorials/pyproject-toml.md
@@ -1,3 +1,9 @@
+---
+:og:description: The pyproject.toml file is the central configuration file for building and packaging Python projects. This lesson explains key sections like name, version, dependencies, and how they support packaging and distribution. You’ll learn how to set up this file to ensure your package is ready for publishing.
+:og:title: Make your Python package PyPI ready - pyproject.toml
+date: 1970-01-08
+---
+
# Make your Python package PyPI ready - pyproject.toml
In [the installable code lesson](installable-code), you learned how to add the bare minimum information to a `pyproject.toml` file to make it installable. You then learned how to [publish a bare minimum version of your package to PyPI](publish-pypi.md).
diff --git a/tutorials/setup-py-to-pyproject-toml.md b/tutorials/setup-py-to-pyproject-toml.md
index c97a06490..534d42c01 100644
--- a/tutorials/setup-py-to-pyproject-toml.md
+++ b/tutorials/setup-py-to-pyproject-toml.md
@@ -1,3 +1,9 @@
+---
+:og:description: If you’re creating a pure Python project, pyproject.toml is preferred over setup.py for packaging and configuration. Learn how to migrate from the older setup.py format to the modern pyproject.toml file. This lesson walks you through updating your package metadata and build settings to align with current Python packaging standards.
+:og:title: Using Hatch to Migrate setup.py to a pyproject.toml
+date: 1970-01-09
+---
+
# Using Hatch to Migrate setup.py to a pyproject.toml
Hatch can be particularly useful to generate your project's `pyproject.toml` if your project already has a `setup.py`.