Skip to content

Commit 3812e7a

Browse files
Add publish-to-pages agent skill (#1009)
* Add publish-to-pages agent skill Agent skill that publishes presentations and web content to GitHub Pages. Works with any AI coding agent (Copilot CLI, Claude Code, Gemini CLI, etc.) Features: - Converts PPTX with full formatting preservation - Creates repo, enables Pages, returns live URL - Zero config — just needs gh CLI Bundled assets: - scripts/publish.sh - Creates GitHub repo and enables Pages - scripts/convert-pptx.py - Converts PPTX to HTML with formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add PDF converter, improve mobile scaling - Add convert-pdf.py: renders PDF pages as images, base64-embeds into self-contained HTML with slide navigation - Replace pandoc dependency with poppler-utils (pdftoppm) - Add JS transform scale for mobile viewports on PPTX converter - Update SKILL.md with PDF conversion instructions --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4bb58e3 commit 3812e7a

File tree

4 files changed

+557
-0
lines changed

4 files changed

+557
-0
lines changed

skills/publish-to-pages/SKILL.md

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
---
2+
name: publish-to-pages
3+
description: 'Publish presentations and web content to GitHub Pages. Converts PPTX, PDF, HTML, or Google Slides to a live GitHub Pages URL. Handles repo creation, file conversion, Pages enablement, and returns the live URL. Use when the user wants to publish, deploy, or share a presentation or HTML file via GitHub Pages.'
4+
---
5+
6+
# publish-to-pages
7+
8+
Publish any presentation or web content to GitHub Pages in one shot.
9+
10+
## 1. Prerequisites Check
11+
12+
Run these silently. Only surface errors:
13+
14+
```bash
15+
command -v gh >/dev/null || echo "MISSING: gh CLI — install from https://cli.github.com"
16+
gh auth status &>/dev/null || echo "MISSING: gh not authenticated — run 'gh auth login'"
17+
command -v python3 >/dev/null || echo "MISSING: python3 (needed for PPTX conversion)"
18+
```
19+
20+
`poppler-utils` is optional (PDF conversion via `pdftoppm`). Don't block on it.
21+
22+
## 2. Input Detection
23+
24+
Determine input type from what the user provides:
25+
26+
| Input | Detection |
27+
|-------|-----------|
28+
| HTML file | Extension `.html` or `.htm` |
29+
| PPTX file | Extension `.pptx` |
30+
| PDF file | Extension `.pdf` |
31+
| Google Slides URL | URL contains `docs.google.com/presentation` |
32+
33+
Ask the user for a **repo name** if not provided. Default: filename without extension.
34+
35+
## 3. Conversion
36+
37+
### HTML
38+
No conversion needed. Use the file directly as `index.html`.
39+
40+
### PPTX
41+
Run the conversion script:
42+
```bash
43+
python3 SKILL_DIR/scripts/convert-pptx.py INPUT_FILE /tmp/output.html
44+
```
45+
If `python-pptx` is missing, tell the user: `pip install python-pptx`
46+
47+
### PDF
48+
Convert with the included script (requires `poppler-utils` for `pdftoppm`):
49+
```bash
50+
python3 SKILL_DIR/scripts/convert-pdf.py INPUT_FILE /tmp/output.html
51+
```
52+
Each page is rendered as a PNG and base64-embedded into a self-contained HTML with slide navigation.
53+
If `pdftoppm` is missing, tell the user: `apt install poppler-utils` (or `brew install poppler` on macOS).
54+
55+
### Google Slides
56+
1. Extract the presentation ID from the URL (the long string between `/d/` and `/`)
57+
2. Download as PPTX:
58+
```bash
59+
curl -L "https://docs.google.com/presentation/d/PRESENTATION_ID/export/pptx" -o /tmp/slides.pptx
60+
```
61+
3. Then convert the PPTX using the convert script above.
62+
63+
## 4. Publishing
64+
65+
### Visibility
66+
Repos are created **public** by default. If the user specifies `private` (or wants a private repo), use `--private` — but note that GitHub Pages on private repos requires a Pro, Team, or Enterprise plan.
67+
68+
### Publish
69+
```bash
70+
bash SKILL_DIR/scripts/publish.sh /path/to/index.html REPO_NAME public "Description"
71+
```
72+
73+
Pass `private` instead of `public` if the user requests it.
74+
75+
The script creates the repo, pushes `index.html`, and enables GitHub Pages.
76+
77+
## 5. Output
78+
79+
Tell the user:
80+
- **Repository:** `https://github.com/USERNAME/REPO_NAME`
81+
- **Live URL:** `https://USERNAME.github.io/REPO_NAME/`
82+
- **Note:** Pages takes 1-2 minutes to go live.
83+
84+
## Error Handling
85+
86+
- **Repo already exists:** Suggest appending a number (`my-slides-2`) or a date (`my-slides-2026`).
87+
- **Pages enablement fails:** Still return the repo URL. User can enable Pages manually in repo Settings.
88+
- **PPTX conversion fails:** Tell user to run `pip install python-pptx`.
89+
- **PDF conversion fails:** Suggest installing `poppler-utils` (`apt install poppler-utils` or `brew install poppler`).
90+
- **Google Slides download fails:** The presentation may not be publicly accessible. Ask user to make it viewable or download the PPTX manually.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
"""Convert a PDF to a self-contained HTML presentation.
3+
4+
Each page is rendered as a PNG image (via pdftoppm) and base64-embedded
5+
into a single HTML file with slide navigation (arrows, swipe, click).
6+
7+
Requirements: poppler-utils (pdftoppm)
8+
Usage: python3 convert-pdf.py input.pdf [output.html]
9+
"""
10+
11+
import base64
12+
import glob
13+
import os
14+
import subprocess
15+
import sys
16+
import tempfile
17+
from pathlib import Path
18+
19+
20+
def convert(pdf_path: str, output_path: str | None = None, dpi: int = 150):
21+
pdf_path = str(Path(pdf_path).resolve())
22+
if not Path(pdf_path).exists():
23+
print(f"Error: {pdf_path} not found")
24+
sys.exit(1)
25+
26+
# Check for pdftoppm
27+
if subprocess.run(["which", "pdftoppm"], capture_output=True).returncode != 0:
28+
print("Error: pdftoppm not found. Install poppler-utils:")
29+
print(" apt install poppler-utils # Debian/Ubuntu")
30+
print(" brew install poppler # macOS")
31+
sys.exit(1)
32+
33+
with tempfile.TemporaryDirectory() as tmpdir:
34+
prefix = os.path.join(tmpdir, "page")
35+
result = subprocess.run(
36+
["pdftoppm", "-png", "-r", str(dpi), pdf_path, prefix],
37+
capture_output=True, text=True
38+
)
39+
if result.returncode != 0:
40+
print(f"Error converting PDF: {result.stderr}")
41+
sys.exit(1)
42+
43+
pages = sorted(glob.glob(f"{prefix}-*.png"))
44+
if not pages:
45+
print("Error: No pages rendered from PDF")
46+
sys.exit(1)
47+
48+
slides_html = []
49+
for i, page_path in enumerate(pages, 1):
50+
with open(page_path, "rb") as f:
51+
b64 = base64.b64encode(f.read()).decode()
52+
slides_html.append(
53+
f'<section class="slide">'
54+
f'<div class="slide-inner">'
55+
f'<img src="data:image/png;base64,{b64}" alt="Page {i}">'
56+
f'</div></section>'
57+
)
58+
59+
# Try to extract title from filename
60+
title = Path(pdf_path).stem.replace("-", " ").replace("_", " ")
61+
62+
html = f'''<!DOCTYPE html>
63+
<html lang="en">
64+
<head>
65+
<meta charset="UTF-8">
66+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
67+
<title>{title}</title>
68+
<style>
69+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
70+
html, body {{ height: 100%; overflow: hidden; background: #000; }}
71+
.slide {{ width: 100vw; height: 100vh; display: none; align-items: center; justify-content: center; }}
72+
.slide.active {{ display: flex; }}
73+
.slide-inner {{ display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; }}
74+
.slide-inner img {{ max-width: 100%; max-height: 100%; object-fit: contain; }}
75+
.progress {{ position: fixed; bottom: 0; left: 0; height: 4px; background: #0366d6; transition: width 0.3s; z-index: 100; }}
76+
.counter {{ position: fixed; bottom: 12px; right: 20px; font-size: 14px; color: rgba(255,255,255,0.4); z-index: 100; }}
77+
</style>
78+
</head>
79+
<body>
80+
{chr(10).join(slides_html)}
81+
<div class="progress" id="progress"></div>
82+
<div class="counter" id="counter"></div>
83+
<script>
84+
const slides = document.querySelectorAll('.slide');
85+
let current = 0;
86+
function show(n) {{
87+
slides.forEach(s => s.classList.remove('active'));
88+
current = Math.max(0, Math.min(n, slides.length - 1));
89+
slides[current].classList.add('active');
90+
document.getElementById('progress').style.width = ((current + 1) / slides.length * 100) + '%';
91+
document.getElementById('counter').textContent = (current + 1) + ' / ' + slides.length;
92+
}}
93+
document.addEventListener('keydown', e => {{
94+
if (e.key === 'ArrowRight' || e.key === ' ') {{ e.preventDefault(); show(current + 1); }}
95+
if (e.key === 'ArrowLeft') {{ e.preventDefault(); show(current - 1); }}
96+
}});
97+
let touchStartX = 0;
98+
document.addEventListener('touchstart', e => {{ touchStartX = e.changedTouches[0].screenX; }});
99+
document.addEventListener('touchend', e => {{
100+
const diff = e.changedTouches[0].screenX - touchStartX;
101+
if (Math.abs(diff) > 50) {{ diff > 0 ? show(current - 1) : show(current + 1); }}
102+
}});
103+
document.addEventListener('click', e => {{
104+
if (e.clientX > window.innerWidth / 2) show(current + 1);
105+
else show(current - 1);
106+
}});
107+
show(0);
108+
</script>
109+
</body></html>'''
110+
111+
output = output_path or str(Path(pdf_path).with_suffix('.html'))
112+
Path(output).write_text(html, encoding='utf-8')
113+
print(f"Converted to: {output}")
114+
print(f"Pages: {len(slides_html)}")
115+
116+
117+
if __name__ == "__main__":
118+
if len(sys.argv) < 2:
119+
print("Usage: convert-pdf.py <file.pdf> [output.html]")
120+
sys.exit(1)
121+
convert(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None)

0 commit comments

Comments
 (0)