Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/favicon-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"hyperbook": minor
"@hyperbook/markdown": patch
---

Add automatic favicon and PWA icon generation from logo

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:

- Generates 60+ files including favicon.ico, Android icons, Apple touch icons, and Apple startup images
- Creates web manifest with full PWA metadata (theme color, scope, language, developer info)
- Smart logo path resolution: checks root folder, book folder, and public folder
- Adds favicon, Apple touch icon, and manifest links to all HTML pages
- Uses hyperbook.json metadata: name, description, colors.brand, basePath, language, author
- Backward compatible: copies favicon.ico to root for browsers expecting it there
78 changes: 78 additions & 0 deletions packages/hyperbook/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,84 @@ async function runBuild(
}
process.stdout.write("\n");

// Generate favicons if logo exists and no favicon.ico is present
const faviconPath = path.join(rootOut, "favicon.ico");
let faviconExists = false;
try {
await fs.access(faviconPath);
faviconExists = true;
} catch (e) {
// Favicon doesn't exist
}

if (!faviconExists && hyperbookJson.logo) {
console.log(`${chalk.blue(`[${prefix}]`)} Generating favicons from logo.`);

// Only generate if logo is a local file (not a URL)
if (!hyperbookJson.logo.includes("://")) {
let logoPath: string | null = null;

// Resolve logo path by checking multiple locations
if (hyperbookJson.logo.startsWith("/")) {
// Absolute path starting with / - check book folder, then public folder
const bookPath = path.join(root, "book", hyperbookJson.logo);
const publicPath = path.join(root, "public", hyperbookJson.logo);

try {
await fs.access(bookPath);
logoPath = bookPath;
} catch (e) {
try {
await fs.access(publicPath);
logoPath = publicPath;
} catch (e2) {
// Not found in either location
}
}
} else {
// Relative path - check root folder, then book folder, then public folder
const rootPath = path.join(root, hyperbookJson.logo);
const bookPath = path.join(root, "book", hyperbookJson.logo);
const publicPath = path.join(root, "public", hyperbookJson.logo);

try {
await fs.access(rootPath);
logoPath = rootPath;
} catch (e) {
try {
await fs.access(bookPath);
logoPath = bookPath;
} catch (e2) {
try {
await fs.access(publicPath);
logoPath = publicPath;
} catch (e3) {
// Not found in any location
}
}
}
}

if (logoPath) {
try {
const { generateFavicons } = await import("./helpers/generate-favicons");
await generateFavicons(logoPath, rootOut, hyperbookJson, ASSETS_FOLDER);
console.log(
`${chalk.green(`[${prefix}]`)} Favicons generated successfully.`,
);
} catch (e) {
console.log(
`${chalk.yellow(`[${prefix}]`)} Warning: Could not generate favicons. Error: ${e instanceof Error ? e.message : String(e)}`,
);
}
} else {
console.log(
`${chalk.yellow(`[${prefix}]`)} Warning: Could not generate favicons. Logo file not found: ${hyperbookJson.logo}`,
);
}
}
}

i = 1;
for (let directive of directives) {
const assetsDirectivePath = path.join(assetsPath, `directive-${directive}`);
Expand Down
68 changes: 68 additions & 0 deletions packages/hyperbook/helpers/generate-favicons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import path from "path";
import fs from "fs/promises";
import { makeDir } from "./make-dir";
import { HyperbookJson } from "@hyperbook/types";

export async function generateFavicons(
logoPath: string,
outputDir: string,
hyperbookConfig: HyperbookJson,
assetsFolder: string,
): Promise<void> {
// Dynamic import to avoid bundling issues with sharp
const { favicons } = await import("favicons");

// Construct the favicon path relative to the base path
const faviconPath = path.posix.join(
hyperbookConfig.basePath || "/",
assetsFolder,
"favicons"
);

const config = {
path: faviconPath,
appName: hyperbookConfig.name,
appShortName: hyperbookConfig.name,
appDescription: hyperbookConfig.description || hyperbookConfig.name,
developerName: hyperbookConfig.author?.name,
developerURL: hyperbookConfig.author?.url,
lang: hyperbookConfig.language || "en",
background: "#fff",
theme_color: hyperbookConfig.colors?.brand || "#007864",
display: "standalone",
orientation: "any",
scope: hyperbookConfig.basePath || "/",
start_url: hyperbookConfig.basePath || "/",
version: "1.0",
icons: {
android: true,
appleIcon: true,
appleStartup: true,
favicons: true,
windows: false,
yandex: false,
},
};

const response = await favicons(logoPath, config);

// Create output directory for favicons
const faviconsDir = path.join(outputDir, assetsFolder, "favicons");
await makeDir(faviconsDir, { recursive: true });

// Write favicon image files
for (const file of response.images) {
await fs.writeFile(path.join(faviconsDir, file.name), file.contents);
}

// Write manifest and other files
for (const file of response.files) {
await fs.writeFile(path.join(faviconsDir, file.name), file.contents);
}

// Also copy favicon.ico to the root for backward compatibility
const faviconIco = response.images.find(img => img.name === "favicon.ico");
if (faviconIco) {
await fs.writeFile(path.join(outputDir, "favicon.ico"), faviconIco.contents);
}
}
5 changes: 4 additions & 1 deletion packages/hyperbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
"version": "pnpm build",
"lint": "tsc --noEmit",
"dev": "ncc build ./index.ts -w -o dist/",
"build": "rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register && node postbuild.mjs"
"build": "rimraf dist && ncc build ./index.ts -o ./dist/ --no-cache --no-source-map-register --external favicons --external sharp && node postbuild.mjs"
},
"dependencies": {
"favicons": "^7.2.0"
},
"devDependencies": {
"@hyperbook/fs": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions packages/hyperbook/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"resolveJsonModule": true,
Expand Down
29 changes: 29 additions & 0 deletions packages/markdown/src/rehypeHtmlStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,35 @@ export default (ctx: HyperbookContext) => () => {
},
children: [],
},
{
type: "element",
tagName: "link",
properties: {
rel: "icon",
type: "image/x-icon",
href: makeUrl(["favicon.ico"], "public"),
},
children: [],
},
{
type: "element",
tagName: "link",
properties: {
rel: "apple-touch-icon",
sizes: "180x180",
href: makeUrl(["favicons", "apple-touch-icon.png"], "assets"),
},
children: [],
},
{
type: "element",
tagName: "link",
properties: {
rel: "manifest",
href: makeUrl(["favicons", "manifest.webmanifest"], "assets"),
},
children: [],
},
{
type: "element",
tagName: "link",
Expand Down
18 changes: 9 additions & 9 deletions packages/markdown/tests/__snapshots__/process.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`process > should add showLineNumbers 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -63,7 +63,7 @@ HYPERBOOK_ASSETS = "assets/"
`;

exports[`process > should hide copy button for block code 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -122,7 +122,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should include copy button for block code 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -181,7 +181,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should include copy button for inline code 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -238,7 +238,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should not include copy button for inline code 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -295,7 +295,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should result in a heading with a colon 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -354,7 +354,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should result in two link 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -418,7 +418,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should transform 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down Expand Up @@ -513,7 +513,7 @@ HYPERBOOK_ASSETS = "/assets/"
`;

exports[`process > should transfrom complex context 1`] = `
"<!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>
"<!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>
html,
body {
overflow: hidden;
Expand Down
Loading