# Save shareable HTML brief
This reference file is loaded by the main `SKILL.md` when the user asked for an HTML brief (either through an HTML-looking prompt argument like `--emit=html` / `--emit:html` / `--html`, or in natural language - "give me a shareable HTML brief", "give it to me in HTML", "for Slack", "for Notion", "export as HTML", etc.). The detection happens in `SKILL.md` so that the common no-HTML path stays short; the implementation lives here. Those prompt arguments are user intent signals for the skill; they are not the full Python CLI contract.
The contract has two modes:
- **HTML as the requested deliverable** (`--emit=html`, `--emit:html`, `--html`, or prose like "give it to me in HTML"): the HTML artifact is the primary output. Write the synthesis to the temp file, render the HTML, then give a concise artifact handoff in chat instead of pasting the full Markdown report again.
- **Normal report plus HTML copy** (the user asks for the normal report and also wants an HTML copy): the synthesis still appears in chat as the primary output. The HTML is an additional artifact saved to disk for sharing. Both happen in the same turn.
## When to fire this flow
- For normal-report-plus-HTML mode: after you have already emitted the full chat response: badge, "What I learned:" (or comparison title), bold-lead-in paragraphs with citations, KEY PATTERNS list, engine footer pass-through, invitation block.
- For HTML-as-deliverable mode: after you have drafted the synthesis that will go into the HTML, before emitting the final chat response.
- BEFORE the closing invitation and wait for the user's response.
- ONLY if the user asked. Do NOT save HTML when the user didn't ask for it.
## How to fire it
```bash
# 1. Write your synthesis prose VERBATIM to a temp file. The synthesis is the
# "What I learned:" prose label, the bold-lead-in paragraphs with their
# inline citations, and the "KEY PATTERNS from the research:" numbered list.
# Do NOT include the badge or the engine footer in the temp file - the engine
# adds those when it renders the HTML.
# - HTML-as-deliverable mode: use the exact synthesis draft you prepared for
# the artifact. Do not paste it to chat first.
# - Normal-report-plus-HTML mode: use the exact synthesis text you already
# wrote in chat.
# In both modes, do not paraphrase, summarize, or reorder. The HTML must read
# identically to the intended report in voice and citations.
SYNTHESIS_FILE="/tmp/last30days-synthesis-${CLAUDE_SESSION_ID}.md"
# >| not >: fixed path may already exist on a same-session re-run; a plain >
# is refused under `set -o noclobber`.
cat >| "${SYNTHESIS_FILE}" << 'SYNTHESIS_EOF'
What I learned:
**{First headline}** - {body with [name](url) inline citations}
**{Second headline}** - {body}
**{Third headline}** - {body}
KEY PATTERNS from the research:
1. {pattern} - per [@handle](url)
2. {pattern} - per [r/sub](url)
3. {pattern} - per [@handle](url)
SYNTHESIS_EOF
# 2. Convert the synthesis to a self-contained HTML file via the engine.
# REPLAY THE SAME SCOPE FLAGS as your original run (--plan, --hiring-signals,
# resolved --x-handle/--subreddits/etc). On a same-topic follow-up, the
# engine reuses the structured last-report cache at
# ~/.config/last30days/last-report.json to build badge metadata and footer
# without re-running source fetchers. That cache is intentionally short-lived
# (default: one hour; tune with LAST30DAYS_REPORT_CACHE_TTL_SECONDS, or set
# it to 0 to disable reuse). If the cache is stale, missing, or for a
# different topic, stderr says "No matching cached report data" and the
# engine falls back to a fresh run; the same scope flags keep that fallback
# aligned with the synthesis body.
SLUG=$(printf '%s\n' "${TOPIC}" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
# Render beside the destination, then create a new name atomically. The hard
# link refuses existing paths even when another exporter wins the same name.
HTML_PATH=$(
HTML_TMP=$(mktemp "${LAST30DAYS_MEMORY_DIR}/.last30days-html.XXXXXXXX") || exit "$?"
trap 'rm -f -- "${HTML_TMP}"' EXIT
"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "${TOPIC}" \
--emit=html \
--synthesis-file "${SYNTHESIS_FILE}" \
"${SCOPE_FLAGS[@]}" \
>| "${HTML_TMP}" || exit "$?"
"${LAST30DAYS_PYTHON}" - "${HTML_TMP}" "${LAST30DAYS_MEMORY_DIR}" "${SLUG}" << 'HTML_PATH_PY'
from datetime import date
import os
from pathlib import Path
import sys
temporary, directory, slug = sys.argv[1:]
base = Path(directory) / f"{slug}-brief"
dated = f"{base}-{date.today().isoformat()}"
candidate = Path(f"{base}.html")
suffix = 1
while True:
try:
os.link(temporary, candidate)
except FileExistsError:
candidate = Path(f"{dated}.html" if suffix == 1 else f"{dated}-{suffix}.html")
suffix += 1
else:
print(candidate)
break
HTML_PATH_PY
) || exit "$?"
# where SCOPE_FLAGS is the same array you passed the first time, e.g.
# SCOPE_FLAGS=(--hiring-signals --plan "$QUERY_PLAN_FILE" --x-handle=acme).
# For a scoped --hiring-signals brief, --hiring-signals MUST be here too so
# the footer reflects the jobs-scoped board, not a generic crawl.
# 3. Finish with the artifact handoff described below. Do not print the saved
# path from the shell block; the chat handoff is the single user-visible
# completion message.
```
## Optional hosted publishing
Only publish after the local HTML file has already been saved and the user chooses a publish option. The local HTML save is always first, and its absolute path is always shown before any publish/upload step.
Respect any existing user, project, or host preference for HTML publishing first. If the user already has a preferred publisher or internal sharing workflow, include that option. If multiple publishing options are available, show each as its own choice and include `ht-ml.app` as one option; label `ht-ml.app` as supporting optional password protection. If no preference exists, use `ht-ml.app` as the fallback publishing option.
Use this decision flow:
- Save the local HTML file.
- Show the absolute saved path.
- Then proactively present next-step choices:
1. Open HTML file
2. Publish to ``; if `ht-ml.app` is shown, say password protection is available
3. Done for now
- Do not upload until the user chooses a publishing option.
When publishing to `ht-ml.app`, ask a second question:
- **Public link** - publish without a password.
- **Password-protected link** - ask the user to type the shared password in free form, then publish with that password.
Before the `ht-ml.app` choice, tell the user that public pages may be crawled or indexed, and that password protection is available. If the user chooses password protection, use a unique shared password they provide for this report; do not use their own account password.
Agents should discover the current publishing mechanics for the selected service when needed, including by visiting the service site, rather than hard-coding detailed service-specific instructions in chat. For the built-in `ht-ml.app` path, the engine supports `--publish-html`; on the password-protected branch, pass the shared password through `LAST30DAYS_PUBLISH_PASSWORD` rather than command-line arguments.
When the user chooses the built-in `ht-ml.app` path, add `--publish-html` to the same `--emit=html` command. Use `--output "$HTML_PATH"` rather than shell redirection so the engine can write the `.publish.json` companion metadata next to the local HTML file. On the password-protected branch, set `LAST30DAYS_PUBLISH_PASSWORD` in the subprocess environment instead of passing `--publish-password` in the shell command.
```bash
LAST30DAYS_PUBLISH_PASSWORD="${PUBLISH_PASSWORD:-}" \
"${LAST30DAYS_PYTHON}" "${SKILL_DIR}/scripts/last30days.py" "${TOPIC}" \
--emit=html \
--synthesis-file "$SYNTHESIS_FILE" \
--output "$HTML_PATH" \
--publish-html \
"${SCOPE_FLAGS[@]}" \
>/dev/null
```
The hosted URL appears on stderr as `[last30days] Published HTML to https://...`. Confirm the result with the hosted URL. If the user chose password protection, also repeat the shared password they selected so they can send the URL and password together. The engine writes URL metadata to `.publish.json`. The provider may return an `update_key`; treat it as secret. The engine deliberately does not write the update key to stdout, the HTML artifact, or `.publish.json` companion metadata.
## Chat handoff after saving
Use the mode that matches the request.
### HTML as the requested deliverable
When HTML is the requested deliverable - whether by `--emit=html`, `--emit:html`, `--html`, or natural-language phrasing - do **not** paste the full Markdown report back into chat after saving the artifact. The user asked for an HTML deliverable; repeating the Markdown makes the run feel like a normal report with an attachment bolted on.
Respond with a concise handoff that includes the next-step choices:
```text
๐ last30days v{VERSION} ยท synced {YYYY-MM-DD}
๐ Shareable brief saved to
What do you want to do next?
1. Open HTML file
2. Publish to ()
3. Done for now
```
If the user chooses open, open the HTML file when the host can safely open local files, leave the saved-path line in chat, and add `Opened locally.` Let the host choose the correct OS-specific mechanism; do not print a menu of shell commands. If opening fails or the host is headless, do not treat that as a failed report; show the path and say the file is ready to open in a browser.
### Normal report plus HTML copy
When the user asked for a normal `/last30days` report and also asked for an HTML copy, keep the full chat synthesis and append this artifact block after the invitation:
```text
๐ Shareable brief saved to
What do you want to do next?
1. Open HTML file
2. Publish to ()
3. Done for now
```
If the user chooses open, open it when the host can safely open local files; otherwise the saved-path line is enough. Do not upload in this flow unless the user chooses a publishing option.
## What ends up in the HTML file
The engine's `--emit=html` renderer combines:
- The badge (`๐ last30days vX.Y.Z ยท synced YYYY-MM-DD`) at the top
- A single inline metadata line (`{date range} ยท {active sources}`) below the badge
- Your synthesis verbatim, with prose labels promoted to `