zarazhangrui/frontend-slides · error

Not found

Error message

Not found

What it means

The thrower is not a library API but the throwaway Node HTTP static file server embedded in export-pdf.sh. For every request it tries readFileSync(join(SERVE_DIR, decodedUrl)) and, on ANY filesystem failure (missing file, EACCES, EISDIR), responds with a bare HTTP 404 and the body 'Not found'. The page itself loads because the server maps '/' to HTML_FILE, so this 404 almost always appears for relative assets (CSS/JS/images/fonts) referenced by the deck. Playwright only waits for networkidle, so a missing asset silently 404s and the PDF exports with missing styles or images.

Source

Thrown at plugins/frontend-slides/skills/frontend-slides/scripts/export-pdf.sh:179

  '.svg': 'image/svg+xml',
  '.webp': 'image/webp',
  '.woff': 'font/woff',
  '.woff2': 'font/woff2',
  '.ttf': 'font/ttf',
  '.eot': 'application/vnd.ms-fontobject',
};

const server = createServer((req, res) => {
  // Decode URL-encoded characters (e.g., %20 → space) so filenames with spaces resolve correctly
  const decodedUrl = decodeURIComponent(req.url);
  let filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl);
  try {
    const content = readFileSync(filePath);
    const ext = extname(filePath).toLowerCase();
    res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });
    res.end(content);
  } catch {
    res.writeHead(404);
    res.end('Not found');
  }
});

// Find a free port
const port = await new Promise((resolve) => {
  server.listen(0, () => resolve(server.address().port));
});

console.log(`  Local server on port ${port}`);

// ─── Screenshot each slide ────────────────────────────────

const browser = await chromium.launch();
const page = await browser.newPage({
  viewport: { width: VP_WIDTH, height: VP_HEIGHT },
});

View on GitHub (pinned to 9906a34d64)

Solutions

  1. Open the deck in a browser with devtools Network tab (or check the headless run) and identify which asset URL returned 404, then make that file exist relative to the HTML file's directory
  2. Move or copy the full presentation folder (HTML plus all assets) so the HTML and its referenced files live together; re-run export-pdf.sh on the HTML in place
  3. Fix case-sensitive filename mismatches and remove or encode '#'/'?' characters in asset filenames
  4. If assets intentionally live outside the served folder, inline them (data: URIs) or use absolute https URLs so the local server is not asked for them
  5. Verify by curling the running server: curl -i http://localhost:<port>/<asset-path> to reproduce the 404 and confirm the resolved path

Example fix

// before (index.html references a missing asset)
<link rel="stylesheet" href="assets/theme.css">

// after (file present next to index.html, or inlined)
<!-- ensure presentation/index.html AND presentation/assets/theme.css exist -->
<link rel="stylesheet" href="assets/theme.css">
Defensive patterns

Strategy: validation

Validate before calling

import { existsSync } from 'fs';
import { join, dirname } from 'path';
// Before exporting, verify every local asset referenced by the HTML exists
// relative to the HTML file's directory:
const htmlDir = dirname(htmlPath);
const refs = [...html.matchAll(/(?:src|href)=["']([^"']+)["']/g)]
  .map(m => m[1])
  .filter(r => !/^(https?:|data:|#|\/)/.test(r));
const missing = refs.filter(r => !existsSync(join(htmlDir, r)));
if (missing.length) throw new Error(`Assets missing for export: ${missing.join(', ')}`);

Type guard

function isServableAsset(htmlDir, url) {
  const decoded = decodeURIComponent(url);
  if (decoded.startsWith('/') || decoded.includes('..')) return false;
  return existsSync(join(htmlDir, decoded));
}

Try / catch

const server = createServer((req, res) => {
  try {
    const content = readFileSync(join(SERVE_DIR, decodeURIComponent(req.url)));
    res.writeHead(200); res.end(content);
  } catch (e) {
    console.error(`404 for ${req.url}: ${e.message}`); // log which asset broke the export
    res.writeHead(404); res.end('Not found');
  }
});

Prevention

When it happens

Trigger: The deck's index.html references a relative asset (e.g. ./assets/logo.png, styles.css, Google-Fonts-fallback local files) that does not exist under SERVE_DIR (the directory containing the HTML); a path contains URL-encoded characters the join/decode mishandles (e.g. '#' in a filename truncating the URL, or an absolute path like /foo.css joining outside SERVE_DIR); or the request resolves to a directory (readFileSync throws EISDIR).

Common situations: Running export-pdf.sh on an HTML file that lives outside its asset folder (file moved without its assets/ directory); typos in asset filenames or case-mismatch on case-sensitive filesystems (Logo.PNG vs logo.png); references to files with spaces or special characters; decks that load assets from a sibling directory via ../ paths that escape SERVE_DIR.

Related errors


AI-assisted analysis of zarazhangrui/frontend-slides@9906a34d64 (2026-08-29). Data as JSON: /api/errors/f7573b3d7efdbfaf. Report an issue: GitHub.