@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.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,6 +7981,9 @@ 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
7989
|
function wrapHtml(bodyHtml, width, height) {
|
|
@@ -8003,13 +8006,47 @@ 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
|
});
|
|
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
|
+
}
|
|
8013
8050
|
const fullHtml = wrapHtml(html, width, height);
|
|
8014
8051
|
await page.setContent(fullHtml, { waitUntil: "networkidle" });
|
|
8015
8052
|
const screenshot = await page.screenshot({
|
|
@@ -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,35 @@ 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
|
+
const scopeId = Component.__scopeId;
|
|
8344
|
+
for (const style of descriptor.styles) if (style.scoped && scopeId) {
|
|
8345
|
+
const result = compilerSfc.compileStyle({
|
|
8346
|
+
id: scopeId,
|
|
8347
|
+
source: style.content,
|
|
8348
|
+
scoped: true,
|
|
8349
|
+
filename: templatePath
|
|
8350
|
+
});
|
|
8351
|
+
if (!result.errors.length) extractedCss += result.code;
|
|
8352
|
+
} else extractedCss += style.content;
|
|
8353
|
+
}
|
|
8354
|
+
} catch {}
|
|
8295
8355
|
const { createSSRApp } = await import("vue");
|
|
8296
8356
|
const { renderToString } = await import("vue/server-renderer");
|
|
8297
8357
|
return async (props) => {
|
|
8298
|
-
|
|
8358
|
+
const html = await renderToString(createSSRApp(Component, props));
|
|
8359
|
+
if (extractedCss) return `<style>${extractedCss}</style>${html}`;
|
|
8360
|
+
return html;
|
|
8299
8361
|
};
|
|
8300
8362
|
}
|
|
8301
8363
|
/**
|
|
@@ -8497,10 +8559,11 @@ async function generateOgImages(pages, options, root) {
|
|
|
8497
8559
|
error: "Chromium not available"
|
|
8498
8560
|
}));
|
|
8499
8561
|
const results = [];
|
|
8562
|
+
const publicDir = path.join(root, "public");
|
|
8500
8563
|
const concurrency = Math.max(1, options.concurrency);
|
|
8501
8564
|
for (let i = 0; i < pages.length; i += concurrency) {
|
|
8502
8565
|
const batch = pages.slice(i, i + concurrency);
|
|
8503
|
-
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session)));
|
|
8566
|
+
const batchResults = await Promise.all(batch.map((entry) => renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session, publicDir)));
|
|
8504
8567
|
results.push(...batchResults);
|
|
8505
8568
|
}
|
|
8506
8569
|
return results;
|
|
@@ -8532,7 +8595,7 @@ async function tryServeAllFromCache(pages, templateSource, options, cacheDir) {
|
|
|
8532
8595
|
/**
|
|
8533
8596
|
* Renders a single page to PNG, with cache support.
|
|
8534
8597
|
*/
|
|
8535
|
-
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session) {
|
|
8598
|
+
async function renderSinglePage(entry, templateFn, templateSource, options, cacheDir, session, publicDir) {
|
|
8536
8599
|
const fs = await import("fs/promises");
|
|
8537
8600
|
try {
|
|
8538
8601
|
if (options.cache) {
|
|
@@ -8547,7 +8610,7 @@ async function renderSinglePage(entry, templateFn, templateSource, options, cach
|
|
|
8547
8610
|
}
|
|
8548
8611
|
}
|
|
8549
8612
|
const html = await templateFn(entry.props);
|
|
8550
|
-
const png = await session.renderPage(html, options.width, options.height);
|
|
8613
|
+
const png = await session.renderPage(html, options.width, options.height, publicDir);
|
|
8551
8614
|
await fs.mkdir(path.dirname(entry.outputPath), { recursive: true });
|
|
8552
8615
|
await fs.writeFile(entry.outputPath, png);
|
|
8553
8616
|
if (options.cache) await writeCache(cacheDir, computeCacheKey(templateSource, entry.props, options.width, options.height), png);
|
|
@@ -8985,7 +9048,8 @@ const DEFAULT_HTML_TEMPLATE = `<!DOCTYPE html>
|
|
|
8985
9048
|
{{#description}}<meta property="og:description" content="{{description}}">{{/description}}
|
|
8986
9049
|
{{#ogImage}}<meta property="og:image" content="{{ogImage}}">{{/ogImage}}
|
|
8987
9050
|
<!-- Twitter Card -->
|
|
8988
|
-
<meta name="twitter:card" content="summary_large_image">
|
|
9051
|
+
{{#ogImage}}<meta name="twitter:card" content="summary_large_image">{{/ogImage}}
|
|
9052
|
+
{{^ogImage}}<meta name="twitter:card" content="summary">{{/ogImage}}
|
|
8989
9053
|
<meta name="twitter:title" content="{{title}}{{#siteName}} - {{siteName}}{{/siteName}}">
|
|
8990
9054
|
{{#description}}<meta name="twitter:description" content="{{description}}">{{/description}}
|
|
8991
9055
|
{{#ogImage}}<meta name="twitter:image" content="{{ogImage}}">{{/ogImage}}
|
|
@@ -9749,6 +9813,9 @@ function renderTemplate(template, data) {
|
|
|
9749
9813
|
result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
|
|
9750
9814
|
return data[key] ? content : "";
|
|
9751
9815
|
});
|
|
9816
|
+
result = result.replace(/\{\{\^(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_, key, content) => {
|
|
9817
|
+
return data[key] ? "" : content;
|
|
9818
|
+
});
|
|
9752
9819
|
result = result.replace(/\{\{(\w+)\}\}/g, (_, key) => {
|
|
9753
9820
|
const value = data[key];
|
|
9754
9821
|
if (value === void 0 || value === null) return "";
|
|
@@ -9762,7 +9829,7 @@ function renderTemplate(template, data) {
|
|
|
9762
9829
|
/**
|
|
9763
9830
|
* Extracts title from content or frontmatter.
|
|
9764
9831
|
*/
|
|
9765
|
-
function extractTitle(content, frontmatter) {
|
|
9832
|
+
function extractTitle$1(content, frontmatter) {
|
|
9766
9833
|
if (frontmatter.title && typeof frontmatter.title === "string") return frontmatter.title;
|
|
9767
9834
|
const h1Match = content.match(/<h1[^>]*>([^<]+)<\/h1>/i);
|
|
9768
9835
|
if (h1Match) return h1Match[1].trim();
|
|
@@ -9847,7 +9914,7 @@ function getOutputPath(inputPath, srcDir, outDir, extension) {
|
|
|
9847
9914
|
/**
|
|
9848
9915
|
* Converts a markdown file path to a relative URL path.
|
|
9849
9916
|
*/
|
|
9850
|
-
function getUrlPath(inputPath, srcDir) {
|
|
9917
|
+
function getUrlPath$1(inputPath, srcDir) {
|
|
9851
9918
|
const baseName = path.relative(srcDir, inputPath).replace(/\.(?:md|markdown)$/i, "");
|
|
9852
9919
|
if (baseName === "index" || baseName.endsWith("/index")) return baseName.replace(/\/?index$/, "") || "/";
|
|
9853
9920
|
return baseName;
|
|
@@ -9856,7 +9923,7 @@ function getUrlPath(inputPath, srcDir) {
|
|
|
9856
9923
|
* Converts a markdown file path to an href.
|
|
9857
9924
|
*/
|
|
9858
9925
|
function getHref(inputPath, srcDir, base, extension) {
|
|
9859
|
-
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9926
|
+
const urlPath = getUrlPath$1(inputPath, srcDir);
|
|
9860
9927
|
if (urlPath === "/" || urlPath === "") return `${base}index${extension}`;
|
|
9861
9928
|
return `${base}${urlPath}/index${extension}`;
|
|
9862
9929
|
}
|
|
@@ -9876,7 +9943,7 @@ function getOgImagePath(inputPath, srcDir, outDir) {
|
|
|
9876
9943
|
* If siteUrl is provided, returns an absolute URL (required for SNS sharing).
|
|
9877
9944
|
*/
|
|
9878
9945
|
function getOgImageUrl(inputPath, srcDir, base, siteUrl) {
|
|
9879
|
-
const urlPath = getUrlPath(inputPath, srcDir);
|
|
9946
|
+
const urlPath = getUrlPath$1(inputPath, srcDir);
|
|
9880
9947
|
let relativePath;
|
|
9881
9948
|
if (urlPath === "/" || urlPath === "") relativePath = `${base}og-image.png`;
|
|
9882
9949
|
else relativePath = `${base}${urlPath}/og-image.png`;
|
|
@@ -9930,7 +9997,7 @@ function buildNavItems(markdownFiles, srcDir, base, extension) {
|
|
|
9930
9997
|
let groupKey = "";
|
|
9931
9998
|
if (parts.length > 1) groupKey = parts[0];
|
|
9932
9999
|
if (!groups.has(groupKey)) groups.set(groupKey, []);
|
|
9933
|
-
const urlPath = getUrlPath(file, srcDir);
|
|
10000
|
+
const urlPath = getUrlPath$1(file, srcDir);
|
|
9934
10001
|
let title;
|
|
9935
10002
|
if (urlPath === "/" || urlPath === "") title = "Overview";
|
|
9936
10003
|
else title = getDisplayTitle(file);
|
|
@@ -9995,6 +10062,7 @@ async function buildSsg(options, root) {
|
|
|
9995
10062
|
if (pkg.name) siteName = formatTitle(pkg.name);
|
|
9996
10063
|
} catch {}
|
|
9997
10064
|
const ogImageEntries = [];
|
|
10065
|
+
const ogImageInputPaths = [];
|
|
9998
10066
|
const ogImageUrlMap = /* @__PURE__ */ new Map();
|
|
9999
10067
|
const shouldGenerateOgImages = (options.ogImage || ssgOptions.generateOgImage) && !ssgOptions.bare;
|
|
10000
10068
|
const pageResults = [];
|
|
@@ -10018,7 +10086,7 @@ async function buildSsg(options, root) {
|
|
|
10018
10086
|
transformedHtml = await transformAllPlugins(transformedHtml, pluginOptions);
|
|
10019
10087
|
if (hasIslands(transformedHtml)) transformedHtml = (await transformIslands(transformedHtml)).html;
|
|
10020
10088
|
transformedHtml = restoreMermaidSvgs(transformedHtml, mermaidSvgs);
|
|
10021
|
-
const title = extractTitle(transformedHtml, result.frontmatter);
|
|
10089
|
+
const title = extractTitle$1(transformedHtml, result.frontmatter);
|
|
10022
10090
|
const description = result.frontmatter.description;
|
|
10023
10091
|
pageResults.push({
|
|
10024
10092
|
inputPath,
|
|
@@ -10040,6 +10108,7 @@ async function buildSsg(options, root) {
|
|
|
10040
10108
|
},
|
|
10041
10109
|
outputPath: ogImageOutputPath
|
|
10042
10110
|
});
|
|
10111
|
+
ogImageInputPaths.push(inputPath);
|
|
10043
10112
|
ogImageUrlMap.set(inputPath, getOgImageUrl(inputPath, srcDir, base, ssgOptions.siteUrl));
|
|
10044
10113
|
}
|
|
10045
10114
|
} catch (err) {
|
|
@@ -10049,10 +10118,15 @@ async function buildSsg(options, root) {
|
|
|
10049
10118
|
if (shouldGenerateOgImages && ogImageEntries.length > 0) try {
|
|
10050
10119
|
const ogResults = await generateOgImages(ogImageEntries, options.ogImageOptions, root);
|
|
10051
10120
|
let ogSuccessCount = 0;
|
|
10052
|
-
for (
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
|
|
10121
|
+
for (let i = 0; i < ogResults.length; i++) {
|
|
10122
|
+
const result = ogResults[i];
|
|
10123
|
+
if (result.error) {
|
|
10124
|
+
errors.push(`OG image failed for ${result.outputPath}: ${result.error}`);
|
|
10125
|
+
ogImageUrlMap.delete(ogImageInputPaths[i]);
|
|
10126
|
+
} else {
|
|
10127
|
+
generatedFiles.push(result.outputPath);
|
|
10128
|
+
ogSuccessCount++;
|
|
10129
|
+
}
|
|
10056
10130
|
}
|
|
10057
10131
|
if (ogSuccessCount > 0) {
|
|
10058
10132
|
const cachedCount = ogResults.filter((r) => r.cached && !r.error).length;
|
|
@@ -10061,6 +10135,7 @@ async function buildSsg(options, root) {
|
|
|
10061
10135
|
} catch (err) {
|
|
10062
10136
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
10063
10137
|
console.warn(`[ox-content:og-image] Batch generation failed: ${errorMessage}`);
|
|
10138
|
+
ogImageUrlMap.clear();
|
|
10064
10139
|
}
|
|
10065
10140
|
for (const pageResult of pageResults) try {
|
|
10066
10141
|
const { inputPath, transformedHtml, title, description, frontmatter, toc } = pageResult;
|
|
@@ -10079,7 +10154,7 @@ async function buildSsg(options, root) {
|
|
|
10079
10154
|
content: transformedHtml,
|
|
10080
10155
|
toc,
|
|
10081
10156
|
frontmatter,
|
|
10082
|
-
path: getUrlPath(inputPath, srcDir),
|
|
10157
|
+
path: getUrlPath$1(inputPath, srcDir),
|
|
10083
10158
|
href: getHref(inputPath, srcDir, base, ssgOptions.extension),
|
|
10084
10159
|
entryPage
|
|
10085
10160
|
}, navItems, siteName, base, pageOgImage, ssgOptions.theme);
|
|
@@ -10378,6 +10453,353 @@ export default { search, searchOptions, loadIndex };
|
|
|
10378
10453
|
`;
|
|
10379
10454
|
}
|
|
10380
10455
|
|
|
10456
|
+
//#endregion
|
|
10457
|
+
//#region src/og-viewer.ts
|
|
10458
|
+
/**
|
|
10459
|
+
* OG Viewer - Dev tool for previewing Open Graph metadata
|
|
10460
|
+
*
|
|
10461
|
+
* Accessible at /__og-viewer during development.
|
|
10462
|
+
* Shows all pages with their OG metadata, validation warnings,
|
|
10463
|
+
* and social card previews.
|
|
10464
|
+
*/
|
|
10465
|
+
function parseFrontmatter(content) {
|
|
10466
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
10467
|
+
if (!match) return {};
|
|
10468
|
+
const yaml = match[1];
|
|
10469
|
+
const result = {};
|
|
10470
|
+
for (const line of yaml.split("\n")) {
|
|
10471
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
10472
|
+
if (!kv) continue;
|
|
10473
|
+
const [, key, rawValue] = kv;
|
|
10474
|
+
let value = rawValue.trim();
|
|
10475
|
+
if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) value = value.slice(1, -1).split(",").map((s) => s.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
|
|
10476
|
+
else if (typeof value === "string" && /^['"].*['"]$/.test(value)) value = value.slice(1, -1);
|
|
10477
|
+
else if (value === "true") value = true;
|
|
10478
|
+
else if (value === "false") value = false;
|
|
10479
|
+
result[key] = value;
|
|
10480
|
+
}
|
|
10481
|
+
return result;
|
|
10482
|
+
}
|
|
10483
|
+
function extractTitle(content, frontmatter) {
|
|
10484
|
+
if (typeof frontmatter.title === "string" && frontmatter.title) return frontmatter.title;
|
|
10485
|
+
const match = content.match(/^#\s+(.+)$/m);
|
|
10486
|
+
return match ? match[1].trim() : "";
|
|
10487
|
+
}
|
|
10488
|
+
function getUrlPath(filePath, srcDir) {
|
|
10489
|
+
let rel = path.relative(srcDir, filePath).replace(/\\/g, "/");
|
|
10490
|
+
rel = rel.replace(/\.md$/, "");
|
|
10491
|
+
if (rel === "index") return "/";
|
|
10492
|
+
if (rel.endsWith("/index")) rel = rel.slice(0, -6);
|
|
10493
|
+
return "/" + rel;
|
|
10494
|
+
}
|
|
10495
|
+
function computeOgImageUrl(urlPath, base, siteUrl, generateOgImage, staticOgImage) {
|
|
10496
|
+
if (!generateOgImage) return staticOgImage || "";
|
|
10497
|
+
const cleanBase = base.endsWith("/") ? base : base + "/";
|
|
10498
|
+
let relativePath;
|
|
10499
|
+
if (urlPath === "/") relativePath = `${cleanBase}og-image.png`;
|
|
10500
|
+
else relativePath = `${cleanBase}${urlPath.replace(/^\//, "")}/og-image.png`;
|
|
10501
|
+
if (siteUrl) return `${siteUrl.replace(/\/$/, "")}${relativePath}`;
|
|
10502
|
+
return relativePath;
|
|
10503
|
+
}
|
|
10504
|
+
function validatePage(page, options) {
|
|
10505
|
+
const warnings = [];
|
|
10506
|
+
if (!page.title) warnings.push({
|
|
10507
|
+
level: "error",
|
|
10508
|
+
message: "title is missing"
|
|
10509
|
+
});
|
|
10510
|
+
else if (page.title.length > 70) warnings.push({
|
|
10511
|
+
level: "warning",
|
|
10512
|
+
message: `title is too long (${page.title.length}/70)`
|
|
10513
|
+
});
|
|
10514
|
+
if (!page.description) warnings.push({
|
|
10515
|
+
level: "warning",
|
|
10516
|
+
message: "description is missing"
|
|
10517
|
+
});
|
|
10518
|
+
else if (page.description.length > 200) warnings.push({
|
|
10519
|
+
level: "warning",
|
|
10520
|
+
message: `description is too long (${page.description.length}/200)`
|
|
10521
|
+
});
|
|
10522
|
+
if ((options.ogImage || options.ssg.generateOgImage) && !options.ssg.siteUrl) warnings.push({
|
|
10523
|
+
level: "warning",
|
|
10524
|
+
message: "ogImage enabled but siteUrl is not set"
|
|
10525
|
+
});
|
|
10526
|
+
return warnings;
|
|
10527
|
+
}
|
|
10528
|
+
async function collectPages(options, root) {
|
|
10529
|
+
const srcDir = path.resolve(root, options.srcDir);
|
|
10530
|
+
const files = await glob("**/*.md", {
|
|
10531
|
+
cwd: srcDir,
|
|
10532
|
+
absolute: true
|
|
10533
|
+
});
|
|
10534
|
+
const pages = [];
|
|
10535
|
+
const generateOgImage = options.ogImage || options.ssg.generateOgImage;
|
|
10536
|
+
for (const file of files.sort()) {
|
|
10537
|
+
const content = fs$1.readFileSync(file, "utf-8");
|
|
10538
|
+
const frontmatter = parseFrontmatter(content);
|
|
10539
|
+
if (frontmatter.layout === "entry") continue;
|
|
10540
|
+
const title = extractTitle(content, frontmatter);
|
|
10541
|
+
const description = typeof frontmatter.description === "string" ? frontmatter.description : "";
|
|
10542
|
+
const author = typeof frontmatter.author === "string" ? frontmatter.author : "";
|
|
10543
|
+
const tags = Array.isArray(frontmatter.tags) ? frontmatter.tags : typeof frontmatter.tags === "string" ? [frontmatter.tags] : [];
|
|
10544
|
+
const urlPath = getUrlPath(file, srcDir);
|
|
10545
|
+
const ogImageUrl = computeOgImageUrl(urlPath, options.base, options.ssg.siteUrl, generateOgImage, options.ssg.ogImage);
|
|
10546
|
+
const page = {
|
|
10547
|
+
path: path.relative(srcDir, file),
|
|
10548
|
+
urlPath,
|
|
10549
|
+
title,
|
|
10550
|
+
description,
|
|
10551
|
+
author,
|
|
10552
|
+
tags,
|
|
10553
|
+
ogImageUrl,
|
|
10554
|
+
warnings: []
|
|
10555
|
+
};
|
|
10556
|
+
page.warnings = validatePage(page, options);
|
|
10557
|
+
pages.push(page);
|
|
10558
|
+
}
|
|
10559
|
+
return pages;
|
|
10560
|
+
}
|
|
10561
|
+
function renderViewerHtml(pages, options) {
|
|
10562
|
+
const generateOgImage = options.ogImage || options.ssg.generateOgImage;
|
|
10563
|
+
const totalWarnings = pages.reduce((sum, p) => sum + p.warnings.filter((w) => w.level === "warning").length, 0);
|
|
10564
|
+
const totalErrors = pages.reduce((sum, p) => sum + p.warnings.filter((w) => w.level === "error").length, 0);
|
|
10565
|
+
return `<!DOCTYPE html>
|
|
10566
|
+
<html lang="en">
|
|
10567
|
+
<head>
|
|
10568
|
+
<meta charset="UTF-8">
|
|
10569
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10570
|
+
<title>OG Viewer - ox-content</title>
|
|
10571
|
+
<style>
|
|
10572
|
+
:root {
|
|
10573
|
+
--bg: #ffffff;
|
|
10574
|
+
--bg-card: #f8f9fa;
|
|
10575
|
+
--bg-preview: #ffffff;
|
|
10576
|
+
--text: #1a1a2e;
|
|
10577
|
+
--text-muted: #6b7280;
|
|
10578
|
+
--border: #e5e7eb;
|
|
10579
|
+
--accent: #e8590c;
|
|
10580
|
+
--accent-light: #fff4e6;
|
|
10581
|
+
--error: #dc2626;
|
|
10582
|
+
--error-bg: #fef2f2;
|
|
10583
|
+
--warning: #d97706;
|
|
10584
|
+
--warning-bg: #fffbeb;
|
|
10585
|
+
--success: #16a34a;
|
|
10586
|
+
--tag-bg: #f0f0f0;
|
|
10587
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
|
10588
|
+
--radius: 8px;
|
|
10589
|
+
}
|
|
10590
|
+
@media (prefers-color-scheme: dark) {
|
|
10591
|
+
:root {
|
|
10592
|
+
--bg: #0f172a;
|
|
10593
|
+
--bg-card: #1e293b;
|
|
10594
|
+
--bg-preview: #334155;
|
|
10595
|
+
--text: #e2e8f0;
|
|
10596
|
+
--text-muted: #94a3b8;
|
|
10597
|
+
--border: #334155;
|
|
10598
|
+
--accent: #fb923c;
|
|
10599
|
+
--accent-light: #431407;
|
|
10600
|
+
--error: #f87171;
|
|
10601
|
+
--error-bg: #450a0a;
|
|
10602
|
+
--warning: #fbbf24;
|
|
10603
|
+
--warning-bg: #451a03;
|
|
10604
|
+
--success: #4ade80;
|
|
10605
|
+
--tag-bg: #334155;
|
|
10606
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
|
10607
|
+
}
|
|
10608
|
+
}
|
|
10609
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
10610
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
|
10611
|
+
.header { padding: 16px 24px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }
|
|
10612
|
+
.header svg { width: 28px; height: 28px; color: var(--accent); }
|
|
10613
|
+
.header h1 { font-size: 18px; font-weight: 600; }
|
|
10614
|
+
.header h1 span { color: var(--text-muted); font-weight: 400; }
|
|
10615
|
+
.header-actions { margin-left: auto; }
|
|
10616
|
+
.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; }
|
|
10617
|
+
.btn:hover { border-color: var(--accent); color: var(--accent); }
|
|
10618
|
+
.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; }
|
|
10619
|
+
.summary-item { display: flex; align-items: center; gap: 4px; }
|
|
10620
|
+
.summary-item strong { color: var(--text); }
|
|
10621
|
+
.summary-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
10622
|
+
.dot-error { background: var(--error); }
|
|
10623
|
+
.dot-warning { background: var(--warning); }
|
|
10624
|
+
.dot-success { background: var(--success); }
|
|
10625
|
+
.toolbar { padding: 12px 24px; display: flex; gap: 8px; border-bottom: 1px solid var(--border); flex-wrap: wrap; align-items: center; }
|
|
10626
|
+
.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; }
|
|
10627
|
+
.filter-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
10628
|
+
.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; }
|
|
10629
|
+
.search-input::placeholder { color: var(--text-muted); }
|
|
10630
|
+
.container { padding: 24px; display: flex; flex-direction: column; gap: 20px; max-width: 1200px; margin: 0 auto; }
|
|
10631
|
+
.card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-card); box-shadow: var(--shadow); overflow: hidden; }
|
|
10632
|
+
.card-header { padding: 16px; border-bottom: 1px solid var(--border); }
|
|
10633
|
+
.card-path { font-size: 12px; color: var(--text-muted); font-family: monospace; margin-bottom: 4px; }
|
|
10634
|
+
.card-title { font-size: 16px; font-weight: 600; }
|
|
10635
|
+
.card-desc { font-size: 13px; color: var(--text-muted); margin-top: 4px; }
|
|
10636
|
+
.card-meta { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; align-items: center; }
|
|
10637
|
+
.tag { padding: 2px 8px; background: var(--tag-bg); border-radius: 4px; font-size: 11px; color: var(--text-muted); }
|
|
10638
|
+
.card-warnings { padding: 8px 16px; display: flex; flex-direction: column; gap: 4px; }
|
|
10639
|
+
.warning-item { font-size: 12px; padding: 4px 8px; border-radius: 4px; }
|
|
10640
|
+
.warning-item.error { background: var(--error-bg); color: var(--error); }
|
|
10641
|
+
.warning-item.warning { background: var(--warning-bg); color: var(--warning); }
|
|
10642
|
+
.card-previews { padding: 16px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
|
10643
|
+
@media (max-width: 768px) { .card-previews { grid-template-columns: 1fr; } }
|
|
10644
|
+
.preview { border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
|
|
10645
|
+
.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; }
|
|
10646
|
+
.preview-card { background: var(--bg-preview); }
|
|
10647
|
+
.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; }
|
|
10648
|
+
.preview-img img { width: 100%; height: 100%; object-fit: cover; }
|
|
10649
|
+
.preview-body { padding: 10px 12px; }
|
|
10650
|
+
.preview-url { font-size: 11px; color: var(--text-muted); margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
10651
|
+
.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; }
|
|
10652
|
+
.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; }
|
|
10653
|
+
.empty { text-align: center; padding: 60px; color: var(--text-muted); }
|
|
10654
|
+
.spin { animation: spin 0.6s linear infinite; }
|
|
10655
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
10656
|
+
</style>
|
|
10657
|
+
</head>
|
|
10658
|
+
<body>
|
|
10659
|
+
<div class="header">
|
|
10660
|
+
<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>
|
|
10661
|
+
<h1>OG Viewer <span>/ ox-content</span></h1>
|
|
10662
|
+
<div class="header-actions">
|
|
10663
|
+
<button class="btn" id="refresh-btn" onclick="refresh()">Refresh</button>
|
|
10664
|
+
</div>
|
|
10665
|
+
</div>
|
|
10666
|
+
<div class="summary" id="summary">
|
|
10667
|
+
<div class="summary-item"><strong id="s-pages">${pages.length}</strong> pages</div>
|
|
10668
|
+
<div class="summary-item"><span class="summary-dot dot-error"></span> <strong id="s-errors">${totalErrors}</strong> errors</div>
|
|
10669
|
+
<div class="summary-item"><span class="summary-dot dot-warning"></span> <strong id="s-warnings">${totalWarnings}</strong> warnings</div>
|
|
10670
|
+
<div class="summary-item"><span class="summary-dot ${generateOgImage ? "dot-success" : "dot-warning"}"></span> ogImage: <strong>${generateOgImage ? "enabled" : "disabled"}</strong></div>
|
|
10671
|
+
</div>
|
|
10672
|
+
<div class="toolbar">
|
|
10673
|
+
<button class="filter-btn active" data-filter="all" onclick="setFilter('all')">All</button>
|
|
10674
|
+
<button class="filter-btn" data-filter="warnings" onclick="setFilter('warnings')">Warnings</button>
|
|
10675
|
+
<button class="filter-btn" data-filter="errors" onclick="setFilter('errors')">Errors</button>
|
|
10676
|
+
<input class="search-input" type="text" placeholder="Search pages..." oninput="applyFilters()" id="search-input">
|
|
10677
|
+
</div>
|
|
10678
|
+
<div class="container" id="container"></div>
|
|
10679
|
+
|
|
10680
|
+
<script>
|
|
10681
|
+
let pages = ${JSON.stringify(pages)};
|
|
10682
|
+
let currentFilter = 'all';
|
|
10683
|
+
|
|
10684
|
+
function setFilter(f) {
|
|
10685
|
+
currentFilter = f;
|
|
10686
|
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.toggle('active', b.dataset.filter === f));
|
|
10687
|
+
applyFilters();
|
|
10688
|
+
}
|
|
10689
|
+
|
|
10690
|
+
function applyFilters() {
|
|
10691
|
+
const q = document.getElementById('search-input').value.toLowerCase();
|
|
10692
|
+
const filtered = pages.filter(p => {
|
|
10693
|
+
if (currentFilter === 'errors' && !p.warnings.some(w => w.level === 'error')) return false;
|
|
10694
|
+
if (currentFilter === 'warnings' && !p.warnings.length) return false;
|
|
10695
|
+
if (q && !p.path.toLowerCase().includes(q) && !p.title.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q)) return false;
|
|
10696
|
+
return true;
|
|
10697
|
+
});
|
|
10698
|
+
renderCards(filtered);
|
|
10699
|
+
}
|
|
10700
|
+
|
|
10701
|
+
function esc(s) {
|
|
10702
|
+
const d = document.createElement('div');
|
|
10703
|
+
d.textContent = s;
|
|
10704
|
+
return d.innerHTML;
|
|
10705
|
+
}
|
|
10706
|
+
|
|
10707
|
+
function renderCards(list) {
|
|
10708
|
+
const c = document.getElementById('container');
|
|
10709
|
+
if (!list.length) {
|
|
10710
|
+
c.innerHTML = '<div class="empty">No pages match the current filter.</div>';
|
|
10711
|
+
return;
|
|
10712
|
+
}
|
|
10713
|
+
c.innerHTML = list.map(p => {
|
|
10714
|
+
const warnings = p.warnings.map(w =>
|
|
10715
|
+
'<div class="warning-item ' + w.level + '">' + (w.level === 'error' ? '\\u2716' : '\\u26A0') + ' ' + esc(w.message) + '</div>'
|
|
10716
|
+
).join('');
|
|
10717
|
+
const tags = p.tags.map(t => '<span class="tag">' + esc(t) + '</span>').join('');
|
|
10718
|
+
const author = p.author ? '<span class="tag">by ' + esc(p.author) + '</span>' : '';
|
|
10719
|
+
const imgHtml = p.ogImageUrl
|
|
10720
|
+
? '<img src="' + esc(p.ogImageUrl) + '" onerror="this.parentNode.innerHTML=\\'No OG image\\'">'
|
|
10721
|
+
: 'No OG image';
|
|
10722
|
+
const siteHost = ${JSON.stringify(options.ssg.siteUrl || "example.com")};
|
|
10723
|
+
return '<div class="card">'
|
|
10724
|
+
+ '<div class="card-header">'
|
|
10725
|
+
+ '<div class="card-path">' + esc(p.path) + ' → ' + esc(p.urlPath) + '</div>'
|
|
10726
|
+
+ '<div class="card-title">' + (esc(p.title) || '<em style="color:var(--error)">No title</em>') + '</div>'
|
|
10727
|
+
+ (p.description ? '<div class="card-desc">' + esc(p.description) + '</div>' : '')
|
|
10728
|
+
+ (tags || author ? '<div class="card-meta">' + author + tags + '</div>' : '')
|
|
10729
|
+
+ '</div>'
|
|
10730
|
+
+ (warnings ? '<div class="card-warnings">' + warnings + '</div>' : '')
|
|
10731
|
+
+ '<div class="card-previews">'
|
|
10732
|
+
+ '<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>'
|
|
10733
|
+
+ '<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>'
|
|
10734
|
+
+ '</div>'
|
|
10735
|
+
+ '</div>';
|
|
10736
|
+
}).join('');
|
|
10737
|
+
}
|
|
10738
|
+
|
|
10739
|
+
async function refresh() {
|
|
10740
|
+
const btn = document.getElementById('refresh-btn');
|
|
10741
|
+
btn.textContent = 'Refreshing...';
|
|
10742
|
+
btn.disabled = true;
|
|
10743
|
+
try {
|
|
10744
|
+
const res = await fetch('/__og-viewer/api/pages');
|
|
10745
|
+
pages = await res.json();
|
|
10746
|
+
updateSummary();
|
|
10747
|
+
applyFilters();
|
|
10748
|
+
} catch(e) {
|
|
10749
|
+
console.error('Refresh failed:', e);
|
|
10750
|
+
} finally {
|
|
10751
|
+
btn.textContent = 'Refresh';
|
|
10752
|
+
btn.disabled = false;
|
|
10753
|
+
}
|
|
10754
|
+
}
|
|
10755
|
+
|
|
10756
|
+
function updateSummary() {
|
|
10757
|
+
document.getElementById('s-pages').textContent = pages.length;
|
|
10758
|
+
document.getElementById('s-errors').textContent = pages.reduce((s,p) => s + p.warnings.filter(w => w.level === 'error').length, 0);
|
|
10759
|
+
document.getElementById('s-warnings').textContent = pages.reduce((s,p) => s + p.warnings.filter(w => w.level === 'warning').length, 0);
|
|
10760
|
+
}
|
|
10761
|
+
|
|
10762
|
+
renderCards(pages);
|
|
10763
|
+
<\/script>
|
|
10764
|
+
</body>
|
|
10765
|
+
</html>`;
|
|
10766
|
+
}
|
|
10767
|
+
function createOgViewerPlugin(options) {
|
|
10768
|
+
return {
|
|
10769
|
+
name: "ox-content:og-viewer",
|
|
10770
|
+
apply: "serve",
|
|
10771
|
+
configureServer(server) {
|
|
10772
|
+
server.middlewares.use(async (req, res, next) => {
|
|
10773
|
+
if (req.url === "/__og-viewer" || req.url === "/__og-viewer/") {
|
|
10774
|
+
const root = server.config.root || process.cwd();
|
|
10775
|
+
try {
|
|
10776
|
+
const html = renderViewerHtml(await collectPages(options, root), options);
|
|
10777
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
10778
|
+
res.end(html);
|
|
10779
|
+
} catch (err) {
|
|
10780
|
+
res.statusCode = 500;
|
|
10781
|
+
res.end(`OG Viewer error: ${err}`);
|
|
10782
|
+
}
|
|
10783
|
+
return;
|
|
10784
|
+
}
|
|
10785
|
+
if (req.url === "/__og-viewer/api/pages") {
|
|
10786
|
+
const root = server.config.root || process.cwd();
|
|
10787
|
+
try {
|
|
10788
|
+
const pages = await collectPages(options, root);
|
|
10789
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
10790
|
+
res.end(JSON.stringify(pages));
|
|
10791
|
+
} catch (err) {
|
|
10792
|
+
res.statusCode = 500;
|
|
10793
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
10794
|
+
}
|
|
10795
|
+
return;
|
|
10796
|
+
}
|
|
10797
|
+
next();
|
|
10798
|
+
});
|
|
10799
|
+
}
|
|
10800
|
+
};
|
|
10801
|
+
}
|
|
10802
|
+
|
|
10381
10803
|
//#endregion
|
|
10382
10804
|
//#region src/jsx-runtime.ts
|
|
10383
10805
|
/**
|
|
@@ -11045,7 +11467,7 @@ function oxContent(options = {}) {
|
|
|
11045
11467
|
}
|
|
11046
11468
|
};
|
|
11047
11469
|
let searchIndexJson = "";
|
|
11048
|
-
|
|
11470
|
+
const plugins = [
|
|
11049
11471
|
mainPlugin,
|
|
11050
11472
|
environmentPlugin,
|
|
11051
11473
|
docsPlugin,
|
|
@@ -11088,6 +11510,8 @@ function oxContent(options = {}) {
|
|
|
11088
11510
|
}
|
|
11089
11511
|
}
|
|
11090
11512
|
];
|
|
11513
|
+
if (resolvedOptions.ogViewer) plugins.push(createOgViewerPlugin(resolvedOptions));
|
|
11514
|
+
return plugins;
|
|
11091
11515
|
}
|
|
11092
11516
|
/**
|
|
11093
11517
|
* Resolves plugin options with defaults.
|
|
@@ -11113,7 +11537,8 @@ function resolveOptions(options) {
|
|
|
11113
11537
|
ogImageOptions: resolveOgImageOptions(options.ogImageOptions),
|
|
11114
11538
|
transformers: options.transformers ?? [],
|
|
11115
11539
|
docs: resolveDocsOptions(options.docs),
|
|
11116
|
-
search: resolveSearchOptions(options.search)
|
|
11540
|
+
search: resolveSearchOptions(options.search),
|
|
11541
|
+
ogViewer: options.ogViewer ?? true
|
|
11117
11542
|
};
|
|
11118
11543
|
}
|
|
11119
11544
|
/**
|