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())