From 776f4087439d936d80b6f28dc6dae75ccb427cf7 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sat, 26 Sep 2026 01:05:38 +0000 Subject: [PATCH] contact messages: {contact: Label} article directive A lone {contact: Label} line in article Markdown renders a .contact-trigger button that opens the visitor panel (pagerite.js binds the click and lazy-loads the contact bundle; inert in the editor preview, whose resolver now matches the served page). The on-demand bundle's URLs ride in the pagerite-assets config like langselect's, and the build gets a contact entry point. Description/card mining renders resolve the tag to nothing. --- frontend/vite.config.js | 1 + pagerite/api.py | 22 ++++++++--------- pagerite/views.py | 54 +++++++++++++++++++++++++++++++++++------ 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index de5135a..32deb64 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -51,6 +51,7 @@ export default defineConfig({ pagerite: fileURLToPath(new URL('./src/pagerite.js', import.meta.url)), analytics: fileURLToPath(new URL('./src/analytics-main.js', import.meta.url)), langselect: fileURLToPath(new URL('./src/langselect-main.js', import.meta.url)), + contact: fileURLToPath(new URL('./src/contact-main.js', import.meta.url)), // Only the base CSS is built; theme/banner-design stylesheets live // in pagerite/themes/{name}/ and are served by the backend as-is. pagerite_base: fileURLToPath(new URL('./src/assets/pagerite.css', import.meta.url)), diff --git a/pagerite/api.py b/pagerite/api.py index 530b9db..fd7f844 100644 --- a/pagerite/api.py +++ b/pagerite/api.py @@ -544,11 +544,19 @@ async def editor_ws(ws: WebSocket) -> None: markdown = msg.get("markdown", "") chain = resolve(data.menu, path) node = chain[-1] if chain else None - # Expand {cards} like page_content does, so the preview + has_cards_tag = views._CARDS_TAG_RE.search(markdown) is not None + # {contact} buttons preview like the served page (inert); + # {cards} expands like page_content does, so the preview # shows real cards, not the literal tag. No translation # context: the preview has no lang of its own, so cards # render in their originals. - has_cards_tag = views._CARDS_TAG_RE.search(markdown) is not None + directives: dict = {"contact": views._contact_tag} + if node is not None and has_cards_tag: + directives["cards"] = ( + lambda args, _env, node=node, path=path: views._cards_tag( + data.menu, data, node, path, args + ) + ) rendered = render( markdown, path, @@ -565,15 +573,7 @@ async def editor_ws(ws: WebSocket) -> None: if node else None ), - directives=( - { - "cards": lambda args, _env, node=node, path=path: ( - views._cards_tag(data.menu, data, node, path, args) - ) - } - if node is not None and has_cards_tag - else None - ), + directives=directives, ) html = rendered.html if node is not None and not has_cards_tag: diff --git a/pagerite/views.py b/pagerite/views.py index 96dbe41..682606d 100644 --- a/pagerite/views.py +++ b/pagerite/views.py @@ -415,13 +415,17 @@ def _layout( vite_url = env.vite_url editor_scripts, editor_css = _editor_assets() langselect_scripts, langselect_css = _langselect_assets() + contact_scripts, contact_css = _contact_assets() config = { "pagerite:editor-src": editor_scripts[-1], "pagerite:analytics-src": _analytics_assets()[0][0], "pagerite:langselect-src": langselect_scripts[-1], + "pagerite:contact-src": contact_scripts[-1], } if langselect_css: config["pagerite:langselect-css"] = ",".join(langselect_css) + if contact_css: + config["pagerite:contact-css"] = ",".join(contact_css) if editor_css: config["pagerite:editor-css"] = editor_css if vite_url: @@ -900,13 +904,11 @@ def page_content( # A {cards} tag places the card rows itself (possibly several); # without one the children are appended after the content as before. has_cards_tag = _CARDS_TAG_RE.search(content) is not None - directives = None + directives = {"contact": _contact_tag} if has_cards_tag: - directives = { - "cards": lambda args, _env: _cards_tag( - menu, data, node, path, args, translation, link_lang, lang - ) - } + directives["cards"] = lambda args, _env: _cards_tag( + menu, data, node, path, args, translation, link_lang, lang + ) rendered = render( content, path, @@ -984,6 +986,18 @@ def _cards( _CARDS_TAG_RE = re.compile(r"^\{cards(?::[^{}\n]*)?\}[ \t]*$", re.MULTILINE) +def _contact_tag(args: str, _env: dict) -> str: + """Expand a ``{contact: Label}`` directive: the button that opens the + visitor contact panel (docs/messages.md). The args are the button + label ("Contact us" when empty). pagerite.js binds the click and + lazy-loads the panel bundle; in the editor preview it stays inert.""" + return str( + E.button( + args.strip() or "Contact us", type="button", class_="contact-trigger" + ) + ) + + def _cards_tag( menu: dict[str, Node], data: Data, @@ -1123,7 +1137,7 @@ def _card( node.modified, # Card heuristics only mine the prose: nested {cards} rows # would just be noise in the description extraction. - directives={"cards": lambda _args, _env: ""}, + directives={"cards": lambda _args, _env: "", "contact": lambda _a, _e: ""}, ).html image, _ = _media(html) if not image: @@ -1143,7 +1157,7 @@ def _card( path, node.created, node.modified, - directives={"cards": lambda _args, _env: ""}, + directives={"cards": lambda _args, _env: "", "contact": lambda _a, _e: ""}, ).html if not large and html: description = _description(html, 150) @@ -1586,6 +1600,30 @@ def _langselect_assets() -> tuple[list[str], list[str]]: return _asset_cache["langselect"] +def _contact_assets() -> tuple[list[str], list[str]]: + """Script and stylesheet URLs for the on-demand contact panel.""" + vite_url = env.vite_url + if vite_url: + return [f"{vite_url}/src/contact-main.js"], [] + if "contact" not in _asset_cache: + manifest = _manifest() + # import() loads no CSS automatically: collect the stylesheets of + # the entry and its imported chunks (same walk as _langselect). + scripts, stylesheets, seen = [], [], set() + queue = ["src/contact-main.js"] + for key in queue: # grows with imported chunks + if key in seen: + continue + seen.add(key) + entry = manifest[key] + if entry.get("isEntry"): + scripts.append(f"/{entry['file']}") + stylesheets += [f"/{css}" for css in entry.get("css", [])] + queue += entry.get("imports", []) + _asset_cache["contact"] = scripts, stylesheets + return _asset_cache["contact"] + + def render_analytics( menu: dict[str, Node], brand: str = SITE_NAME,