diff --git a/scripts/shared-utils.ts b/scripts/shared-utils.ts index 7516579..e37ce75 100644 --- a/scripts/shared-utils.ts +++ b/scripts/shared-utils.ts @@ -7,6 +7,7 @@ import fs from "node:fs"; import path from "node:path"; import https from "node:https"; +import sharp from "sharp"; // Re-export all parsing functions from the shared module export { @@ -44,6 +45,14 @@ function contentTypeToExt(contentType: string): string { return ".png"; } +/** Convert an SVG file to PNG in-place, returns the new filepath */ +async function convertSvgToPng(svgPath: string): Promise { + const pngPath = svgPath.replace(/\.svg$/i, ".png"); + await sharp(svgPath, { density: 144 }).png().toFile(pngPath); + fs.unlinkSync(svgPath); + return pngPath; +} + /** Download an image from a URL to a local file. Returns the detected content-type. */ export async function downloadImage( url: string, @@ -80,7 +89,7 @@ export async function downloadImage( filepath, redirectCount + 1, ) - .then(resolve) + .then((ct) => resolve(ct)) .catch(reject); } @@ -198,30 +207,38 @@ export async function processImages( return { filename, publicPath }; } - // Process banner image — saved as {slug}/banner.ext + // Process banner image — saved as {slug}/banner.png (converted from SVG if needed) if (bannerImages.length > 0) { const img = bannerImages[0]; const urlExt = img.url.match(/\.(png|jpg|jpeg|gif|svg|webp)/i)?.[0]; const tempPath = path.join(imagesDir, `banner_tmp`); const detectedType = await downloadImage(img.url, tempPath); - const ext = urlExt || contentTypeToExt(detectedType); - const filename = `banner${ext}`; - fs.renameSync(tempPath, path.join(imagesDir, filename)); - banner = `/content-images/${contentType}/${slug}/${filename}`; - console.log(` ✓ ${filename} (banner)`); + let ext = urlExt || contentTypeToExt(detectedType); + let finalPath = path.join(imagesDir, `banner${ext}`); + fs.renameSync(tempPath, finalPath); + if (ext === ".svg") { + finalPath = await convertSvgToPng(finalPath); + ext = ".png"; + } + banner = `/content-images/${contentType}/${slug}/banner${ext}`; + console.log(` ✓ banner${ext} (banner)`); } - // Process logo image — saved as {slug}/logo.ext + // Process logo image — saved as {slug}/logo.png (converted from SVG if needed) if (logoImages.length > 0) { const img = logoImages[0]; const urlExt = img.url.match(/\.(png|jpg|jpeg|gif|svg|webp)/i)?.[0]; const tempPath = path.join(imagesDir, `logo_tmp`); const detectedType = await downloadImage(img.url, tempPath); - const ext = urlExt || contentTypeToExt(detectedType); - const filename = `logo${ext}`; - fs.renameSync(tempPath, path.join(imagesDir, filename)); - logo = `/content-images/${contentType}/${slug}/${filename}`; - console.log(` ✓ ${filename} (logo)`); + let ext = urlExt || contentTypeToExt(detectedType); + let finalPath = path.join(imagesDir, `logo${ext}`); + fs.renameSync(tempPath, finalPath); + if (ext === ".svg") { + finalPath = await convertSvgToPng(finalPath); + ext = ".png"; + } + logo = `/content-images/${contentType}/${slug}/logo${ext}`; + console.log(` ✓ logo${ext} (logo)`); } // Process description images diff --git a/src/app/preview/page.tsx b/src/app/preview/page.tsx index 45e3cd3..3827e95 100644 --- a/src/app/preview/page.tsx +++ b/src/app/preview/page.tsx @@ -1,6 +1,11 @@ +import type { Metadata } from "next"; import { Suspense } from "react"; import PreviewForm from "./PreviewForm"; +export const metadata: Metadata = { + robots: { index: false, follow: false }, +}; + export default function PreviewPage() { return (
diff --git a/src/lib/parse-issue.ts b/src/lib/parse-issue.ts index d31584f..eabe967 100644 --- a/src/lib/parse-issue.ts +++ b/src/lib/parse-issue.ts @@ -95,7 +95,7 @@ export function parseSection(markdown: string, sectionName: string): string { sectionName === "Description" ? DESC_STOP : "\\n## [^#]|---|$"; const section = markdown.match( new RegExp( - `## ${sectionName}\\s+(?:\\s*)?([\\s\\S]*?)(?=${stop})`, + `## ${sectionName}\\s+(?:[ \\t]*)?([\\s\\S]*?)(?=${stop})`, ), ); return section ? section[1].trim() : ""; @@ -105,7 +105,7 @@ export function parseSection(markdown: string, sectionName: string): string { export function parseList(markdown: string, sectionName: string): string[] { const section = markdown.match( new RegExp( - `## ${sectionName}\\s+(?:\\s*)?([\\s\\S]*?)(?=\\n## [^#]|---|$)`, + `## ${sectionName}\\s+(?:[ \\t]*)?([\\s\\S]*?)(?=\\n## [^#]|---|$)`, ), ); if (!section) return []; @@ -150,7 +150,7 @@ export function extractFirstImage( ): string { const section = markdown.match( new RegExp( - `## ${sectionName}\\s+(?:\\s*)?([\\s\\S]*?)(?=\\n## [^#]|$)`, + `## ${sectionName}\\s+(?:[ \\t]*)?([\\s\\S]*?)(?=\\n## [^#]|$)`, ), ); if (!section) return ""; @@ -169,18 +169,18 @@ export function extractImagesBySections(issueBody: string) { const descriptionImages: ParsedImage[] = []; const bannerSection = issueBody.match( - /## Banner Image\s+(?:\s*)?([\s\S]*?)(?=\n## [^#]|$)/, + /## Banner Image\s+(?:[ \t]*)?([\s\S]*?)(?=\n## [^#]|$)/, ); if (bannerSection) bannerImages.push(...extractImages(bannerSection[1])); const logoSection = issueBody.match( - /## Logo\s+(?:\s*)?([\s\S]*?)(?=\n## [^#]|$)/, + /## Logo\s+(?:[ \t]*)?([\s\S]*?)(?=\n## [^#]|$)/, ); if (logoSection) logoImages.push(...extractImages(logoSection[1])); const descSection = issueBody.match( new RegExp( - `## Description\\s+(?:\\s*)?([\\s\\S]*?)(?=${DESC_STOP})`, + `## Description\\s+(?:[ \\t]*)?([\\s\\S]*?)(?=${DESC_STOP})`, ), ); if (descSection) descriptionImages.push(...extractImages(descSection[1]));