From 5e2e709496d363bca81bb33cd47575ff0752f771 Mon Sep 17 00:00:00 2001 From: randogoth Date: Sun, 11 Oct 2026 21:07:07 +0300 Subject: [PATCH 1/2] feat: let a site choose which suffix the plugin treats as Mews --- pelican-mews/README.md | 1 + pelican-mews/pelican/plugins/mews/mews.py | 16 ++++++++-- pelican-mews/tests/test_mews.py | 38 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/pelican-mews/README.md b/pelican-mews/README.md index 5c11a88..0088b0e 100644 --- a/pelican-mews/README.md +++ b/pelican-mews/README.md @@ -52,6 +52,7 @@ Three optional settings: | `MEWS_STRICT` | `False` | Fail the build on content outside the subset instead of stripping it. Removals are always logged either way. | | `MEWS_CSS_DIR` | `"css"` | Directory below the output root where `mews-0.1.css` is copied. Themes see the resulting path as the `MEWS_CSS` global. | | `MEWS_OUTPUT_PATH` | unset | Put the finished pages in this folder instead of `OUTPUT_PATH`. | +| `MEWS_SUFFIX` | `".xhtml"` | Which written files are Mews pages. Set it to `".html"` on a site that is Mews throughout, and save your pages under that suffix too: servers send `.xhtml` as `application/xhtml+xml`, which makes a browser parse the page as XML and fail outright on a small markup error, while section 7.1 asks for `text/html` so the page still shows. `index.html` is also the directory index everywhere, which `index.xhtml` is not. Note that `.html` puts every page Pelican writes through the plugin, including the index, tag and archive pages it falls back to when the theme has no template for them, so give those a Mews template or switch them off with `DIRECT_TEMPLATES` and the matching `*_SAVE_AS` settings. | ### Rendering beside an existing blog diff --git a/pelican-mews/pelican/plugins/mews/mews.py b/pelican-mews/pelican/plugins/mews/mews.py index babe147..983be2c 100644 --- a/pelican-mews/pelican/plugins/mews/mews.py +++ b/pelican-mews/pelican/plugins/mews/mews.py @@ -47,7 +47,13 @@ LOGGER = logging.getLogger(__name__) CSS_NAME = "mews-0.1.css" DEFAULT_CSS_DIR = "css" -XHTML_SUFFIX = ".xhtml" +# Which files Pelican writes are Mews pages. The default keeps the plugin off +# everything else, so it can run on a site that also produces ordinary HTML. +# A site that is Mews throughout wants ".html" instead: servers send .xhtml as +# application/xhtml+xml, which makes a browser parse as XML and fail hard on a +# small markup error, and section 7.1 asks for text/html precisely to avoid +# that. Set MEWS_SUFFIX to choose. +DEFAULT_SUFFIX = ".xhtml" ALLOWLIST = parse_allowlist( files(__package__).joinpath("dtd", "mews.dtd").read_text(encoding="utf-8") @@ -67,6 +73,12 @@ def _on_initialized(pelican) -> None: overrides.append(templates) +def _suffix() -> str: + """Return the file extension a Mews page is written under, with its dot.""" + suffix = str(_settings.get("MEWS_SUFFIX", DEFAULT_SUFFIX)).strip() + return suffix if suffix.startswith(".") else f".{suffix}" + + def _on_generator_init(generator) -> None: css_dir = generator.settings.get("MEWS_CSS_DIR", DEFAULT_CSS_DIR) generator.env.globals["MEWS_CSS"] = f"{css_dir}/{CSS_NAME}" @@ -74,7 +86,7 @@ def _on_generator_init(generator) -> None: def _on_content_written(path, **_) -> None: file = Path(path) - if file.suffix != XHTML_SUFFIX: + if file.suffix != _suffix(): return try: tree, doctype = parse_page(file.read_bytes()) diff --git a/pelican-mews/tests/test_mews.py b/pelican-mews/tests/test_mews.py index 7267aa0..3b22e83 100644 --- a/pelican-mews/tests/test_mews.py +++ b/pelican-mews/tests/test_mews.py @@ -28,6 +28,44 @@ def test_written_page_matches_expected(tmp_path, case): assert normalize(page) == normalize(expected) +def test_a_site_can_choose_its_own_suffix(tmp_path): + """A site that is Mews throughout writes .html, so servers send text/html.""" + output = build_case( + "basic", + tmp_path, + MEWS_SUFFIX=".html", + PAGE_SAVE_AS="{slug}.html", + PAGE_URL="{slug}.html", + # With .html the plugin sees every page Pelican writes, including the + # index, tags and archives it falls back to when the theme has no + # template for them. Those fallbacks are HTML, not XML, so a site + # using .html has to give every direct template a Mews template or + # switch the lot off, as here. + DIRECT_TEMPLATES=[], + ) + page = (output / "page.html").read_bytes() + assert normalize(page) == normalize( + (FIXTURES / "basic" / "expected.xhtml").read_bytes() + ) + + +def test_the_default_suffix_leaves_other_html_alone(tmp_path): + """Left at its default the plugin must not touch a plain HTML page.""" + output = build_case( + "strip", + tmp_path, + PAGE_SAVE_AS="{slug}.html", + PAGE_URL="{slug}.html", + INDEX_SAVE_AS="", + ) + # The strip fixture carries a , a class and a comment, all three of + # which the plugin removes. Untouched, every one of them survives. + written = (output / "page.html").read_bytes() + assert b" Date: Sun, 11 Oct 2026 21:16:45 +0300 Subject: [PATCH 2/2] feat: scaffold plain .html pages that need no server config --- mews/quickstart.py | 33 ++++++++++++++++++--------------- tests/test_quickstart.py | 20 +++++++++++++------- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/mews/quickstart.py b/mews/quickstart.py index 504ade4..0b1f488 100644 --- a/mews/quickstart.py +++ b/mews/quickstart.py @@ -41,13 +41,16 @@ DEFAULT_LANG = "en" THEME = "theme" # The plugin reads the allowed elements and attributes from the Mews DTD and -# brings every page inside that subset. It only touches files ending .xhtml, -# so everything has to be written out under that suffix. +# brings every page inside that subset. PLUGINS = ["pelican.plugins.mews"] -ARTICLE_URL = ARTICLE_SAVE_AS = "{{slug}}.xhtml" -PAGE_URL = PAGE_SAVE_AS = "{{slug}}.xhtml" -INDEX_SAVE_AS = "index.xhtml" +# This whole site is Mews, so its pages are plain .html: a server sends those +# as text/html, which section 7.1 asks for so a small markup error still shows +# as a page, and index.html is the directory index everywhere. The plugin +# defaults to .xhtml instead, for sites that are only partly Mews. +MEWS_SUFFIX = ".html" +ARTICLE_URL = ARTICLE_SAVE_AS = "{{slug}}.html" +PAGE_URL = PAGE_SAVE_AS = "{{slug}}.html" # Markdown has to produce XML for the plugin to parse. Tables and definition # lists are both in the Mews subset but need their extensions turned on. @@ -59,7 +62,11 @@ MARKDOWN = {{ }}, }} -# Mews pages carry no author styling, so there is nothing for these to show. +# Every page written under MEWS_SUFFIX goes through the plugin, and Pelican +# falls back to its own HTML templates for these when the theme has none. They +# have nothing to show on a site like this, so they stay off; give them +# templates in theme/templates/ if you want them. +DIRECT_TEMPLATES = ["index"] AUTHOR_SAVE_AS = CATEGORY_SAVE_AS = TAG_SAVE_AS = "" AUTHORS_SAVE_AS = CATEGORIES_SAVE_AS = TAGS_SAVE_AS = ARCHIVES_SAVE_AS = "" # Pelican writes several feeds by default and they are all broken until @@ -115,7 +122,7 @@ ARTICLE = """\ {% block content %}

{{ article.title }}

{{ article.content }} -

All posts

+

All posts

{% endblock %} """ @@ -125,7 +132,7 @@ PAGE = """\ {% block content %}

{{ page.title }}

{{ page.content }} -

All posts

+

All posts

{% endblock %} """ @@ -157,7 +164,8 @@ A Mews site. The pages follow [Mews Profile 0.1](https://mews.page/spec/0.1). uv run pelican content ``` -The finished pages land in `output/`. To work on it with a reload on save: +The finished pages land in `output/`, as plain `.html` that any web server +serves correctly with no configuration. To work on it with a reload on save: ```shell uv run pelican --autoreload --listen content @@ -165,12 +173,7 @@ uv run pelican --autoreload --listen content ## Publish it -Copy `output/` to any web server. Two things are worth setting: - -- Serve `.xhtml` files as `text/html`. Section 7.1 of the spec asks for that, - so a browser still shows a page carrying a small markup error. -- Make `index.xhtml` the directory index, since the plugin only processes - files under that suffix. In Caddy: `file_server {{ index index.xhtml }}`. +Copy `output/` to any web server. Nothing to configure. ## Subscribe to it diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index bebd20b..25c2446 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -35,9 +35,15 @@ def test_the_settings_are_valid_python(tmp_path): ) assert settings["SITENAME"] == "Signal Bars" assert settings["PLUGINS"] == ["pelican.plugins.mews"] - # The plugin only touches this suffix, so everything has to be saved under it. - assert settings["ARTICLE_SAVE_AS"].endswith(".xhtml") - assert settings["INDEX_SAVE_AS"].endswith(".xhtml") + # A site that is Mews throughout writes plain .html: servers send that as + # text/html (section 7.1) and index.html is the directory index everywhere. + # The suffix the plugin acts on has to agree with where pages are saved. + assert settings["MEWS_SUFFIX"] == ".html" + assert settings["ARTICLE_SAVE_AS"].endswith(".html") + assert settings["PAGE_SAVE_AS"].endswith(".html") + # Pelican falls back to its own HTML for any direct template the theme + # lacks, and with .html the plugin would choke on it. + assert settings["DIRECT_TEMPLATES"] == ["index"] assert settings["MARKDOWN"]["output_format"] == "xhtml" @@ -62,11 +68,11 @@ def test_the_scaffolded_site_builds_conforming_pages(tmp_path): check=False, ) assert built.returncode == 0, built.stderr - pages = sorted((site / "output").glob("*.xhtml")) - assert [p.name for p in pages] == ["first-post.xhtml", "index.xhtml"] + pages = sorted((site / "output").glob("*.html")) + assert [p.name for p in pages] == ["first-post.html", "index.html"] for page in pages: report = validate_file(str(page)) assert report.conforms, "\n".join(str(f) for f in report.failures) # Section 6.1: the index has to be subscribable. - index = (site / "output" / "index.xhtml").read_text() - assert '
  • ' in index + index = (site / "output" / "index.html").read_text() + assert '
  • ' in index