Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
node_modules/
.vercel/
*.log
.idea/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,8 @@ bash scripts/deploy.sh ./my-deck/
bash scripts/deploy.sh ./presentation.html
```

When you deploy a standalone `.html` file, the script rewrites local image references to `data:` URLs first, so the shared output is self-contained. Remote `http(s)` images stay unchanged.

Uses [Vercel](https://vercel.com) (free tier). The skill walks you through signup and login if it's your first time.

### Export to PDF
Expand Down
2 changes: 1 addition & 1 deletion SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ This deploys the presentation to Vercel — a free hosting platform. The link wo

**⚠ Deployment gotchas:**

- **Local images/videos must travel with the HTML.** The deploy script auto-detects files referenced via `src="..."` in the HTML and bundles them. But if the presentation references files via CSS `background-image` or unusual paths, those may be missed. **Before deploying, verify:** open the deployed URL and check that all images load. If any are broken, the safest fix is to put the HTML and all its assets into a single folder and deploy the folder instead of a standalone HTML file.
- **Standalone HTML deployments inline local images automatically.** When you run `bash scripts/deploy.sh ./presentation.html`, the script converts local image refs in the HTML into `data:` URLs before upload. Remote `http(s)` images stay unchanged. Other local assets like CSS, JS, videos, or unusual file refs are still copied separately, so **before deploying, verify:** open the deployed URL and check that everything loads. If anything is broken, the safest fix is to put the HTML and all its assets into a single folder and deploy the folder instead of a standalone HTML file.
- **Prefer folder deployments when the presentation has many assets.** If the presentation lives in a folder with images alongside it (e.g., `my-deck/index.html` + `my-deck/logo.png`), deploy the folder directly: `bash scripts/deploy.sh ./my-deck/`. This is more reliable than deploying a single HTML file because the entire folder contents are uploaded as-is.
- **Filenames with spaces work but can cause issues.** The script handles spaces in filenames, but Vercel URLs encode spaces as `%20`. If possible, avoid spaces in image filenames. If the user's images have spaces, the script handles it — but if images still break, renaming files to use hyphens instead of spaces is the fix.
- **Redeploying updates the same URL.** Running the deploy script again on the same presentation overwrites the previous deployment. The URL stays the same — no need to share a new link.
Expand Down
4 changes: 2 additions & 2 deletions html-template.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Save processed images with `_processed` suffix. Never overwrite originals.

### Image Placement

**Use direct file paths** (not base64) — presentations are viewed locally:
**Use direct file paths during authoring** — presentations are edited locally, and `scripts/deploy.sh ./presentation.html` will inline local image refs into `data:` URLs when you share a standalone HTML file:

```html
<img src="assets/logo_round.png" alt="Logo" class="slide-image logo">
Expand Down Expand Up @@ -340,7 +340,7 @@ Save processed images with `_processed` suffix. Never overwrite originals.
Single presentations:
```
presentation.html # Self-contained, all CSS/JS inline
assets/ # Images only, if any
assets/ # Optional authoring-time images, if any
```

Multiple presentations in one project:
Expand Down
2 changes: 1 addition & 1 deletion plugins/frontend-slides/skills/frontend-slides/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ This deploys the presentation to Vercel — a free hosting platform. The link wo

**⚠ Deployment gotchas:**

- **Local images/videos must travel with the HTML.** The deploy script auto-detects files referenced via `src="..."` in the HTML and bundles them. But if the presentation references files via CSS `background-image` or unusual paths, those may be missed. **Before deploying, verify:** open the deployed URL and check that all images load. If any are broken, the safest fix is to put the HTML and all its assets into a single folder and deploy the folder instead of a standalone HTML file.
- **Standalone HTML deployments inline local images automatically.** When you run `bash scripts/deploy.sh ./presentation.html`, the script converts local image refs in the HTML into `data:` URLs before upload. Remote `http(s)` images stay unchanged. Other local assets like CSS, JS, videos, or unusual file refs are still copied separately, so **before deploying, verify:** open the deployed URL and check that everything loads. If anything is broken, the safest fix is to put the HTML and all its assets into a single folder and deploy the folder instead of a standalone HTML file.
- **Prefer folder deployments when the presentation has many assets.** If the presentation lives in a folder with images alongside it (e.g., `my-deck/index.html` + `my-deck/logo.png`), deploy the folder directly: `bash scripts/deploy.sh ./my-deck/`. This is more reliable than deploying a single HTML file because the entire folder contents are uploaded as-is.
- **Filenames with spaces work but can cause issues.** The script handles spaces in filenames, but Vercel URLs encode spaces as `%20`. If possible, avoid spaces in image filenames. If the user's images have spaces, the script handles it — but if images still break, renaming files to use hyphens instead of spaces is the fix.
- **Redeploying updates the same URL.** Running the deploy script again on the same presentation overwrites the previous deployment. The URL stays the same — no need to share a new link.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Save processed images with `_processed` suffix. Never overwrite originals.

### Image Placement

**Use direct file paths** (not base64) — presentations are viewed locally:
**Use direct file paths during authoring** — presentations are edited locally, and `scripts/deploy.sh ./presentation.html` will inline local image refs into `data:` URLs when you share a standalone HTML file:

```html
<img src="assets/logo_round.png" alt="Logo" class="slide-image logo">
Expand Down Expand Up @@ -340,7 +340,7 @@ Save processed images with `_processed` suffix. Never overwrite originals.
Single presentations:
```
presentation.html # Self-contained, all CSS/JS inline
assets/ # Images only, if any
assets/ # Optional authoring-time images, if any
```

Multiple presentations in one project:
Expand Down
216 changes: 189 additions & 27 deletions plugins/frontend-slides/skills/frontend-slides/scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
# What this does:
# 1. Checks if Vercel CLI is installed (installs if not)
# 2. Checks if user is logged in (guides through login if not)
# 3. Deploys the slide deck to a public URL
# 4. Prints the live URL
# 3. Inlines local image refs for standalone HTML inputs
# 4. Deploys the slide deck to a public URL
# 5. Prints the live URL
#
# The deployed URL is permanent and works on any device (mobile, tablet, desktop).
# No server to maintain — Vercel hosts it for free.
Expand All @@ -31,6 +32,163 @@ ok() { echo -e "${GREEN}✓${NC} $*"; }
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
err() { echo -e "${RED}✗${NC} $*" >&2; }

inline_local_images() {
local html_file="$1"
local base_dir="$2"

node - "$html_file" "$base_dir" <<'NODE'
const fs = require('fs');
const path = require('path');
const { fileURLToPath } = require('url');

const htmlFile = process.argv[2];
const baseDir = process.argv[3];
let html = fs.readFileSync(htmlFile, 'utf8');
let replacements = 0;

const mimeByExt = {
'.apng': 'image/apng',
'.avif': 'image/avif',
'.bmp': 'image/bmp',
'.gif': 'image/gif',
'.ico': 'image/x-icon',
'.jpeg': 'image/jpeg',
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.tif': 'image/tiff',
'.tiff': 'image/tiff',
'.webp': 'image/webp',
};

const cache = new Map();

function stripSuffixes(ref) {
return ref.replace(/[?#].*$/, '');
}

function isRemoteReference(ref) {
return /^(?:https?:)?\/\//i.test(ref)
|| /^(?:data:|mailto:|tel:|javascript:|#)/i.test(ref);
}

function resolveLocalFile(ref) {
if (!ref) return null;

const trimmed = ref.trim();
if (!trimmed) return null;

if (/^file:\/\//i.test(trimmed)) {
try {
return fileURLToPath(trimmed);
} catch {
return null;
}
}

if (isRemoteReference(trimmed) || trimmed.startsWith('/')) {
return null;
}

return path.resolve(baseDir, stripSuffixes(trimmed));
}

function getDataUrl(ref) {
if (cache.has(ref)) {
return cache.get(ref);
}

const resolved = resolveLocalFile(ref);
if (!resolved) {
cache.set(ref, null);
return null;
}

let stats;
try {
stats = fs.statSync(resolved);
} catch {
cache.set(ref, null);
return null;
}

if (!stats.isFile()) {
cache.set(ref, null);
return null;
}

const ext = path.extname(stripSuffixes(ref)).toLowerCase();
const mime = mimeByExt[ext];
if (!mime) {
cache.set(ref, null);
return null;
}

const dataUrl = `data:${mime};base64,${fs.readFileSync(resolved).toString('base64')}`;
cache.set(ref, dataUrl);
return dataUrl;
}

function replaceAttribute(attrName) {
const regex = new RegExp(`(${attrName}\\s*=\\s*)(["'])([^"']+?)\\2`, 'gi');
html = html.replace(regex, (match, prefix, quote, value) => {
const dataUrl = getDataUrl(value);
if (!dataUrl) {
return match;
}
replacements += 1;
return `${prefix}${quote}${dataUrl}${quote}`;
});
}

function replaceSrcset() {
html = html.replace(/(srcset\s*=\s*)(["'])([^"']+?)\2/gi, (match, prefix, quote, value) => {
let changed = false;
const rewritten = value
.split(',')
.map((candidate) => {
const trimmed = candidate.trim();
if (!trimmed) {
return candidate;
}

const [url, ...descriptorParts] = trimmed.split(/\s+/);
const dataUrl = getDataUrl(url);
if (!dataUrl) {
return candidate;
}

replacements += 1;
changed = true;
const descriptor = descriptorParts.length ? ` ${descriptorParts.join(' ')}` : '';
return `${dataUrl}${descriptor}`;
})
.join(', ');

return changed ? `${prefix}${quote}${rewritten}${quote}` : match;
});
}

function replaceCssUrls() {
html = html.replace(/(url\(\s*)(['"]?)([^"')]+?)\2(\s*\))/gi, (match, prefix, quote, value, suffix) => {
const dataUrl = getDataUrl(value);
if (!dataUrl) {
return match;
}
replacements += 1;
return `${prefix}${quote}${dataUrl}${quote}${suffix}`;
});
}

['src', 'poster', 'href'].forEach(replaceAttribute);
replaceSrcset();
replaceCssUrls();

fs.writeFileSync(htmlFile, html);
process.stdout.write(String(replacements));
NODE
}

# ─── Input validation ─────────────────────────────────────

if [[ $# -lt 1 ]]; then
Expand All @@ -44,23 +202,42 @@ fi

INPUT="$1"

echo ""
echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ Deploy Slides to Vercel ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
echo ""

if ! command -v node &>/dev/null || ! command -v npx &>/dev/null; then
err "Node.js and npm are required but not installed."
err ""
err "Install Node.js:"
err " macOS: brew install node"
err " or visit https://nodejs.org and download the installer"
exit 1
fi

# If input is a single HTML file, create a temp directory with it as index.html
if [[ -f "$INPUT" && "$INPUT" == *.html ]]; then
DEPLOY_DIR=$(mktemp -d)
cp "$INPUT" "$DEPLOY_DIR/index.html"
PARENT_DIR=$(dirname "$INPUT")
PARENT_DIR=$(cd "$(dirname "$INPUT")" && pwd)

INLINE_COUNT=$(inline_local_images "$DEPLOY_DIR/index.html" "$PARENT_DIR")
if [[ "$INLINE_COUNT" =~ ^[0-9]+$ && "$INLINE_COUNT" -gt 0 ]]; then
info "Inlined $INLINE_COUNT local image reference(s) into data URLs."
fi

# Parse the HTML for local file references (src="...", url('...'), href="...")
# and copy any referenced local files into the deploy directory
grep -oE '(src|href|url\()["'"'"']?[^"'"'"'>)]+' "$INPUT" 2>/dev/null | \
sed "s/^src=//; s/^href=//; s/^url(//; s/[\"']//g" | \
# Parse the rewritten HTML for remaining local file references and copy them.
grep -oE '(src|href|poster|url\()["'"'"']?[^"'"'"'>)]+' "$DEPLOY_DIR/index.html" 2>/dev/null | \
sed "s/^src=//; s/^href=//; s/^poster=//; s/^url(//; s/[\"']//g" | \
grep -v '^http' | grep -v '^data:' | grep -v '^#' | grep -v '^/' | \
sort -u | while read -r ref; do
# Resolve the reference relative to the HTML file's directory
SOURCE_FILE="$PARENT_DIR/$ref"
if [[ -e "$SOURCE_FILE" ]]; then
sort -u | while IFS= read -r ref; do
SOURCE_REF=$(printf '%s' "$ref" | sed 's/[?#].*$//')
SOURCE_FILE="$PARENT_DIR/$SOURCE_REF"
if [[ -n "$SOURCE_REF" && -e "$SOURCE_FILE" ]]; then
# Preserve directory structure for nested paths (e.g., assets/img.png)
TARGET_DIR="$DEPLOY_DIR/$(dirname "$ref")"
TARGET_DIR="$DEPLOY_DIR/$(dirname "$SOURCE_REF")"
mkdir -p "$TARGET_DIR"
cp -r "$SOURCE_FILE" "$TARGET_DIR/"
fi
Expand Down Expand Up @@ -89,21 +266,6 @@ fi

# ─── Step 1: Check for Vercel CLI ─────────────────────────

echo ""
echo -e "${BOLD}╔══════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ Deploy Slides to Vercel ║${NC}"
echo -e "${BOLD}╚══════════════════════════════════════╝${NC}"
echo ""

if ! command -v npx &>/dev/null; then
err "Node.js is required but not installed."
err ""
err "Install Node.js:"
err " macOS: brew install node"
err " or visit https://nodejs.org and download the installer"
exit 1
fi

info "Checking Vercel CLI..."

# Check if vercel is available (either globally or via npx)
Expand Down
Loading