diff --git a/docs/public/og-image.png b/docs/public/og-image.png
new file mode 100644
index 0000000..8253a57
Binary files /dev/null and b/docs/public/og-image.png differ
diff --git a/website/assets/og-image.svg b/website/assets/og-image.svg
new file mode 100644
index 0000000..aa199e1
--- /dev/null
+++ b/website/assets/og-image.svg
@@ -0,0 +1,50 @@
+
diff --git a/website/package.json b/website/package.json
index f66359a..1a91b29 100644
--- a/website/package.json
+++ b/website/package.json
@@ -6,6 +6,7 @@
"scripts": {
"dev": "rspress dev",
"build": "rspress build",
+ "render:og-image": "node scripts/render-og-image.cjs",
"preview": "rspress preview",
"type-check": "tsc --noEmit -p tsconfig.json",
"check:links": "node ../scripts/validation/check-site-links.cjs",
diff --git a/website/rspress.config.ts b/website/rspress.config.ts
index aaf93d5..889483f 100644
--- a/website/rspress.config.ts
+++ b/website/rspress.config.ts
@@ -8,6 +8,13 @@ import type {
UserConfig,
} from '@rspress/core';
import { pluginSitemap } from '@rspress/plugin-sitemap';
+import {
+ applyBuiltPageMeta,
+ docsSiteOrigin,
+ localeDescriptions,
+ siteBase,
+ socialHeadEntries,
+} from './social-meta';
type MarkdownAstNode = {
type?: string;
@@ -386,7 +393,6 @@ const isExternalLink = (link: string) => /^https?:\/\//.test(link);
const repositoryRoot = path.join(import.meta.dirname, '..');
const docsRoot = path.join(repositoryRoot, 'docs');
const githubRepositoryUrl = 'https://github.com/devcodex-labs/monSQLize';
-const siteBase = '/monSQLize/';
const isExternalOrSiteLink = (url: string) =>
/^(?:[a-z][a-z0-9+.-]*:|\/\/|#|\/)/i.test(url);
@@ -535,8 +541,13 @@ const rewriteGeneratedLanguageSwitchLinks: RspressPlugin = {
name: 'rewrite-generated-language-switch-links',
afterBuild(config) {
const builtSiteDir = path.resolve(import.meta.dirname, config.outDir || 'dist');
+ const htmlFiles = collectHtmlFiles(builtSiteDir);
+ let descriptionCorrected = 0;
+ let socialAdded = 0;
+ const correctedPages: string[] = [];
+ const socialPages: string[] = [];
- collectHtmlFiles(builtSiteDir).forEach(htmlFile => {
+ htmlFiles.forEach(htmlFile => {
const originalContent = fs.readFileSync(htmlFile, 'utf8');
let rewrittenContent = originalContent;
@@ -544,10 +555,30 @@ const rewriteGeneratedLanguageSwitchLinks: RspressPlugin = {
rewrittenContent = rewrittenContent.split(source).join(target);
});
+ const relativePath = path.relative(builtSiteDir, htmlFile);
+ const meta = applyBuiltPageMeta(rewrittenContent, relativePath);
+ if (meta.descriptionCorrected) {
+ descriptionCorrected += 1;
+ correctedPages.push(relativePath);
+ }
+ if (meta.socialAdded) {
+ socialAdded += 1;
+ socialPages.push(relativePath);
+ }
+ rewrittenContent = meta.html;
+
if (rewrittenContent !== originalContent) {
fs.writeFileSync(htmlFile, rewrittenContent);
}
});
+
+ console.log(`[docs-meta] pages=${htmlFiles.length} description-corrections=${descriptionCorrected} social-tags-added=${socialAdded}`);
+ if (correctedPages.length > 0) {
+ console.log(`[docs-meta] corrected descriptions: ${correctedPages.join(', ')}`);
+ }
+ if (socialPages.length > 0) {
+ console.log(`[docs-meta] inserted social tags: ${socialPages.join(', ')}`);
+ }
}
};
@@ -555,12 +586,15 @@ const config: UserConfig = {
root: path.join(import.meta.dirname, '..', 'docs'),
themeDir: path.join(import.meta.dirname, 'theme'),
base: siteBase,
+ siteOrigin: docsSiteOrigin,
lang: 'en',
title: 'monSQLize',
icon: '/favicon.svg',
globalStyles: path.join(import.meta.dirname, 'styles', 'home.css'),
- description: 'A database-native TypeScript production data runtime layer with MongoDB stable today, plus cache, pools, transactions, models, sync, and observability.',
+ // Locale descriptions are the fallback. A root description would override them
+ // for every page that has no extracted summary, including Chinese pages.
outDir: 'dist',
+ head: socialHeadEntries(),
builderConfig: {
performance: {
buildCache: false,
@@ -584,13 +618,13 @@ const config: UserConfig = {
lang: 'en',
label: 'English',
title: 'monSQLize',
- description: 'A database-native TypeScript production data runtime layer.'
+ description: localeDescriptions.en
},
{
lang: 'zh',
label: '简体中文',
title: 'monSQLize',
- description: '数据库原生 TypeScript 生产数据运行时增强层。'
+ description: localeDescriptions.zh
}
],
plugins: [
@@ -618,7 +652,7 @@ const config: UserConfig = {
lang: 'en',
label: 'English',
title: 'monSQLize',
- description: 'A database-native TypeScript production data runtime layer.',
+ description: localeDescriptions.en,
nav: englishNav,
sidebar: {
'/': englishSidebar
@@ -628,7 +662,7 @@ const config: UserConfig = {
lang: 'zh',
label: '简体中文',
title: 'monSQLize',
- description: '数据库原生 TypeScript 生产数据运行时增强层。',
+ description: localeDescriptions.zh,
nav: chineseNav,
sidebar: {
'/zh/': chineseSidebar
diff --git a/website/scripts/check-browser.cjs b/website/scripts/check-browser.cjs
index 2308024..502a1fa 100644
--- a/website/scripts/check-browser.cjs
+++ b/website/scripts/check-browser.cjs
@@ -14,6 +14,9 @@ const contentTypes = {
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.svg': 'image/svg+xml',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
'.webp': 'image/webp',
'.woff2': 'font/woff2',
};
diff --git a/website/scripts/render-og-image.cjs b/website/scripts/render-og-image.cjs
new file mode 100644
index 0000000..94442fe
--- /dev/null
+++ b/website/scripts/render-og-image.cjs
@@ -0,0 +1,72 @@
+#!/usr/bin/env node
+'use strict';
+
+/**
+ * Render the docs social card.
+ *
+ * Source: website/assets/og-image.svg
+ * Output: docs/public/og-image.png (1200x630, committed, copied into the site build)
+ *
+ * Requires rsvg-convert (librsvg2-bin) and optipng. The SVG uses Inter, with
+ * Liberation Sans as a fallback. Re-run with: npm run render:og-image
+ */
+
+const fs = require('node:fs');
+const path = require('node:path');
+const { spawnSync } = require('node:child_process');
+
+const websiteRoot = path.resolve(__dirname, '..');
+const svgPath = path.join(websiteRoot, 'assets', 'og-image.svg');
+const pngPath = path.resolve(websiteRoot, '..', 'docs', 'public', 'og-image.png');
+const width = 1200;
+const height = 630;
+const maxBytes = 300 * 1024;
+
+function run(command, args) {
+ const result = spawnSync(command, args, { stdio: 'inherit' });
+ if (result.error) {
+ console.error(`[og-image] failed to run ${command}: ${result.error.message}`);
+ process.exit(1);
+ }
+ if (result.status !== 0) {
+ process.exit(result.status ?? 1);
+ }
+}
+
+function readPngSize(filePath) {
+ const header = Buffer.alloc(24);
+ const fd = fs.openSync(filePath, 'r');
+ try {
+ fs.readSync(fd, header, 0, 24, 0);
+ } finally {
+ fs.closeSync(fd);
+ }
+ if (header.toString('ascii', 1, 4) !== 'PNG') {
+ throw new Error(`${filePath} is not a PNG`);
+ }
+ return {
+ width: header.readUInt32BE(16),
+ height: header.readUInt32BE(20),
+ };
+}
+
+run('rsvg-convert', [
+ '--width', String(width),
+ '--height', String(height),
+ '--output', pngPath,
+ svgPath,
+]);
+run('optipng', ['-o7', '-strip', 'all', '-quiet', pngPath]);
+
+const size = readPngSize(pngPath);
+const bytes = fs.statSync(pngPath).size;
+if (size.width !== width || size.height !== height) {
+ console.error(`[og-image] expected ${width}x${height}, got ${size.width}x${size.height}`);
+ process.exit(1);
+}
+if (bytes > maxBytes) {
+ console.error(`[og-image] ${bytes} bytes exceeds ${maxBytes} bytes`);
+ process.exit(1);
+}
+
+console.log(`[og-image] wrote ${path.relative(path.resolve(websiteRoot, '..'), pngPath)} ${size.width}x${size.height} ${bytes} bytes`);
diff --git a/website/social-meta.ts b/website/social-meta.ts
new file mode 100644
index 0000000..3e3f09a
--- /dev/null
+++ b/website/social-meta.ts
@@ -0,0 +1,141 @@
+import type { RouteMeta } from '@rspress/core';
+
+export const docsSiteOrigin = 'https://devcodex-labs.github.io';
+export const siteBase = '/monSQLize/';
+export const shareImageUrl = `${docsSiteOrigin}${siteBase}og-image.png`;
+
+export const localeDescriptions = {
+ en: 'A database-native TypeScript production data runtime layer with MongoDB stable today, plus cache, pools, transactions, models, sync, and observability.',
+ zh: '数据库原生 TypeScript 生产数据运行时增强层。当前 MongoDB 稳定,并提供缓存、连接池、事务、Model、同步与可观测能力。',
+} as const;
+
+export const shareImageAlt = {
+ en: 'monSQLize: MongoDB today, MySQL and PostgreSQL adapters next',
+ zh: 'monSQLize:当前支持 MongoDB,后续接入 MySQL 与 PostgreSQL 适配器',
+} as const;
+
+export type DocsLocale = keyof typeof localeDescriptions;
+
+const cjkPattern = /[\u4e00-\u9fff]/;
+
+type SocialRoute = Pick & { lang?: string };
+
+export function localeFromLanguage(language: string | undefined, routePath = ''): DocsLocale {
+ if (language?.toLowerCase().startsWith('zh')) return 'zh';
+ if (routePath === '/zh' || routePath.startsWith('/zh/')) return 'zh';
+ return 'en';
+}
+
+export function absolutePageUrl(routePath: string): string {
+ const normalized = routePath.startsWith('/') ? routePath : `/${routePath}`;
+ const prefix = `${docsSiteOrigin}${siteBase}`;
+ if (normalized === '/') return prefix;
+ if (normalized.endsWith('/')) return `${prefix}${normalized.slice(1)}`;
+ return `${prefix}${normalized.slice(1)}.html`;
+}
+
+export function routePathFromHtmlFile(relativePath: string): string {
+ const normalized = relativePath.replace(/\\/g, '/');
+ if (normalized === 'index.html') return '/';
+ if (normalized.endsWith('/index.html')) return `/${normalized.slice(0, -'index.html'.length)}`;
+ if (normalized.endsWith('.html')) return `/${normalized.slice(0, -'.html'.length)}`;
+ return `/${normalized}`;
+}
+
+export function localeFromHtml(html: string, relativePath: string): DocsLocale {
+ const language = html.match(/]*\blang=["']([^"']+)["']/i)?.[1];
+ return localeFromLanguage(language, routePathFromHtmlFile(relativePath));
+}
+
+export function socialMetaTags(route: SocialRoute): Array<['meta', Record]> {
+ const locale = localeFromLanguage(route.lang, route.routePath);
+ return [
+ ['meta', { property: 'og:image', content: shareImageUrl }],
+ ['meta', { property: 'og:image:width', content: '1200' }],
+ ['meta', { property: 'og:image:height', content: '630' }],
+ ['meta', { property: 'og:image:alt', content: shareImageAlt[locale] }],
+ ['meta', { property: 'og:url', content: absolutePageUrl(route.routePath) }],
+ ['meta', { name: 'twitter:card', content: 'summary_large_image' }],
+ ['meta', { name: 'twitter:image', content: shareImageUrl }],
+ ];
+}
+
+export function socialHeadEntries(): Array<(route: RouteMeta) => ['meta', Record] | undefined> {
+ return socialMetaTags({ lang: 'en', routePath: '/' }).map((_, index) => (route: RouteMeta) => {
+ if (!route?.routePath) return undefined;
+ return socialMetaTags(route)[index];
+ });
+}
+
+function escapeAttribute(value: string): string {
+ return value.replace(/&/g, '&').replace(/"/g, '"').replace(/): string {
+ const rendered = Object.entries(attrs)
+ .map(([key, value]) => `${key}="${escapeAttribute(value)}"`)
+ .join(' ');
+ return ``;
+}
+
+function descriptionIsWrongLanguage(content: string, locale: DocsLocale): boolean {
+ const trimmed = content.trim();
+ if (!trimmed) return true;
+ const hasCjk = cjkPattern.test(trimmed);
+ return locale === 'en' ? hasCjk : !hasCjk;
+}
+
+function replaceMetaContent(tag: string, content: string): string {
+ if (/\bcontent="[^"]*"/.test(tag)) {
+ return tag.replace(/\bcontent="[^"]*"/, `content="${escapeAttribute(content)}"`);
+ }
+ if (/\bcontent='[^']*'/.test(tag)) {
+ return tag.replace(/\bcontent='[^']*'/, `content="${escapeAttribute(content)}"`);
+ }
+ return tag;
+}
+
+export function applyBuiltPageMeta(html: string, relativePath: string): {
+ html: string;
+ descriptionCorrected: boolean;
+ socialAdded: boolean;
+} {
+ const locale = localeFromHtml(html, relativePath);
+ const description = localeDescriptions[locale];
+ let descriptionCorrected = false;
+
+ if (!html.includes('')) {
+ return { html, descriptionCorrected: false, socialAdded: false };
+ }
+
+ let next = html.replace(/"']|"[^"]*"|'[^']*')*>/gi, tag => {
+ const isDescription = /\b(?:name=["']description["']|property=["']og:description["'])/i.test(tag);
+ if (!isDescription) return tag;
+ const contentMatch = /\bcontent=(["'])([\s\S]*?)\1/i.exec(tag);
+ const content = contentMatch?.[2] ?? '';
+ if (!descriptionIsWrongLanguage(content, locale)) return tag;
+ descriptionCorrected = true;
+ return replaceMetaContent(tag, description);
+ });
+
+ const missingDescription: string[] = [];
+ if (!/\bname=["']description["']/i.test(next)) {
+ missingDescription.push(renderMetaTag({ name: 'description', content: description }));
+ }
+ if (!/\bproperty=["']og:description["']/i.test(next)) {
+ missingDescription.push(renderMetaTag({ property: 'og:description', content: description }));
+ }
+ if (missingDescription.length > 0) {
+ descriptionCorrected = true;
+ next = next.replace('', `${missingDescription.join('')}`);
+ }
+
+ const socialAdded = !/\bproperty=["']og:image["']/i.test(next);
+ if (socialAdded) {
+ const routePath = routePathFromHtmlFile(relativePath);
+ const tags = socialMetaTags({ lang: locale, routePath }).map(([, attrs]) => renderMetaTag(attrs)).join('');
+ next = next.replace('', `${tags}`);
+ }
+
+ return { html: next, descriptionCorrected, socialAdded };
+}