fix: Skip content whose empty slug is used in paths

A title made only of punctuation, such as "???", slugifies to an empty
string. When URL patterns use `{slug}`, the content was written to
".html", and two such articles crashed the build. Skip content with such
titles with an error that suggests setting a slug. Patterns that don't
use the slug, such as date-based `ARTICLE_SAVE_AS` patterns, are
unaffected.
This commit is contained in:
Justin Mayer 2026-10-11 21:15:31 +02:00
commit 4e8ee91ae3
2 changed files with 72 additions and 0 deletions

View file

@ -179,6 +179,29 @@ class Content:
return False
return True
def _has_valid_slug(self) -> bool:
"""Return false if the slug is empty (e.g., for a title made only of
punctuation) but used in the URL or save_as, which would produce file
names such as ".html"."""
# An empty title is already reported as a missing mandatory property.
if getattr(self, "slug", None) != "" or not getattr(self, "title", None):
return True
# Detect whether the slug is used by expanding with a placeholder.
paths = (self.url, self.save_as)
self.slug = "slug"
try:
uses_slug = (self.url, self.save_as) != paths
finally:
self.slug = ""
if not uses_slug:
return True
logger.error(
"Skipping %s: its slug is empty; set a 'slug' in its metadata", self
)
return False
def _has_valid_save_as(self) -> bool:
"""Return true if save_as doesn't write outside output path, false
otherwise."""
@ -220,6 +243,7 @@ class Content:
return all(
[
self._has_valid_mandatory_properties(),
self._has_valid_slug(),
self._has_valid_save_as(),
self._has_valid_status(),
]

View file

@ -97,6 +97,54 @@ class TestPage(TestBase):
page = Page("content", metadata={"title": "foobar"})
self.assertTrue(page._has_valid_mandatory_properties())
def test_empty_slug_invalid(self):
# A title made only of punctuation produces an empty slug, which would
# otherwise result in a file named ".html".
page = Page("content", metadata={"title": "???"})
self.assertEqual(page.slug, "")
self.assertFalse(page.is_valid())
self.assertLogCountEqual(
count=1,
msg="Skipping .*: its slug is empty; set a 'slug' in its metadata",
level=logging.ERROR,
)
# An explicit slug, or a URL and save_as that don't use the slug, avoid
# the empty file name.
for metadata in (
{"slug": "question-marks"},
{"save_as": "q/index.html", "url": "q/"},
):
with self.subTest(metadata=metadata):
page = Page("content", metadata={"title": "???", **metadata})
self.assertTrue(page.is_valid())
settings = get_settings(
ARTICLE_URL="{date:%Y}/{date:%m}/{date:%d}/",
ARTICLE_SAVE_AS="{date:%Y}/{date:%m}/{date:%d}/index.html",
)
article = Article(
"content",
metadata={"title": "???", "date": datetime.datetime(2026, 2, 9)},
settings=settings,
)
self.assertTrue(article.is_valid())
# Overriding save_as alone still leaves the URL as ".html".
page = Page("content", metadata={"title": "???", "save_as": "q.html"})
self.assertFalse(page.is_valid())
self.assertLogCountEqual(
count=2,
msg="Skipping .*: its slug is empty; set a 'slug' in its metadata",
level=logging.ERROR,
)
# An empty title is only reported as a missing title.
page = Page("content", metadata={"title": ""})
self.assertFalse(page.is_valid())
self.assertLogCountEqual(
count=2, msg="Skipping .*: its slug is empty.*", level=logging.ERROR
)
def test_summary_from_metadata(self):
# If a :summary: metadata is given, it should be used
page = Page(**self.page_kwargs)