r"""Scaffold a Pelican site that writes Mews pages. Run it straight off the repository, with no checkout and nothing installed: uvx --from git+https://code.randogoth.com/randogoth/mews.page.git \\ mews-quickstart mysite It writes a project, pins Pelican and the Mews plugin in its pyproject.toml, installs them, and leaves a first post to edit. """ import argparse from datetime import UTC, datetime from pathlib import Path import shutil import subprocess import sys REPO = "git+https://code.randogoth.com/randogoth/mews.page.git" PYPROJECT = """\ [project] name = "{slug}" version = "0.1.0" description = "A Mews site" requires-python = ">=3.10" dependencies = [ "pelican>=4.9", "markdown>=3.4", "pelican-mews @ {repo}#subdirectory=pelican-mews", ] """ PELICANCONF = """\ AUTHOR = {author!r} SITENAME = {title!r} SITEURL = "" PATH = "content" TIMEZONE = "UTC" DEFAULT_LANG = "en" THEME = "theme" # The plugin reads the allowed elements and attributes from the Mews DTD and # brings every page inside that subset. PLUGINS = ["pelican.plugins.mews"] # 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. MARKDOWN = {{ "output_format": "xhtml", "extension_configs": {{ "markdown.extensions.tables": {{}}, "markdown.extensions.def_list": {{}}, }}, }} # 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 # SITEURL is set. Section 6.2 makes an Atom feed optional and the dated links # on the index are already a subscription, so they start off. FEED_ALL_ATOM = FEED_ALL_RSS = None CATEGORY_FEED_ATOM = CATEGORY_FEED_RSS = None AUTHOR_FEED_ATOM = AUTHOR_FEED_RSS = None TAG_FEED_ATOM = TAG_FEED_RSS = None TRANSLATION_FEED_ATOM = TRANSLATION_FEED_RSS = None # Raise this to stop a build that would strip something, rather than logging # the removal and carrying on. MEWS_STRICT = False """ BASE = """\ {% block title %}{{ SITENAME }}{% endblock %} {% include "mews/head.html" %} {% block content %}{% endblock %}
{{ SITENAME }}
""" INDEX = """\ {% extends "base.html" %} {% block content %}

{{ SITENAME }}

{% if articles %} {% else %}

Nothing posted yet.

{% endif %} {% endblock %} """ ARTICLE = """\ {% extends "base.html" %} {% block title %}{{ article.title }}{% endblock %} {% block content %}

{{ article.title }}

{{ article.content }}

All posts

{% endblock %} """ PAGE = """\ {% extends "base.html" %} {% block title %}{{ page.title }}{% endblock %} {% block content %}

{{ page.title }}

{{ page.content }}

All posts

{% endblock %} """ POST = """\ Title: First post Date: {date} Slug: first-post Write here. The plugin keeps whatever fits the Mews subset and strips the rest, so you can use the ordinary Markdown you already know: *emphasis*, `code`, [links](https://mews.page/spec/0.1), lists, quotes and tables. - One - Two > Authors provide structure. Readers control presentation. Delete this file once you have something of your own to say. """ README = """\ # {title} A Mews site. The pages follow [Mews Profile 0.1](https://mews.page/spec/0.1). ## Build it ```shell uv run pelican content ``` 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 ``` ## Publish it Copy `output/` to any web server. Nothing to configure. ## Subscribe to it The index lists each post as a dated link, which is all a Mews client needs (section 6.1). If you also want an Atom feed for ordinary feed readers, set `SITEURL` in `pelicanconf.py` and put back the `FEED_ALL_ATOM` default. ## Check it ```shell uvx --from {repo} mewslint https://your.site/ ``` Or list your site in the directory at . """ def write(root: Path, files: dict[str, str]) -> None: """Write each file, creating the directories it needs.""" for name, text in files.items(): path = root / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") def scaffold(root: Path, title: str, author: str) -> None: """Write a complete Pelican project that produces Mews pages.""" slug = "".join(c if c.isalnum() else "-" for c in root.name.lower()).strip("-") write( root, { "pyproject.toml": PYPROJECT.format(slug=slug or "mews-site", repo=REPO), "pelicanconf.py": PELICANCONF.format(title=title, author=author), "theme/templates/base.html": BASE, "theme/templates/index.html": INDEX, "theme/templates/article.html": ARTICLE, "theme/templates/page.html": PAGE, "content/first-post.md": POST.format( date=datetime.now(UTC).strftime("%Y-%m-%d %H:%M") ), "README.md": README.format(title=title, repo=REPO), }, ) def main(argv: list[str] | None = None) -> int: """Create the project, install its dependencies, and say what to run.""" parser = argparse.ArgumentParser( prog="mews-quickstart", description="Start a Pelican site that writes Mews pages.", ) parser.add_argument( "directory", nargs="?", default="mysite", help="where to put it" ) parser.add_argument("--title", help="site name (default: the directory name)") parser.add_argument("--author", default="", help="your name, for the metadata") parser.add_argument( "--no-install", action="store_true", help="write the files and stop" ) args = parser.parse_args(argv) root = Path(args.directory).resolve() if root.exists() and any(root.iterdir()): print( f"{root} already has something in it. Pick an empty directory.", file=sys.stderr, ) return 1 title = args.title or root.name scaffold(root, title, args.author) print(f"Wrote {root}") if not args.no_install: uv = shutil.which("uv") if uv is None: print("uv isn't on the path, so nothing was installed.", file=sys.stderr) else: print("Installing Pelican and the Mews plugin...") result = subprocess.run([uv, "sync"], cwd=root, check=False) if result.returncode != 0: print( "The install didn't finish. Run `uv sync` yourself to see " "what went wrong.", file=sys.stderr, ) return 1 print( f"\nNext:\n" f" cd {root.name}\n" f" uv run pelican --autoreload --listen content\n\n" f"Then open http://localhost:8000/ and edit content/first-post.md." ) return 0 if __name__ == "__main__": sys.exit(main())