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.
This commit is contained in:
LeoVasanko committed 2026-09-26 01:05:38 +00:00
1 parent 98c66440f6
commit 776f408743
3 files changed
+58 -19

No files matched your search

+1
View File
@@ -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)),
+11 -11
View File
@@ -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:
+46 -8
View File
@@ -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,