| name | p-md-to-slides |
| license | MIT |
| description | Convert a structured Markdown deck source into a styled Google Slides presentation. Use this skill when the user has a slide.md file (or wants to write one) and wants Google Slides output — talks, LT decks, lecture slides, conference presentations, internal explainers. Trigger phrases include "md からスライド作って", "slide.md を Google Slides にして", "発表資料を Google Slides で", "convert this markdown to slides", "build a Google Slides deck from this markdown", "make slides from md", "/md-to-slides", and similar. Also use when the user references a presentation/<date>/slide.md file structure with `## Slide N — title`, `**話すこと:**`, and `**スライド要素:**` sections. |
| metadata | {"author":"jackchuka","scope":"personal"} |
p-md-to-slides
Convert a Markdown deck source into a styled Google Slides presentation, with a chosen design theme and speaker notes pushed from the source.
When to use
- The user has a
slide.md (or similar) with ## Slide N — title sections and wants Google Slides
- The user is preparing a talk / LT / conference presentation and has drafted the content in Markdown
- The user asks to convert / build / generate slides from a Markdown source
Not for activity-driven decks (see o-tailor-allhands-deck for those) — this is for hand-authored Markdown → Google Slides.
Prerequisites
gws CLI installed and authenticated (gws auth login if needed)
- Python 3 on PATH (for one-shot heredoc snippets)
- Network access to Google Slides API
Input format
See references/md-format.md for the full spec. Quick summary:
# <deck title>
## Slide N — <subtitle>
**話すこと:**
<speaker notes — pushed to the slide's notes BODY>
**スライド要素:**
- <visual element hint 1>
- <visual element hint 2>
Both **話すこと:** and **スライド要素:** are optional per slide.
Workflow
The skill is intentionally lightweight — no shipped scripts. Every step is a concrete shell or Python heredoc that Claude executes inline, with patterns referenced from references/.
Step 1: Read the source
Use the Read tool on the user's slide.md. Confirm:
- The file exists and parses (look for at least one
## Slide N — line)
- The slide count matches what the user expects
- Identify which slides have
**話すこと:** and which don't
If the user is starting from scratch, offer to scaffold a slide.md from a brief.
Step 2: Pick a theme
Read the relevant theme reference file based on user preference (default tailor if not specified):
references/themes/tailor.md — Tailor brand (navy/cream/blue, DM Sans + Roboto Mono, alternating rhythm)
references/themes/minimal.md — All-cream, navy text
references/themes/dark.md — All-navy, cream text, electric blue accents
If the user wants something custom, ask for brand colors + display font and adapt inline.
Step 3: Parse the markdown
A short Python heredoc handles the parsing. The format is small enough that this beats a separate script.
python3 - <<'PY' > /tmp/p-md-to-slides/parsed.json
import json, re, sys
from pathlib import Path
SRC = "/path/to/slide.md"
SLIDE_HEADER = re.compile(r"^##\s*Slide\s*(\d+)\s*[—–-]\s*(.+?)\s*$")
BLOCK_MARKER = re.compile(r"^\*\*([^*]+):\*\*\s*$")
BULLET = re.compile(r"^\s*[-*]\s+(.*)$")
text = Path(SRC).read_text(encoding="utf-8")
lines = text.splitlines()
title = next((l[2:].strip() for l in lines if l.startswith("# ") and not l.startswith("## ")), "")
starts = [i for i, l in enumerate(lines) if SLIDE_HEADER.match(l)]
slides = []
for idx, start in enumerate(starts):
end = starts[idx+1] if idx+1 < len(starts) else len(lines)
m = SLIDE_HEADER.match(lines[start])
blocks = {}
code_blocks = []
current = None
in_code = False
code_buf = []
for line in lines[start+1:end]:
if line.lstrip().startswith("```"):
if in_code:
code_blocks.append("\n".join(code_buf)); code_buf = []; in_code = False
else:
current = None; in_code = True
continue
if in_code: code_buf.append(line); continue
mm = BLOCK_MARKER.match(line)
if mm:
current = mm.group(1).strip()
blocks.setdefault(current, []); continue
if line.strip() == "---":
current = None; continue
if current is not None:
blocks[current].append(line)
notes = "\n".join(blocks.get("話すこと", [])).strip()
elements_raw = "\n".join(blocks.get("スライド要素", [])).strip()
elements = [BULLET.match(l).group(1).strip() for l in elements_raw.splitlines() if BULLET.match(l)]
slides.append({"index": idx, "number": int(m.group(1)), "subtitle": m.group(2).strip(),
"notes": notes, "elements": elements, "elements_raw": elements_raw,
"code_blocks": code_blocks})
print(json.dumps({"title": title, "slides": slides}, ensure_ascii=False, indent=2))
PY
Inspect the JSON briefly (slide count, notes lengths) before continuing.
Step 4: Create the presentation shell
gws slides presentations create --json '{"title":"<deck title>"}' \
| python3 -c "import json,sys; d=json.load(sys.stdin); print('PRES_ID=' + d['presentationId']); print('BLANK=' + next(l['objectId'] for l in d.get('layouts',[]) if l.get('layoutProperties',{}).get('displayName')=='Blank')); print('DEFAULT=' + d['slides'][0]['objectId'])"
Capture PRES_ID, BLANK (Blank layout ID), and DEFAULT (default slide ID — must delete later).
Step 5: Build the deck
Write a one-off /tmp/p-md-to-slides/<deck-name>/build.py that emits batchUpdate JSON. Read references/slides-helpers.md for the helper code pattern (page constants, color blending, Slide builder class) — copy the helpers into your build script, then add per-slide content.
The base recipe per slide:
create_slide(slide_id, index, BLANK, bg=theme_bg_for_index) — bg cycles per theme rhythm
top_bar(...) — small uppercase labels (e.g. "05 · 工夫① ...")
- Heading —
22–24pt w700 for content slides, 40pt w600 bottom-anchored for the title slide
- Body —
16–18pt w600–700 bullets from elements, or fall back to a clean prose dump
Projection-readable defaults. Sizes are calibrated for talks projected on a 3–4m screen viewed from 5–10m back. A deck that "looks balanced" in your laptop preview is usually too small in the room. Default heavy and big; trim only when content actually overflows. See the typography table in the chosen theme file for the full role-by-role scale.
At the end, delete(DEFAULT) to remove the auto-created blank slide, then push the batch:
BODY=$(cat /tmp/p-md-to-slides/<deck-name>/batch.json)
gws slides presentations batchUpdate \
--params '{"presentationId":"<PRES_ID>"}' \
--json "$BODY"
batchUpdate is transactional — if anything fails, nothing applies. Inspect the error response carefully.
Step 6: Push speaker notes
Discover the notes BODY shape per slide, then insertText:
gws slides presentations get \
--params '{"presentationId":"<PRES_ID>","fields":"slides(objectId,slideProperties.notesPage.pageElements(objectId,shape.placeholder))"}' \
| python3 -c "
import json, sys
d = json.load(sys.stdin)
for s in d['slides']:
sid = s['objectId']
for el in s.get('slideProperties', {}).get('notesPage', {}).get('pageElements', []):
if el.get('shape', {}).get('placeholder', {}).get('type') == 'BODY':
print(f'{sid}|{el[\"objectId\"]}')
"
For each slide, build an insertText request against the notes BODY objectId with the slide's notes text. Skip slides without notes. Don't use deleteText on empty notes — see gotcha in references/slides-api-patterns.md.
Step 7: Verify with thumbnails
mkdir -p /tmp/p-md-to-slides/<deck-name>/thumbs
gws slides presentations get --params '{"presentationId":"<PRES_ID>","fields":"slides.objectId"}' \
| python3 -c "import json,sys; [print(s['objectId']) for s in json.load(sys.stdin)['slides']]" \
| nl | while read i sid; do
url=$(gws slides presentations pages getThumbnail \
--params "{\"presentationId\":\"<PRES_ID>\",\"pageObjectId\":\"$sid\",\"thumbnailProperties.mimeType\":\"PNG\",\"thumbnailProperties.thumbnailSize\":\"LARGE\"}" \
| python3 -c "import json,sys;print(json.load(sys.stdin)['contentUrl'])")
curl -s -o "/tmp/p-md-to-slides/<deck-name>/thumbs/$(printf %02d $i)_${sid}.png" "$url"
done
Then Read each PNG to inspect. The slide is only 405pt tall — content past y≈395 is invisible. The thumbnail is the only catch.
Step 8: Iterate on visual polish
The base deck has heading + body only. Editorial polish is content-aware and applied as a per-deck upgrade.py. Common moves (all using the same Slide builder pattern from references/slides-helpers.md):
- Decorative anchor character (top-right): a single oversized Roboto Mono glyph in dim color. Numbers (
"01" / "02" / "03") for ordered section slides, but also "?" on question slides, "/" on transitions, "※" on caveats. The anchor labels the slide without adding reading load.
- Subhead (tagline) under heading: 12–14pt muted single line at
y≈95. Names the slide's thesis — the "why am I looking at this?" — not just the topic. Particularly effective on section / content slides where the heading alone is the topic name.
- "Before / After" or "Journey" slides: colored rectangles with arrow between, accent badge for the "why"
- Flow diagrams: colored boxes per stage, contrasting fills for branches
- Title slide: small accent bar (theme
accent) above the headline
- Summary slides (simple): 2×2 cards with light fill and left accent bar
- Summary slides (story-structured): narrative-numbered cards (
01 課題 / 02 解決 / 03 品質 / 04 未来). Use when the recap has a sequence, not just 4 independent points — the numbers tell the audience this is a story arc.
- Dark footer band (cream summary closer): navy band at the bottom of a cream summary slide holding CTA / contact info. Acts as a visual period — physically separates "what we covered" from "thank you / find me here."
Apply by writing a fresh batchUpdate, re-fetch thumbnails, iterate.
Step 9: Deliver
https://docs.google.com/presentation/d/<PRES_ID>/edit
Key gotchas (full list: references/slides-api-patterns.md)
- Page height is 405pt — not 540. Anything past y≈395 is off-canvas.
- Object IDs must be unique across the entire presentation. Don't reuse.
- Empty notes:
startIndex==endIndex==0; use insertText only, never deleteText.
- batchUpdate is transactional: one failure rolls back the whole batch.
- z-order is creation order: backgrounds must be
SEND_TO_BACK explicitly.
- DM Sans + Japanese: falls back, may render slightly oblique. Acceptable; verify with thumbnails.
Reference files
references/md-format.md — input format spec
references/slides-api-patterns.md — gotchas, color math, request shapes
references/slides-helpers.md — reusable Python helper class (copy into build.py)
references/themes/{tailor,minimal,dark}.md — design tokens per theme