{"record":{"id":"f7573b3d7efdbfaf","repo":"zarazhangrui/frontend-slides","slug":"not-found","errorCode":null,"errorMessage":"Not found","messagePattern":"Not found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"plugins/frontend-slides/skills/frontend-slides/scripts/export-pdf.sh","lineNumber":179,"sourceCode":"  '.svg': 'image/svg+xml',\n  '.webp': 'image/webp',\n  '.woff': 'font/woff',\n  '.woff2': 'font/woff2',\n  '.ttf': 'font/ttf',\n  '.eot': 'application/vnd.ms-fontobject',\n};\n\nconst server = createServer((req, res) => {\n  // Decode URL-encoded characters (e.g., %20 → space) so filenames with spaces resolve correctly\n  const decodedUrl = decodeURIComponent(req.url);\n  let filePath = join(SERVE_DIR, decodedUrl === '/' ? HTML_FILE : decodedUrl);\n  try {\n    const content = readFileSync(filePath);\n    const ext = extname(filePath).toLowerCase();\n    res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });\n    res.end(content);\n  } catch {\n    res.writeHead(404);\n    res.end('Not found');\n  }\n});\n\n// Find a free port\nconst port = await new Promise((resolve) => {\n  server.listen(0, () => resolve(server.address().port));\n});\n\nconsole.log(`  Local server on port ${port}`);\n\n// ─── Screenshot each slide ────────────────────────────────\n\nconst browser = await chromium.launch();\nconst page = await browser.newPage({\n  viewport: { width: VP_WIDTH, height: VP_HEIGHT },\n});\n","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/zarazhangrui/frontend-slides/blob/9906a34d640d2111f724544cbc50f7f130569ae1/plugins/frontend-slides/skills/frontend-slides/scripts/export-pdf.sh#L161-L197","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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","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","Fix case-sensitive filename mismatches and remove or encode '#'/'?' characters in asset filenames","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","Verify by curling the running server: curl -i http://localhost:<port>/<asset-path> to reproduce the 404 and confirm the resolved path"],"exampleFix":"// before (index.html references a missing asset)\n<link rel=\"stylesheet\" href=\"assets/theme.css\">\n\n// after (file present next to index.html, or inlined)\n<!-- ensure presentation/index.html AND presentation/assets/theme.css exist -->\n<link rel=\"stylesheet\" href=\"assets/theme.css\">","handlingStrategy":"validation","validationCode":"import { existsSync } from 'fs';\nimport { join, dirname } from 'path';\n// Before exporting, verify every local asset referenced by the HTML exists\n// relative to the HTML file's directory:\nconst htmlDir = dirname(htmlPath);\nconst refs = [...html.matchAll(/(?:src|href)=[\"']([^\"']+)[\"']/g)]\n  .map(m => m[1])\n  .filter(r => !/^(https?:|data:|#|\\/)/.test(r));\nconst missing = refs.filter(r => !existsSync(join(htmlDir, r)));\nif (missing.length) throw new Error(`Assets missing for export: ${missing.join(', ')}`);","typeGuard":"function isServableAsset(htmlDir, url) {\n  const decoded = decodeURIComponent(url);\n  if (decoded.startsWith('/') || decoded.includes('..')) return false;\n  return existsSync(join(htmlDir, decoded));\n}","tryCatchPattern":"const server = createServer((req, res) => {\n  try {\n    const content = readFileSync(join(SERVE_DIR, decodeURIComponent(req.url)));\n    res.writeHead(200); res.end(content);\n  } catch (e) {\n    console.error(`404 for ${req.url}: ${e.message}`); // log which asset broke the export\n    res.writeHead(404); res.end('Not found');\n  }\n});","preventionTips":["Keep the HTML file and all its assets in the same folder tree; never move the HTML without its assets/ directory","Open the deck in a real browser with the Network tab before exporting and confirm zero 404s","Avoid '#' and '?' in asset filenames; URL-unsafe characters break naive path joining","On case-sensitive filesystems, match asset filename case exactly as referenced in the HTML","Watch the export run's console — add per-request logging to the embedded server so missing assets are named, not silent"],"tags":["http-404","static-file-server","playwright","missing-asset","bash"],"backgroundTag":"http-404-asset-not-found","analyzedSha":"9906a34d640d2111f724544cbc50f7f130569ae1","analyzedAt":"2026-08-29T08:54:05.395Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}