Skip to content

Commit 4ad31b6

Browse files
Add publish-to-pages agent skill (#1035)
* 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 and PDF with full formatting preservation - Creates repo, enables Pages, returns live URL - Zero config — just needs gh CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: update README.skills.md --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e2c763d commit 4ad31b6

5 files changed

Lines changed: 558 additions & 0 deletions

File tree

docs/README.skills.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ See [CONTRIBUTING.md](../CONTRIBUTING.md#adding-skills) for guidelines on how to
196196
| [prd](../skills/prd/SKILL.md) | Generate high-quality Product Requirements Documents (PRDs) for software systems and AI-powered features. Includes executive summaries, user stories, technical specifications, and risk analysis. | None |
197197
| [project-workflow-analysis-blueprint-generator](../skills/project-workflow-analysis-blueprint-generator/SKILL.md) | Comprehensive technology-agnostic prompt generator for documenting end-to-end application workflows. Automatically detects project architecture patterns, technology stacks, and data flow patterns to generate detailed implementation blueprints covering entry points, service layers, data access, error handling, and testing approaches across multiple technologies including .NET, Java/Spring, React, and microservices architectures. | None |
198198
| [prompt-builder](../skills/prompt-builder/SKILL.md) | Guide users through creating high-quality GitHub Copilot prompts with proper structure, tools, and best practices. | None |
199+
| [publish-to-pages](../skills/publish-to-pages/SKILL.md) | 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. | `scripts/convert-pdf.py`<br />`scripts/convert-pptx.py`<br />`scripts/publish.sh` |
199200
| [pytest-coverage](../skills/pytest-coverage/SKILL.md) | Run pytest tests with coverage, discover lines missing coverage, and increase coverage to 100%. | None |
200201
| [python-mcp-server-generator](../skills/python-mcp-server-generator/SKILL.md) | Generate a complete MCP server project in Python with tools, resources, and proper configuration | None |
201202
| [quasi-coder](../skills/quasi-coder/SKILL.md) | Expert 10x engineer skill for interpreting and implementing code from shorthand, quasi-code, and natural language descriptions. Use when collaborators provide incomplete code snippets, pseudo-code, or descriptions with potential typos or incorrect terminology. Excels at translating non-technical or semi-technical descriptions into production-quality code. | None |

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)