Skill: Check Template Rendering
Use this to verify a layout renders without errors and inspect template variable values.
Сначала — CLI, потом HTTP
Предпочитай CLI (scripts/renderlayout.py из этой инструкции). Он протестирован, обрабатывает auth, парсит warnings, корректно выходит с кодом ошибки.
Если renderlayout.py ещё не в PATH — следуй instructions/extract_cli_from_skill для извлечения. Кодблок с исходником — в конце этой инструкции.
Прямой POST на /_system/renderlayout через curl — fallback на крайний случай (нет Python, нет прав на запись). Глюков больше: ручной auth, ручной парсинг ответа, нет exit-кода для CI.
How it works
/_system/renderlayout compiles and executes a Jet template server-side against a real note from the vault. It returns a previewURL — a direct link to the rendered HTML. No sync needed: share this link with the user while they're actively editing, it's faster than waiting for vault sync.
Tool: scripts/renderlayout.py
The repo ships scripts/renderlayout.py — a CLI wrapper that reads the API key automatically from .obsidian/plugins/trip2g/data.json (walks up from cwd).
# run from vault dir (where .obsidian/ lives), e.g. docs/
# smoke test — no files needed
python3 ../scripts/renderlayout.py \
--layout-src "{{ note.HTMLString() }}" --layout-path "/_debug.html" \
--note-src "hello"
# → http://localhost:8081/_system/renderlayout?preview_id=abc123
# test a layout file against a real note
python3 ../scripts/renderlayout.py \
--layout-file _layouts/mesh/index.html \
--note-path /en/user/templates
# fetch rendered HTML directly
python3 ../scripts/renderlayout.py \
--layout-src "{{ note.M().Debug() }}" --layout-path "/_debug.html" \
--note-path /demo/template_meta_test \
--fetch
Warnings and errors go to stderr. Exit code 1 on failure.
Share the printed URL with the user — they can open it in browser.
Inspect variables with debug()
{{ debug(note.M()) }}
{* → *templateviews.Meta: &{raw:map[extra_content:[channels prices]]}
methods: [Debug Get GetBool GetInt GetString GetStrings Has Raw] *}
{{ note.M().Debug() }}
{* → {"extra_content":["channels","prices"],"title":"Sales"} *}
Always use parentheses when passing methods to
debug():
{{ debug(note.Title()) }}✓ →string: My Title
{{ debug(note.Title) }}✗ →func() string: 0x...(method reference, not value)
Preview vs production: known differences
| Feature | Preview | Production |
|---|---|---|
| autoimport components | ✅ works | ✅ works |
yield_blocks() CSS |
✅ works | ✅ works |
{{ asset("file.css") }} |
❌ returns path as-is | ✅ resolves to CDN URL |
htmlInjectionsHead |
✅ empty slice (no error) | ✅ real injections |
note.M() frontmatter |
✅ works (both note.path and note.src) |
✅ works |
{{ asset() }} doesn't resolve in preview because assets are tied to note version IDs in the production loader. Use note.path to get real asset URLs, or inline CSS/JS directly in the template during development.
Rendering a single BEM component
To preview one component in isolation, explicitly import its file and yield the block.
See en/user/bem for BEM conventions and @lid/@did naming.
Important caveats:
- Import paths must be relative to the layout path (after
/_layouts/prefix is stripped).
From/_layouts/mesh/_preview.html→ import"bar"resolves to/mesh/bar✓ yield_blocks()returns empty in preview (wire phase is skipped). Yield style blocks directly instead.- Dependencies must be imported manually — autoimport doesn't work in preview.
# Render mesh/bar.html component with its styles
python3 ../scripts/renderlayout.py \
--layout-path "/_layouts/mesh/_preview.html" \
--layout-src '{{ import "_blocks" }}{{ import "bar" }}{{ import "button" }}<style>{{ yield _style_mesh_bar() }}{{ yield _style_mesh_button() }}</style>{{ yield mesh_bar() }}' \
--note-src "hello"
Pattern:
{{ import "_blocks" }}— shared layout wrapper (if needed){{ import "component" }}— the component file (and its dependencies)<style>{{ yield _style_component() }}</style>— inline CSS (instead ofyield_blocks){{ yield component() }}— the HTML block
Tip: /_system/renderlayout without params always serves the latest render — keep it open in a browser while iterating.
Jet range gotcha
Single-variable range gives the index (0, 1, 2…), not the value:
{* WRONG — item = 0, 1, 2 *}
{{ range item := note.M().GetStrings("extra_content") }}{{ item }}{{ end }}
{* CORRECT *}
{{ range i, item := note.M().GetStrings("extra_content") }}{{ item }}{{ end }}
scripts/renderlayout.py source
#!/usr/bin/env python3
"""
renderlayout — CLI wrapper for /_system/renderlayout.
Reads API key and URL from .obsidian/plugins/trip2g/data.json
(walks up from cwd) or from TRIP2G_API_KEY / TRIP2G_API_URL env vars.
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from pathlib import Path
def find_config() -> dict:
current = Path.cwd()
for directory in [current, *current.parents]:
candidate = directory / ".obsidian" / "plugins" / "trip2g" / "data.json"
if candidate.exists():
with open(candidate) as f:
data = json.load(f)
sync_dirs = data.get("syncDirs", [])
if sync_dirs:
return {
"api_key": sync_dirs[0].get("apiKey", ""),
"api_url": sync_dirs[0].get("apiUrl", "http://localhost:8081"),
}
return {}
def main():
parser = argparse.ArgumentParser(description="Render a Jet layout template")
parser.add_argument("--layout-path", help="Layout path on server, e.g. /_layouts/foo.html")
parser.add_argument("--layout-file", help="Read layout src from this local file")
parser.add_argument("--layout-src", help="Inline layout template source")
parser.add_argument("--note-path", help="Note path on server, e.g. /my-note")
parser.add_argument("--note-file", help="Read note markdown from this local file")
parser.add_argument("--note-src", help="Inline note markdown source")
parser.add_argument("--api-key", help="API key (overrides config)")
parser.add_argument("--api-url", help="Server URL (overrides config)")
parser.add_argument("--fetch", action="store_true", help="Fetch and print rendered HTML")
args = parser.parse_args()
config = find_config()
api_key = args.api_key or os.environ.get("TRIP2G_API_KEY") or config.get("api_key", "")
api_url = (args.api_url or os.environ.get("TRIP2G_API_URL") or config.get("api_url", "http://localhost:8081")).rstrip("/")
if not api_key:
print("Error: no API key found. Set TRIP2G_API_KEY or --api-key.", file=sys.stderr)
sys.exit(1)
layout_path = args.layout_path
if args.layout_file:
layout_path = layout_path or ("/" + args.layout_file.lstrip("/"))
with open(args.layout_file) as f:
layout_src = f.read()
else:
layout_src = args.layout_src
if not layout_path:
print("Error: --layout-path or --layout-file is required.", file=sys.stderr)
sys.exit(1)
layout = {"path": layout_path}
if layout_src is not None:
layout["src"] = layout_src
note = None
if args.note_path:
note = {"path": args.note_path}
elif args.note_file:
with open(args.note_file) as f:
note = {"src": f.read()}
elif args.note_src is not None:
note = {"src": args.note_src}
payload = {"layout": layout}
if note is not None:
payload["note"] = note
body = json.dumps(payload).encode()
req = urllib.request.Request(
f"{api_url}/_system/renderlayout",
data=body,
headers={"Content-Type": "application/json", "X-API-Key": api_key},
method="POST",
)
try:
with urllib.request.urlopen(req) as resp:
result = json.loads(resp.read())
except urllib.error.HTTPError as e:
result = json.loads(e.read())
warnings = result.get("warnings", {}).get("layout", [])
if warnings:
print("WARNINGS:", file=sys.stderr)
for w in warnings:
print(" ", w, file=sys.stderr)
if "error" in result:
print(f"ERROR: {result['error']}", file=sys.stderr)
sys.exit(1)
full_url = f"{api_url}{result.get('previewURL', '')}"
if args.fetch:
with urllib.request.urlopen(urllib.request.Request(full_url)) as resp:
print(resp.read().decode())
else:
print(full_url)
if warnings:
sys.exit(1)
if __name__ == "__main__":
main()