@ox-content/vite-plugin 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +425 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +425 -21
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -7006,7 +7006,7 @@ async function loadNapiBindings() {
|
|
|
7006
7006
|
async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
7007
7007
|
const napi = await loadNapiBindings();
|
|
7008
7008
|
if (!napi) throw new Error("[ox-content] NAPI bindings not available. Please ensure @ox-content/napi is built.");
|
|
7009
|
-
const { content: markdownContent, frontmatter } = parseFrontmatter(source);
|
|
7009
|
+
const { content: markdownContent, frontmatter } = parseFrontmatter$1(source);
|
|
7010
7010
|
const result = napi.transform(markdownContent, {
|
|
7011
7011
|
gfm: options.gfm,
|
|
7012
7012
|
footnotes: options.footnotes,
|
|
@@ -7041,7 +7041,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
7041
7041
|
* Parses YAML frontmatter from Markdown content.
|
|
7042
7042
|
* Uses proper YAML parser for full nested object support.
|
|
7043
7043
|
*/
|
|
7044
|
-
function parseFrontmatter(source) {
|
|
7044
|
+
function parseFrontmatter$1(source) {
|
|
7045
7045
|
if (!source.startsWith("---")) return {
|
|
7046
7046
|
content: source,
|
|
7047
7047
|
frontmatter: {}
|
|
@@ -7981,13 +7981,16 @@ function resolveDocsOptions(options) {
|
|
|
7981
7981
|
//#endregion
|
|
7982
7982
|
//#region src/og-image/renderer.ts
|
|
7983
7983
|
/**
|
|
7984
|
+
* HTML → PNG renderer using Chromium screenshots via Playwright.
|
|
7985
|
+
*/
|
|
7986
|
+
/**
|
|
7984
7987
|
* Wraps template HTML in a minimal document with viewport locked to given dimensions.
|
|
7985
7988
|
*/
|
|
7986
|
-
function wrapHtml(bodyHtml, width, height) {
|
|
7989
|
+
function wrapHtml(bodyHtml, width, height, useBaseUrl) {
|
|
7987
7990
|
return `<!DOCTYPE html>
|
|
7988
7991
|
<html>
|
|
7989
7992
|
<head>
|
|
7990
|
-
<meta charset="UTF-8"
|
|
7993
|
+
<meta charset="UTF-8">${useBaseUrl ? `\n<base href="http://localhost/">` : ""}
|
|
7991
7994
|
<style>
|
|
7992
7995
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
7993
7996
|
html, body { width: ${width}px; height: ${height}px; overflow: hidden; }
|
|
@@ -8003,14 +8006,48 @@ html, body { width: ${width}px; height: ${height}px; overflow: hidden; }
|
|
|
8003
8006
|
* @param html - HTML string from template function
|
|
8004
8007
|
* @param width - Image width
|
|
8005
8008
|
* @param height - Image height
|
|
8009
|
+
* @param publicDir - Optional public directory for serving local assets (images, fonts, etc.)
|
|
8006
8010
|
* @returns PNG buffer
|
|
8007
8011
|
*/
|
|
8008
|
-
async function renderHtmlToPng(page, html, width, height) {
|
|
8012
|
+
async function renderHtmlToPng(page, html, width, height, publicDir) {
|
|
8009
8013
|
await page.setViewportSize({
|
|
8010
8014
|
width,
|
|
8011
8015
|
height
|
|
8012
8016
|
});
|
|
8013
|
-
|
|
8017
|
+
if (publicDir) {
|
|
8018
|
+
const fs = await import("fs/promises");
|
|
8019
|
+
await page.route("**/*", async (route) => {
|
|
8020
|
+
const url = new URL(route.request().url());
|
|
8021
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
8022
|
+
await route.continue();
|
|
8023
|
+
return;
|
|
8024
|
+
}
|
|
8025
|
+
const filePath = path.join(publicDir, url.pathname);
|
|
8026
|
+
try {
|
|
8027
|
+
const body = await fs.readFile(filePath);
|
|
8028
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
8029
|
+
await route.fulfill({
|
|
8030
|
+
body,
|
|
8031
|
+
contentType: {
|
|
8032
|
+
".svg": "image/svg+xml",
|
|
8033
|
+
".png": "image/png",
|
|
8034
|
+
".jpg": "image/jpeg",
|
|
8035
|
+
".jpeg": "image/jpeg",
|
|
8036
|
+
".gif": "image/gif",
|
|
8037
|
+
".webp": "image/webp",
|
|
8038
|
+
".woff": "font/woff",
|
|
8039
|
+
".woff2": "font/woff2",
|
|
8040
|
+
".ttf": "font/ttf",
|
|
8041
|
+
".css": "text/css",
|
|
8042
|
+
".js": "application/javascript"
|
|
8043
|
+
}[ext] || "application/octet-stream"
|
|
8044
|
+
});
|
|
8045
|
+
} catch {
|
|
8046
|
+
await route.continue();
|
|
8047
|
+
}
|
|
8048
|
+
});
|
|
8049
|
+
}
|
|
8050
|
+
const fullHtml = wrapHtml(html, width, height, !!publicDir);
|
|
8014
8051
|
await page.setContent(fullHtml, { waitUntil: "networkidle" });
|
|
8015
8052
|
const screenshot = await page.screenshot({
|
|
8016
8053
|
type: "png",
|
|
@@ -8050,10 +8087,10 @@ async function openBrowser() {
|
|
|
8050
8087
|
]
|
|
8051
8088
|
});
|
|
8052
8089
|
return {
|
|
8053
|
-
async renderPage(html, width, height) {
|
|
8090
|
+
async renderPage(html, width, height, publicDir) {
|
|
8054
8091
|
const page = await browser.newPage();
|
|
8055
8092
|
try {
|
|
8056
|
-
return await renderHtmlToPng(page, html, width, height);
|
|
8093
|
+
return await renderHtmlToPng(page, html, width, height, publicDir);
|
|
8057
8094
|
} finally {
|
|
8058
8095
|
await page.close();
|
|
8059
8096
|
}
|
|
@@ -8292,10 +8329,26 @@ async function resolveVueTemplate(templatePath, options, root) {
|
|
|
8292
8329
|
await bundle.close();
|
|
8293
8330
|
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8294
8331
|
if (!Component) throw new Error(`[ox-content:og-image] Vue template must have a default export: ${templatePath}`);
|
|
8332
|
+
let extractedCss = "";
|
|
8333
|
+
try {
|
|
8334
|
+
let compilerSfc;
|
|
8335
|
+
try {
|
|
8336
|
+
compilerSfc = await import("@vue/compiler-sfc");
|
|
8337
|
+
} catch {
|
|
8338
|
+
compilerSfc = null;
|
|
8339
|
+
}
|
|
8340
|
+
if (compilerSfc) {
|
|
8341
|
+
const sfcSource = await fs.readFile(templatePath, "utf-8");
|
|
8342
|
+
const { descriptor } = compilerSfc.parse(sfcSource, { filename: templatePath });
|
|
8343
|
+
for (const style of descriptor.styles) extractedCss += style.content;
|
|
8344
|
+
}
|
|
8345
|
+
} catch {}
|
|
8295
8346
|
const { createSSRApp } = await import("vue");
|
|
8296
8347
|
const { renderToString } = await import("vue/server-renderer");
|
|
8297
8348
|
return async (props) => {
|
|
8298
|
-
|
|
8349
|
+
const html = await renderToString(createSSRApp(Component, props));
|
|
8350
|
+
if (extractedCss) return `<style>${extractedCss}</style>${html}`;
|
|
8351
|
+
return html;
|
|
8299
8352
|
};
|
|
8300
8353
|
}
|
|
8301
8354
|
/**
|
|
@@ -8497,10 +8550,11 @@ async function generateOgImages(pages, options, root) {
|
|
|
8497
8550
|
error: "Chromium not available"
|
|
8498
8551
|
}));
|
|
8499
8552
|
const results = [];
|
|
8553
|
+
const publicDir = path.join(root, "public");
|
|
8500
8554
|
const concurrency = Math.max(1, options.concurrency);
|
|
8501
8555
|
for (let i = 0; i < pages.length; i += concurrency) {
|
|
8502
8556
|
const batch = pages.slice(i, i + concurrency);
|
|
8503
|
-
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session)));
|
|
8557
|
+
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session, publicDir)));
|
|
8504
8558
|
results.push(...batchResults);
|
|
8505
8559
|
}
|
|
8506
8560
|
return results;
|
|
@@ -8532,7 +8586,7 @@ async function tryServeAllFromCache(pages, templateSource, options, cacheDir) {
|
|
|
8532
8586
|
/**
|
|
8533
8587
|
* Renders a single page to PNG, with cache support.
|
|
8534
8588
|
*/
|
|
8535
|
-
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session) {
|
|
8589
|
+
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session, publicDir) {
|
|
8536
8590
|
const fs = await import("fs/promises");
|
|
8537
8591
|
try {
|
|
8538
8592
|
if (options.cache) {
|
|
@@ -8547,7 +8601,7 @@ async function renderSinglePage(entry, templateFn, templateSource, options, cach
|
|
|
8547
8601
|
}
|
|
8548
8602
|
}
|
|
8549
8603
|
const html = await templateFn(entry.props);
|
|
8550
|
-
const png = await session.renderPage(html, options.width, options.height);
|
|
8604
|
+
const png = await session.renderPage(html, options.width, options.height, publicDir);
|
|
8551
8605
|
await fs.mkdir(path.dirname(entry.outputPath), { recursive: true });
|
|
8552
8606
|
await fs.writeFile(entry.outputPath, png);
|
|
8553
8607
|
if (options.cache) await writeCache(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height), png);
|
|
@@ -9766,7 +9820,7 @@ function renderTemplate(template, data) {
|
|
|
9766
9820
|
/**
|
|
9767
9821
|
* Extracts title from content or frontmatter.
|
|
9768
9822
|
*/
|
|
9769
|
-
function extractTitle(content, frontmatter) {
|
|
9823
|
+
function extractTitle$1(content, frontmatter) {
|
|
9770
9824
|
if (frontmatter.title && typeof frontmatter.title === "string") return frontmatter.title;
|
|
9771
9825
|
const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
|
|
9772
9826
|
if (h1Match) return h1Match[1].trim();
|
|
@@ -9851,7 +9905,7 @@ function getOutputPath(inputPath, srcDir, outDir, extension) {
|
|
|
9851
9905
|
/**
|
|
9852
9906
|
* Converts a markdown file path to a relative URL path.
|
|
9853
9907
|
*/
|
|
9854
|
-
function getUrlPath(inputPath, srcDir) {
|
|
9908
|
+
function getUrlPath$1(inputPath, srcDir) {
|
|
9855
9909
|
const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
|
|
9856
9910
|
if (baseName === "index" || baseName.endsWith("/index")) return baseName.replace(/\/?index$/, "") || "/";
|
|
9857
9911
|
return baseName;
|
|
@@ -9860,7 +9914,7 @@ function getUrlPath(inputPath, srcDir) {
|
|
|
9860
9914
|
* Converts a markdown file path to an href.
|
|
9861
9915
|
*/
|
|
9862
9916
|
function getHref(inputPath, srcDir, base, extension) {
|
|
9863
|
-
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9917
|
+
const urlPath = getUrlPath$1(inputPath, srcDir);
|
|
9864
9918
|
if (urlPath === "/" || urlPath === "") return `${base}index${extension}`;
|
|
9865
9919
|
return `${base}${urlPath}/index${extension}`;
|
|
9866
9920
|
}
|
|
@@ -9880,7 +9934,7 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9880
9934
|
* If siteUrl is provided, returns an absolute URL (required for SNS sharing).
|
|
9881
9935
|
*/
|
|
9882
9936
|
function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
|
|
9883
|
-
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9937
|
+
const urlPath = getUrlPath$1(inputPath, srcDir);
|
|
9884
9938
|
let relativePath;
|
|
9885
9939
|
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
|
|
9886
9940
|
else relativePath = `${base}${urlPath}/og-image.png`;
|
|
@@ -9934,7 +9988,7 @@ function buildNavItems(markdownFiles, srcDir, base, extension) {
|
|
|
9934
9988
|
let groupKey = "";
|
|
9935
9989
|
if (parts.length > 1) groupKey = parts[0];
|
|
9936
9990
|
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
9937
|
-
const urlPath = getUrlPath(file, srcDir);
|
|
9991
|
+
const urlPath = getUrlPath$1(file, srcDir);
|
|
9938
9992
|
let title;
|
|
9939
9993
|
if (urlPath === "/" || urlPath === "") title = "Overview";
|
|
9940
9994
|
else title = getDisplayTitle(file);
|
|
@@ -10023,7 +10077,7 @@ async function buildSsg(options, root) {
|
|
|
10023
10077
|
transformedHtml = await transformAllPlugins(transformedHtml, pluginOptions);
|
|
10024
10078
|
if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
|
|
10025
10079
|
transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
10026
|
-
const title = extractTitle(transformedHtml, result.frontmatter);
|
|
10080
|
+
const title = extractTitle$1(transformedHtml, result.frontmatter);
|
|
10027
10081
|
const description = result.frontmatter.description;
|
|
10028
10082
|
pageResults.push({
|
|
10029
10083
|
inputPath,
|
|
@@ -10091,7 +10145,7 @@ async function buildSsg(options, root) {
|
|
|
10091
10145
|
content: transformedHtml,
|
|
10092
10146
|
toc,
|
|
10093
10147
|
frontmatter,
|
|
10094
|
-
path: getUrlPath(inputPath, srcDir),
|
|
10148
|
+
path: getUrlPath$1(inputPath, srcDir),
|
|
10095
10149
|
href: getHref(inputPath, srcDir, base, ssgOptions.extension),
|
|
10096
10150
|
entryPage
|
|
10097
10151
|
}, navItems, siteName, base, pageOgImage, ssgOptions.theme);
|
|
@@ -10390,6 +10444,353 @@ export default { search, searchOptions, loadIndex };
|
|
|
10390
10444
|
`;
|
|
10391
10445
|
}
|
|
10392
10446
|
|
|
10447
|
+
//#endregion
|
|
10448
|
+
//#region src/og-viewer.ts
|
|
10449
|
+
/**
|
|
10450
|
+
* OG Viewer - Dev tool for previewing Open Graph metadata
|
|
10451
|
+
*
|
|
10452
|
+
* Accessible at /__og-viewer during development.
|
|
10453
|
+
* Shows all pages with their OG metadata, validation warnings,
|
|
10454
|
+
* and social card previews.
|
|
10455
|
+
*/
|
|
10456
|
+
function parseFrontmatter(content) {
|
|
10457
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
10458
|
+
if (!match) return {};
|
|
10459
|
+
const yaml = match[1];
|
|
10460
|
+
const result = {};
|
|
10461
|
+
for (const line of yaml.split("\n")) {
|
|
10462
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
10463
|
+
if (!kv) continue;
|
|
10464
|
+
const [, key, rawValue] = kv;
|
|
10465
|
+
let value = rawValue.trim();
|
|
10466
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
10467
|
+
else if (typeof value === "string" && /^['"].*['"]$/.test(value)) value = value.slice(1, -1);
|
|
10468
|
+
else if (value === "true") value = true;
|
|
10469
|
+
else if (value === "false") value = false;
|
|
10470
|
+
result[key] = value;
|
|
10471
|
+
}
|
|
10472
|
+
return result;
|
|
10473
|
+
}
|
|
10474
|
+
function extractTitle(content, frontmatter) {
|
|
10475
|
+
if (typeof frontmatter.title === "string" && frontmatter.title) return frontmatter.title;
|
|
10476
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
10477
|
+
return match ? match[1].trim() : "";
|
|
10478
|
+
}
|
|
10479
|
+
function getUrlPath(filePath, srcDir) {
|
|
10480
|
+
let rel = path.relative(srcDir, filePath).replace(/\\/g, "/");
|
|
10481
|
+
rel = rel.replace(/\.md$/, "");
|
|
10482
|
+
if (rel === "index") return "/";
|
|
10483
|
+
if (rel.endsWith("/index")) rel = rel.slice(0, -6);
|
|
10484
|
+
return "/" + rel;
|
|
10485
|
+
}
|
|
10486
|
+
function computeOgImageUrl(urlPath, base, siteUrl, generateOgImage, staticOgImage) {
|
|
10487
|
+
if (!generateOgImage) return staticOgImage || "";
|
|
10488
|
+
const cleanBase = base.endsWith("/") ? base : base + "/";
|
|
10489
|
+
let relativePath;
|
|
10490
|
+
if (urlPath === "/") relativePath = `${cleanBase}og-image.png`;
|
|
10491
|
+
else relativePath = `${cleanBase}${urlPath.replace(/^\//, "")}/og-image.png`;
|
|
10492
|
+
if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
|
|
10493
|
+
return relativePath;
|
|
10494
|
+
}
|
|
10495
|
+
function validatePage(page, options) {
|
|
10496
|
+
const warnings = [];
|
|
10497
|
+
if (!page.title) warnings.push({
|
|
10498
|
+
level: "error",
|
|
10499
|
+
message: "title is missing"
|
|
10500
|
+
});
|
|
10501
|
+
else if (page.title.length > 70) warnings.push({
|
|
10502
|
+
level: "warning",
|
|
10503
|
+
message: `title is too long (${page.title.length}/70)`
|
|
10504
|
+
});
|
|
10505
|
+
if (!page.description) warnings.push({
|
|
10506
|
+
level: "warning",
|
|
10507
|
+
message: "description is missing"
|
|
10508
|
+
});
|
|
10509
|
+
else if (page.description.length > 200) warnings.push({
|
|
10510
|
+
level: "warning",
|
|
10511
|
+
message: `description is too long (${page.description.length}/200)`
|
|
10512
|
+
});
|
|
10513
|
+
if ((options.ogImage || options.ssg.generateOgImage) && !options.ssg.siteUrl) warnings.push({
|
|
10514
|
+
level: "warning",
|
|
10515
|
+
message: "ogImage enabled but siteUrl is not set"
|
|
10516
|
+
});
|
|
10517
|
+
return warnings;
|
|
10518
|
+
}
|
|
10519
|
+
async function collectPages(options, root) {
|
|
10520
|
+
const srcDir = path.resolve(root, options.srcDir);
|
|
10521
|
+
const files = await glob("**/*.md", {
|
|
10522
|
+
cwd: srcDir,
|
|
10523
|
+
absolute: true
|
|
10524
|
+
});
|
|
10525
|
+
const pages = [];
|
|
10526
|
+
const generateOgImage = options.ogImage || options.ssg.generateOgImage;
|
|
10527
|
+
for (const file of files.sort()) {
|
|
10528
|
+
const content = fs$1.readFileSync(file, "utf-8");
|
|
10529
|
+
const frontmatter = parseFrontmatter(content);
|
|
10530
|
+
if (frontmatter.layout === "entry") continue;
|
|
10531
|
+
const title = extractTitle(content, frontmatter);
|
|
10532
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
|
|
10533
|
+
const author = typeof frontmatter.author === "string" ? frontmatter.author : "";
|
|
10534
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : typeof frontmatter.tags === "string" ? [frontmatter.tags] : [];
|
|
10535
|
+
const urlPath = getUrlPath(file, srcDir);
|
|
10536
|
+
const ogImageUrl = computeOgImageUrl(urlPath, options.base, options.ssg.siteUrl, generateOgImage, options.ssg.ogImage);
|
|
10537
|
+
const page = {
|
|
10538
|
+
path: path.relative(srcDir, file),
|
|
10539
|
+
urlPath,
|
|
10540
|
+
title,
|
|
10541
|
+
description,
|
|
10542
|
+
author,
|
|
10543
|
+
tags,
|
|
10544
|
+
ogImageUrl,
|
|
10545
|
+
warnings: []
|
|
10546
|
+
};
|
|
10547
|
+
page.warnings = validatePage(page, options);
|
|
10548
|
+
pages.push(page);
|
|
10549
|
+
}
|
|
10550
|
+
return pages;
|
|
10551
|
+
}
|
|
10552
|
+
function renderViewerHtml(pages, options) {
|
|
10553
|
+
const generateOgImage = options.ogImage || options.ssg.generateOgImage;
|
|
10554
|
+
const totalWarnings = pages.reduce((sum, p) => sum + p.warnings.filter((w) => w.level === "warning").length, 0);
|
|
10555
|
+
const totalErrors = pages.reduce((sum, p) => sum + p.warnings.filter((w) => w.level === "error").length, 0);
|
|
10556
|
+
return `<!DOCTYPE html>
|
|
10557
|
+
<html lang="en">
|
|
10558
|
+
<head>
|
|
10559
|
+
<meta charset="UTF-8">
|
|
10560
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10561
|
+
<title>OG Viewer - ox-content</title>
|
|
10562
|
+
<style>
|
|
10563
|
+
:root {
|
|
10564
|
+
--bg: #ffffff;
|
|
10565
|
+
--bg-card: #f8f9fa;
|
|
10566
|
+
--bg-preview: #ffffff;
|
|
10567
|
+
--text: #1a1a2e;
|
|
10568
|
+
--text-muted: #6b7280;
|
|
10569
|
+
--border: #e5e7eb;
|
|
10570
|
+
--accent: #e8590c;
|
|
10571
|
+
--accent-light: #fff4e6;
|
|
10572
|
+
--error: #dc2626;
|
|
10573
|
+
--error-bg: #fef2f2;
|
|
10574
|
+
--warning: #d97706;
|
|
10575
|
+
--warning-bg: #fffbeb;
|
|
10576
|
+
--success: #16a34a;
|
|
10577
|
+
--tag-bg: #f0f0f0;
|
|
10578
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
10579
|
+
--radius: 8px;
|
|
10580
|
+
}
|
|
10581
|
+
@media (prefers-color-scheme: dark) {
|
|
10582
|
+
:root {
|
|
10583
|
+
--bg: #0f172a;
|
|
10584
|
+
--bg-card: #1e293b;
|
|
10585
|
+
--bg-preview: #334155;
|
|
10586
|
+
--text: #e2e8f0;
|
|
10587
|
+
--text-muted: #94a3b8;
|
|
10588
|
+
--border: #334155;
|
|
10589
|
+
--accent: #fb923c;
|
|
10590
|
+
--accent-light: #431407;
|
|
10591
|
+
--error: #f87171;
|
|
10592
|
+
--error-bg: #450a0a;
|
|
10593
|
+
--warning: #fbbf24;
|
|
10594
|
+
--warning-bg: #451a03;
|
|
10595
|
+
--success: #4ade80;
|
|
10596
|
+
--tag-bg: #334155;
|
|
10597
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10601
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
|
10602
|
+
.header { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
|
|
10603
|
+
.header svg { width: 28px; height: 28px; color: var(--accent); }
|
|
10604
|
+
.header h1 { font-size: 18px; font-weight: 600; }
|
|
10605
|
+
.header h1 span { color: var(--text-muted); font-weight: 400; }
|
|
10606
|
+
.header-actions { margin-left: auto; }
|
|
10607
|
+
.btn { padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-card); color: var(--text); cursor: pointer; font-size: 13px; transition: all 0.15s; }
|
|
10608
|
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
10609
|
+
.summary { padding: 12px 24px; display: flex; gap: 20px; border-bottom: 1px solid var(--border); font-size: 13px; color: var(--text-muted); flex-wrap: wrap; align-items: center; }
|
|
10610
|
+
.summary-item { display: flex; align-items: center; gap: 4px; }
|
|
10611
|
+
.summary-item strong { color: var(--text); }
|
|
10612
|
+
.summary-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
10613
|
+
.dot-error { background: var(--error); }
|
|
10614
|
+
.dot-warning { background: var(--warning); }
|
|
10615
|
+
.dot-success { background: var(--success); }
|
|
10616
|
+
.toolbar { padding: 12px 24px; display: flex; gap: 8px; border-bottom: 1px solid var(--border); flex-wrap: wrap; align-items: center; }
|
|
10617
|
+
.filter-btn { padding: 4px 12px; border: 1px solid var(--border); border-radius: 16px; background: transparent; color: var(--text-muted); cursor: pointer; font-size: 12px; transition: all 0.15s; }
|
|
10618
|
+
.filter-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
10619
|
+
.search-input { padding: 6px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: 13px; flex: 1; min-width: 200px; }
|
|
10620
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
10621
|
+
.container { padding: 24px; display: flex; flex-direction: column; gap: 20px; max-width: 1200px; margin: 0 auto; }
|
|
10622
|
+
.card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-card); box-shadow: var(--shadow); overflow: hidden; }
|
|
10623
|
+
.card-header { padding: 16px; border-bottom: 1px solid var(--border); }
|
|
10624
|
+
.card-path { font-size: 12px; color: var(--text-muted); font-family: monospace; margin-bottom: 4px; }
|
|
10625
|
+
.card-title { font-size: 16px; font-weight: 600; }
|
|
10626
|
+
.card-desc { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
|
10627
|
+
.card-meta { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
|
|
10628
|
+
.tag { padding: 2px 8px; background: var(--tag-bg); border-radius: 4px; font-size: 11px; color: var(--text-muted); }
|
|
10629
|
+
.card-warnings { padding: 8px 16px; display: flex; flex-direction: column; gap: 4px; }
|
|
10630
|
+
.warning-item { font-size: 12px; padding: 4px 8px; border-radius: 4px; }
|
|
10631
|
+
.warning-item.error { background: var(--error-bg); color: var(--error); }
|
|
10632
|
+
.warning-item.warning { background: var(--warning-bg); color: var(--warning); }
|
|
10633
|
+
.card-previews { padding: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
10634
|
+
@media (max-width: 768px) { .card-previews { grid-template-columns: 1fr; } }
|
|
10635
|
+
.preview { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
10636
|
+
.preview-label { padding: 6px 10px; font-size: 11px; font-weight: 600; color: var(--text-muted); background: var(--bg); border-bottom: 1px solid var(--border); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
10637
|
+
.preview-card { background: var(--bg-preview); }
|
|
10638
|
+
.preview-img { width: 100%; aspect-ratio: 1200/630; background: linear-gradient(135deg, var(--accent-light), var(--bg-card)); display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: 12px; overflow: hidden; }
|
|
10639
|
+
.preview-img img { width: 100%; height: 100%; object-fit: cover; }
|
|
10640
|
+
.preview-body { padding: 10px 12px; }
|
|
10641
|
+
.preview-url { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
10642
|
+
.preview-title { font-size: 14px; font-weight: 600; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
10643
|
+
.preview-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
10644
|
+
.empty { text-align: center; padding: 60px; color: var(--text-muted); }
|
|
10645
|
+
.spin { animation: spin 0.6s linear infinite; }
|
|
10646
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
10647
|
+
</style>
|
|
10648
|
+
</head>
|
|
10649
|
+
<body>
|
|
10650
|
+
<div class="header">
|
|
10651
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
|
10652
|
+
<h1>OG Viewer <span>/ ox-content</span></h1>
|
|
10653
|
+
<div class="header-actions">
|
|
10654
|
+
<button class="btn" id="refresh-btn" onclick="refresh()">Refresh</button>
|
|
10655
|
+
</div>
|
|
10656
|
+
</div>
|
|
10657
|
+
<div class="summary" id="summary">
|
|
10658
|
+
<div class="summary-item"><strong id="s-pages">${pages.length}</strong> pages</div>
|
|
10659
|
+
<div class="summary-item"><span class="summary-dot dot-error"></span> <strong id="s-errors">${totalErrors}</strong> errors</div>
|
|
10660
|
+
<div class="summary-item"><span class="summary-dot dot-warning"></span> <strong id="s-warnings">${totalWarnings}</strong> warnings</div>
|
|
10661
|
+
<div class="summary-item"><span class="summary-dot ${generateOgImage ? "dot-success" : "dot-warning"}"></span> ogImage: <strong>${generateOgImage ? "enabled" : "disabled"}</strong></div>
|
|
10662
|
+
</div>
|
|
10663
|
+
<div class="toolbar">
|
|
10664
|
+
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All</button>
|
|
10665
|
+
<button class="filter-btn" data-filter="warnings" onclick="setFilter('warnings')">Warnings</button>
|
|
10666
|
+
<button class="filter-btn" data-filter="errors" onclick="setFilter('errors')">Errors</button>
|
|
10667
|
+
<input class="search-input" type="text" placeholder="Search pages..." oninput="applyFilters()" id="search-input">
|
|
10668
|
+
</div>
|
|
10669
|
+
<div class="container" id="container"></div>
|
|
10670
|
+
|
|
10671
|
+
<script>
|
|
10672
|
+
let pages = ${JSON.stringify(pages)};
|
|
10673
|
+
let currentFilter = 'all';
|
|
10674
|
+
|
|
10675
|
+
function setFilter(f) {
|
|
10676
|
+
currentFilter = f;
|
|
10677
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.dataset.filter === f));
|
|
10678
|
+
applyFilters();
|
|
10679
|
+
}
|
|
10680
|
+
|
|
10681
|
+
function applyFilters() {
|
|
10682
|
+
const q = document.getElementById('search-input').value.toLowerCase();
|
|
10683
|
+
const filtered = pages.filter(p => {
|
|
10684
|
+
if (currentFilter === 'errors' && !p.warnings.some(w => w.level === 'error')) return false;
|
|
10685
|
+
if (currentFilter === 'warnings' && !p.warnings.length) return false;
|
|
10686
|
+
if (q && !p.path.toLowerCase().includes(q) && !p.title.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q)) return false;
|
|
10687
|
+
return true;
|
|
10688
|
+
});
|
|
10689
|
+
renderCards(filtered);
|
|
10690
|
+
}
|
|
10691
|
+
|
|
10692
|
+
function esc(s) {
|
|
10693
|
+
const d = document.createElement('div');
|
|
10694
|
+
d.textContent = s;
|
|
10695
|
+
return d.innerHTML;
|
|
10696
|
+
}
|
|
10697
|
+
|
|
10698
|
+
function renderCards(list) {
|
|
10699
|
+
const c = document.getElementById('container');
|
|
10700
|
+
if (!list.length) {
|
|
10701
|
+
c.innerHTML = '<div class="empty">No pages match the current filter.</div>';
|
|
10702
|
+
return;
|
|
10703
|
+
}
|
|
10704
|
+
c.innerHTML = list.map(p => {
|
|
10705
|
+
const warnings = p.warnings.map(w =>
|
|
10706
|
+
'<div class="warning-item ' + w.level + '">' + (w.level === 'error' ? '\\u2716' : '\\u26A0') + ' ' + esc(w.message) + '</div>'
|
|
10707
|
+
).join('');
|
|
10708
|
+
const tags = p.tags.map(t => '<span class="tag">' + esc(t) + '</span>').join('');
|
|
10709
|
+
const author = p.author ? '<span class="tag">by ' + esc(p.author) + '</span>' : '';
|
|
10710
|
+
const imgHtml = p.ogImageUrl
|
|
10711
|
+
? '<img src="' + esc(p.ogImageUrl) + '" onerror="this.parentNode.innerHTML=\\'No OG image\\'">'
|
|
10712
|
+
: 'No OG image';
|
|
10713
|
+
const siteHost = ${JSON.stringify(options.ssg.siteUrl || "example.com")};
|
|
10714
|
+
return '<div class="card">'
|
|
10715
|
+
+ '<div class="card-header">'
|
|
10716
|
+
+ '<div class="card-path">' + esc(p.path) + ' → ' + esc(p.urlPath) + '</div>'
|
|
10717
|
+
+ '<div class="card-title">' + (esc(p.title) || '<em style="color:var(--error)">No title</em>') + '</div>'
|
|
10718
|
+
+ (p.description ? '<div class="card-desc">' + esc(p.description) + '</div>' : '')
|
|
10719
|
+
+ (tags || author ? '<div class="card-meta">' + author + tags + '</div>' : '')
|
|
10720
|
+
+ '</div>'
|
|
10721
|
+
+ (warnings ? '<div class="card-warnings">' + warnings + '</div>' : '')
|
|
10722
|
+
+ '<div class="card-previews">'
|
|
10723
|
+
+ '<div class="preview"><div class="preview-label">Twitter (summary_large_image)</div><div class="preview-card"><div class="preview-img">' + imgHtml + '</div><div class="preview-body"><div class="preview-url">' + esc(siteHost) + '</div><div class="preview-title">' + esc(p.title) + '</div><div class="preview-desc">' + esc(p.description) + '</div></div></div></div>'
|
|
10724
|
+
+ '<div class="preview"><div class="preview-label">Facebook (Open Graph)</div><div class="preview-card"><div class="preview-img">' + imgHtml + '</div><div class="preview-body"><div class="preview-url">' + esc(siteHost) + '</div><div class="preview-title">' + esc(p.title) + '</div><div class="preview-desc">' + esc(p.description) + '</div></div></div></div>'
|
|
10725
|
+
+ '</div>'
|
|
10726
|
+
+ '</div>';
|
|
10727
|
+
}).join('');
|
|
10728
|
+
}
|
|
10729
|
+
|
|
10730
|
+
async function refresh() {
|
|
10731
|
+
const btn = document.getElementById('refresh-btn');
|
|
10732
|
+
btn.textContent = 'Refreshing...';
|
|
10733
|
+
btn.disabled = true;
|
|
10734
|
+
try {
|
|
10735
|
+
const res = await fetch('/__og-viewer/api/pages');
|
|
10736
|
+
pages = await res.json();
|
|
10737
|
+
updateSummary();
|
|
10738
|
+
applyFilters();
|
|
10739
|
+
} catch(e) {
|
|
10740
|
+
console.error('Refresh failed:', e);
|
|
10741
|
+
} finally {
|
|
10742
|
+
btn.textContent = 'Refresh';
|
|
10743
|
+
btn.disabled = false;
|
|
10744
|
+
}
|
|
10745
|
+
}
|
|
10746
|
+
|
|
10747
|
+
function updateSummary() {
|
|
10748
|
+
document.getElementById('s-pages').textContent = pages.length;
|
|
10749
|
+
document.getElementById('s-errors').textContent = pages.reduce((s,p) => s + p.warnings.filter(w => w.level === 'error').length, 0);
|
|
10750
|
+
document.getElementById('s-warnings').textContent = pages.reduce((s,p) => s + p.warnings.filter(w => w.level === 'warning').length, 0);
|
|
10751
|
+
}
|
|
10752
|
+
|
|
10753
|
+
renderCards(pages);
|
|
10754
|
+
<\/script>
|
|
10755
|
+
</body>
|
|
10756
|
+
</html>`;
|
|
10757
|
+
}
|
|
10758
|
+
function createOgViewerPlugin(options) {
|
|
10759
|
+
return {
|
|
10760
|
+
name: "ox-content:og-viewer",
|
|
10761
|
+
apply: "serve",
|
|
10762
|
+
configureServer(server) {
|
|
10763
|
+
server.middlewares.use(async (req, res, next) => {
|
|
10764
|
+
if (req.url === "/__og-viewer" || req.url === "/__og-viewer/") {
|
|
10765
|
+
const root = server.config.root || process.cwd();
|
|
10766
|
+
try {
|
|
10767
|
+
const html = renderViewerHtml(await collectPages(options, root), options);
|
|
10768
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
10769
|
+
res.end(html);
|
|
10770
|
+
} catch (err) {
|
|
10771
|
+
res.statusCode = 500;
|
|
10772
|
+
res.end(`OG Viewer error: ${err}`);
|
|
10773
|
+
}
|
|
10774
|
+
return;
|
|
10775
|
+
}
|
|
10776
|
+
if (req.url === "/__og-viewer/api/pages") {
|
|
10777
|
+
const root = server.config.root || process.cwd();
|
|
10778
|
+
try {
|
|
10779
|
+
const pages = await collectPages(options, root);
|
|
10780
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
10781
|
+
res.end(JSON.stringify(pages));
|
|
10782
|
+
} catch (err) {
|
|
10783
|
+
res.statusCode = 500;
|
|
10784
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
10785
|
+
}
|
|
10786
|
+
return;
|
|
10787
|
+
}
|
|
10788
|
+
next();
|
|
10789
|
+
});
|
|
10790
|
+
}
|
|
10791
|
+
};
|
|
10792
|
+
}
|
|
10793
|
+
|
|
10393
10794
|
//#endregion
|
|
10394
10795
|
//#region src/jsx-runtime.ts
|
|
10395
10796
|
/**
|
|
@@ -11057,7 +11458,7 @@ function oxContent(options = {}) {
|
|
|
11057
11458
|
}
|
|
11058
11459
|
};
|
|
11059
11460
|
let searchIndexJson = "";
|
|
11060
|
-
|
|
11461
|
+
const plugins = [
|
|
11061
11462
|
mainPlugin,
|
|
11062
11463
|
environmentPlugin,
|
|
11063
11464
|
docsPlugin,
|
|
@@ -11100,6 +11501,8 @@ function oxContent(options = {}) {
|
|
|
11100
11501
|
}
|
|
11101
11502
|
}
|
|
11102
11503
|
];
|
|
11504
|
+
if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
|
|
11505
|
+
return plugins;
|
|
11103
11506
|
}
|
|
11104
11507
|
/**
|
|
11105
11508
|
* Resolves plugin options with defaults.
|
|
@@ -11125,7 +11528,8 @@ function resolveOptions(options) {
|
|
|
11125
11528
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
11126
11529
|
transformers: options.transformers ?? [],
|
|
11127
11530
|
docs: resolveDocsOptions(options.docs),
|
|
11128
|
-
search: resolveSearchOptions(options.search)
|
|
11531
|
+
search: resolveSearchOptions(options.search),
|
|
11532
|
+
ogViewer: options.ogViewer ?? true
|
|
11129
11533
|
};
|
|
11130
11534
|
}
|
|
11131
11535
|
/**
|