From 01f89d75fcaae32a9b23344d8f7093d24a25d431 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E9=9F=A9=E8=B4=A4=E6=B6=9B?= <601803023@qq.com>
Date: Thu, 2 Jul 2026 08:32:20 +0800
Subject: [PATCH] feat: inline local images as data URLs for standalone HTML
deploys
---
.gitignore | 1 +
README.md | 2 +
SKILL.md | 2 +-
html-template.md | 4 +-
.../skills/frontend-slides/SKILL.md | 2 +-
.../skills/frontend-slides/html-template.md | 4 +-
.../skills/frontend-slides/scripts/deploy.sh | 216 +++++++++++++++---
scripts/deploy.sh | 216 +++++++++++++++---
8 files changed, 387 insertions(+), 60 deletions(-)
diff --git a/.gitignore b/.gitignore
index a0f52f66..17ac76ce 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
node_modules/
.vercel/
*.log
+.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
index bd4bd929..67e26574 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/SKILL.md b/SKILL.md
index 9b8374bf..15a19f9e 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -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.
diff --git a/html-template.md b/html-template.md
index 511aa9b9..98c02e60 100644
--- a/html-template.md
+++ b/html-template.md
@@ -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
@@ -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:
diff --git a/plugins/frontend-slides/skills/frontend-slides/SKILL.md b/plugins/frontend-slides/skills/frontend-slides/SKILL.md
index 9b8374bf..15a19f9e 100644
--- a/plugins/frontend-slides/skills/frontend-slides/SKILL.md
+++ b/plugins/frontend-slides/skills/frontend-slides/SKILL.md
@@ -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.
diff --git a/plugins/frontend-slides/skills/frontend-slides/html-template.md b/plugins/frontend-slides/skills/frontend-slides/html-template.md
index 511aa9b9..98c02e60 100644
--- a/plugins/frontend-slides/skills/frontend-slides/html-template.md
+++ b/plugins/frontend-slides/skills/frontend-slides/html-template.md
@@ -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
@@ -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:
diff --git a/plugins/frontend-slides/skills/frontend-slides/scripts/deploy.sh b/plugins/frontend-slides/skills/frontend-slides/scripts/deploy.sh
index a5f79f2b..86442146 100755
--- a/plugins/frontend-slides/skills/frontend-slides/scripts/deploy.sh
+++ b/plugins/frontend-slides/skills/frontend-slides/scripts/deploy.sh
@@ -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.
@@ -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
@@ -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
@@ -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)
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
index a5f79f2b..86442146 100755
--- a/scripts/deploy.sh
+++ b/scripts/deploy.sh
@@ -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.
@@ -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
@@ -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
@@ -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)