@brillout/docpress 0.16.5 → 0.16.7-commit-fc2ed19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ export { remarkPkgManager }
2
+
3
+ import type { Code, Root } from 'mdast'
4
+ import { visit } from 'unist-util-visit'
5
+ import convert from 'npm-to-yarn'
6
+ import { parseMetaString } from './rehypeMetaToProps.js'
7
+ import { generateChoiceGroup } from './utils/generateChoiceGroup.js'
8
+
9
+ const PKG_MANAGERS = ['pnpm', 'yarn', 'bun'] as const
10
+
11
+ function remarkPkgManager() {
12
+ return function (tree: Root) {
13
+ visit(tree, 'code', (node, index, parent) => {
14
+ if (!parent || typeof index === 'undefined') return
15
+ if (!['sh', 'shell'].includes(node.lang || '')) return
16
+ if (node.value.indexOf('npm') === -1 && node.value.indexOf('npx') === -1) return
17
+
18
+ let choice: string | undefined = undefined
19
+ const nodes = new Map<string, Code>()
20
+
21
+ if (node.meta) {
22
+ const meta = parseMetaString(node.meta, ['choice'])
23
+ choice = meta.props['choice']
24
+ node.meta = meta.rest
25
+ }
26
+
27
+ nodes.set('npm', node)
28
+
29
+ for (const pm of PKG_MANAGERS) {
30
+ nodes.set(pm, {
31
+ type: node.type,
32
+ lang: node.lang,
33
+ meta: node.meta,
34
+ value: convert(node.value, pm),
35
+ })
36
+ }
37
+
38
+ const groupedNodes = [...nodes].map(([name, node]) => ({ value: name, children: [node] }))
39
+ const replacement = generateChoiceGroup(groupedNodes)
40
+
41
+ replacement.data ??= { choice }
42
+ parent.children.splice(index, 1, replacement)
43
+ })
44
+ }
45
+ }
@@ -0,0 +1,87 @@
1
+ export { generateChoiceGroup }
2
+ export type { CodeChoice }
3
+
4
+ import type { BlockContent, DefinitionContent } from 'mdast'
5
+ import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
6
+
7
+ type CodeChoice = {
8
+ value: string
9
+ children: (BlockContent | DefinitionContent)[]
10
+ }
11
+
12
+ function generateChoiceGroup(codeChoices: CodeChoice[]): MdxJsxFlowElement {
13
+ const attributes: MdxJsxAttribute[] = []
14
+ const children: MdxJsxFlowElement[] = []
15
+
16
+ attributes.push({
17
+ type: 'mdxJsxAttribute',
18
+ name: 'choices',
19
+ value: {
20
+ type: 'mdxJsxAttributeValueExpression',
21
+ value: '',
22
+ data: {
23
+ estree: {
24
+ type: 'Program',
25
+ sourceType: 'module',
26
+ comments: [],
27
+ body: [
28
+ {
29
+ type: 'ExpressionStatement',
30
+ expression: {
31
+ type: 'ArrayExpression',
32
+ // @ts-ignore: Missing properties in type definition
33
+ elements: codeChoices.map((choice) => ({
34
+ type: 'Literal',
35
+ value: choice.value,
36
+ })),
37
+ },
38
+ },
39
+ ],
40
+ },
41
+ },
42
+ },
43
+ })
44
+
45
+ for (const codeChoice of codeChoices) {
46
+ const classNames = ['choice']
47
+ if (findHasJsToggle(codeChoice.children[0])) {
48
+ classNames.push('has-toggle')
49
+ }
50
+
51
+ children.push({
52
+ type: 'mdxJsxFlowElement',
53
+ name: 'div',
54
+ attributes: [
55
+ { type: 'mdxJsxAttribute', name: 'id', value: codeChoice.value },
56
+ { type: 'mdxJsxAttribute', name: 'className', value: classNames.join(' ') },
57
+ ],
58
+ children: codeChoice.children.every((node) => node.type === 'containerDirective')
59
+ ? codeChoice.children.flatMap((node) => [...node.children])
60
+ : codeChoice.children,
61
+ })
62
+ }
63
+
64
+ return {
65
+ type: 'mdxJsxFlowElement',
66
+ name: 'ChoiceGroup',
67
+ attributes,
68
+ children,
69
+ }
70
+ }
71
+
72
+ function findHasJsToggle(node: BlockContent | DefinitionContent) {
73
+ if (node.type === 'containerDirective' && node.name === 'Choice') {
74
+ return (
75
+ node.children[0].type === 'mdxJsxFlowElement' &&
76
+ node.children[0].name === 'CodeSnippets' &&
77
+ node.children[0].attributes.every(
78
+ (attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle',
79
+ )
80
+ )
81
+ }
82
+ return (
83
+ node.type === 'mdxJsxFlowElement' &&
84
+ node.name === 'CodeSnippets' &&
85
+ node.attributes.every((attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle')
86
+ )
87
+ }
@@ -18,18 +18,24 @@ import type { ElementData, Root } from 'hast';
18
18
  */
19
19
  declare function rehypeMetaToProps(): (tree: Root) => void;
20
20
  /**
21
- * Minimal parser for a metadata string into key-value pairs.
21
+ * Simple parser for a metadata string into key-value pairs and a remaining unparsed string.
22
22
  *
23
- * Supports simple patterns: key or key="value".
23
+ * Supports simple patterns: key or key=value.
24
24
  *
25
- * Keys must contain only letters, dashes, or underscores (no digits).
26
- * Keys are converted to kebab-case. Values default to "true" if missing.
25
+ * - Keys must contain only letters, dashes, or underscores (no digits).
26
+ * - Keys are converted to kebab-case. Values default to "true" if missing.
27
+ * - Keys and values are stored in `props`.
28
+ * - If `propNames` is provided, only keys included in that list are extracted.
29
+ * - Unextracted tokens remain in `rest`.
27
30
  *
28
- * Example:
29
- * parseMetaString('foo fooBar="value"')
30
- * => { foo: 'true', foo_bar: 'value' }
31
- *
32
- * @param metaString - The input metadata string.
33
- * @returns A plain object of parsed key-value pairs.
31
+ * @param meta - The input metadata string.
32
+ * @param propNames - Optional whitelist of property names to extract.
33
+ * @returns An object containing:
34
+ * - `props`: a map of extracted properties
35
+ * - `rest`: the remaining metadata string after extraction
34
36
  */
35
- declare function parseMetaString(metaString: ElementData['meta']): Record<string, string>;
37
+ declare function parseMetaString<Name extends string = string>(meta: ElementData['meta'], propNames?: Name[]): PropsType<Name>;
38
+ interface PropsType<Name extends string = string> {
39
+ props: Partial<Record<Name, string>>;
40
+ rest: string;
41
+ }
@@ -20,38 +20,43 @@ function rehypeMetaToProps() {
20
20
  return (tree) => {
21
21
  visit(tree, 'element', (node, _index, parent) => {
22
22
  if (node.tagName === 'code' && parent?.type === 'element' && parent.tagName === 'pre') {
23
- const props = parseMetaString(node.data?.meta);
23
+ const meta = parseMetaString(node.data?.meta);
24
24
  parent.properties ??= {};
25
- parent.properties = { ...parent.properties, ...props };
25
+ parent.properties = { ...parent.properties, ...meta.props };
26
26
  }
27
27
  });
28
28
  };
29
29
  }
30
30
  /**
31
- * Minimal parser for a metadata string into key-value pairs.
31
+ * Simple parser for a metadata string into key-value pairs and a remaining unparsed string.
32
32
  *
33
- * Supports simple patterns: key or key="value".
33
+ * Supports simple patterns: key or key=value.
34
34
  *
35
- * Keys must contain only letters, dashes, or underscores (no digits).
36
- * Keys are converted to kebab-case. Values default to "true" if missing.
35
+ * - Keys must contain only letters, dashes, or underscores (no digits).
36
+ * - Keys are converted to kebab-case. Values default to "true" if missing.
37
+ * - Keys and values are stored in `props`.
38
+ * - If `propNames` is provided, only keys included in that list are extracted.
39
+ * - Unextracted tokens remain in `rest`.
37
40
  *
38
- * Example:
39
- * parseMetaString('foo fooBar="value"')
40
- * => { foo: 'true', foo_bar: 'value' }
41
- *
42
- * @param metaString - The input metadata string.
43
- * @returns A plain object of parsed key-value pairs.
41
+ * @param meta - The input metadata string.
42
+ * @param propNames - Optional whitelist of property names to extract.
43
+ * @returns An object containing:
44
+ * - `props`: a map of extracted properties
45
+ * - `rest`: the remaining metadata string after extraction
44
46
  */
45
- function parseMetaString(metaString) {
46
- if (!metaString)
47
- return {};
47
+ function parseMetaString(meta, propNames) {
48
+ if (!meta)
49
+ return { props: {}, rest: '' };
50
+ let str = meta;
51
+ const keyValuePairRE = /(?<name>[a-zA-Z_-]+)(?:=([^"'\s]+))?/g;
48
52
  const props = {};
49
- const keyValuePairRE = /([a-zA-Z_-]+)(?:=([^"'\s]+))?(?=\s|$)/g;
50
- for (const match of metaString.matchAll(keyValuePairRE)) {
51
- let [_, key, value] = match;
52
- props[kebabCase(key)] = value || 'true';
53
- }
54
- return props;
53
+ str = str.replaceAll(keyValuePairRE, (match, name, value) => {
54
+ if (propNames && !propNames.includes(name))
55
+ return match;
56
+ props[kebabCase(name)] = value || 'true';
57
+ return '';
58
+ });
59
+ return { props, rest: str.trim() };
55
60
  }
56
61
  // Simple function to convert a camelCase or PascalCase string to kebab-case.
57
62
  function kebabCase(str) {
@@ -0,0 +1,8 @@
1
+ export { remarkChoiceGroup };
2
+ import type { Root } from 'mdast';
3
+ declare function remarkChoiceGroup(): (tree: Root) => void;
4
+ declare module 'mdast' {
5
+ interface Data {
6
+ choice?: string;
7
+ }
8
+ }
@@ -0,0 +1,84 @@
1
+ export { remarkChoiceGroup };
2
+ import { visit } from 'unist-util-visit';
3
+ import { parseMetaString } from './rehypeMetaToProps.js';
4
+ import { generateChoiceGroup } from './utils/generateChoiceGroup.js';
5
+ function remarkChoiceGroup() {
6
+ return function (tree) {
7
+ visit(tree, (node) => {
8
+ if (node.type === 'code') {
9
+ if (!node.meta)
10
+ return;
11
+ const meta = parseMetaString(node.meta, ['choice']);
12
+ const { choice } = meta.props;
13
+ node.meta = meta.rest;
14
+ if (choice)
15
+ node.data ??= { choice };
16
+ }
17
+ if (node.type === 'containerDirective' && node.name === 'Choice') {
18
+ if (!node.attributes)
19
+ return;
20
+ const { id: choice } = node.attributes;
21
+ if (choice) {
22
+ node.data ??= { choice };
23
+ node.attributes = {};
24
+ }
25
+ }
26
+ });
27
+ const replaced = new WeakSet();
28
+ visit(tree, (node) => {
29
+ if (!('children' in node) || replaced.has(node))
30
+ return 'skip';
31
+ if (node.type === 'mdxJsxFlowElement')
32
+ return 'skip';
33
+ let start = -1;
34
+ let end = 0;
35
+ const process = () => {
36
+ if (start === -1 || start === end)
37
+ return;
38
+ const nodes = node.children.slice(start, end);
39
+ const groupedNodes = groupByNodeType(nodes);
40
+ const replacements = [];
41
+ for (const groupedNode of groupedNodes) {
42
+ const replacement = generateChoiceGroup(groupedNode);
43
+ replacements.push(replacement);
44
+ replaced.add(replacement);
45
+ }
46
+ node.children.splice(start, end - start, ...replacements);
47
+ end = start;
48
+ start = -1;
49
+ };
50
+ for (; end < node.children.length; end++) {
51
+ const child = node.children[end];
52
+ if (!['code', 'mdxJsxFlowElement', 'containerDirective'].includes(child.type)) {
53
+ process();
54
+ continue;
55
+ }
56
+ if (!child.data?.choice) {
57
+ process();
58
+ continue;
59
+ }
60
+ if (start === -1)
61
+ start = end;
62
+ }
63
+ process();
64
+ });
65
+ };
66
+ }
67
+ function groupByNodeType(nodes) {
68
+ const groupedNodes = new Set();
69
+ const filters = [...new Set(nodes.flat().map((node) => (node.type === 'code' ? node.lang : node.name)))];
70
+ filters.map((filter) => {
71
+ const nodesByChoice = new Map();
72
+ nodes
73
+ .filter((node) => (node.type === 'code' ? node.lang : node.name) === filter)
74
+ .map((node) => {
75
+ const choice = node.data.choice;
76
+ const nodes = nodesByChoice.get(choice) ?? [];
77
+ nodes.push(node);
78
+ node.data = {};
79
+ nodesByChoice.set(choice, nodes);
80
+ });
81
+ groupedNodes.add([...nodesByChoice].map(([name, nodes]) => ({ value: name, children: nodes })));
82
+ });
83
+ return [...groupedNodes];
84
+ }
@@ -65,7 +65,10 @@ function transformYaml(node) {
65
65
  }
66
66
  async function transformTsToJs(node, file) {
67
67
  const { codeBlock, index, parent } = node;
68
- const maxWidth = Number(parseMetaString(codeBlock.meta)['max-width']);
68
+ const meta = parseMetaString(codeBlock.meta, ['max-width', 'choice']);
69
+ const maxWidth = Number(meta.props['max-width']);
70
+ const { choice } = meta.props;
71
+ codeBlock.meta = meta.rest;
69
72
  let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value);
70
73
  let codeBlockContentJs = '';
71
74
  // Remove TypeScript from the TS/TSX/Vue code node
@@ -113,8 +116,8 @@ async function transformTsToJs(node, file) {
113
116
  // Add `hideToggle` attribute (prop) to `CodeSnippets` if the only change was replacing `.ts` with `.js`
114
117
  if (codeBlockReplacedJs === codeBlockContentJs) {
115
118
  attributes.push({
116
- name: 'hideToggle',
117
119
  type: 'mdxJsxAttribute',
120
+ name: 'hideToggle',
118
121
  });
119
122
  }
120
123
  // Wrap both the original `codeBlock` and `jsCode` with <CodeSnippets>
@@ -124,6 +127,8 @@ async function transformTsToJs(node, file) {
124
127
  children: [jsCode, codeBlock],
125
128
  attributes,
126
129
  };
130
+ if (choice)
131
+ container.data ??= { choice };
127
132
  parent.children.splice(index, 1, container);
128
133
  }
129
134
  // Replace all '.ts' extensions with '.js'
@@ -138,7 +143,7 @@ function cleanUpCode(code, isJsCode = false) {
138
143
  }
139
144
  function processMagicComments(code) {
140
145
  // @detype-replace DummyLayout Layout
141
- const renameCommentRE = /^\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm;
146
+ const renameCommentRE = /^\s*\/\/\s@detype-replace\s([^ ]+) ([^ ]+)\n/gm;
142
147
  const matches = Array.from(code.matchAll(renameCommentRE));
143
148
  if (matches.length) {
144
149
  for (let i = matches.length - 1; i >= 0; i--) {
@@ -0,0 +1,3 @@
1
+ export { remarkPkgManager };
2
+ import type { Root } from 'mdast';
3
+ declare function remarkPkgManager(): (tree: Root) => void;
@@ -0,0 +1,38 @@
1
+ export { remarkPkgManager };
2
+ import { visit } from 'unist-util-visit';
3
+ import convert from 'npm-to-yarn';
4
+ import { parseMetaString } from './rehypeMetaToProps.js';
5
+ import { generateChoiceGroup } from './utils/generateChoiceGroup.js';
6
+ const PKG_MANAGERS = ['pnpm', 'yarn', 'bun'];
7
+ function remarkPkgManager() {
8
+ return function (tree) {
9
+ visit(tree, 'code', (node, index, parent) => {
10
+ if (!parent || typeof index === 'undefined')
11
+ return;
12
+ if (!['sh', 'shell'].includes(node.lang || ''))
13
+ return;
14
+ if (node.value.indexOf('npm') === -1 && node.value.indexOf('npx') === -1)
15
+ return;
16
+ let choice = undefined;
17
+ const nodes = new Map();
18
+ if (node.meta) {
19
+ const meta = parseMetaString(node.meta, ['choice']);
20
+ choice = meta.props['choice'];
21
+ node.meta = meta.rest;
22
+ }
23
+ nodes.set('npm', node);
24
+ for (const pm of PKG_MANAGERS) {
25
+ nodes.set(pm, {
26
+ type: node.type,
27
+ lang: node.lang,
28
+ meta: node.meta,
29
+ value: convert(node.value, pm),
30
+ });
31
+ }
32
+ const groupedNodes = [...nodes].map(([name, node]) => ({ value: name, children: [node] }));
33
+ const replacement = generateChoiceGroup(groupedNodes);
34
+ replacement.data ??= { choice };
35
+ parent.children.splice(index, 1, replacement);
36
+ });
37
+ };
38
+ }
@@ -0,0 +1,9 @@
1
+ export { generateChoiceGroup };
2
+ export type { CodeChoice };
3
+ import type { BlockContent, DefinitionContent } from 'mdast';
4
+ import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
5
+ type CodeChoice = {
6
+ value: string;
7
+ children: (BlockContent | DefinitionContent)[];
8
+ };
9
+ declare function generateChoiceGroup(codeChoices: CodeChoice[]): MdxJsxFlowElement;
@@ -0,0 +1,66 @@
1
+ export { generateChoiceGroup };
2
+ function generateChoiceGroup(codeChoices) {
3
+ const attributes = [];
4
+ const children = [];
5
+ attributes.push({
6
+ type: 'mdxJsxAttribute',
7
+ name: 'choices',
8
+ value: {
9
+ type: 'mdxJsxAttributeValueExpression',
10
+ value: '',
11
+ data: {
12
+ estree: {
13
+ type: 'Program',
14
+ sourceType: 'module',
15
+ comments: [],
16
+ body: [
17
+ {
18
+ type: 'ExpressionStatement',
19
+ expression: {
20
+ type: 'ArrayExpression',
21
+ // @ts-ignore: Missing properties in type definition
22
+ elements: codeChoices.map((choice) => ({
23
+ type: 'Literal',
24
+ value: choice.value,
25
+ })),
26
+ },
27
+ },
28
+ ],
29
+ },
30
+ },
31
+ },
32
+ });
33
+ for (const codeChoice of codeChoices) {
34
+ const classNames = ['choice'];
35
+ if (findHasJsToggle(codeChoice.children[0])) {
36
+ classNames.push('has-toggle');
37
+ }
38
+ children.push({
39
+ type: 'mdxJsxFlowElement',
40
+ name: 'div',
41
+ attributes: [
42
+ { type: 'mdxJsxAttribute', name: 'id', value: codeChoice.value },
43
+ { type: 'mdxJsxAttribute', name: 'className', value: classNames.join(' ') },
44
+ ],
45
+ children: codeChoice.children.every((node) => node.type === 'containerDirective')
46
+ ? codeChoice.children.flatMap((node) => [...node.children])
47
+ : codeChoice.children,
48
+ });
49
+ }
50
+ return {
51
+ type: 'mdxJsxFlowElement',
52
+ name: 'ChoiceGroup',
53
+ attributes,
54
+ children,
55
+ };
56
+ }
57
+ function findHasJsToggle(node) {
58
+ if (node.type === 'containerDirective' && node.name === 'Choice') {
59
+ return (node.children[0].type === 'mdxJsxFlowElement' &&
60
+ node.children[0].name === 'CodeSnippets' &&
61
+ node.children[0].attributes.every((attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle'));
62
+ }
63
+ return (node.type === 'mdxJsxFlowElement' &&
64
+ node.name === 'CodeSnippets' &&
65
+ node.attributes.every((attribute) => attribute.type !== 'mdxJsxAttribute' || attribute.name !== 'hideToggle'));
66
+ }
@@ -7,7 +7,10 @@ type Config = {
7
7
  /** Sets `<meta name="description" content="${tagline}" />` */
8
8
  tagline: string;
9
9
  logo: string;
10
- favicon?: string;
10
+ favicon?: string | {
11
+ browser: string;
12
+ google: string;
13
+ };
11
14
  banner?: string;
12
15
  github: string;
13
16
  discord?: string;
@@ -34,6 +37,7 @@ type Config = {
34
37
  navLogoStyle?: React.CSSProperties;
35
38
  navLogoTextStyle?: React.CSSProperties;
36
39
  globalNote?: React.ReactNode;
40
+ choices?: Record<string, Choice>;
37
41
  };
38
42
  /** Order in Algolia search results */
39
43
  type Category = string | {
@@ -41,3 +45,7 @@ type Category = string | {
41
45
  /** Hide from Algolia search */
42
46
  hide?: boolean;
43
47
  };
48
+ type Choice = {
49
+ choices: string[];
50
+ default: string;
51
+ };
@@ -4,10 +4,13 @@ import react from '@vitejs/plugin-react-swc';
4
4
  import { parsePageSections } from './parsePageSections.js';
5
5
  import rehypePrettyCode from 'rehype-pretty-code';
6
6
  import remarkGfm from 'remark-gfm';
7
+ import remarkDirective from 'remark-directive';
7
8
  import { transformerNotationDiff } from '@shikijs/transformers';
8
9
  import { rehypeMetaToProps } from './code-blocks/rehypeMetaToProps.js';
9
10
  import { remarkDetype } from './code-blocks/remarkDetype.js';
10
11
  import { shikiTransformerAutoLinks } from './code-blocks/shikiTransformerAutoLinks.js';
12
+ import { remarkPkgManager } from './code-blocks/remarkPkgManager.js';
13
+ import { remarkChoiceGroup } from './code-blocks/remarkChoiceGroup.js';
11
14
  const root = process.cwd();
12
15
  const prettyCode = [
13
16
  rehypePrettyCode,
@@ -18,7 +21,7 @@ const prettyCode = [
18
21
  },
19
22
  ];
20
23
  const rehypePlugins = [prettyCode, [rehypeMetaToProps]];
21
- const remarkPlugins = [remarkGfm, remarkDetype];
24
+ const remarkPlugins = [remarkGfm, remarkDirective, remarkDetype, remarkPkgManager, remarkChoiceGroup];
22
25
  const config = {
23
26
  root,
24
27
  plugins: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.16.5",
3
+ "version": "0.16.7-commit-fc2ed19",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",
@@ -12,7 +12,9 @@
12
12
  "@shikijs/transformers": "1.2.0",
13
13
  "@vitejs/plugin-react-swc": "^3.10.2",
14
14
  "detype": "^1.1.3",
15
+ "npm-to-yarn": "^3.0.1",
15
16
  "rehype-pretty-code": "0.13.0",
17
+ "remark-directive": "^4.0.0",
16
18
  "remark-gfm": "4.0.0",
17
19
  "shiki": "1.2.0",
18
20
  "unist-util-visit": "^5.0.0",
@@ -65,6 +67,7 @@
65
67
  "@types/node": "^24.10.0",
66
68
  "@types/react": "^19.2.2",
67
69
  "@types/react-dom": "^19.2.2",
70
+ "mdast-util-directive": "^3.1.0",
68
71
  "mdast-util-mdx-jsx": "^3.2.0"
69
72
  },
70
73
  "repository": "https://github.com/brillout/docpress",
@@ -18,16 +18,13 @@ async function onRenderHtml(pageContext: PageContextServer): Promise<any> {
18
18
 
19
19
  const pageHtml = ReactDOMServer.renderToString(page)
20
20
 
21
- const faviconUrl = pageContext.globalContext.config.docpress.favicon ?? pageContext.globalContext.config.docpress.logo
22
- assert(faviconUrl)
23
-
24
21
  const { documentTitle } = pageContext.resolved
25
22
  assert(documentTitle)
26
23
  return escapeInject`<!DOCTYPE html>
27
24
  <html>
28
25
  <head>
29
26
  <meta charset="UTF-8" />
30
- <link rel="icon" href="${faviconUrl ?? ''}" />
27
+ ${getFaviconTags(pageContext.globalContext.config.docpress)}
31
28
  <title>${documentTitle}</title>
32
29
  ${descriptionTag}
33
30
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -98,3 +95,44 @@ function getOpenGraphTags(url: string, documentTitle: string, config: Config) {
98
95
  ${metaTwitter}
99
96
  `
100
97
  }
98
+
99
+ // Resources:
100
+ // - https://www.google.com/s2/favicons?domain=vike.dev
101
+ // - https://stackoverflow.com/questions/59568586/favicon-don%c2%b4t-show-up-in-google-search-result/59577456#59577456
102
+ // - https://developers.google.com/search/docs/appearance/favicon-in-search
103
+ //
104
+ // Examples:
105
+ // - Nice looking on Goolge Search Results:
106
+ // https://www.wikipedia.org
107
+ // - Single PNG:
108
+ // https://rubyonrails.org
109
+ // - Favicon shown in browser is different than favicon shown in Google:
110
+ // https://evilmartians.com
111
+ // Shown in Google: <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
112
+ // Shown in Browser: <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
113
+ function getFaviconTags(config: Config) {
114
+ const { faviconBrowser, faviconGoogle } = getFavicons(config)
115
+ assert(faviconBrowser)
116
+ const faviconTagGoogle = !faviconGoogle
117
+ ? ''
118
+ : escapeInject`
119
+ <link rel="apple-touch-icon" href="${faviconGoogle}" />
120
+ `
121
+ return escapeInject`
122
+ <link rel="icon" href="${faviconBrowser}" type="image/svg+xml" />
123
+ ${faviconTagGoogle}
124
+ `
125
+ }
126
+ function getFavicons(config: Config) {
127
+ let faviconBrowser: string
128
+ let faviconGoogle: null | string = null
129
+ if (!config.favicon) {
130
+ faviconBrowser = config.logo
131
+ } else if (typeof config.favicon === 'string') {
132
+ faviconBrowser = config.favicon
133
+ } else {
134
+ faviconBrowser = config.favicon.browser
135
+ faviconGoogle = config.favicon.google
136
+ }
137
+ return { faviconBrowser, faviconGoogle }
138
+ }
package/types/Config.ts CHANGED
@@ -9,7 +9,12 @@ type Config = {
9
9
  /** Sets `<meta name="description" content="${tagline}" />` */
10
10
  tagline: string
11
11
  logo: string
12
- favicon?: string
12
+ favicon?:
13
+ | string
14
+ | {
15
+ browser: string
16
+ google: string
17
+ }
13
18
  banner?: string
14
19
 
15
20
  github: string
@@ -43,6 +48,7 @@ type Config = {
43
48
  navLogoTextStyle?: React.CSSProperties
44
49
 
45
50
  globalNote?: React.ReactNode
51
+ choices?: Record<string, Choice>
46
52
  }
47
53
 
48
54
  /** Order in Algolia search results */
@@ -53,3 +59,8 @@ type Category =
53
59
  /** Hide from Algolia search */
54
60
  hide?: boolean
55
61
  }
62
+
63
+ type Choice = {
64
+ choices: string[]
65
+ default: string
66
+ }