Skip to content

Commit 9c96045

Browse files
authored
Generate favicon and PWA icons based on logo with full metadata support (#1013)
1 parent 5130944 commit 9c96045

9 files changed

Lines changed: 509 additions & 45 deletions

File tree

‎.changeset/favicon-generation.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"hyperbook": minor
3+
"@hyperbook/markdown": patch
4+
---
5+
6+
Add automatic favicon and PWA icon generation from logo
7+
8+
When building a Hyperbook project, if no favicon.ico exists and a logo is defined in hyperbook.json, a complete set of favicons and PWA assets are automatically generated:
9+
10+
- Generates 60+ files including favicon.ico, Android icons, Apple touch icons, and Apple startup images
11+
- Creates web manifest with full PWA metadata (theme color, scope, language, developer info)
12+
- Smart logo path resolution: checks root folder, book folder, and public folder
13+
- Adds favicon, Apple touch icon, and manifest links to all HTML pages
14+
- Uses hyperbook.json metadata: name, description, colors.brand, basePath, language, author
15+
- Backward compatible: copies favicon.ico to root for browsers expecting it there

‎packages/hyperbook/build.ts‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,84 @@ async function runBuild(
378378
}
379379
process.stdout.write("\n");
380380

381+
// Generate favicons if logo exists and no favicon.ico is present
382+
const faviconPath = path.join(rootOut, "favicon.ico");
383+
let faviconExists = false;
384+
try {
385+
await fs.access(faviconPath);
386+
faviconExists = true;
387+
} catch (e) {
388+
// Favicon doesn't exist
389+
}
390+
391+
if (!faviconExists && hyperbookJson.logo) {
392+
console.log(`${chalk.blue(`[${prefix}]`)} Generating favicons from logo.`);
393+
394+
// Only generate if logo is a local file (not a URL)
395+
if (!hyperbookJson.logo.includes("://")) {
396+
let logoPath: string | null = null;
397+
398+
// Resolve logo path by checking multiple locations
399+
if (hyperbookJson.logo.startsWith("/")) {
400+
// Absolute path starting with / - check book folder, then public folder
401+
const bookPath = path.join(root, "book", hyperbookJson.logo);
402+
const publicPath = path.join(root, "public", hyperbookJson.logo);
403+
404+
try {
405+
await fs.access(bookPath);
406+
logoPath = bookPath;
407+
} catch (e) {
408+
try {
409+
await fs.access(publicPath);
410+
logoPath = publicPath;
411+
} catch (e2) {
412+
// Not found in either location
413+
}
414+
}
415+
} else {
416+
// Relative path - check root folder, then book folder, then public folder
417+
const rootPath = path.join(root, hyperbookJson.logo);
418+
const bookPath = path.join(root, "book", hyperbookJson.logo);
419+
const publicPath = path.join(root, "public", hyperbookJson.logo);
420+
421+
try {
422+
await fs.access(rootPath);
423+
logoPath = rootPath;
424+
} catch (e) {
425+
try {
426+
await fs.access(bookPath);
427+
logoPath = bookPath;
428+
} catch (e2) {
429+
try {
430+
await fs.access(publicPath);
431+
logoPath = publicPath;
432+
} catch (e3) {
433+
// Not found in any location
434+
}
435+
}
436+
}
437+
}
438+
439+
if (logoPath) {
440+
try {
441+
const { generateFavicons } = await import("./helpers/generate-favicons");
442+
await generateFavicons(logoPath, rootOut, hyperbookJson, ASSETS_FOLDER);
443+
console.log(
444+
`${chalk.green(`[${prefix}]`)} Favicons generated successfully.`,
445+
);
446+
} catch (e) {
447+
console.log(
448+
`${chalk.yellow(`[${prefix}]`)} Warning: Could not generate favicons. Error: ${e instanceof Error ? e.message : String(e)}`,
449+
);
450+
}
451+
} else {
452+
console.log(
453+
`${chalk.yellow(`[${prefix}]`)} Warning: Could not generate favicons. Logo file not found: ${hyperbookJson.logo}`,
454+
);
455+
}
456+
}
457+
}
458+
381459
i = 1;
382460
for (let directive of directives) {
383461
const assetsDirectivePath = path.join(assetsPath, `directive-${directive}`);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import path from "path";
2+
import fs from "fs/promises";
3+
import { makeDir } from "./make-dir";
4+
import { HyperbookJson } from "@hyperbook/types";
5+
6+
export async function generateFavicons(
7+
logoPath: string,
8+
outputDir: string,
9+
hyperbookConfig: HyperbookJson,
10+
assetsFolder: string,
11+
): Promise<void> {
12+
// Dynamic import to avoid bundling issues with sharp
13+
const { favicons } = await import("favicons");
14+
15+
// Construct the favicon path relative to the base path
16+
const faviconPath = path.posix.join(
17+
hyperbookConfig.basePath || "/",
18+
assetsFolder,
19+
"favicons"
20+
);
21+
22+
const config = {
23+
path: faviconPath,
24+
appName: hyperbookConfig.name,
25+
appShortName: hyperbookConfig.name,
26+
appDescription: hyperbookConfig.description || hyperbookConfig.name,
27+
developerName: hyperbookConfig.author?.name,
28+
developerURL: hyperbookConfig.author?.url,
29+
lang: hyperbookConfig.language || "en",
30+
background: "#fff",
31+
theme_color: hyperbookConfig.colors?.brand || "#007864",
32+
display: "standalone",
33+
orientation: "any",
34+
scope: hyperbookConfig.basePath || "/",
35+
start_url: hyperbookConfig.basePath || "/",
36+
version: "1.0",
37+
icons: {
38+
android: true,
39+
appleIcon: true,
40+
appleStartup: true,
41+
favicons: true,
42+
windows: false,
43+
yandex: false,
44+
},
45+
};
46+
47+
const response = await favicons(logoPath, config);
48+
49+
// Create output directory for favicons
50+
const faviconsDir = path.join(outputDir, assetsFolder, "favicons");
51+
await makeDir(faviconsDir, { recursive: true });
52+
53+
// Write favicon image files
54+
for (const file of response.images) {
55+
await fs.writeFile(path.join(faviconsDir, file.name), file.contents);
56+
}
57+
58+
// Write manifest and other files
59+
for (const file of response.files) {
60+
await fs.writeFile(path.join(faviconsDir, file.name), file.contents);
61+
}
62+
63+
// Also copy favicon.ico to the root for backward compatibility
64+
const faviconIco = response.images.find(img => img.name === "favicon.ico");
65+
if (faviconIco) {
66+
await fs.writeFile(path.join(outputDir, "favicon.ico"), faviconIco.contents);
67+
}
68+
}

‎packages/hyperbook/package.json‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@
2828
"version": "pnpm build",
2929
"lint": "tsc --noEmit",
3030
"dev": "ncc build ./index.ts -w -o dist/",
31-
"build": "rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register && node postbuild.mjs"
31+
"build": "rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register --external favicons --external sharp && node postbuild.mjs"
32+
},
33+
"dependencies": {
34+
"favicons": "^7.2.0"
3235
},
3336
"devDependencies": {
3437
"@hyperbook/fs": "workspace:*",

‎packages/hyperbook/tsconfig.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"compilerOptions": {
33
"target": "es2019",
4+
"module": "commonjs",
45
"moduleResolution": "node",
56
"strict": true,
67
"resolveJsonModule": true,

‎packages/markdown/src/rehypeHtmlStructure.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,35 @@ export default (ctx: HyperbookContext) => () => {
270270
},
271271
children: [],
272272
},
273+
{
274+
type: "element",
275+
tagName: "link",
276+
properties: {
277+
rel: "icon",
278+
type: "image/x-icon",
279+
href: makeUrl(["favicon.ico"], "public"),
280+
},
281+
children: [],
282+
},
283+
{
284+
type: "element",
285+
tagName: "link",
286+
properties: {
287+
rel: "apple-touch-icon",
288+
sizes: "180x180",
289+
href: makeUrl(["favicons", "apple-touch-icon.png"], "assets"),
290+
},
291+
children: [],
292+
},
293+
{
294+
type: "element",
295+
tagName: "link",
296+
properties: {
297+
rel: "manifest",
298+
href: makeUrl(["favicons", "manifest.webmanifest"], "assets"),
299+
},
300+
children: [],
301+
},
273302
{
274303
type: "element",
275304
tagName: "link",

‎packages/markdown/tests/__snapshots__/process.test.ts.snap‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

33
exports[`process > should add showLineNumbers 1`] = `
4-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Markdown Referenz - Hyperbook Dokumenation</title><meta property="og:title" value="Markdown Referenz - Hyperbook Dokumenation"><meta name="description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta property="og:description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta name="keywords"><link rel="stylesheet" href="assets/normalize.css"><style>
4+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Markdown Referenz - Hyperbook Dokumenation</title><meta property="og:title" value="Markdown Referenz - Hyperbook Dokumenation"><meta name="description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta property="og:description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta name="keywords"><link rel="icon" type="image/x-icon" href="favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="assets/favicons/apple-touch-icon.png"><link rel="manifest" href="assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="assets/normalize.css"><style>
55
html,
66
body {
77
overflow: hidden;
@@ -63,7 +63,7 @@ HYPERBOOK_ASSETS = "assets/"
6363
`;
6464

6565
exports[`process > should hide copy button for block code 1`] = `
66-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
66+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
6767
html,
6868
body {
6969
overflow: hidden;
@@ -122,7 +122,7 @@ HYPERBOOK_ASSETS = "/assets/"
122122
`;
123123

124124
exports[`process > should include copy button for block code 1`] = `
125-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
125+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
126126
html,
127127
body {
128128
overflow: hidden;
@@ -181,7 +181,7 @@ HYPERBOOK_ASSETS = "/assets/"
181181
`;
182182

183183
exports[`process > should include copy button for inline code 1`] = `
184-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
184+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
185185
html,
186186
body {
187187
overflow: hidden;
@@ -238,7 +238,7 @@ HYPERBOOK_ASSETS = "/assets/"
238238
`;
239239

240240
exports[`process > should not include copy button for inline code 1`] = `
241-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
241+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
242242
html,
243243
body {
244244
overflow: hidden;
@@ -295,7 +295,7 @@ HYPERBOOK_ASSETS = "/assets/"
295295
`;
296296

297297
exports[`process > should result in a heading with a colon 1`] = `
298-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
298+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
299299
html,
300300
body {
301301
overflow: hidden;
@@ -354,7 +354,7 @@ HYPERBOOK_ASSETS = "/assets/"
354354
`;
355355

356356
exports[`process > should result in two link 1`] = `
357-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
357+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
358358
html,
359359
body {
360360
overflow: hidden;
@@ -418,7 +418,7 @@ HYPERBOOK_ASSETS = "/assets/"
418418
`;
419419

420420
exports[`process > should transform 1`] = `
421-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="stylesheet" href="/assets/normalize.css"><style>
421+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Hi - My Hyperbook</title><meta property="og:title" value="Hi - My Hyperbook"><meta name="description" content="undefined"><meta property="og:description" content="undefined"><meta name="keywords"><link rel="icon" type="image/x-icon" href="/public/favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="/assets/favicons/apple-touch-icon.png"><link rel="manifest" href="/assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="/assets/normalize.css"><style>
422422
html,
423423
body {
424424
overflow: hidden;
@@ -513,7 +513,7 @@ HYPERBOOK_ASSETS = "/assets/"
513513
`;
514514

515515
exports[`process > should transfrom complex context 1`] = `
516-
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Markdown Referenz - Hyperbook Dokumenation</title><meta property="og:title" value="Markdown Referenz - Hyperbook Dokumenation"><meta name="description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta property="og:description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta name="keywords"><link rel="stylesheet" href="assets/normalize.css"><style>
516+
"<!doctype html><html lang="es"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1,, interactive-widget=resizes-content"><title>Markdown Referenz - Hyperbook Dokumenation</title><meta property="og:title" value="Markdown Referenz - Hyperbook Dokumenation"><meta name="description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta property="og:description" content="Dokumentation für Hyperbook erstellt mit Hyperbook"><meta name="keywords"><link rel="icon" type="image/x-icon" href="favicon.ico"><link rel="apple-touch-icon" sizes="180x180" href="assets/favicons/apple-touch-icon.png"><link rel="manifest" href="assets/favicons/manifest.webmanifest"><link rel="stylesheet" href="assets/normalize.css"><style>
517517
html,
518518
body {
519519
overflow: hidden;

0 commit comments

Comments
 (0)