@ox-content/vite-plugin 0.4.0 → 0.7.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/github2.cjs.map +1 -1
- package/dist/github2.js.map +1 -1
- package/dist/index.cjs +448 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +448 -23
- package/dist/index.js.map +1 -1
- package/dist/mermaid2.cjs.map +1 -1
- package/dist/mermaid2.js.map +1 -1
- package/dist/ogp2.cjs.map +1 -1
- package/dist/ogp2.js.map +1 -1
- package/dist/tabs2.cjs.map +1 -1
- package/dist/tabs2.js.map +1 -1
- package/dist/youtube2.cjs.map +1 -1
- package/dist/youtube2.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -6967,7 +6967,7 @@ async function loadNapiBindings() {
|
|
|
6967
6967
|
async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
6968
6968
|
const napi = await loadNapiBindings();
|
|
6969
6969
|
if (!napi) throw new Error("[ox-content] NAPI bindings not available. Please ensure @ox-content/napi is built.");
|
|
6970
|
-
const { content: markdownContent, frontmatter } = parseFrontmatter(source);
|
|
6970
|
+
const { content: markdownContent, frontmatter } = parseFrontmatter$1(source);
|
|
6971
6971
|
const result = napi.transform(markdownContent, {
|
|
6972
6972
|
gfm: options.gfm,
|
|
6973
6973
|
footnotes: options.footnotes,
|
|
@@ -7002,7 +7002,7 @@ async function transformMarkdown(source, filePath, options, ssgOptions) {
|
|
|
7002
7002
|
* Parses YAML frontmatter from Markdown content.
|
|
7003
7003
|
* Uses proper YAML parser for full nested object support.
|
|
7004
7004
|
*/
|
|
7005
|
-
function parseFrontmatter(source) {
|
|
7005
|
+
function parseFrontmatter$1(source) {
|
|
7006
7006
|
if (!source.startsWith("---")) return {
|
|
7007
7007
|
content: source,
|
|
7008
7008
|
frontmatter: {}
|
|
@@ -7942,6 +7942,9 @@ function resolveDocsOptions(options) {
|
|
|
7942
7942
|
//#endregion
|
|
7943
7943
|
//#region src/og-image/renderer.ts
|
|
7944
7944
|
/**
|
|
7945
|
+
* HTML → PNG renderer using Chromium screenshots via Playwright.
|
|
7946
|
+
*/
|
|
7947
|
+
/**
|
|
7945
7948
|
* Wraps template HTML in a minimal document with viewport locked to given dimensions.
|
|
7946
7949
|
*/
|
|
7947
7950
|
function wrapHtml(bodyHtml, width, height) {
|
|
@@ -7964,13 +7967,47 @@ html, body { width: ${width}px; height: ${height}px; overflow: hidden; }
|
|
|
7964
7967
|
* @param html - HTML string from template function
|
|
7965
7968
|
* @param width - Image width
|
|
7966
7969
|
* @param height - Image height
|
|
7970
|
+
* @param publicDir - Optional public directory for serving local assets (images, fonts, etc.)
|
|
7967
7971
|
* @returns PNG buffer
|
|
7968
7972
|
*/
|
|
7969
|
-
async function renderHtmlToPng(page, html, width, height) {
|
|
7973
|
+
async function renderHtmlToPng(page, html, width, height, publicDir) {
|
|
7970
7974
|
await page.setViewportSize({
|
|
7971
7975
|
width,
|
|
7972
7976
|
height
|
|
7973
7977
|
});
|
|
7978
|
+
if (publicDir) {
|
|
7979
|
+
const fs = await import("fs/promises");
|
|
7980
|
+
await page.route("**/*", async (route) => {
|
|
7981
|
+
const url = new URL(route.request().url());
|
|
7982
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
7983
|
+
await route.continue();
|
|
7984
|
+
return;
|
|
7985
|
+
}
|
|
7986
|
+
const filePath = path$1.join(publicDir, url.pathname);
|
|
7987
|
+
try {
|
|
7988
|
+
const body = await fs.readFile(filePath);
|
|
7989
|
+
const ext = path$1.extname(filePath).toLowerCase();
|
|
7990
|
+
await route.fulfill({
|
|
7991
|
+
body,
|
|
7992
|
+
contentType: {
|
|
7993
|
+
".svg": "image/svg+xml",
|
|
7994
|
+
".png": "image/png",
|
|
7995
|
+
".jpg": "image/jpeg",
|
|
7996
|
+
".jpeg": "image/jpeg",
|
|
7997
|
+
".gif": "image/gif",
|
|
7998
|
+
".webp": "image/webp",
|
|
7999
|
+
".woff": "font/woff",
|
|
8000
|
+
".woff2": "font/woff2",
|
|
8001
|
+
".ttf": "font/ttf",
|
|
8002
|
+
".css": "text/css",
|
|
8003
|
+
".js": "application/javascript"
|
|
8004
|
+
}[ext] || "application/octet-stream"
|
|
8005
|
+
});
|
|
8006
|
+
} catch {
|
|
8007
|
+
await route.continue();
|
|
8008
|
+
}
|
|
8009
|
+
});
|
|
8010
|
+
}
|
|
7974
8011
|
const fullHtml = wrapHtml(html, width, height);
|
|
7975
8012
|
await page.setContent(fullHtml, { waitUntil: "networkidle" });
|
|
7976
8013
|
const screenshot = await page.screenshot({
|
|
@@ -8011,10 +8048,10 @@ async function openBrowser() {
|
|
|
8011
8048
|
]
|
|
8012
8049
|
});
|
|
8013
8050
|
return {
|
|
8014
|
-
async renderPage(html, width, height) {
|
|
8051
|
+
async renderPage(html, width, height, publicDir) {
|
|
8015
8052
|
const page = await browser.newPage();
|
|
8016
8053
|
try {
|
|
8017
|
-
return await renderHtmlToPng(page, html, width, height);
|
|
8054
|
+
return await renderHtmlToPng(page, html, width, height, publicDir);
|
|
8018
8055
|
} finally {
|
|
8019
8056
|
await page.close();
|
|
8020
8057
|
}
|
|
@@ -8253,10 +8290,35 @@ async function resolveVueTemplate(templatePath, options, root) {
|
|
|
8253
8290
|
await bundle.close();
|
|
8254
8291
|
const Component = (await import(`${outfile}?t=${Date.now()}`)).default;
|
|
8255
8292
|
if (!Component) throw new Error(`[ox-content:og-image] Vue template must have a default export: ${templatePath}`);
|
|
8293
|
+
let extractedCss = "";
|
|
8294
|
+
try {
|
|
8295
|
+
let compilerSfc;
|
|
8296
|
+
try {
|
|
8297
|
+
compilerSfc = await import("@vue/compiler-sfc");
|
|
8298
|
+
} catch {
|
|
8299
|
+
compilerSfc = null;
|
|
8300
|
+
}
|
|
8301
|
+
if (compilerSfc) {
|
|
8302
|
+
const sfcSource = await fs.readFile(templatePath, "utf-8");
|
|
8303
|
+
const { descriptor } = compilerSfc.parse(sfcSource, { filename: templatePath });
|
|
8304
|
+
const scopeId = Component.__scopeId;
|
|
8305
|
+
for (const style of descriptor.styles) if (style.scoped && scopeId) {
|
|
8306
|
+
const result = compilerSfc.compileStyle({
|
|
8307
|
+
id: scopeId,
|
|
8308
|
+
source: style.content,
|
|
8309
|
+
scoped: true,
|
|
8310
|
+
filename: templatePath
|
|
8311
|
+
});
|
|
8312
|
+
if (!result.errors.length) extractedCss += result.code;
|
|
8313
|
+
} else extractedCss += style.content;
|
|
8314
|
+
}
|
|
8315
|
+
} catch {}
|
|
8256
8316
|
const { createSSRApp } = await import("vue");
|
|
8257
8317
|
const { renderToString } = await import("vue/server-renderer");
|
|
8258
8318
|
return async (props) => {
|
|
8259
|
-
|
|
8319
|
+
const html = await renderToString(createSSRApp(Component, props));
|
|
8320
|
+
if (extractedCss) return `<style>${extractedCss}</style>${html}`;
|
|
8321
|
+
return html;
|
|
8260
8322
|
};
|
|
8261
8323
|
}
|
|
8262
8324
|
/**
|
|
@@ -8458,10 +8520,11 @@ async function generateOgImages(pages, options, root) {
|
|
|
8458
8520
|
error: "Chromium not available"
|
|
8459
8521
|
}));
|
|
8460
8522
|
const results = [];
|
|
8523
|
+
const publicDir = path$1.join(root, "public");
|
|
8461
8524
|
const concurrency = Math.max(1, options.concurrency);
|
|
8462
8525
|
for (let i = 0; i < pages.length; i += concurrency) {
|
|
8463
8526
|
const batch = pages.slice(i, i + concurrency);
|
|
8464
|
-
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session)));
|
|
8527
|
+
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session, publicDir)));
|
|
8465
8528
|
results.push(...batchResults);
|
|
8466
8529
|
}
|
|
8467
8530
|
return results;
|
|
@@ -8493,7 +8556,7 @@ async function tryServeAllFromCache(pages, templateSource, options, cacheDir) {
|
|
|
8493
8556
|
/**
|
|
8494
8557
|
* Renders a single page to PNG, with cache support.
|
|
8495
8558
|
*/
|
|
8496
|
-
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session) {
|
|
8559
|
+
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session, publicDir) {
|
|
8497
8560
|
const fs = await import("fs/promises");
|
|
8498
8561
|
try {
|
|
8499
8562
|
if (options.cache) {
|
|
@@ -8508,7 +8571,7 @@ async function renderSinglePage(entry, templateFn, templateSource, options, cach
|
|
|
8508
8571
|
}
|
|
8509
8572
|
}
|
|
8510
8573
|
const html = await templateFn(entry.props);
|
|
8511
|
-
const png = await session.renderPage(html, options.width, options.height);
|
|
8574
|
+
const png = await session.renderPage(html, options.width, options.height, publicDir);
|
|
8512
8575
|
await fs.mkdir(path$1.dirname(entry.outputPath), { recursive: true });
|
|
8513
8576
|
await fs.writeFile(entry.outputPath, png);
|
|
8514
8577
|
if (options.cache) await writeCache(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height), png);
|
|
@@ -8946,7 +9009,8 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
|
|
|
8946
9009
|
{{#description}}<meta property="og:description" content="{{description}}">{{/description}}
|
|
8947
9010
|
{{#ogImage}}<meta property="og:image" content="{{ogImage}}">{{/ogImage}}
|
|
8948
9011
|
<!-- Twitter Card -->
|
|
8949
|
-
<meta name="twitter:card" content="summary_large_image">
|
|
9012
|
+
{{#ogImage}}<meta name="twitter:card" content="summary_large_image">{{/ogImage}}
|
|
9013
|
+
{{^ogImage}}<meta name="twitter:card" content="summary">{{/ogImage}}
|
|
8950
9014
|
<meta name="twitter:title" content="{{title}}{{#siteName}} - {{siteName}}{{/siteName}}">
|
|
8951
9015
|
{{#description}}<meta name="twitter:description" content="{{description}}">{{/description}}
|
|
8952
9016
|
{{#ogImage}}<meta name="twitter:image" content="{{ogImage}}">{{/ogImage}}
|
|
@@ -9710,6 +9774,9 @@ function renderTemplate(template, data) {
|
|
|
9710
9774
|
result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
|
|
9711
9775
|
return data[key] ? content : "";
|
|
9712
9776
|
});
|
|
9777
|
+
result = result.replace(/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
|
|
9778
|
+
return data[key] ? "" : content;
|
|
9779
|
+
});
|
|
9713
9780
|
result = result.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
9714
9781
|
const value = data[key];
|
|
9715
9782
|
if (value === void 0 || value === null) return "";
|
|
@@ -9723,7 +9790,7 @@ function renderTemplate(template, data) {
|
|
|
9723
9790
|
/**
|
|
9724
9791
|
* Extracts title from content or frontmatter.
|
|
9725
9792
|
*/
|
|
9726
|
-
function extractTitle(content, frontmatter) {
|
|
9793
|
+
function extractTitle$1(content, frontmatter) {
|
|
9727
9794
|
if (frontmatter.title && typeof frontmatter.title === "string") return frontmatter.title;
|
|
9728
9795
|
const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
|
|
9729
9796
|
if (h1Match) return h1Match[1].trim();
|
|
@@ -9808,7 +9875,7 @@ function getOutputPath(inputPath, srcDir, outDir, extension) {
|
|
|
9808
9875
|
/**
|
|
9809
9876
|
* Converts a markdown file path to a relative URL path.
|
|
9810
9877
|
*/
|
|
9811
|
-
function getUrlPath(inputPath, srcDir) {
|
|
9878
|
+
function getUrlPath$1(inputPath, srcDir) {
|
|
9812
9879
|
const baseName = path$1.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
|
|
9813
9880
|
if (baseName === "index" || baseName.endsWith("/index")) return baseName.replace(/\/?index$/, "") || "/";
|
|
9814
9881
|
return baseName;
|
|
@@ -9817,7 +9884,7 @@ function getUrlPath(inputPath, srcDir) {
|
|
|
9817
9884
|
* Converts a markdown file path to an href.
|
|
9818
9885
|
*/
|
|
9819
9886
|
function getHref(inputPath, srcDir, base, extension) {
|
|
9820
|
-
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9887
|
+
const urlPath = getUrlPath$1(inputPath, srcDir);
|
|
9821
9888
|
if (urlPath === "/" || urlPath === "") return `${base}index${extension}`;
|
|
9822
9889
|
return `${base}${urlPath}/index${extension}`;
|
|
9823
9890
|
}
|
|
@@ -9837,7 +9904,7 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9837
9904
|
* If siteUrl is provided, returns an absolute URL (required for SNS sharing).
|
|
9838
9905
|
*/
|
|
9839
9906
|
function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
|
|
9840
|
-
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9907
|
+
const urlPath = getUrlPath$1(inputPath, srcDir);
|
|
9841
9908
|
let relativePath;
|
|
9842
9909
|
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
|
|
9843
9910
|
else relativePath = `${base}${urlPath}/og-image.png`;
|
|
@@ -9891,7 +9958,7 @@ function buildNavItems(markdownFiles, srcDir, base, extension) {
|
|
|
9891
9958
|
let groupKey = "";
|
|
9892
9959
|
if (parts.length > 1) groupKey = parts[0];
|
|
9893
9960
|
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
9894
|
-
const urlPath = getUrlPath(file, srcDir);
|
|
9961
|
+
const urlPath = getUrlPath$1(file, srcDir);
|
|
9895
9962
|
let title;
|
|
9896
9963
|
if (urlPath === "/" || urlPath === "") title = "Overview";
|
|
9897
9964
|
else title = getDisplayTitle(file);
|
|
@@ -9956,6 +10023,7 @@ async function buildSsg(options, root) {
|
|
|
9956
10023
|
if (pkg.name) siteName = formatTitle(pkg.name);
|
|
9957
10024
|
} catch {}
|
|
9958
10025
|
const ogImageEntries = [];
|
|
10026
|
+
const ogImageInputPaths = [];
|
|
9959
10027
|
const ogImageUrlMap = /* @__PURE__ */ new Map();
|
|
9960
10028
|
const shouldGenerateOgImages = (options.ogImage || ssgOptions.generateOgImage) && !ssgOptions.bare;
|
|
9961
10029
|
const pageResults = [];
|
|
@@ -9979,7 +10047,7 @@ async function buildSsg(options, root) {
|
|
|
9979
10047
|
transformedHtml = await transformAllPlugins(transformedHtml, pluginOptions);
|
|
9980
10048
|
if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
|
|
9981
10049
|
transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
9982
|
-
const title = extractTitle(transformedHtml, result.frontmatter);
|
|
10050
|
+
const title = extractTitle$1(transformedHtml, result.frontmatter);
|
|
9983
10051
|
const description = result.frontmatter.description;
|
|
9984
10052
|
pageResults.push({
|
|
9985
10053
|
inputPath,
|
|
@@ -10001,6 +10069,7 @@ async function buildSsg(options, root) {
|
|
|
10001
10069
|
},
|
|
10002
10070
|
outputPath: ogImageOutputPath
|
|
10003
10071
|
});
|
|
10072
|
+
ogImageInputPaths.push(inputPath);
|
|
10004
10073
|
ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
|
|
10005
10074
|
}
|
|
10006
10075
|
} catch (err) {
|
|
@@ -10010,10 +10079,15 @@ async function buildSsg(options, root) {
|
|
|
10010
10079
|
if (shouldGenerateOgImages && ogImageEntries.length > 0) try {
|
|
10011
10080
|
const ogResults = await generateOgImages(ogImageEntries, options.ogImageOptions, root);
|
|
10012
10081
|
let ogSuccessCount = 0;
|
|
10013
|
-
for (
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10082
|
+
for (let i = 0; i < ogResults.length; i++) {
|
|
10083
|
+
const result = ogResults[i];
|
|
10084
|
+
if (result.error) {
|
|
10085
|
+
errors.push(`OG image failed for ${result.outputPath}: ${result.error}`);
|
|
10086
|
+
ogImageUrlMap.delete(ogImageInputPaths[i]);
|
|
10087
|
+
} else {
|
|
10088
|
+
generatedFiles.push(result.outputPath);
|
|
10089
|
+
ogSuccessCount++;
|
|
10090
|
+
}
|
|
10017
10091
|
}
|
|
10018
10092
|
if (ogSuccessCount > 0) {
|
|
10019
10093
|
const cachedCount = ogResults.filter((r) => r.cached && !r.error).length;
|
|
@@ -10022,6 +10096,7 @@ async function buildSsg(options, root) {
|
|
|
10022
10096
|
} catch (err) {
|
|
10023
10097
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
10024
10098
|
console.warn(`[ox-content:og-image] Batch generation failed: ${errorMessage}`);
|
|
10099
|
+
ogImageUrlMap.clear();
|
|
10025
10100
|
}
|
|
10026
10101
|
for (const pageResult of pageResults) try {
|
|
10027
10102
|
const { inputPath, transformedHtml, title, description, frontmatter, toc } = pageResult;
|
|
@@ -10040,7 +10115,7 @@ async function buildSsg(options, root) {
|
|
|
10040
10115
|
content: transformedHtml,
|
|
10041
10116
|
toc,
|
|
10042
10117
|
frontmatter,
|
|
10043
|
-
path: getUrlPath(inputPath, srcDir),
|
|
10118
|
+
path: getUrlPath$1(inputPath, srcDir),
|
|
10044
10119
|
href: getHref(inputPath, srcDir, base, ssgOptions.extension),
|
|
10045
10120
|
entryPage
|
|
10046
10121
|
}, navItems, siteName, base, pageOgImage, ssgOptions.theme);
|
|
@@ -10339,6 +10414,353 @@ export default { search, searchOptions, loadIndex };
|
|
|
10339
10414
|
`;
|
|
10340
10415
|
}
|
|
10341
10416
|
|
|
10417
|
+
//#endregion
|
|
10418
|
+
//#region src/og-viewer.ts
|
|
10419
|
+
/**
|
|
10420
|
+
* OG Viewer - Dev tool for previewing Open Graph metadata
|
|
10421
|
+
*
|
|
10422
|
+
* Accessible at /__og-viewer during development.
|
|
10423
|
+
* Shows all pages with their OG metadata, validation warnings,
|
|
10424
|
+
* and social card previews.
|
|
10425
|
+
*/
|
|
10426
|
+
function parseFrontmatter(content) {
|
|
10427
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
10428
|
+
if (!match) return {};
|
|
10429
|
+
const yaml = match[1];
|
|
10430
|
+
const result = {};
|
|
10431
|
+
for (const line of yaml.split("\n")) {
|
|
10432
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
10433
|
+
if (!kv) continue;
|
|
10434
|
+
const [, key, rawValue] = kv;
|
|
10435
|
+
let value = rawValue.trim();
|
|
10436
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
10437
|
+
else if (typeof value === "string" && /^['"].*['"]$/.test(value)) value = value.slice(1, -1);
|
|
10438
|
+
else if (value === "true") value = true;
|
|
10439
|
+
else if (value === "false") value = false;
|
|
10440
|
+
result[key] = value;
|
|
10441
|
+
}
|
|
10442
|
+
return result;
|
|
10443
|
+
}
|
|
10444
|
+
function extractTitle(content, frontmatter) {
|
|
10445
|
+
if (typeof frontmatter.title === "string" && frontmatter.title) return frontmatter.title;
|
|
10446
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
10447
|
+
return match ? match[1].trim() : "";
|
|
10448
|
+
}
|
|
10449
|
+
function getUrlPath(filePath, srcDir) {
|
|
10450
|
+
let rel = path$1.relative(srcDir, filePath).replace(/\\/g, "/");
|
|
10451
|
+
rel = rel.replace(/\.md$/, "");
|
|
10452
|
+
if (rel === "index") return "/";
|
|
10453
|
+
if (rel.endsWith("/index")) rel = rel.slice(0, -6);
|
|
10454
|
+
return "/" + rel;
|
|
10455
|
+
}
|
|
10456
|
+
function computeOgImageUrl(urlPath, base, siteUrl, generateOgImage, staticOgImage) {
|
|
10457
|
+
if (!generateOgImage) return staticOgImage || "";
|
|
10458
|
+
const cleanBase = base.endsWith("/") ? base : base + "/";
|
|
10459
|
+
let relativePath;
|
|
10460
|
+
if (urlPath === "/") relativePath = `${cleanBase}og-image.png`;
|
|
10461
|
+
else relativePath = `${cleanBase}${urlPath.replace(/^\//, "")}/og-image.png`;
|
|
10462
|
+
if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
|
|
10463
|
+
return relativePath;
|
|
10464
|
+
}
|
|
10465
|
+
function validatePage(page, options) {
|
|
10466
|
+
const warnings = [];
|
|
10467
|
+
if (!page.title) warnings.push({
|
|
10468
|
+
level: "error",
|
|
10469
|
+
message: "title is missing"
|
|
10470
|
+
});
|
|
10471
|
+
else if (page.title.length > 70) warnings.push({
|
|
10472
|
+
level: "warning",
|
|
10473
|
+
message: `title is too long (${page.title.length}/70)`
|
|
10474
|
+
});
|
|
10475
|
+
if (!page.description) warnings.push({
|
|
10476
|
+
level: "warning",
|
|
10477
|
+
message: "description is missing"
|
|
10478
|
+
});
|
|
10479
|
+
else if (page.description.length > 200) warnings.push({
|
|
10480
|
+
level: "warning",
|
|
10481
|
+
message: `description is too long (${page.description.length}/200)`
|
|
10482
|
+
});
|
|
10483
|
+
if ((options.ogImage || options.ssg.generateOgImage) && !options.ssg.siteUrl) warnings.push({
|
|
10484
|
+
level: "warning",
|
|
10485
|
+
message: "ogImage enabled but siteUrl is not set"
|
|
10486
|
+
});
|
|
10487
|
+
return warnings;
|
|
10488
|
+
}
|
|
10489
|
+
async function collectPages(options, root) {
|
|
10490
|
+
const srcDir = path$1.resolve(root, options.srcDir);
|
|
10491
|
+
const files = await (0, glob.glob)("**/*.md", {
|
|
10492
|
+
cwd: srcDir,
|
|
10493
|
+
absolute: true
|
|
10494
|
+
});
|
|
10495
|
+
const pages = [];
|
|
10496
|
+
const generateOgImage = options.ogImage || options.ssg.generateOgImage;
|
|
10497
|
+
for (const file of files.sort()) {
|
|
10498
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
10499
|
+
const frontmatter = parseFrontmatter(content);
|
|
10500
|
+
if (frontmatter.layout === "entry") continue;
|
|
10501
|
+
const title = extractTitle(content, frontmatter);
|
|
10502
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
|
|
10503
|
+
const author = typeof frontmatter.author === "string" ? frontmatter.author : "";
|
|
10504
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : typeof frontmatter.tags === "string" ? [frontmatter.tags] : [];
|
|
10505
|
+
const urlPath = getUrlPath(file, srcDir);
|
|
10506
|
+
const ogImageUrl = computeOgImageUrl(urlPath, options.base, options.ssg.siteUrl, generateOgImage, options.ssg.ogImage);
|
|
10507
|
+
const page = {
|
|
10508
|
+
path: path$1.relative(srcDir, file),
|
|
10509
|
+
urlPath,
|
|
10510
|
+
title,
|
|
10511
|
+
description,
|
|
10512
|
+
author,
|
|
10513
|
+
tags,
|
|
10514
|
+
ogImageUrl,
|
|
10515
|
+
warnings: []
|
|
10516
|
+
};
|
|
10517
|
+
page.warnings = validatePage(page, options);
|
|
10518
|
+
pages.push(page);
|
|
10519
|
+
}
|
|
10520
|
+
return pages;
|
|
10521
|
+
}
|
|
10522
|
+
function renderViewerHtml(pages, options) {
|
|
10523
|
+
const generateOgImage = options.ogImage || options.ssg.generateOgImage;
|
|
10524
|
+
const totalWarnings = pages.reduce((sum, p) => sum + p.warnings.filter((w) => w.level === "warning").length, 0);
|
|
10525
|
+
const totalErrors = pages.reduce((sum, p) => sum + p.warnings.filter((w) => w.level === "error").length, 0);
|
|
10526
|
+
return `<!DOCTYPE html>
|
|
10527
|
+
<html lang="en">
|
|
10528
|
+
<head>
|
|
10529
|
+
<meta charset="UTF-8">
|
|
10530
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10531
|
+
<title>OG Viewer - ox-content</title>
|
|
10532
|
+
<style>
|
|
10533
|
+
:root {
|
|
10534
|
+
--bg: #ffffff;
|
|
10535
|
+
--bg-card: #f8f9fa;
|
|
10536
|
+
--bg-preview: #ffffff;
|
|
10537
|
+
--text: #1a1a2e;
|
|
10538
|
+
--text-muted: #6b7280;
|
|
10539
|
+
--border: #e5e7eb;
|
|
10540
|
+
--accent: #e8590c;
|
|
10541
|
+
--accent-light: #fff4e6;
|
|
10542
|
+
--error: #dc2626;
|
|
10543
|
+
--error-bg: #fef2f2;
|
|
10544
|
+
--warning: #d97706;
|
|
10545
|
+
--warning-bg: #fffbeb;
|
|
10546
|
+
--success: #16a34a;
|
|
10547
|
+
--tag-bg: #f0f0f0;
|
|
10548
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
10549
|
+
--radius: 8px;
|
|
10550
|
+
}
|
|
10551
|
+
@media (prefers-color-scheme: dark) {
|
|
10552
|
+
:root {
|
|
10553
|
+
--bg: #0f172a;
|
|
10554
|
+
--bg-card: #1e293b;
|
|
10555
|
+
--bg-preview: #334155;
|
|
10556
|
+
--text: #e2e8f0;
|
|
10557
|
+
--text-muted: #94a3b8;
|
|
10558
|
+
--border: #334155;
|
|
10559
|
+
--accent: #fb923c;
|
|
10560
|
+
--accent-light: #431407;
|
|
10561
|
+
--error: #f87171;
|
|
10562
|
+
--error-bg: #450a0a;
|
|
10563
|
+
--warning: #fbbf24;
|
|
10564
|
+
--warning-bg: #451a03;
|
|
10565
|
+
--success: #4ade80;
|
|
10566
|
+
--tag-bg: #334155;
|
|
10567
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
10568
|
+
}
|
|
10569
|
+
}
|
|
10570
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10571
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
|
10572
|
+
.header { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
|
|
10573
|
+
.header svg { width: 28px; height: 28px; color: var(--accent); }
|
|
10574
|
+
.header h1 { font-size: 18px; font-weight: 600; }
|
|
10575
|
+
.header h1 span { color: var(--text-muted); font-weight: 400; }
|
|
10576
|
+
.header-actions { margin-left: auto; }
|
|
10577
|
+
.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; }
|
|
10578
|
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
10579
|
+
.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; }
|
|
10580
|
+
.summary-item { display: flex; align-items: center; gap: 4px; }
|
|
10581
|
+
.summary-item strong { color: var(--text); }
|
|
10582
|
+
.summary-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
10583
|
+
.dot-error { background: var(--error); }
|
|
10584
|
+
.dot-warning { background: var(--warning); }
|
|
10585
|
+
.dot-success { background: var(--success); }
|
|
10586
|
+
.toolbar { padding: 12px 24px; display: flex; gap: 8px; border-bottom: 1px solid var(--border); flex-wrap: wrap; align-items: center; }
|
|
10587
|
+
.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; }
|
|
10588
|
+
.filter-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
10589
|
+
.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; }
|
|
10590
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
10591
|
+
.container { padding: 24px; display: flex; flex-direction: column; gap: 20px; max-width: 1200px; margin: 0 auto; }
|
|
10592
|
+
.card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-card); box-shadow: var(--shadow); overflow: hidden; }
|
|
10593
|
+
.card-header { padding: 16px; border-bottom: 1px solid var(--border); }
|
|
10594
|
+
.card-path { font-size: 12px; color: var(--text-muted); font-family: monospace; margin-bottom: 4px; }
|
|
10595
|
+
.card-title { font-size: 16px; font-weight: 600; }
|
|
10596
|
+
.card-desc { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
|
10597
|
+
.card-meta { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
|
|
10598
|
+
.tag { padding: 2px 8px; background: var(--tag-bg); border-radius: 4px; font-size: 11px; color: var(--text-muted); }
|
|
10599
|
+
.card-warnings { padding: 8px 16px; display: flex; flex-direction: column; gap: 4px; }
|
|
10600
|
+
.warning-item { font-size: 12px; padding: 4px 8px; border-radius: 4px; }
|
|
10601
|
+
.warning-item.error { background: var(--error-bg); color: var(--error); }
|
|
10602
|
+
.warning-item.warning { background: var(--warning-bg); color: var(--warning); }
|
|
10603
|
+
.card-previews { padding: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
10604
|
+
@media (max-width: 768px) { .card-previews { grid-template-columns: 1fr; } }
|
|
10605
|
+
.preview { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
10606
|
+
.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; }
|
|
10607
|
+
.preview-card { background: var(--bg-preview); }
|
|
10608
|
+
.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; }
|
|
10609
|
+
.preview-img img { width: 100%; height: 100%; object-fit: cover; }
|
|
10610
|
+
.preview-body { padding: 10px 12px; }
|
|
10611
|
+
.preview-url { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
10612
|
+
.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; }
|
|
10613
|
+
.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; }
|
|
10614
|
+
.empty { text-align: center; padding: 60px; color: var(--text-muted); }
|
|
10615
|
+
.spin { animation: spin 0.6s linear infinite; }
|
|
10616
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
10617
|
+
</style>
|
|
10618
|
+
</head>
|
|
10619
|
+
<body>
|
|
10620
|
+
<div class="header">
|
|
10621
|
+
<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>
|
|
10622
|
+
<h1>OG Viewer <span>/ ox-content</span></h1>
|
|
10623
|
+
<div class="header-actions">
|
|
10624
|
+
<button class="btn" id="refresh-btn" onclick="refresh()">Refresh</button>
|
|
10625
|
+
</div>
|
|
10626
|
+
</div>
|
|
10627
|
+
<div class="summary" id="summary">
|
|
10628
|
+
<div class="summary-item"><strong id="s-pages">${pages.length}</strong> pages</div>
|
|
10629
|
+
<div class="summary-item"><span class="summary-dot dot-error"></span> <strong id="s-errors">${totalErrors}</strong> errors</div>
|
|
10630
|
+
<div class="summary-item"><span class="summary-dot dot-warning"></span> <strong id="s-warnings">${totalWarnings}</strong> warnings</div>
|
|
10631
|
+
<div class="summary-item"><span class="summary-dot ${generateOgImage ? "dot-success" : "dot-warning"}"></span> ogImage: <strong>${generateOgImage ? "enabled" : "disabled"}</strong></div>
|
|
10632
|
+
</div>
|
|
10633
|
+
<div class="toolbar">
|
|
10634
|
+
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All</button>
|
|
10635
|
+
<button class="filter-btn" data-filter="warnings" onclick="setFilter('warnings')">Warnings</button>
|
|
10636
|
+
<button class="filter-btn" data-filter="errors" onclick="setFilter('errors')">Errors</button>
|
|
10637
|
+
<input class="search-input" type="text" placeholder="Search pages..." oninput="applyFilters()" id="search-input">
|
|
10638
|
+
</div>
|
|
10639
|
+
<div class="container" id="container"></div>
|
|
10640
|
+
|
|
10641
|
+
<script>
|
|
10642
|
+
let pages = ${JSON.stringify(pages)};
|
|
10643
|
+
let currentFilter = 'all';
|
|
10644
|
+
|
|
10645
|
+
function setFilter(f) {
|
|
10646
|
+
currentFilter = f;
|
|
10647
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.dataset.filter === f));
|
|
10648
|
+
applyFilters();
|
|
10649
|
+
}
|
|
10650
|
+
|
|
10651
|
+
function applyFilters() {
|
|
10652
|
+
const q = document.getElementById('search-input').value.toLowerCase();
|
|
10653
|
+
const filtered = pages.filter(p => {
|
|
10654
|
+
if (currentFilter === 'errors' && !p.warnings.some(w => w.level === 'error')) return false;
|
|
10655
|
+
if (currentFilter === 'warnings' && !p.warnings.length) return false;
|
|
10656
|
+
if (q && !p.path.toLowerCase().includes(q) && !p.title.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q)) return false;
|
|
10657
|
+
return true;
|
|
10658
|
+
});
|
|
10659
|
+
renderCards(filtered);
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
function esc(s) {
|
|
10663
|
+
const d = document.createElement('div');
|
|
10664
|
+
d.textContent = s;
|
|
10665
|
+
return d.innerHTML;
|
|
10666
|
+
}
|
|
10667
|
+
|
|
10668
|
+
function renderCards(list) {
|
|
10669
|
+
const c = document.getElementById('container');
|
|
10670
|
+
if (!list.length) {
|
|
10671
|
+
c.innerHTML = '<div class="empty">No pages match the current filter.</div>';
|
|
10672
|
+
return;
|
|
10673
|
+
}
|
|
10674
|
+
c.innerHTML = list.map(p => {
|
|
10675
|
+
const warnings = p.warnings.map(w =>
|
|
10676
|
+
'<div class="warning-item ' + w.level + '">' + (w.level === 'error' ? '\\u2716' : '\\u26A0') + ' ' + esc(w.message) + '</div>'
|
|
10677
|
+
).join('');
|
|
10678
|
+
const tags = p.tags.map(t => '<span class="tag">' + esc(t) + '</span>').join('');
|
|
10679
|
+
const author = p.author ? '<span class="tag">by ' + esc(p.author) + '</span>' : '';
|
|
10680
|
+
const imgHtml = p.ogImageUrl
|
|
10681
|
+
? '<img src="' + esc(p.ogImageUrl) + '" onerror="this.parentNode.innerHTML=\\'No OG image\\'">'
|
|
10682
|
+
: 'No OG image';
|
|
10683
|
+
const siteHost = ${JSON.stringify(options.ssg.siteUrl || "example.com")};
|
|
10684
|
+
return '<div class="card">'
|
|
10685
|
+
+ '<div class="card-header">'
|
|
10686
|
+
+ '<div class="card-path">' + esc(p.path) + ' → ' + esc(p.urlPath) + '</div>'
|
|
10687
|
+
+ '<div class="card-title">' + (esc(p.title) || '<em style="color:var(--error)">No title</em>') + '</div>'
|
|
10688
|
+
+ (p.description ? '<div class="card-desc">' + esc(p.description) + '</div>' : '')
|
|
10689
|
+
+ (tags || author ? '<div class="card-meta">' + author + tags + '</div>' : '')
|
|
10690
|
+
+ '</div>'
|
|
10691
|
+
+ (warnings ? '<div class="card-warnings">' + warnings + '</div>' : '')
|
|
10692
|
+
+ '<div class="card-previews">'
|
|
10693
|
+
+ '<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>'
|
|
10694
|
+
+ '<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>'
|
|
10695
|
+
+ '</div>'
|
|
10696
|
+
+ '</div>';
|
|
10697
|
+
}).join('');
|
|
10698
|
+
}
|
|
10699
|
+
|
|
10700
|
+
async function refresh() {
|
|
10701
|
+
const btn = document.getElementById('refresh-btn');
|
|
10702
|
+
btn.textContent = 'Refreshing...';
|
|
10703
|
+
btn.disabled = true;
|
|
10704
|
+
try {
|
|
10705
|
+
const res = await fetch('/__og-viewer/api/pages');
|
|
10706
|
+
pages = await res.json();
|
|
10707
|
+
updateSummary();
|
|
10708
|
+
applyFilters();
|
|
10709
|
+
} catch(e) {
|
|
10710
|
+
console.error('Refresh failed:', e);
|
|
10711
|
+
} finally {
|
|
10712
|
+
btn.textContent = 'Refresh';
|
|
10713
|
+
btn.disabled = false;
|
|
10714
|
+
}
|
|
10715
|
+
}
|
|
10716
|
+
|
|
10717
|
+
function updateSummary() {
|
|
10718
|
+
document.getElementById('s-pages').textContent = pages.length;
|
|
10719
|
+
document.getElementById('s-errors').textContent = pages.reduce((s,p) => s + p.warnings.filter(w => w.level === 'error').length, 0);
|
|
10720
|
+
document.getElementById('s-warnings').textContent = pages.reduce((s,p) => s + p.warnings.filter(w => w.level === 'warning').length, 0);
|
|
10721
|
+
}
|
|
10722
|
+
|
|
10723
|
+
renderCards(pages);
|
|
10724
|
+
<\/script>
|
|
10725
|
+
</body>
|
|
10726
|
+
</html>`;
|
|
10727
|
+
}
|
|
10728
|
+
function createOgViewerPlugin(options) {
|
|
10729
|
+
return {
|
|
10730
|
+
name: "ox-content:og-viewer",
|
|
10731
|
+
apply: "serve",
|
|
10732
|
+
configureServer(server) {
|
|
10733
|
+
server.middlewares.use(async (req, res, next) => {
|
|
10734
|
+
if (req.url === "/__og-viewer" || req.url === "/__og-viewer/") {
|
|
10735
|
+
const root = server.config.root || process.cwd();
|
|
10736
|
+
try {
|
|
10737
|
+
const html = renderViewerHtml(await collectPages(options, root), options);
|
|
10738
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
10739
|
+
res.end(html);
|
|
10740
|
+
} catch (err) {
|
|
10741
|
+
res.statusCode = 500;
|
|
10742
|
+
res.end(`OG Viewer error: ${err}`);
|
|
10743
|
+
}
|
|
10744
|
+
return;
|
|
10745
|
+
}
|
|
10746
|
+
if (req.url === "/__og-viewer/api/pages") {
|
|
10747
|
+
const root = server.config.root || process.cwd();
|
|
10748
|
+
try {
|
|
10749
|
+
const pages = await collectPages(options, root);
|
|
10750
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
10751
|
+
res.end(JSON.stringify(pages));
|
|
10752
|
+
} catch (err) {
|
|
10753
|
+
res.statusCode = 500;
|
|
10754
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
10755
|
+
}
|
|
10756
|
+
return;
|
|
10757
|
+
}
|
|
10758
|
+
next();
|
|
10759
|
+
});
|
|
10760
|
+
}
|
|
10761
|
+
};
|
|
10762
|
+
}
|
|
10763
|
+
|
|
10342
10764
|
//#endregion
|
|
10343
10765
|
//#region src/jsx-runtime.ts
|
|
10344
10766
|
/**
|
|
@@ -11006,7 +11428,7 @@ function oxContent(options = {}) {
|
|
|
11006
11428
|
}
|
|
11007
11429
|
};
|
|
11008
11430
|
let searchIndexJson = "";
|
|
11009
|
-
|
|
11431
|
+
const plugins = [
|
|
11010
11432
|
mainPlugin,
|
|
11011
11433
|
environmentPlugin,
|
|
11012
11434
|
docsPlugin,
|
|
@@ -11049,6 +11471,8 @@ function oxContent(options = {}) {
|
|
|
11049
11471
|
}
|
|
11050
11472
|
}
|
|
11051
11473
|
];
|
|
11474
|
+
if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
|
|
11475
|
+
return plugins;
|
|
11052
11476
|
}
|
|
11053
11477
|
/**
|
|
11054
11478
|
* Resolves plugin options with defaults.
|
|
@@ -11074,7 +11498,8 @@ function resolveOptions(options) {
|
|
|
11074
11498
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
11075
11499
|
transformers: options.transformers ?? [],
|
|
11076
11500
|
docs: resolveDocsOptions(options.docs),
|
|
11077
|
-
search: resolveSearchOptions(options.search)
|
|
11501
|
+
search: resolveSearchOptions(options.search),
|
|
11502
|
+
ogViewer: options.ogViewer ?? true
|
|
11078
11503
|
};
|
|
11079
11504
|
}
|
|
11080
11505
|
/**
|