
Publish To Pages
Turn PDF decks into HTML slide presentations suitable for GitHub Pages using pdftoppm, including large-PDF external-assets mode.
Overview
Publish-to-pages is an agent skill most often used in Build (also Launch) that converts PDFs to HTML presentations with pdftoppm for publishing on GitHub Pages.
Install
npx skills add https://github.com/github/awesome-copilot --skill publish-to-pagesWhat is this skill?
- Python workflow converts each PDF page to PNG via pdftoppm (poppler-utils)
- Supports external assets mode for large PDFs to avoid giant single-file HTML
- Warns when PDFs exceed 150MB about memory and conversion time
- Documents install paths for poppler on Debian/Ubuntu and macOS
- 150MB PDF size warning threshold
- Default rasterization DPI setting of 150 in conversion script
Adoption & trust: 1.6k installs on skills.sh; 34.6k GitHub stars; 2/3 security scanners passed (skills.sh audits).
What problem does it solve?
You have a PDF deck but need a lightweight HTML slide site without manually exporting every page as images.
Who is it for?
Indie hackers documenting talks, workshops, or internal PDFs who already use GitHub and can install poppler-utils.
Skip if: Teams needing editable Google Slides or Figma-native decks—this is rasterized page images, not live slide authoring.
When should I use this skill?
You need to publish a PDF as an HTML slide presentation and have poppler-utils available locally or in CI.
What do I get? / Deliverables
You get an HTML presentation (optionally with external assets) ready to commit and publish via GitHub Pages or any static host.
- HTML presentation file
- Optional external image assets directory
- Console guidance when dependencies are missing
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
Primary shelf is Build → docs because the skill produces publishable HTML documentation assets from PDFs. Output is static doc/site collateral (HTML presentations), not application backend code.
Where it fits
Convert a workshop PDF into an HTML deck checked into the repo docs folder.
Ship the generated HTML to GitHub Pages for a conference follow-up link.
Republish an updated investor PDF as the same HTML slide path for email campaigns.
How it compares
Local poppler-based generator skill, not Marp markdown slides or a managed slide SaaS.
Common Questions / FAQ
Who is publish-to-pages for?
Solo builders and doc maintainers in awesome-copilot workflows who publish PDF-origin decks as static HTML on GitHub Pages.
When should I use publish-to-pages?
Use it in Build → docs when converting PDFs to HTML slides, and at Launch → distribution when you need a public URL for a deck after the HTML is generated.
Is publish-to-pages safe to install?
The skill runs local subprocesses (pdftoppm, pdfinfo) on files you point at—review script contents and the Security Audits panel on this Prism page before running in CI with secrets nearby.
SKILL.md
READMESKILL.md - Publish To Pages
#!/usr/bin/env python3 """Convert a PDF to an HTML presentation. Each page is rendered as a PNG image (via pdftoppm). Supports external assets mode for large files to avoid huge single-file HTML. Requirements: poppler-utils (pdftoppm) """ import argparse import base64 import glob import os import subprocess import sys import tempfile from pathlib import Path def get_page_count(pdf_path): """Get page count using pdfinfo if available.""" try: result = subprocess.run(["pdfinfo", pdf_path], capture_output=True, text=True) for line in result.stdout.splitlines(): if line.startswith("Pages:"): return int(line.split(":")[1].strip()) except: pass return None def convert(pdf_path: str, output_path: str | None = None, dpi: int = 150, external_assets=None): pdf_path = str(Path(pdf_path).resolve()) if not Path(pdf_path).exists(): print(f"Error: {pdf_path} not found") sys.exit(1) if subprocess.run(["which", "pdftoppm"], capture_output=True).returncode != 0: print("Error: pdftoppm not found. Install poppler-utils:") print(" apt install poppler-utils # Debian/Ubuntu") print(" brew install poppler # macOS") sys.exit(1) file_size_mb = os.path.getsize(pdf_path) / (1024 * 1024) if file_size_mb > 150: print(f"WARNING: PDF is {file_size_mb:.0f}MB — conversion may be slow and memory-intensive.") page_count = get_page_count(pdf_path) # Auto-detect external assets mode if external_assets is None: external_assets = file_size_mb > 20 or (page_count is not None and page_count > 50) if external_assets: print(f"Auto-enabling external assets mode (file: {file_size_mb:.1f}MB, pages: {page_count or 'unknown'})") output = output_path or str(Path(pdf_path).with_suffix('.html')) output_dir = Path(output).parent if external_assets: assets_dir = output_dir / "assets" assets_dir.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory() as tmpdir: prefix = os.path.join(tmpdir, "page") result = subprocess.run( ["pdftoppm", "-png", "-r", str(dpi), pdf_path, prefix], capture_output=True, text=True ) if result.returncode != 0: print(f"Error converting PDF: {result.stderr}") sys.exit(1) pages = sorted(glob.glob(f"{prefix}-*.png")) if not pages: print("Error: No pages rendered from PDF") sys.exit(1) slides_html = [] for i, page_path in enumerate(pages, 1): with open(page_path, "rb") as f: page_bytes = f.read() if external_assets: img_name = f"img-{i:03d}.png" (assets_dir / img_name).write_bytes(page_bytes) src = f"assets/{img_name}" else: b64 = base64.b64encode(page_bytes).decode() src = f"data:image/png;base64,{b64}" slides_html.append( f'<section class="slide">' f'<div class="slide-inner">' f'<img src="{src}" alt="Page {i}">' f'</div></section>' ) title = Path(pdf_path).stem.replace("-", " ").replace("_", " ") html = f'''<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}</title> <style> * {{ margin: 0; padding: 0; box-sizing: border-box; }} html, body {{ height: 100%; overflow: hidden; background: #000; }} .slide {{ width: 100vw; height: 100vh; display: none; align-items: center; justify-content: center; }} .slide.active {{ display: flex; }} .slide-inner {{ display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }} .slide-inner img {{ max-width: 100%; max-height: 100%; object-fit: contain; }} .progress {{ position: fixed; bottom: 0;