11import {
2+ cpSync ,
23 copyFileSync ,
34 mkdirSync ,
45 mkdtempSync ,
@@ -101,9 +102,15 @@ const writeSite = (outputDirectory, site) => {
101102 for ( const asset of [ "styles.css" , "client.js" ] ) {
102103 copyFileSync ( join ( scriptDirectory , "assets" , asset ) , join ( assetDirectory , asset ) )
103104 }
105+ for ( const entry of readdirSync ( join ( scriptDirectory , "public" ) ) ) {
106+ cpSync ( join ( scriptDirectory , "public" , entry ) , join ( outputDirectory , entry ) , { recursive : true } )
107+ }
108+ writeJson ( join ( outputDirectory , "site.webmanifest" ) , siteManifest ( site ) )
109+ writeFileSync ( join ( outputDirectory , "robots.txt" ) , renderRobots ( site ) )
110+ writeFileSync ( join ( outputDirectory , "sitemap.xml" ) , renderSitemap ( site ) )
104111}
105112
106- const renderIndexPage = ( site ) => {
113+ export const renderIndexPage = ( site ) => {
107114 const moduleCards = site . modules . map ( ( module ) => `
108115 <a class="module-card" href="${ siteUrl ( site , module . route ) } ">
109116 <span class="module-card__path">${ escapeHtml ( displayExport ( module . export ) ) } </span>
@@ -113,7 +120,7 @@ const renderIndexPage = (site) => {
113120 </a>` ) . join ( "" )
114121 const content = `
115122 <section class="reference-hero" data-pagefind-meta="type:overview">
116- <div class="eyebrow">${ escapeHtml ( site . channel ) } API reference</div>
123+ <div class="eyebrow">API reference</div>
117124 <h1>${ escapeHtml ( site . package . name ) } </h1>
118125 <p>${ escapeHtml ( site . package . description ) } </p>
119126 <div class="hero-meta">
@@ -233,15 +240,39 @@ const renderDeclaration = (declaration, id) => {
233240 </article>`
234241}
235242
236- const renderLayout = ( site , { content, currentRoute, description, pageKind, title, toc = "" } ) => `<!doctype html>
243+ export const renderLayout = ( site , { content, currentRoute, description, pageKind, title, toc = "" } ) => {
244+ const pageDescription = description ?? site . description
245+ const canonical = canonicalUrl ( site , currentRoute )
246+ const socialImage = absoluteSiteUrl ( site , site . socialImage )
247+ return `<!doctype html>
237248<html lang="en">
238249 <head>
239250 <meta charset="utf-8">
240251 <meta name="viewport" content="width=device-width, initial-scale=1">
241- <meta name="description" content="${ escapeAttribute ( description ?? site . description ) } ">
252+ <meta name="description" content="${ escapeAttribute ( pageDescription ) } ">
253+ <meta name="theme-color" media="(prefers-color-scheme: light)" content="${ escapeAttribute ( site . themeColor . light ) } ">
254+ <meta name="theme-color" media="(prefers-color-scheme: dark)" content="${ escapeAttribute ( site . themeColor . dark ) } ">
242255 <meta name="api-reference-base" content="${ escapeAttribute ( site . basePath ) } ">
243256 <meta name="generator" content="effect-machine-api-reference-site">
257+ <meta property="og:type" content="website">
258+ <meta property="og:site_name" content="${ escapeAttribute ( site . title ) } ">
259+ <meta property="og:title" content="${ escapeAttribute ( title ) } ">
260+ <meta property="og:description" content="${ escapeAttribute ( pageDescription ) } ">
261+ <meta property="og:url" content="${ escapeAttribute ( canonical ) } ">
262+ <meta property="og:image" content="${ escapeAttribute ( socialImage ) } ">
263+ <meta property="og:image:width" content="1200">
264+ <meta property="og:image:height" content="630">
265+ <meta property="og:image:alt" content="${ escapeAttribute ( `${ site . package . name } API reference` ) } ">
266+ <meta name="twitter:card" content="summary_large_image">
267+ <meta name="twitter:title" content="${ escapeAttribute ( title ) } ">
268+ <meta name="twitter:description" content="${ escapeAttribute ( pageDescription ) } ">
269+ <meta name="twitter:image" content="${ escapeAttribute ( socialImage ) } ">
244270 <title>${ escapeHtml ( title ) } </title>
271+ <link rel="canonical" href="${ escapeAttribute ( canonical ) } ">
272+ <link rel="icon" href="${ siteUrl ( site , "favicon.svg" ) } " type="image/svg+xml">
273+ <link rel="icon" href="${ siteUrl ( site , "favicon.ico" ) } " sizes="any">
274+ <link rel="apple-touch-icon" href="${ siteUrl ( site , "apple-touch-icon.png" ) } ">
275+ <link rel="manifest" href="${ siteUrl ( site , "site.webmanifest" ) } ">
245276 <script>document.documentElement.dataset.theme=localStorage.getItem("api-theme")||"auto"</script>
246277 <link rel="stylesheet" href="${ siteUrl ( site , "assets/styles.css" ) } ">
247278 <script type="module" src="${ siteUrl ( site , "assets/client.js" ) } "></script>
@@ -261,13 +292,15 @@ const renderLayout = (site, { content, currentRoute, description, pageKind, titl
261292 </body>
262293</html>
263294`
295+ }
264296
265297const renderHeader = ( site ) => `
266298 <a class="skip-link" href="#main-content">Skip to content</a>
267299 <header class="site-header" data-pagefind-ignore>
268300 <div class="site-header__brand">
269301 <button class="icon-button mobile-navigation-button" type="button" aria-label="Open navigation" aria-controls="module-navigation" aria-expanded="false">Menu</button>
270302 <a href="${ siteUrl ( site , "" ) } " aria-label="${ escapeAttribute ( site . package . name ) } API reference home">
303+ <img class="brand-mark" src="${ siteUrl ( site , "logo.svg" ) } " alt="" width="28" height="28">
271304 <span class="brand-name">${ escapeHtml ( site . package . name ) } </span>
272305 </a>
273306 <span class="version">v${ escapeHtml ( site . package . version . replace ( / ^ v / , "" ) ) } </span>
@@ -433,6 +466,39 @@ export const uniqueDeclarationIds = (groups) => {
433466}
434467
435468const siteUrl = ( site , path ) => `${ site . basePath } ${ path } ` . replace ( / \/ { 2 , } / g, "/" )
469+ const absoluteSiteUrl = ( site , path ) => new URL ( siteUrl ( site , path ) , `${ site . origin } /` ) . href
470+ const canonicalUrl = ( site , route ) => absoluteSiteUrl ( site , route === "" ? "" : `${ route } /` )
471+
472+ export const siteManifest = ( site ) => ( {
473+ name : `${ site . package . name } API reference` ,
474+ short_name : "effect-machine" ,
475+ description : site . description ,
476+ id : site . basePath ,
477+ start_url : site . basePath ,
478+ scope : site . basePath ,
479+ display : "standalone" ,
480+ background_color : site . themeColor . light ,
481+ theme_color : site . themeColor . light ,
482+ icons : [
483+ { src : siteUrl ( site , "icon-192.png" ) , sizes : "192x192" , type : "image/png" } ,
484+ { src : siteUrl ( site , "icon-512.png" ) , sizes : "512x512" , type : "image/png" } ,
485+ { src : siteUrl ( site , "icon-maskable-512.png" ) , sizes : "512x512" , type : "image/png" , purpose : "maskable" }
486+ ]
487+ } )
488+
489+ export const renderRobots = ( site ) => `User-agent: *
490+ Allow: ${ site . basePath }
491+
492+ Sitemap: ${ absoluteSiteUrl ( site , "sitemap.xml" ) }
493+ `
494+
495+ export const renderSitemap = ( site ) => `<?xml version="1.0" encoding="UTF-8"?>
496+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
497+ ${ [ "" , ...site . modules . map ( ( module ) => `${ module . route } /` ) ]
498+ . map ( ( route ) => ` <url><loc>${ escapeXml ( absoluteSiteUrl ( site , route ) ) } </loc></url>` )
499+ . join ( "\n" ) }
500+ </urlset>
501+ `
436502const displayExport = ( value ) => value === "." ? "root" : value . replace ( / ^ \. \/ / , "" )
437503const titleCase = ( value ) => value . replace ( / ( ^ | \s ) ( \p{ L} ) / gu, ( _ , space , letter ) => `${ space } ${ letter . toUpperCase ( ) } ` )
438504const slugify = ( value ) => value . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 ] + / g, "-" ) . replace ( / ^ - | - $ / g, "" ) || "api"
@@ -447,12 +513,41 @@ const readConfig = (path) => {
447513 if ( typeof config . title !== "string" || typeof config . description !== "string" ) {
448514 throw new Error ( "API reference site title and description must be configured" )
449515 }
516+ if (
517+ typeof config . origin !== "string" ||
518+ typeof config . socialImage !== "string" ||
519+ typeof config . themeColor ?. light !== "string" ||
520+ typeof config . themeColor ?. dark !== "string"
521+ ) {
522+ throw new Error ( "API reference site origin, social image, and theme colors must be configured" )
523+ }
450524 return {
451525 ...config ,
526+ origin : normalizeOrigin ( config . origin ) ,
452527 basePath : normalizeBasePath ( process . env . API_REFERENCE_BASE_PATH ?? config . basePath )
453528 }
454529}
455530
531+ export const normalizeOrigin = ( value ) => {
532+ let url
533+ try {
534+ url = new URL ( value )
535+ } catch {
536+ throw new Error ( "API reference site origin must be an absolute URL" )
537+ }
538+ if (
539+ url . protocol !== "https:" ||
540+ url . username !== "" ||
541+ url . password !== "" ||
542+ url . pathname !== "/" ||
543+ url . search !== "" ||
544+ url . hash !== ""
545+ ) {
546+ throw new Error ( "API reference site origin must be an HTTPS origin without a path" )
547+ }
548+ return url . origin
549+ }
550+
456551export const normalizeBasePath = ( value ) => {
457552 if ( typeof value !== "string" || ( value . length > 0 && ! value . startsWith ( "/" ) ) ) {
458553 throw new Error ( "API reference site basePath must be empty or start with a slash" )
@@ -489,6 +584,17 @@ const validateSite = (outputDirectory, site) => {
489584 "index.html" ,
490585 "assets/styles.css" ,
491586 "assets/client.js" ,
587+ "apple-touch-icon.png" ,
588+ "favicon.ico" ,
589+ "favicon.svg" ,
590+ "icon-192.png" ,
591+ "icon-512.png" ,
592+ "icon-maskable-512.png" ,
593+ "logo.svg" ,
594+ "robots.txt" ,
595+ "site.webmanifest" ,
596+ "sitemap.xml" ,
597+ site . socialImage ,
492598 ...site . modules . map ( ( module ) => join ( module . route , "index.html" ) )
493599 ] ) {
494600 if ( ! isFile ( safeResolve ( outputDirectory , path ) ) ) throw new Error ( `Missing generated site file: ${ path } ` )
@@ -510,6 +616,7 @@ const writePage = (path, html) => {
510616 writeFileSync ( path , html )
511617}
512618const readJson = ( path ) => JSON . parse ( readFileSync ( path , "utf8" ) )
619+ const writeJson = ( path , value ) => writeFileSync ( path , `${ JSON . stringify ( value , undefined , 2 ) } \n` )
513620const isFile = ( path ) => statSync ( path , { throwIfNoEntry : false } ) ?. isFile ( ) === true
514621const escapeHtml = ( value ) => String ( value )
515622 . replaceAll ( "&" , "&" )
@@ -518,6 +625,7 @@ const escapeHtml = (value) => String(value)
518625 . replaceAll ( '"' , """ )
519626 . replaceAll ( "'" , "'" )
520627const escapeAttribute = escapeHtml
628+ const escapeXml = escapeHtml
521629
522630const isMain = process . argv [ 1 ] !== undefined && resolve ( process . argv [ 1 ] ) === fileURLToPath ( import . meta. url )
523631if ( isMain ) {
0 commit comments