1- import { readFileSync } from "node:fs" ;
1+ import { existsSync , readFileSync } from "node:fs" ;
22import { dirname , join } from "node:path" ;
33import { fileURLToPath } from "node:url" ;
4+ import removeMarkdown from "remove-markdown" ;
45
56const __dirname = dirname ( fileURLToPath ( import . meta. url ) ) ;
67const 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 ( / ( ^ | \/ ) i n d e x \. m d $ / , "$1" ) ;
36+ pathname = pathname . replace ( / \. m d $ / , 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+
10102export 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" ,
0 commit comments