Skip to main content

slidesync

Bidirectional sync between a Slidev markdown deck and Google Slides — as native, editable objects (title/body/bullets/tables/positioned images, brand-styled text boxes), not pasted screenshots.

Version: 0.12.0

uvx slidesync --help            # run without installing
pip install slidesync           # or install the CLI + library

Why

Exporting a deck to images gives you something you can't edit; pasting markdown by hand gives you something you can't version. slidesync keeps a .slidev.md file as the source of truth and renders it into real Slides objects, so the result stays fully editable in Google Slides — and pull reconstructs the markdown back from those objects, so the loop is reversible.

  • push — markdown → Slides (idempotent upsert, never a blind append).
  • pull — Slides → markdown (handles multi-text-box and externally-authored decks, bullet nesting, tables, images, and speaker notes).
  • roundtrip — push a sample to a scratch deck, pull it back, assert the two are semantically identical, delete the scratch deck.

Auth (no setup)

Auth is borrowed from the gog CLI — no separate OAuth client. slidesync reads the client id/secret from ~/Library/Application Support/gogcli/credentials.json and the refresh token via gog auth tokens export, then mints a short-lived access token. The stored token already carries the slides + drive scopes; the Slides API must be enabled on the gog Cloud project. Override the account with --account or $SLIDESYNC_ACCOUNT. (Currently macOS-only — it reads gog's macOS Application Support path.)

Commands

Command Purpose
slidesync push <file.slidev.md>... [--deck ID] [--new "Title"] [--anchor SLIDE] [--prune] [--force] [--allow-rekey] markdown → Slides (rejected if it would discard live edits; --force overrides)
slidesync pull <deckId> --out <file.md> [--all] Slides → markdown (--all includes non-managed slides)
slidesync roundtrip [--keep] self-test: push a sample, pull, assert identical
slidesync layouts <deckId> list a deck's theme layouts + placeholders
slidesync make-templates <deckId> inject branded {{token}} template slides
slidesync comments <deckId> list comment threads as JSON (page anchor, author, content, replies)
slidesync sync <file.slidev.md>... [--deck ID] [--prune] [--allow-rekey] reconcile with the live deck: pull comments + live edits into the markdown, push local changes; conflicts stop it (exit 1)

push resolves the target deck from (in order) --deck, --new, or a top-level deck: frontmatter key. Relative image paths resolve against each slide's own source file.

Multi-file decks: push/sync accept several files (e.g. slidesync sync $(ls -r meetings/*.slidev.md) — one file per meeting, newest first). Deck order follows the argument order; slide ids namespace as <file-stem>-<id> (2026-06-15-overview) so files can reuse ids; intra-file [text](#id) links rewrite to the namespaced target, while fully-qualified cross-file targets pass through. sync routes comment capture and live-edit write-backs into the right source file under its local id.

slidesync push deck.slidev.md            # targets `deck:` frontmatter
slidesync push deck.slidev.md --new "Talk"
slidesync pull <id> --out deck.slidev.md
slidesync roundtrip

Idempotent sync (upsert)

Each managed slide is created with objectId = s2g_<keyHash>_<contentHash>. keyHash = per-slide id: frontmatter, else title slug, else index (survives edits/reorders); contentHash is over a canonical render, so push → pull → push is a no-op. Diff per run: identical hash → skip; same key, new content → replace; new key → create. Removed slides are kept unless --prune. Only s2g_ slides are ever touched — hand-authored slides are invisible to the sync. A hidden <!-- s2g {...} --> marker in speaker notes carries the human id, image path, template vars — and, for template slides, the authored body markdown (base64) — so pull recovers the source verbatim.

Sync & drift

push is guarded like a non-fast-forward git push: if a slide it would replace (or prune) was edited in Google Slides since the last push — and the local markdown doesn't already carry that edit — the push is rejected with no changes made (--force overwrites). Live edits on slides the push wouldn't touch are left alone.

Mass re-key guard: a push that would recreate a deck-scale number of slides under new ids — max(10, 30% of managed slides) created while at least as many live s2g_ slides match no local slide — is refused outright, even with --force (an id-scheme change or key bug re-keys every slide; pushing that plan destroys live styling/edits on all the old copies). Capture the live deck first (pull/syncsync matches re-keyed slides via their notes-marker id and writes live edits, including styling-only highlights, back into the markdown), then re-run with --allow-rekey.

sync reconciles the two sides, applying whatever is safe. The marker's last-pushed source is a true per-slide merge base, so each slide classifies three-way without timestamps (the APIs expose no per-slide edit times — only file-level modifiedTime; the marker's at stamp records our last push):

status meaning sync does
clean / converged nothing changed, or both sides made the same change nothing
local-edit markdown changed, deck untouched pushes it
live-drift slide edited in Google Slides writes the live content back into the markdown (reconstructed from its styled boxes, formatting runs included), then pushes
conflict both changed since last push prints both diffs vs the base for a human/LLM to resolve; skips the push; exits 1

Unresolved comment threads are appended to their slide as <!-- @Author: text --> blocks (replies as extra @Author: lines). These mirrors are comments, not presenter notes: they stay out of the speaker-notes pane, and when a re-render orphans the live thread, push re-creates it anchored to the slide's new objectId (replies preserved; the re-created thread is authored by the authenticated account). Resolving a thread in Slides retires it — sync stops capturing and push won't revive it. Write-back caveat: a slide edited live is rewritten canonically, so its authored comments collapse into one trailing block (untouched slides keep comments in place).

Markdown dialect

Top-level frontmatter: theme:, deck:. Slides separated by ---; each slide may have its own frontmatter (id:, template:, layout:, hidden:).

  • # h1 = headline, ## h2 above an # h1 = kicker; a lone ## is the title.
  • Hidden slides: hidden: true (or hide: true) marks a slide skipped in the presentation — still a native, editable slide, just hidden in present mode (via the Slides API's isSkipped), the analogue of Slidev's hidden frontmatter. It's part of the content hash, so toggling it re-pushes, and it round-trips: pull reads the live skipped state back to hidden: true (so a slide skipped natively in Slides comes back hidden too).
  • Bullets -/*; ordered 1. (nest with 2-space indent). Inline **bold** / *italic* / `code` / ==highlight== / [link](url). GFM tables. ![alt](path) images (uploaded to Drive; alt becomes the accessibility description, round-tripped on pull). Blank lines preserved as spacing. <!-- notes --> become speaker notes — and round-trip as comments, in place: template slides carry their authored source in the marker, so pull re-emits each comment where it was written instead of one merged trailing blob. Speaker notes edited live in Slides come back as one extra trailing comment.
  • Highlight: ==text== (the markdown-it-mark dialect; delimiters must be non-space-adjacent, so a bare a == b stays prose) renders as a warm amber wash (#FFE08A) behind the run, with the text pinned to ink so the mark stays legible on the dark templates too. Composes with the other inline styles run-by-run, and round-trips: pull reads the background wash back to ==text== byte-identically.
  • Internal links: [text](#slide-id) becomes a native Slides link to the slide whose id: (or title slug) is slide-id, and round-trips: pull reads the native page-link back to [text](#slide-id) (so it no longer churns).
  • Display equations: a paragraph that is exactly a $$...$$ block (single- or multi-line; several per slide allowed) is rendered to a tight-bbox transparent PNG via matplotlib mathtext (no TeX install needed) in the Computer Modern fontset — the classic TeX serif math face, so equations read like paper typography — and embedded as a centred image, sized above body text — presentation-equation scale (template: equation goes further: a full-slide focal equation, rendered at double density so the blow-up stays crisp). Covers the common constructs (\frac, \approx, sub/superscripts, \times, \max, \text{}); a construct outside the mathtext subset logs a warning and skips that graphic. Renders are cached by source hash. The LaTeX source is part of the content hash (editing an equation re-pushes the slide) and is stashed verbatim in the hidden notes marker, so pull reconstructs the original $$...$$ block byte-identically. Inline $x$ maths is out of scope.
  • Mermaid diagrams: a fenced ```mermaid block is rendered to a PNG and embedded as an image (Slides has no native Mermaid renderer). Renders are cached by diagram hash, so an unchanged diagram is never re-rendered or re-uploaded; a render failure logs a warning and skips the graphic rather than aborting the push. Backend: mmdc (mermaid-cli) if it's on PATH (offline), else the kroki.io HTTP API (no extra dependency). The diagram source lives in the markdown, so it's the source of truth — pull recovers the rendered image, not the Mermaid source.

Built-in brand kit (IBM Plex; red #C0392B kicker)

Select per slide via template: — native styled boxes, no in-deck templates:

template: Layout
dark / title dark title card: centred kicker + 72pt headline; body lines render as a small dimmed byline (e.g. Project · Presenter)
appendix light title card (same shape as dark, paper background)
question / label red kicker + 50pt headline + centred body
topic one-line 40pt headline, left-aligned body
content the ## kicker is the title (red, 18pt); left-aligned bullet body
equation focal $$…$$ equation, centred and LARGE (scaled up to fill ~85% of the width, clamped to fit); one ## red kicker on top; # h1 is parsed but not rendered (like graph ignoring title/body — it still round-trips via the marker); body text renders as a short centred plain-text caption under the equation
graph / full single full-bleed image, no text
prompt / code verbatim monospace block at the largest size that fits

Title cards and equation/graph/full have no linkable body region. Slides with no template: fall back to a generative path (section / title+body / table / image) that also brands the background + IBM Plex.

Custom slides (diagrams) — pull-authoritative

Give a slide a fenced ```gslides block holding literal Slides API requests (use __PAGE__ for the slide page id). Sync is pull-authoritative / push-if-missing: the Slides copy is the source of truth — push only creates the slide when missing, pull captures the live drawing back into the block.

Template-slot validation

push/sync refuse up front when a slide carries content its template has no slot for (it would render as nothing, silently): a heading, table, or prose paragraph on a text-free graph/full slide; an # h1 alongside the kicker on an equation slide; an image/table on prompt/code; an image on equation. Each violation names the slide and the fix — usually moving the content into a <!-- comment -->, which becomes speaker notes. Link-only paragraphs on text-free templates are exempt (the crop → full-figure trace-link convention).

Overlays — raw requests on top of a templated slide

A ```gslides-overlay block rides on a normal templated/generative slide (unlike ```gslides, which replaces the whole slide): its literal Slides API requests are replayed after the slide's own render on every push, with __PAGE__ substituted by the slide page id. Use it for annotation text boxes, arrows, or callouts a template can't express:

---
template: graph
id: fig-thinking-off
---
![...](figure.png)

```gslides-overlay
{"requests": [
  {"createShape": {"objectId": "__PAGE___label", "shapeType": "TEXT_BOX",
    "elementProperties": {"pageObjectId": "__PAGE__", "size": {...}, "transform": {...}}}},
  {"insertText": {"objectId": "__PAGE___label", "text": "Thinking Off"}}
]}
```

The markdown is the source of truth: the block is part of the content hash (edits re-push), it round-trips through the notes marker on pull, and a content-changing push recreates the drawn elements — native edits to them in Slides are not written back. Drift detection counts the overlay's insertText lines as visible text, so an overlaid slide reads as clean. Prefix element ids with __PAGE__ so they stay unique across re-pushes.

Development

uv sync
uv run pytest -q          # offline tests (no network/auth)

Releases publish to PyPI via Trusted Publishing (OIDC) on a v*.*.* tag — see .github/workflows/release.yml. Bump with uvx bumpver update --patch.

Caveats

  • Slidev-only constructs (<v-clicks>, <div grid>, CSS) are flattened/stripped — this is a content mapper, not a CSS renderer.
  • On pull, the slide model holds a single image, so a slide with multiple images keeps the first; image contentUrls from foreign decks are ephemeral.
  • Verbatim-source markers are seeded at push time, so comment preservation applies from the first push with v0.2+ (older slides re-render once: the content hash is now over the authored source). Generative-path slides (no template:) still merge comments into a single trailing comment on pull, since their live Slides edits — not the marker — are the source of truth.

License

MIT © Daniel Hails

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

slidesync-0.12.0.tar.gz (97.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

slidesync-0.12.0-py3-none-any.whl (59.7 kB view details)

Uploaded Python 3

File details

Details for the file slidesync-0.12.0.tar.gz.

File metadata

  • Download URL: slidesync-0.12.0.tar.gz
  • Upload date:
  • Size: 97.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for slidesync-0.12.0.tar.gz
Algorithm Hash digest
SHA256 9e1cbb7f8121e7a4a219471a6af6700d9c93e1d815bbd0a4c002fed9fba0d47e
MD5 391dde4bca35f87906af54d65251fb57
BLAKE2b-256 e585718717c8130468f46eefa425854241a6855315729f18dd4bdae93d3a7ffd

See more details on using hashes here.

Provenance

The following attestation bundles were made for slidesync-0.12.0.tar.gz:

Publisher: release.yml on DJRHails/slidesync

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file slidesync-0.12.0-py3-none-any.whl.

File metadata

  • Download URL: slidesync-0.12.0-py3-none-any.whl
  • Upload date:
  • Size: 59.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for slidesync-0.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6f72315923b04c8ae81bab368d960d438b898c1fe71c18d3b36f2808dc802a8c
MD5 742d626b0c882110f258859f73c4fd19
BLAKE2b-256 03a159793e3676eb1d3b4b93f4da12faee645c7f10097cbd97adafb7709239ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for slidesync-0.12.0-py3-none-any.whl:

Publisher: release.yml on DJRHails/slidesync

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.14.0

2 files

0.13.0

2 files

This release

0.12.0 This release

2 files

0.11.2

2 files

0.11.1

2 files

0.11.0

2 files

0.10.5

2 files

0.10.4

2 files

0.10.3

2 files

0.10.2

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.3

2 files

0.8.2

2 files

0.8.1

2 files

0.8.0

2 files

0.7.0

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page