From edd1f37cd4968f4da5e4098e5dda3007abb47c6b Mon Sep 17 00:00:00 2001 From: Justin Mayer Date: Sun, 11 Oct 2026 19:01:27 +0200 Subject: [PATCH] fix: Always assign DEFAULT_CATEGORY PR #3589 tied category assignment to `CATEGORY_SAVE_AS`, so disabling category pages also removed article categories and broke `{category}` in URL patterns (#3596). PR #3597 restored folder-based categories, but articles outside a subfolder still lost `DEFAULT_CATEGORY`. This change assigns it again, so `CATEGORY_SAVE_AS` only controls whether category pages are generated. Setting `DEFAULT_CATEGORY = None` is now the documented way to have no categories. This also stops the Simple and NotMyIdea themes from linking to category pages that don't exist. --- docs/settings.rst | 6 +++ pelican/readers.py | 6 +-- pelican/tests/test_generators.py | 2 +- pelican/tests/test_readers.py | 46 +++++++++++++++---- pelican/tests/test_theme.py | 45 +++++++++++++++++- .../notmyidea/templates/article_infos.html | 4 +- pelican/themes/notmyidea/templates/base.html | 2 +- pelican/themes/simple/templates/article.html | 7 ++- pelican/themes/simple/templates/base.html | 2 +- 9 files changed, 101 insertions(+), 19 deletions(-) diff --git a/docs/settings.rst b/docs/settings.rst index 3fac5f18..7b78a75f 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -65,6 +65,10 @@ Basic settings The default category to fall back on. The default is ``"misc"``. + To publish articles without any category, set ``DEFAULT_CATEGORY = None`` + and ``USE_FOLDER_AS_CATEGORY = False``. Articles will then only have a + category if one is specified in their metadata. + .. data:: DISPLAY_PAGES_ON_MENU Whether to display pages on the menu of the template. Templates may or may @@ -590,6 +594,8 @@ This would save your articles into something like .. data:: CATEGORY_SAVE_AS The location to save a category. The default is ``"category/{slug}.html"``. + Set this to ``""`` to disable category pages. Articles still keep their + categories, which can be used in URL patterns such as ``ARTICLE_SAVE_AS``. .. data:: TAG_URL diff --git a/pelican/readers.py b/pelican/readers.py index e10ab655..038485c1 100644 --- a/pelican/readers.py +++ b/pelican/readers.py @@ -82,9 +82,9 @@ def ensure_metadata_list(text): def _process_if_nonempty(processor, name, settings): """Removes extra whitespace from name and applies a metadata processor. - If name is empty or all whitespace, returns _DISCARD instead. + If name is None, empty, or all whitespace, returns _DISCARD instead. """ - name = name.strip() + name = (name or "").strip() return processor(name, settings) if name else _DISCARD @@ -746,7 +746,7 @@ def default_metadata(settings=None, process=None): if process: value = process(name, value) metadata[name] = value - if "DEFAULT_CATEGORY" in settings and settings.get("CATEGORY_SAVE_AS"): + if "DEFAULT_CATEGORY" in settings: value = settings["DEFAULT_CATEGORY"] if process: value = process("category", value) diff --git a/pelican/tests/test_generators.py b/pelican/tests/test_generators.py index a07bef7b..71b5d959 100644 --- a/pelican/tests/test_generators.py +++ b/pelican/tests/test_generators.py @@ -405,7 +405,7 @@ class TestArticlesGenerator(unittest.TestCase): settings = get_settings(PATH=content_dir) settings["CACHE_PATH"] = self.temp_cache settings["USE_FOLDER_AS_CATEGORY"] = False - settings["CATEGORY_SAVE_AS"] = None # No default category fallback + settings["DEFAULT_CATEGORY"] = None # No default category fallback context = get_context(settings) generator = ArticlesGenerator( diff --git a/pelican/tests/test_readers.py b/pelican/tests/test_readers.py index f5b311b9..6ee64b0a 100644 --- a/pelican/tests/test_readers.py +++ b/pelican/tests/test_readers.py @@ -161,16 +161,44 @@ class DefaultReaderTest(ReaderTest): ) def test_path_metadata_categories(self): + """Ensure folder names become categories even when category pages are + disabled, so {category} still works in URL patterns.""" nested_content_path = os.path.join(CUR_DIR, "nested_content") - r = readers.Readers( - settings=get_settings(USE_FOLDER_AS_CATEGORY=True, CATEGORY_SAVE_AS=None) - ) - mainpage = r.read_file(base_path=nested_content_path, path="maindir/maindir.md") - self.assertDictHasSubset(mainpage.metadata, {"category": "maindir"}) - subpage = r.read_file( - base_path=nested_content_path, path="maindir/subdir/subdir.md" - ) - self.assertDictHasSubset(subpage.metadata, {"category": "subdir"}) + for category_save_as in ("", None, False): + with self.subTest(CATEGORY_SAVE_AS=category_save_as): + r = readers.Readers( + settings=get_settings( + USE_FOLDER_AS_CATEGORY=True, CATEGORY_SAVE_AS=category_save_as + ) + ) + mainpage = r.read_file( + base_path=nested_content_path, path="maindir/maindir.md" + ) + self.assertDictHasSubset(mainpage.metadata, {"category": "maindir"}) + subpage = r.read_file( + base_path=nested_content_path, path="maindir/subdir/subdir.md" + ) + self.assertDictHasSubset(subpage.metadata, {"category": "subdir"}) + + def test_default_category_when_category_pages_disabled(self): + for category_save_as in ("", None, False): + with self.subTest(CATEGORY_SAVE_AS=category_save_as): + page = self.read_file( + path="article.rst", + DEFAULT_CATEGORY="misc", + CATEGORY_SAVE_AS=category_save_as, + ) + self.assertEqual(page.metadata["category"], "misc") + + def test_empty_default_category_omits_category(self): + for default_category in ("", None): + with self.subTest(DEFAULT_CATEGORY=default_category): + page = self.read_file( + path="article.rst", + DEFAULT_CATEGORY=default_category, + USE_FOLDER_AS_CATEGORY=False, + ) + self.assertNotIn("category", page.metadata) class RstReaderTest(ReaderTest): diff --git a/pelican/tests/test_theme.py b/pelican/tests/test_theme.py index c4bd6f14..7e0fff6a 100644 --- a/pelican/tests/test_theme.py +++ b/pelican/tests/test_theme.py @@ -134,8 +134,8 @@ class TestTemplateInheritance(LoggedTestCase): self.assertIn("New footer", content) def test_disabled_categories_no_links(self): - """When CATEGORY_SAVE_AS is empty, articles without an explicit - category should not get a default category or show category links.""" + """When DEFAULT_CATEGORY is empty, articles without an explicit + category should not show a category.""" # Create an article without a Category metadata line content_dir = mkdtemp(prefix="pelican_test_content.") @@ -155,6 +155,8 @@ class TestTemplateInheritance(LoggedTestCase): "OUTPUT_PATH": self.temp_output, "CACHE_PATH": self.temp_cache, "SITEURL": "http://example.com", + "DEFAULT_CATEGORY": "", + "USE_FOLDER_AS_CATEGORY": False, "CATEGORY_SAVE_AS": None, "CATEGORY_URL": None, "CATEGORIES_SAVE_AS": None, @@ -171,6 +173,45 @@ class TestTemplateInheritance(LoggedTestCase): finally: rmtree(content_dir) + def test_disabled_category_pages_no_links(self): + """When CATEGORY_SAVE_AS is empty, articles keep their category, but + themes should not link to category pages that are not generated.""" + + content_dir = mkdtemp(prefix="pelican_test_content.") + os.makedirs(os.path.join(content_dir, "blog")) + with open(os.path.join(content_dir, "blog", "in_blog.md"), "w") as f: + f.write("Title: In Blog\nDate: 2024-01-01\n\nArticle in a folder.\n") + + try: + for theme in ("simple", "notmyidea"): + with self.subTest(theme=theme): + settings = read_settings( + path=None, + override={ + "THEME": theme, + "PATH": content_dir, + "OUTPUT_PATH": self.temp_output, + "CACHE_PATH": self.temp_cache, + "SITEURL": "http://example.com", + "ARTICLE_SAVE_AS": "{category}/{slug}.html", + "CATEGORY_SAVE_AS": False, + "CATEGORIES_SAVE_AS": False, + }, + ) + + pelican = Pelican(settings=settings) + mute(True)(pelican.run)() + + with open( + os.path.join(self.temp_output, "blog", "in-blog.html") + ) as f: + content = f.read() + + self.assertIn("blog", content) + self.assertNotIn("/category/", content) + finally: + rmtree(content_dir) + def test_category_and_tag_feed_titles_use_slug(self): """Feed link titles on category/tag pages should have unique titles.""" diff --git a/pelican/themes/notmyidea/templates/article_infos.html b/pelican/themes/notmyidea/templates/article_infos.html index 335dcbb1..fc128df4 100644 --- a/pelican/themes/notmyidea/templates/article_infos.html +++ b/pelican/themes/notmyidea/templates/article_infos.html @@ -16,7 +16,9 @@ {% endfor %} {% endif %} -

In {{ article.category }}.

+ {% if article.category %} +

In {% if CATEGORY_SAVE_AS %}{{ article.category }}{% else %}{{ article.category }}{% endif %}.

+ {% endif %} {% include 'taglist.html' %} {% import 'translations.html' as translations with context %} {{ translations.translations_for(article) }} diff --git a/pelican/themes/notmyidea/templates/base.html b/pelican/themes/notmyidea/templates/base.html index e5fabb09..6d91e832 100644 --- a/pelican/themes/notmyidea/templates/base.html +++ b/pelican/themes/notmyidea/templates/base.html @@ -30,7 +30,7 @@ {{ pg.title }} {% endfor %} {% endif %} - {% if DISPLAY_CATEGORIES_ON_MENU -%} + {% if DISPLAY_CATEGORIES_ON_MENU and CATEGORY_SAVE_AS -%} {% for cat, null in categories %} {{ cat }} {% endfor %} diff --git a/pelican/themes/simple/templates/article.html b/pelican/themes/simple/templates/article.html index a01de100..95082139 100644 --- a/pelican/themes/simple/templates/article.html +++ b/pelican/themes/simple/templates/article.html @@ -49,7 +49,12 @@ {% endif %} {% if article.category %}

- Category: {{ article.category }} + Category: + {% if CATEGORY_SAVE_AS %} + {{ article.category }} + {% else %} + {{ article.category }} + {% endif %}

{% endif %} {% if article.tags %} diff --git a/pelican/themes/simple/templates/base.html b/pelican/themes/simple/templates/base.html index d86e38f8..23f5603e 100644 --- a/pelican/themes/simple/templates/base.html +++ b/pelican/themes/simple/templates/base.html @@ -58,7 +58,7 @@
  • {{ p.title }}
  • {% endfor %} {% endif %} - {% if DISPLAY_CATEGORIES_ON_MENU %} + {% if DISPLAY_CATEGORIES_ON_MENU and CATEGORY_SAVE_AS %} {% for cat, null in categories %}
  • {{ cat}}
  • {% endfor %}