
Publish To Pages
- 1.8k installs
- 37.5k repo stars
- Updated August 5, 2026
- github/awesome-copilot
publish-to-pages is an agent skill for 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 enableme
About
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 The publish-to-pages skill documents workflows and patterns from the repository SKILL md name publish-to-pages 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 publish-to-pages Publish any presentation or web content to GitHub Pages in one shot Prerequisites Check Run these silently Only surface errors bash command v gh dev null echo MISSING gh CLI install from https cli github com gh auth status dev null echo MISSING gh not authenticated run gh auth login command v
- **PPTX:** Files >20MB or with >50 images → images saved as separate files in `assets/`
- **PDF:** Files >20MB or with >50 pages → page PNGs saved in `assets/`
- Files >150MB print a warning (PPTX suggests PDF path instead)
- For large files, force external assets:
- For large files, force external assets:
Publish To Pages by the numbers
- 1,774 all-time installs (skills.sh)
- +22 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #305 of 2,203 Security skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
publish-to-pages capabilities & compatibility
- Capabilities
- **pptx:** files >20mb or with >50 images → image · **pdf:** files >20mb or with >50 pages → page pn · files >150mb print a warning (pptx suggests pdf · for large files, force external assets:
- Use cases
- documentation
What publish-to-pages says it does
--- name: publish-to-pages 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.' --- # publish-to-pages Publish any presentation or web content to GitHub Pages in one shot.
npx skills add https://github.com/github/awesome-copilot --skill publish-to-pagesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 37.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | github/awesome-copilot ↗ |
What problem does publish-to-pages solve for developers using the documented workflows?
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
Who is it for?
Developers working with publish-to-pages patterns described in the skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill documented scope.
When should I use this skill?
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
What you get
Grounded guidance and workflows from SKILL.md for publish-to-pages.
- HTML presentation file
- Per-page PNG slide images
Files
publish-to-pages
Publish any presentation or web content to GitHub Pages in one shot.
1. Prerequisites Check
Run these silently. Only surface errors:
command -v gh >/dev/null || echo "MISSING: gh CLI — install from https://cli.github.com"
gh auth status &>/dev/null || echo "MISSING: gh not authenticated — run 'gh auth login'"
command -v python3 >/dev/null || echo "MISSING: python3 (needed for PPTX conversion)"poppler-utils is optional (PDF conversion via pdftoppm). Don't block on it.
2. Input Detection
Determine input type from what the user provides:
| Input | Detection |
|---|---|
| HTML file | Extension .html or .htm |
| PPTX file | Extension .pptx |
| PDF file | Extension .pdf |
| Google Slides URL | URL contains docs.google.com/presentation |
Ask the user for a repo name if not provided. Default: filename without extension.
3. Conversion
Large File Handling
Both conversion scripts automatically detect large files and switch to external assets mode:
- PPTX: Files >20MB or with >50 images → images saved as separate files in
assets/ - PDF: Files >20MB or with >50 pages → page PNGs saved in
assets/ - Files >150MB print a warning (PPTX suggests PDF path instead)
This keeps individual files well under GitHub's 100MB limit. Small files still produce a single self-contained HTML.
You can force the behavior with --external-assets or --no-external-assets.
HTML
No conversion needed. Use the file directly as index.html.
PPTX
Run the conversion script:
python3 SKILL_DIR/scripts/convert-pptx.py INPUT_FILE /tmp/output.html
# For large files, force external assets:
python3 SKILL_DIR/scripts/convert-pptx.py INPUT_FILE /tmp/output.html --external-assetsIf python-pptx is missing, tell the user: pip install python-pptx
Convert with the included script (requires poppler-utils for pdftoppm):
python3 SKILL_DIR/scripts/convert-pdf.py INPUT_FILE /tmp/output.html
# For large files, force external assets:
python3 SKILL_DIR/scripts/convert-pdf.py INPUT_FILE /tmp/output.html --external-assetsEach page is rendered as a PNG and embedded into HTML with slide navigation. If pdftoppm is missing, tell the user: apt install poppler-utils (or brew install poppler on macOS).
Google Slides
1. Extract the presentation ID from the URL (the long string between /d/ and /) 2. Download as PPTX:
curl -L "https://docs.google.com/presentation/d/PRESENTATION_ID/export/pptx" -o /tmp/slides.pptx3. Then convert the PPTX using the convert script above.
4. Publishing
Visibility
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.
Publish
bash SKILL_DIR/scripts/publish.sh /path/to/index.html REPO_NAME public "Description"Pass private instead of public if the user requests it.
The script creates the repo, pushes index.html (plus assets/ if present), and enables GitHub Pages.
Note: When external assets mode is used, the output HTML references files in assets/. The publish script automatically detects and copies the assets/ directory alongside the HTML file. Make sure the HTML file and its assets/ directory are in the same parent directory.
5. Output
Tell the user:
- Repository:
https://github.com/USERNAME/REPO_NAME - Live URL:
https://USERNAME.github.io/REPO_NAME/ - Note: Pages takes 1-2 minutes to go live.
Error Handling
- Repo already exists: Suggest appending a number (
my-slides-2) or a date (my-slides-2026). - Pages enablement fails: Still return the repo URL. User can enable Pages manually in repo Settings.
- PPTX conversion fails: Tell user to run
pip install python-pptx. - PDF conversion fails: Suggest installing
poppler-utils(apt install poppler-utilsorbrew install poppler). - Google Slides download fails: The presentation may not be publicly accessible. Ask user to make it viewable or download the PPTX manually.
#!/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; left: 0; height: 4px; background: #0366d6; transition: width 0.3s; z-index: 100; }}
.counter {{ position: fixed; bottom: 12px; right: 20px; font-size: 14px; color: rgba(255,255,255,0.4); z-index: 100; }}
</style>
</head>
<body>
{chr(10).join(slides_html)}
<div class="progress" id="progress"></div>
<div class="counter" id="counter"></div>
<script>
const slides = document.querySelectorAll('.slide');
let current = 0;
function show(n) {{
slides.forEach(s => s.classList.remove('active'));
current = Math.max(0, Math.min(n, slides.length - 1));
slides[current].classList.add('active');
document.getElementById('progress').style.width = ((current + 1) / slides.length * 100) + '%';
document.getElementById('counter').textContent = (current + 1) + ' / ' + slides.length;
}}
document.addEventListener('keydown', e => {{
if (e.key === 'ArrowRight' || e.key === ' ') {{ e.preventDefault(); show(current + 1); }}
if (e.key === 'ArrowLeft') {{ e.preventDefault(); show(current - 1); }}
}});
let touchStartX = 0;
document.addEventListener('touchstart', e => {{ touchStartX = e.changedTouches[0].screenX; }});
document.addEventListener('touchend', e => {{
const diff = e.changedTouches[0].screenX - touchStartX;
if (Math.abs(diff) > 50) {{ diff > 0 ? show(current - 1) : show(current + 1); }}
}});
document.addEventListener('click', e => {{
if (e.clientX > window.innerWidth / 2) show(current + 1);
else show(current - 1);
}});
show(0);
</script>
</body></html>'''
Path(output).write_text(html, encoding='utf-8')
output_size = os.path.getsize(output)
print(f"Converted to: {output}")
print(f"Pages: {len(slides_html)}")
print(f"Output size: {output_size / (1024*1024):.1f}MB")
print(f"External assets: {'yes' if external_assets else 'no'}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert PDF to HTML presentation")
parser.add_argument("input", help="Path to .pdf file")
parser.add_argument("output", nargs="?", help="Output HTML path (default: same name with .html)")
parser.add_argument("--external-assets", action="store_true", default=None,
help="Save page images as separate files in assets/ directory (auto-detected for large files)")
parser.add_argument("--no-external-assets", action="store_true",
help="Force inline base64 even for large files")
parser.add_argument("--dpi", type=int, default=150, help="Render DPI (default: 150)")
args = parser.parse_args()
ext_assets = None
if args.external_assets:
ext_assets = True
elif args.no_external_assets:
ext_assets = False
convert(args.input, args.output, dpi=args.dpi, external_assets=ext_assets)
#!/usr/bin/env python3
"""Convert a PPTX file to an HTML presentation with formatting preserved.
Supports external assets mode for large files to avoid huge single-file HTML.
"""
import argparse
import base64
import io
import os
import re
import sys
from pathlib import Path
def _ensure_pptx():
try:
from pptx import Presentation
from pptx.enum.text import PP_ALIGN
return True
except ImportError:
print("ERROR: python-pptx not installed. Install with: pip install python-pptx")
sys.exit(1)
def rgb_to_hex(rgb_color):
if rgb_color is None:
return None
try:
return f"#{rgb_color}"
except:
return None
def get_text_style(run):
styles = []
try:
if run.font.bold:
styles.append("font-weight:bold")
if run.font.italic:
styles.append("font-style:italic")
if run.font.underline:
styles.append("text-decoration:underline")
if run.font.size:
styles.append(f"font-size:{run.font.size.pt}pt")
if run.font.color and run.font.color.rgb:
styles.append(f"color:{rgb_to_hex(run.font.color.rgb)}")
if run.font.name:
styles.append(f"font-family:'{run.font.name}',sans-serif")
except:
pass
return ";".join(styles)
def get_alignment(paragraph):
from pptx.enum.text import PP_ALIGN
try:
align = paragraph.alignment
if align == PP_ALIGN.CENTER:
return "center"
elif align == PP_ALIGN.RIGHT:
return "right"
elif align == PP_ALIGN.JUSTIFY:
return "justify"
except:
pass
return "left"
def get_shape_position(shape, slide_width, slide_height):
try:
left = (shape.left / slide_width) * 100 if shape.left else 0
top = (shape.top / slide_height) * 100 if shape.top else 0
width = (shape.width / slide_width) * 100 if shape.width else 50
height = (shape.height / slide_height) * 100 if shape.height else 30
return left, top, width, height
except:
return 5, 5, 90, 40
def get_slide_background(slide, prs):
from pptx.oxml.ns import qn
for source in [slide, slide.slide_layout]:
try:
bg_el = source.background._element
for sf in bg_el.iter(qn('a:solidFill')):
clr = sf.find(qn('a:srgbClr'))
if clr is not None and clr.get('val'):
return f"background-color:#{clr.get('val')}"
except:
pass
return "background-color:#ffffff"
def get_shape_fill(shape):
from pptx.oxml.ns import qn
try:
sp_pr = shape._element.find(qn('p:spPr'))
if sp_pr is None:
sp_pr = shape._element.find(qn('a:spPr'))
if sp_pr is None:
for tag in ['{http://schemas.openxmlformats.org/drawingml/2006/main}spPr',
'{http://schemas.openxmlformats.org/presentationml/2006/main}spPr']:
sp_pr = shape._element.find(tag)
if sp_pr is not None:
break
if sp_pr is not None:
sf = sp_pr.find(qn('a:solidFill'))
if sf is not None:
clr = sf.find(qn('a:srgbClr'))
if clr is not None and clr.get('val'):
return f"#{clr.get('val')}"
except:
pass
return None
def render_paragraph(paragraph):
align = get_alignment(paragraph)
parts = []
for run in paragraph.runs:
text = run.text
if not text:
continue
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
style = get_text_style(run)
if style:
parts.append(f'<span style="{style}">{text}</span>')
else:
parts.append(text)
if not parts:
return ""
content = "".join(parts)
return f'<p style="text-align:{align};margin:0.3em 0;line-height:1.4">{content}</p>'
def extract_image_data(shape):
"""Extract raw image bytes and content type from a shape."""
try:
image = shape.image
return image.blob, image.content_type
except:
return None, None
def count_images(prs):
"""Count total images across all slides."""
count = 0
for slide in prs.slides:
for shape in slide.shapes:
if shape.shape_type == 13 or hasattr(shape, "image"):
try:
_ = shape.image
count += 1
except:
pass
return count
CONTENT_TYPE_TO_EXT = {
'image/png': '.png',
'image/jpeg': '.jpg',
'image/jpg': '.jpg',
'image/gif': '.gif',
'image/bmp': '.bmp',
'image/tiff': '.tiff',
'image/svg+xml': '.svg',
'image/webp': '.webp',
}
def convert(pptx_path, output_path=None, external_assets=None):
_ensure_pptx()
from pptx import Presentation
file_size_mb = os.path.getsize(pptx_path) / (1024 * 1024)
# Pre-flight warning for very large files
if file_size_mb > 150:
print(f"WARNING: File is {file_size_mb:.0f}MB — consider using PDF conversion (convert-pdf.py) for better performance.")
prs = Presentation(pptx_path)
slide_width = prs.slide_width
slide_height = prs.slide_height
aspect_ratio = slide_width / slide_height if slide_height else 16/9
total_images = count_images(prs)
# Auto-detect external assets mode
if external_assets is None:
external_assets = file_size_mb > 20 or total_images > 50
if external_assets:
print(f"Auto-enabling external assets mode (file: {file_size_mb:.1f}MB, images: {total_images})")
output = output_path or str(Path(pptx_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)
img_counter = 0
slides_html = []
for i, slide in enumerate(prs.slides, 1):
bg_style = get_slide_background(slide, prs)
elements = []
for shape in sorted(slide.shapes, key=lambda s: (s.top or 0, s.left or 0)):
left, top, width, height = get_shape_position(shape, slide_width, slide_height)
pos_style = f"position:absolute;left:{left:.1f}%;top:{top:.1f}%;width:{width:.1f}%;height:{height:.1f}%"
# Image
if shape.shape_type == 13 or hasattr(shape, "image"):
blob, content_type = extract_image_data(shape)
if blob:
img_counter += 1
if external_assets:
ext = CONTENT_TYPE_TO_EXT.get(content_type, '.png')
img_name = f"img-{img_counter:03d}{ext}"
(assets_dir / img_name).write_bytes(blob)
src = f"assets/{img_name}"
else:
b64 = base64.b64encode(blob).decode('utf-8')
src = f"data:{content_type};base64,{b64}"
elements.append(
f'<div style="{pos_style};display:flex;align-items:center;justify-content:center">'
f'<img src="{src}" style="max-width:100%;max-height:100%;object-fit:contain" alt="">'
f'</div>'
)
continue
# Table
if shape.has_table:
table = shape.table
table_html = '<table style="width:100%;border-collapse:collapse;font-size:0.9em">'
for row in table.rows:
table_html += "<tr>"
for cell in row.cells:
cell_text = cell.text.replace("&", "&").replace("<", "<")
table_html += f'<td style="border:1px solid #ccc;padding:6px 10px">{cell_text}</td>'
table_html += "</tr>"
table_html += "</table>"
elements.append(f'<div style="{pos_style};overflow:auto">{table_html}</div>')
continue
# Text
if shape.has_text_frame:
text_parts = []
for para in shape.text_frame.paragraphs:
rendered = render_paragraph(para)
if rendered:
text_parts.append(rendered)
if text_parts:
content = "".join(text_parts)
fill = get_shape_fill(shape)
fill_style = f"background-color:{fill};padding:1em;border-radius:8px;" if fill else ""
elements.append(
f'<div style="{pos_style};{fill_style}overflow:hidden;display:flex;flex-direction:column;justify-content:center">'
f'{content}</div>'
)
continue
# Decorative shape with fill
fill = get_shape_fill(shape)
if fill:
elements.append(
f'<div style="{pos_style};background-color:{fill};border-radius:4px"></div>'
)
slide_content = "\n".join(elements)
slides_html.append(
f'<section class="slide" style="{bg_style}">\n<div class="slide-inner">\n{slide_content}\n</div>\n</section>'
)
title = "Presentation"
if prs.slides:
for shape in prs.slides[0].shapes:
if hasattr(shape, "text") and shape.text.strip() and len(shape.text.strip()) < 150:
title = shape.text.strip()
break
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;
overflow: hidden;
}}
.slide.active {{ display: flex; }}
.slide-inner {{
position: relative;
width: 1280px; height: {int(1280 / aspect_ratio)}px;
transform-origin: center center;
flex-shrink: 0;
}}
.progress {{ position: fixed; bottom: 0; left: 0; height: 4px; background: #0366d6; transition: width 0.3s; z-index: 100; }}
.counter {{ position: fixed; bottom: 12px; right: 20px; font-size: 14px; color: rgba(255,255,255,0.4); z-index: 100; }}
</style>
</head>
<body>
{chr(10).join(slides_html)}
<div class="progress" id="progress"></div>
<div class="counter" id="counter"></div>
<script>
const slides = document.querySelectorAll('.slide');
let current = 0;
function show(n) {{
slides.forEach(s => s.classList.remove('active'));
current = Math.max(0, Math.min(n, slides.length - 1));
slides[current].classList.add('active');
document.getElementById('progress').style.width = ((current + 1) / slides.length * 100) + '%';
document.getElementById('counter').textContent = (current + 1) + ' / ' + slides.length;
}}
document.addEventListener('keydown', e => {{
if (e.key === 'ArrowRight' || e.key === ' ') {{ e.preventDefault(); show(current + 1); }}
if (e.key === 'ArrowLeft') {{ e.preventDefault(); show(current - 1); }}
}});
let touchStartX = 0;
document.addEventListener('touchstart', e => {{ touchStartX = e.changedTouches[0].screenX; }});
document.addEventListener('touchend', e => {{
const diff = e.changedTouches[0].screenX - touchStartX;
if (Math.abs(diff) > 50) {{ diff > 0 ? show(current - 1) : show(current + 1); }}
}});
document.addEventListener('click', e => {{
if (e.clientX > window.innerWidth / 2) show(current + 1);
else show(current - 1);
}});
show(0);
function scaleSlides() {{
document.querySelectorAll('.slide-inner').forEach(inner => {{
const scaleX = window.innerWidth / inner.offsetWidth;
const scaleY = window.innerHeight / inner.offsetHeight;
const scale = Math.min(scaleX, scaleY);
inner.style.transform = 'scale(' + scale + ')';
}});
}}
window.addEventListener('resize', scaleSlides);
scaleSlides();
</script>
</body></html>'''
Path(output).write_text(html, encoding='utf-8')
output_size = os.path.getsize(output)
# Summary
print(f"Converted to: {output}")
print(f"Slides: {len(slides_html)}")
print(f"Images: {img_counter}")
print(f"Output size: {output_size / (1024*1024):.1f}MB")
print(f"External assets: {'yes' if external_assets else 'no'}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Convert PPTX to HTML presentation")
parser.add_argument("input", help="Path to .pptx file")
parser.add_argument("output", nargs="?", help="Output HTML path (default: same name with .html)")
parser.add_argument("--external-assets", action="store_true", default=None,
help="Save images as separate files in assets/ directory (auto-detected for large files)")
parser.add_argument("--no-external-assets", action="store_true",
help="Force inline base64 even for large files")
args = parser.parse_args()
ext_assets = None # auto-detect
if args.external_assets:
ext_assets = True
elif args.no_external_assets:
ext_assets = False
convert(args.input, args.output, external_assets=ext_assets)
#!/bin/bash
# Main publish script
# Args: $1 = path to index.html, $2 = repo name, $3 = visibility (private|public), $4 = description
set -euo pipefail
HTML_FILE="$1"
REPO_NAME="$2"
VISIBILITY="${3:-public}"
DESCRIPTION="${4:-Published via publish-to-pages}"
USERNAME=$(gh api user --jq '.login')
# Check if repo exists
if gh repo view "$USERNAME/$REPO_NAME" &>/dev/null; then
echo "ERROR: Repository $USERNAME/$REPO_NAME already exists"
exit 1
fi
# Create repo
gh repo create "$REPO_NAME" --"$VISIBILITY" --description "$DESCRIPTION"
# Clone, push, enable pages
TMPDIR=$(mktemp -d)
git clone "https://github.com/$USERNAME/$REPO_NAME.git" "$TMPDIR"
HTML_DIR=$(dirname "$HTML_FILE")
# Copy HTML file as index.html
cp "$HTML_FILE" "$TMPDIR/index.html"
# Copy assets directory if it exists alongside the HTML file
if [ -d "$HTML_DIR/assets" ]; then
cp -r "$HTML_DIR/assets" "$TMPDIR/assets"
echo "Copied assets/ directory ($(find "$HTML_DIR/assets" -type f | wc -l) files)"
fi
cd "$TMPDIR"
git add -A
git commit -m "Publish content"
git push origin main
# Enable GitHub Pages
gh api "repos/$USERNAME/$REPO_NAME/pages" -X POST -f source[branch]=main -f source[path]=/ 2>/dev/null || true
echo "REPO_URL=https://github.com/$USERNAME/$REPO_NAME"
echo "PAGES_URL=https://$USERNAME.github.io/$REPO_NAME/"
echo ""
echo "GitHub Pages may take 1-2 minutes to deploy."
# Cleanup
rm -rf "$TMPDIR"
Related skills
FAQ
Who is Publish To Pages for?
Developers and software engineers working with publish-to-pages patterns from the skill documentation.
When should I use Publish To Pages?
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 t
Is Publish To Pages safe to install?
Review the Security Audits panel on this page before installing in production.