2026-10-09 21:46:05 +08:00
|
|
|
|
"""Prepare downloads, the sitemap and the sharing image for the static website."""
|
2026-10-07 20:59:56 +08:00
|
|
|
|
|
|
|
|
|
|
from io import BytesIO
|
|
|
|
|
|
from pathlib import Path
|
2026-10-09 21:46:05 +08:00
|
|
|
|
import sys
|
|
|
|
|
|
import xml.etree.ElementTree as ET
|
2026-10-07 20:59:56 +08:00
|
|
|
|
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
|
|
|
|
|
|
2026-10-10 19:56:37 +08:00
|
|
|
|
from website import PageMetadata, page_url
|
|
|
|
|
|
|
2026-10-07 20:59:56 +08:00
|
|
|
|
|
|
|
|
|
|
EXAMPLE_FILES = (
|
|
|
|
|
|
"package.json",
|
|
|
|
|
|
"main.js",
|
|
|
|
|
|
"preload.js",
|
|
|
|
|
|
"renderer.js",
|
|
|
|
|
|
"csv.js",
|
|
|
|
|
|
"index.html",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-09 21:46:05 +08:00
|
|
|
|
def prepare_sitemap(site):
|
|
|
|
|
|
"""Discover HTML routes automatically, excluding pages marked noindex."""
|
|
|
|
|
|
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
|
|
|
|
ET.register_namespace("", namespace)
|
|
|
|
|
|
sitemap = ET.Element(f"{{{namespace}}}urlset")
|
|
|
|
|
|
for path in sorted(site.rglob("*.html")):
|
2026-10-10 19:56:37 +08:00
|
|
|
|
policy = PageMetadata()
|
2026-10-09 21:46:05 +08:00
|
|
|
|
policy.feed(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
if policy.noindex:
|
|
|
|
|
|
continue
|
|
|
|
|
|
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
|
|
|
|
|
|
ET.SubElement(
|
|
|
|
|
|
entry, f"{{{namespace}}}loc"
|
2026-10-10 19:56:37 +08:00
|
|
|
|
).text = page_url(site, path)
|
2026-10-09 21:46:05 +08:00
|
|
|
|
ET.indent(sitemap, space=" ")
|
|
|
|
|
|
ET.ElementTree(sitemap).write(
|
|
|
|
|
|
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
|
|
|
|
|
|
)
|
|
|
|
|
|
print(f"Prepared sitemap.xml ({len(sitemap)} indexable pages).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_sharing_image(site):
|
|
|
|
|
|
"""Rasterize the maintained SVG for clients that need a PNG preview."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
import cairosvg
|
|
|
|
|
|
except (ImportError, OSError) as error:
|
|
|
|
|
|
sys.exit(
|
|
|
|
|
|
f"Cannot prepare sharing image: {error}. Install website/requirements.txt and native Cairo; see website/README.md."
|
|
|
|
|
|
)
|
|
|
|
|
|
source = site / "assets/social-card.svg"
|
|
|
|
|
|
destination = site / "assets/social-card.png"
|
|
|
|
|
|
cairosvg.svg2png(
|
|
|
|
|
|
url=str(source), write_to=str(destination), output_width=1200, output_height=630
|
|
|
|
|
|
)
|
|
|
|
|
|
print("Prepared assets/social-card.png (1200 × 630).")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-07 20:59:56 +08:00
|
|
|
|
def prepare_notes_example(site):
|
|
|
|
|
|
"""Create a ZIP with public source only and reproducible metadata."""
|
|
|
|
|
|
source = site / "examples/notes-electron"
|
|
|
|
|
|
contents = BytesIO()
|
|
|
|
|
|
with ZipFile(contents, "w") as archive:
|
|
|
|
|
|
for name in EXAMPLE_FILES:
|
|
|
|
|
|
entry = ZipInfo(f"notes-example/{name}", (1980, 1, 1, 0, 0, 0))
|
|
|
|
|
|
entry.create_system = 3
|
|
|
|
|
|
entry.external_attr = 0o100644 << 16
|
|
|
|
|
|
archive.writestr(
|
|
|
|
|
|
entry,
|
|
|
|
|
|
(source / name).read_bytes(),
|
|
|
|
|
|
compress_type=ZIP_DEFLATED,
|
|
|
|
|
|
compresslevel=9,
|
|
|
|
|
|
)
|
|
|
|
|
|
destination = site / "examples/notes-example.zip"
|
|
|
|
|
|
destination.write_bytes(contents.getvalue())
|
|
|
|
|
|
print(f"Prepared examples/notes-example.zip ({len(EXAMPLE_FILES)} files).")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-10-11 16:12:46 +08:00
|
|
|
|
def prepare_assets(site):
|
|
|
|
|
|
"""Finish the shared publish artifact before it replaces the current preview."""
|
2026-10-09 21:46:05 +08:00
|
|
|
|
prepare_sharing_image(site)
|
|
|
|
|
|
prepare_sitemap(site)
|
|
|
|
|
|
prepare_notes_example(site)
|
2026-10-11 16:12:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
from website_i18n import build_translations
|
|
|
|
|
|
build_translations(root, root / "website/_site", finalize=prepare_assets)
|