Skip to content

Commit e3d9959

Browse files
authored
Implement Open Graph metadata tags (#208)
- Open Graph metadat tags are now on all pages - On pages without defined descriptions, descriptions are set to a snippet of the page's contents
1 parent f1cbb67 commit e3d9959

2 files changed

Lines changed: 135 additions & 1 deletion

File tree

docs/.vitepress/config.js

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,104 @@
1-
import { readFileSync } from "node:fs";
1+
import { existsSync, readFileSync } from "node:fs";
22
import { dirname, join } from "node:path";
33
import { fileURLToPath } from "node:url";
4+
import removeMarkdown from "remove-markdown";
45

56
const __dirname = dirname(fileURLToPath(import.meta.url));
67
const bnfGrammar = JSON.parse(
78
readFileSync(join(__dirname, "grammars/bnf.tmLanguage.json"), "utf-8"),
89
);
910

11+
/** Public site origin for absolute Open Graph URLs (no trailing slash). */
12+
const SITE_ORIGIN = (
13+
process.env.VITEPRESS_SITE_ORIGIN ?? "https://docs.vala.dev"
14+
).replace(/\/+$/, "");
15+
16+
/** @param siteData Resolved VitePress site data (see SiteData in vitepress types). */
17+
function siteBasePrefix(siteData) {
18+
let base = siteData.base || "/";
19+
if (!base.endsWith("/")) {
20+
base = `${base}/`;
21+
}
22+
if (base === "/") {
23+
return "";
24+
}
25+
return base.slice(0, -1);
26+
}
27+
28+
/**
29+
* Turn VitePress page file path into site URL pathname (aligned with sitemap).
30+
* @param {string} page e.g. "tutorials/index.md"
31+
* @param siteConfig Resolved VitePress site config (rewrites, cleanUrls, etc.).
32+
*/
33+
function pageMdToUrlPath(page, siteConfig) {
34+
const resolved = siteConfig.rewrites.map[page] || page;
35+
let pathname = resolved.replace(/(^|\/)index\.md$/, "$1");
36+
pathname = pathname.replace(/\.md$/, siteConfig.cleanUrls ? "" : ".html");
37+
if (pathname === "" || pathname === "/") {
38+
return siteConfig.cleanUrls ? "/" : "/index.html";
39+
}
40+
return pathname.startsWith("/") ? pathname : `/${pathname}`;
41+
}
42+
43+
/**
44+
* Canonical absolute URL for the page (for og:url).
45+
* @param {string} page
46+
* @param siteData Resolved VitePress site data.
47+
* @param siteConfig Resolved VitePress site config.
48+
*/
49+
function canonicalPageUrl(page, siteData, siteConfig) {
50+
const pathname = pageMdToUrlPath(page, siteConfig);
51+
const pathForUrl = pathname === "/index.html" ? "/" : pathname;
52+
return `${SITE_ORIGIN}${siteBasePrefix(siteData)}${pathForUrl}`;
53+
}
54+
55+
/**
56+
* Absolute URL for a static asset under docs/public.
57+
* @param siteData Resolved VitePress site data.
58+
* @param {string} assetPath e.g. "/logo.png"
59+
*/
60+
function absoluteAssetUrl(siteData, assetPath) {
61+
const path = assetPath.startsWith("/") ? assetPath : `/${assetPath}`;
62+
return `${SITE_ORIGIN}${siteBasePrefix(siteData)}${path}`;
63+
}
64+
65+
const MAX_DESCRIPTION_SNIPPET_LENGTH = 200;
66+
67+
/**
68+
* @param {string} markdown
69+
*/
70+
function stripYamlFrontmatter(markdown) {
71+
return markdown.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\s*\r?\n?/, "");
72+
}
73+
74+
/**
75+
* Plain-text excerpt from Markdown for meta / Open Graph when no `description` is set.
76+
* Strips YAML front matter, runs remove-markdown, then truncates for meta tags.
77+
* @param {string} markdown
78+
* @param {number} [maxLen]
79+
*/
80+
function markdownToDescriptionSnippet(
81+
markdown,
82+
maxLen = MAX_DESCRIPTION_SNIPPET_LENGTH,
83+
) {
84+
if (!markdown || typeof markdown !== "string") {
85+
return "";
86+
}
87+
const withoutFrontmatter = stripYamlFrontmatter(markdown);
88+
let plain = removeMarkdown(withoutFrontmatter, { gfm: true });
89+
plain = plain.replace(/\s+/g, " ").trim();
90+
if (!plain.length) {
91+
return "";
92+
}
93+
if (plain.length <= maxLen) {
94+
return plain;
95+
}
96+
const slice = plain.slice(0, maxLen);
97+
const lastSpace = slice.lastIndexOf(" ");
98+
const cut = lastSpace > maxLen * 0.55 ? lastSpace : maxLen;
99+
return `${plain.slice(0, cut).trim()}...`;
100+
}
101+
10102
export default {
11103
// site-level options
12104
lang: "en-US",
@@ -22,6 +114,47 @@ export default {
22114
},
23115
],
24116
],
117+
transformPageData(pageData, { siteConfig }) {
118+
if (
119+
typeof pageData.description === "string" &&
120+
pageData.description.trim() !== ""
121+
) {
122+
return;
123+
}
124+
if (!pageData.relativePath) {
125+
return;
126+
}
127+
const filePath = join(siteConfig.srcDir, pageData.relativePath);
128+
if (!existsSync(filePath)) {
129+
return;
130+
}
131+
const raw = readFileSync(filePath, "utf-8");
132+
const snippet = markdownToDescriptionSnippet(raw);
133+
if (!snippet.trim()) {
134+
return;
135+
}
136+
return { description: snippet };
137+
},
138+
transformHead({ page, siteData, siteConfig, title, description }) {
139+
const url = canonicalPageUrl(page, siteData, siteConfig);
140+
const image = absoluteAssetUrl(siteData, "/logo.png");
141+
const desc = description.replace(/\s+/g, " ").trim();
142+
const ogLocale = (siteData.lang || "en-US").replace(/-/g, "_");
143+
return [
144+
["meta", { property: "og:title", content: title }],
145+
["meta", { property: "og:description", content: desc }],
146+
["meta", { property: "og:url", content: url }],
147+
["meta", { property: "og:type", content: "website" }],
148+
["meta", { property: "og:site_name", content: siteData.title }],
149+
["meta", { property: "og:locale", content: ogLocale }],
150+
["meta", { property: "og:image", content: image }],
151+
["meta", { property: "og:image:alt", content: siteData.title }],
152+
["meta", { name: "twitter:card", content: "summary" }],
153+
["meta", { name: "twitter:title", content: title }],
154+
["meta", { name: "twitter:description", content: desc }],
155+
["meta", { name: "twitter:image", content: image }],
156+
];
157+
},
25158
locales: {
26159
root: {
27160
label: "English",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"docs:preview": "vitepress preview docs"
1111
},
1212
"devDependencies": {
13+
"remove-markdown": "^0.6.4",
1314
"vitepress": "^1.6.4"
1415
},
1516
"private": true

0 commit comments

Comments
 (0)