| name | markdown-to-html |
| description | Use when turning markdown documentation, specs, READMEs, notes, or articles into a polished, self-contained, single-file HTML page. Output has a clean docs/blog aesthetic (Fraunces serif headings + DM Sans body + IBM Plex Mono code, neutral palette) with a built-in dark-mode toggle that persists to localStorage and respects the system preference. Triggers on "make this markdown an HTML page", "create an HTML version of...", "turn this doc into a webpage", "render as HTML". |
Markdown to HTML
Overview
Renders a markdown document as a polished single-file HTML page with a clean modern aesthetic — Fraunces serif headings, DM Sans body, IBM Plex Mono code, subtle borders, generous reading width, no decorative flourishes. Every page always includes a working dark-mode toggle in the top bar.
Output is always one self-contained .html file: all CSS inlined, fonts from Google Fonts, JS for the theme toggle inlined. No build step, no dependencies, no broken links when shared.
When to Use
Trigger when the user wants:
- A standalone HTML version of a markdown doc, README, spec, or notes
- A clean reader-focused page (not a magazine-style explainer)
- An update to an existing page generated by this skill
Skip when:
- The user wants a multi-page site or production frontend (use
frontend-design)
- The user explicitly asks for the magazine/editorial aesthetic with hero stats, ticker tape, and 2×2 SVG card grids (those layouts don't fit here)
- The doc is purely tabular data
File Layout
The skill ships three files:
| File | Purpose |
|---|
SKILL.md | This document — workflow and conventions |
style.css | Canonical style library (light + dark palettes via CSS variables, inlined into every output) |
template.html | Structural HTML scaffold with theme-toggle, breadcrumb, article header, body, footer, and a <!-- INJECT-MARKDOWN-CSS --> marker where the library is spliced in |
Edits to style.css propagate to all NEW pages generated afterwards.
Process
- Read the source markdown file(s).
- Identify metadata:
- Title — first H1 of the markdown
- Lead — first paragraph (if missing, summarize from the doc's actual content; never invent from nothing)
- Eyebrow — optional category/section tag (project name, document type)
- Updated date — from frontmatter,
git log, or today's date
- Choose destination path (default: sibling to the source markdown, e.g.
README.md → README.html).
- Generate the destination by running the recipe below — it reads
template.html, splices style.css into the <!-- INJECT-MARKDOWN-CSS --> marker, and writes the result.
- Render the markdown body to HTML and inject it into
<div class="article-body">. Either:
- Write the HTML semantically yourself (preferred — produces cleaner output)
- Or run
python3 -m markdown SOURCE.md if the markdown package is installed (pip install markdown if missing — but writing HTML directly produces cleaner output, so prefer that)
- Set the page chrome:
<title> and the H1 in .article-header h1
<p class="eyebrow"> text (or remove the element if no eyebrow)
<p class="lead"> content (or remove)
.article-meta — fill with real values from frontmatter/git, or remove the whole block
- Breadcrumb — adjust
home / page to match the doc's place (e.g., home / docs / phase 7); link home to ./index.html only if a hub page actually exists
- Verify with
open <destination>.
CSS injection recipe
python3 - <<'PY'
from pathlib import Path
import re
candidates = [Path.home() / ".claude/skills/markdown-to-html",
Path.home() / ".codex/skills/markdown-to-html"]
skill_dir = next((p for p in candidates if (p / "style.css").exists()), None)
assert skill_dir, f"markdown-to-html skill files not found in {candidates}"
css = (skill_dir / "style.css").read_text()
template = (skill_dir / "template.html").read_text()
out, n = re.subn(
r"<!-- INJECT-MARKDOWN-CSS -->",
f"<style>\n{css}</style>",
template,
count=1,
)
assert n == 1 and "INJECT-MARKDOWN-CSS" not in out, "marker not replaced"
Path("DESTINATION.html").write_text(out)
PY
After injection, edit the destination to fill in the article content.
Markdown → HTML Mapping
When rendering the markdown body, use these semantic mappings inside .article-body:
| Markdown | HTML element | Notes |
|---|
# H1 | (skipped — already in article header) | Don't repeat the H1 in body |
## H2 | <h2> | Auto-styled with top border |
### H3 | <h3> | |
#### H4 | <h4> | |
**bold** | <strong> | |
*italic* | <em> | |
`code` | <code> | Auto-styled pill background |
```lang blocks | <pre><code>...</code></pre> | Optionally class="lang-X" for future syntax highlighting |
> quote | <blockquote> | Auto-styled left border |
- list | <ul><li> | |
1. list | <ol><li> | |
[text](url) | <a href="url"> | |
 | <img alt="alt" src="src"> | Auto bordered/rounded |
--- | <hr> | |
| Tables (GFM) | <table> with <thead>, <tbody>, <th>, <td> | Auto-styled |
Special semantic blocks
For callouts (notes, warnings), wrap content in <aside class="callout TYPE">:
<aside class="callout warn">
<p class="callout-title">Heads up</p>
<p>Body of the callout.</p>
</aside>
Available types: note (blue), warn (amber), info (cyan), success (green).
For inline highlighted terms (e.g. ticker symbols, identifiers), use <code class="pill">TERM</code>.
For keyboard shortcuts, use <kbd>D</kbd>.
Aesthetic Principles
Typography:
- Display (h1, h2): Fraunces (variable serif, opsz 144, SOFT 50, WONK 1) — gives headings character without going full-magazine
- Body, h3, h4: DM Sans (variable, 300–700)
- Mono: IBM Plex Mono (400, 500, 600)
- For an italic word inside an h1 or h2, wrap it in
<em> — Fraunces' italic axis with SOFT 100 makes it sing
- Don't substitute Inter, Roboto, Arial, Geist, or system-only fallbacks
Colors — both palettes auto-handled by CSS variables:
| Token | Light | Dark |
|---|
--bg | #fafafa | #09090b |
--bg-elevated | #ffffff | #18181b |
--bg-code | #f4f4f5 | #1f1f23 |
--text | #18181b | #fafafa |
--text-muted | #52525b | #a1a1aa |
--border | #e4e4e7 | #27272a |
--accent | #2563eb | #60a5fa |
Layout:
- Single-column reading view, max-width 720px
- Generous vertical rhythm (1.4em between block elements)
- Sticky page header with breadcrumb + theme toggle
- Footer with mono-set meta info
Motion:
- Smooth
0.2s transition on theme switch
- No animations otherwise — this is a docs aesthetic, not a marketing page
- Respects
prefers-reduced-motion
Dark Mode Toggle (always included)
Built into template.html. The toggle:
- Lives in the top-right of the sticky header
- Reads initial theme from
localStorage first, then prefers-color-scheme
- Persists user choice to
localStorage under key theme
- Sets
data-theme="dark|light" on <html>
- No flash on page load — the inline
<head> script applies the theme before paint
- Keyboard shortcut
D toggles theme (skipped when typing in input fields)
Do not remove the toggle — it's part of the skill's contract.
Updating an Existing Page
- Read the existing HTML file.
- Edit only the article content, breadcrumb, header, and meta — leave the inlined CSS, theme JS, and structural HTML alone.
- To refresh the styling on an existing page (after editing
style.css), use the same regex but on the existing file:
from pathlib import Path
import re
candidates = [Path.home() / ".claude/skills/markdown-to-html/style.css",
Path.home() / ".codex/skills/markdown-to-html/style.css"]
new_css = next(p for p in candidates if p.exists()).read_text()
existing = Path("docs/some-page.html").read_text()
updated = re.sub(r"<style>.*?</style>", f"<style>\n{new_css}</style>", existing, count=1, flags=re.DOTALL)
Path("docs/some-page.html").write_text(updated)
The user must explicitly opt in to a styling refresh — old pages are otherwise frozen at the styling they were generated with.
Quick Reference
| Element | Selector / class | Notes |
|---|
| Sticky top bar | .page-header | Auto-shadows on scroll |
| Theme toggle button | .theme-toggle | Sun/moon SVGs swap via CSS |
| Breadcrumb | .breadcrumb | Mono font, muted |
| Article header | .article-header | Eyebrow + h1 + lead + meta |
| Article body | .article-body | Auto-styles all markdown HTML |
| Callout | .callout.note/warn/info/success | Left-border colored |
| Inline highlighted term | code.pill | Accent-colored badge |
Common Mistakes
| Mistake | Fix |
|---|
| Substituting Inter / Roboto / Arial / Geist for the font stack | Always use Fraunces (h1, h2) + DM Sans (body, h3, h4) + IBM Plex Mono (code, eyebrow, kbd). The aesthetic depends on the type. |
| Removing the theme toggle | Don't. Every page must have it — it's part of the skill's promise. |
Adding the literal closing-style-tag string anywhere inside style.css | The HTML parser closes a <style> element on the first matching string, even inside CSS comments. The whole rest of the CSS would render as visible text. Describe the tag in prose only. |
Repeating the H1 inside .article-body | The H1 already lives in .article-header — body should start at H2. |
| Inventing meta values like reading time / dates | Use real source data or remove the .article-meta block. Don't fake. |
| Filling the body with placeholder Lorem Ipsum | If the source has no content for a section, omit that section. Never fabricate. |
Linking to ~/.claude/skills/... from the output | The output must be self-contained. Always inline the CSS at generation time. |
| Forcing the editorial template (ticker tape, hero stats, 2×2 SVG cards) | This skill is intentionally calmer. If the user wants the magazine layout, they should ask explicitly for it (or use a different skill). |
Real-World Reference
This skill replaces the older editorial-html-pages skill, which produced more decorative magazine-style explainer pages. The output here is intended for ordinary docs, READMEs, and specs — pages where readability and clarity beat visual flourish.