vike-react 0.4.18-commit-5c88c7b → 0.5.0

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.
package/dist/+config.d.ts CHANGED
@@ -30,11 +30,26 @@ declare const _default: {
30
30
  client: true;
31
31
  };
32
32
  };
33
+ description: {
34
+ env: {
35
+ server: true;
36
+ };
37
+ };
38
+ image: {
39
+ env: {
40
+ server: true;
41
+ };
42
+ };
43
+ viewport: {
44
+ env: {
45
+ server: true;
46
+ };
47
+ };
33
48
  favicon: {
34
49
  env: {
35
50
  server: true;
36
- client: true;
37
51
  };
52
+ global: true;
38
53
  };
39
54
  lang: {
40
55
  env: {
@@ -42,6 +57,20 @@ declare const _default: {
42
57
  client: true;
43
58
  };
44
59
  };
60
+ htmlAttributes: {
61
+ env: {
62
+ server: true;
63
+ };
64
+ global: true;
65
+ cumulative: true;
66
+ };
67
+ bodyAttributes: {
68
+ env: {
69
+ server: true;
70
+ };
71
+ global: true;
72
+ cumulative: true;
73
+ };
45
74
  ssr: {
46
75
  env: {
47
76
  config: true;
package/dist/+config.js CHANGED
@@ -5,7 +5,7 @@ import './types/index.js';
5
5
  export default {
6
6
  name: 'vike-react',
7
7
  require: {
8
- vike: '>=0.4.178'
8
+ vike: '>=0.4.182'
9
9
  },
10
10
  Loading: 'import:vike-react/components/Loading:default',
11
11
  // https://vike.dev/onRenderHtml
@@ -13,6 +13,7 @@ export default {
13
13
  // https://vike.dev/onRenderClient
14
14
  onRenderClient: 'import:vike-react/renderer/onRenderClient:onRenderClient',
15
15
  passToClient: [
16
+ '_configFromHook',
16
17
  // https://github.com/vikejs/vike-react/issues/25
17
18
  process.env.NODE_ENV !== 'production' && '$$typeof'
18
19
  ].filter(isNotFalse),
@@ -31,12 +32,32 @@ export default {
31
32
  title: {
32
33
  env: { server: true, client: true }
33
34
  },
35
+ description: {
36
+ env: { server: true }
37
+ },
38
+ image: {
39
+ env: { server: true }
40
+ },
41
+ viewport: {
42
+ env: { server: true }
43
+ },
34
44
  favicon: {
35
- env: { server: true, client: true }
45
+ env: { server: true },
46
+ global: true
36
47
  },
37
48
  lang: {
38
49
  env: { server: true, client: true }
39
50
  },
51
+ htmlAttributes: {
52
+ env: { server: true },
53
+ global: true,
54
+ cumulative: true // for Vike extensions
55
+ },
56
+ bodyAttributes: {
57
+ env: { server: true },
58
+ global: true,
59
+ cumulative: true // for Vike extensions
60
+ },
40
61
  ssr: {
41
62
  env: { config: true },
42
63
  effect: ssrEffect
@@ -0,0 +1,8 @@
1
+ export { Config };
2
+ import type { ConfigFromHook } from '../../types/Config.js';
3
+ /**
4
+ * Set configurations inside React components.
5
+ *
6
+ * https://vike.dev/useConfig
7
+ */
8
+ declare function Config(props: ConfigFromHook): null;
@@ -0,0 +1,13 @@
1
+ export { Config };
2
+ // Same as ./Config-server.ts but importing useConfig-client.js
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-client.js';
4
+ /**
5
+ * Set configurations inside React components.
6
+ *
7
+ * https://vike.dev/useConfig
8
+ */
9
+ function Config(props) {
10
+ const config = useConfig();
11
+ config(props);
12
+ return null;
13
+ }
@@ -0,0 +1,8 @@
1
+ export { Config };
2
+ import type { ConfigFromHook } from '../../types/Config.js';
3
+ /**
4
+ * Set configurations inside React components.
5
+ *
6
+ * https://vike.dev/useConfig
7
+ */
8
+ declare function Config(props: ConfigFromHook): null;
@@ -0,0 +1,13 @@
1
+ export { Config };
2
+ // Same as ./Config-client.ts but importing useConfig-server.js
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-server.js';
4
+ /**
5
+ * Set configurations inside React components.
6
+ *
7
+ * https://vike.dev/useConfig
8
+ */
9
+ function Config(props) {
10
+ const config = useConfig();
11
+ config(props);
12
+ return null;
13
+ }
@@ -0,0 +1,11 @@
1
+ export { Head };
2
+ /**
3
+ * Add arbitrary `<head>` tags.
4
+ *
5
+ * (The children are teleported to `<head>`.)
6
+ *
7
+ * https://vike.dev/Head
8
+ */
9
+ declare function Head({ children }: {
10
+ children: React.ReactNode;
11
+ }): null;
@@ -0,0 +1,15 @@
1
+ export { Head };
2
+ // Same as ./Head-server.ts but importing useConfig-client.js
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-client.js';
4
+ /**
5
+ * Add arbitrary `<head>` tags.
6
+ *
7
+ * (The children are teleported to `<head>`.)
8
+ *
9
+ * https://vike.dev/Head
10
+ */
11
+ function Head({ children }) {
12
+ const config = useConfig();
13
+ config({ Head: children });
14
+ return null;
15
+ }
@@ -0,0 +1,11 @@
1
+ export { Head };
2
+ /**
3
+ * Add arbitrary `<head>` tags.
4
+ *
5
+ * (The children are teleported to `<head>`.)
6
+ *
7
+ * https://vike.dev/Head
8
+ */
9
+ declare function Head({ children }: {
10
+ children: React.ReactNode;
11
+ }): null;
@@ -0,0 +1,15 @@
1
+ export { Head };
2
+ // Same as ./Head-client.ts but importing useConfig-server.js
3
+ import { useConfig } from '../../hooks/useConfig/useConfig-server.js';
4
+ /**
5
+ * Add arbitrary `<head>` tags.
6
+ *
7
+ * (The children are teleported to `<head>`.)
8
+ *
9
+ * https://vike.dev/Head
10
+ */
11
+ function Head({ children }) {
12
+ const config = useConfig();
13
+ config({ Head: children });
14
+ return null;
15
+ }
@@ -0,0 +1,3 @@
1
+ export type { ConfigSetter };
2
+ import type { ConfigFromHook } from '../../types/Config.js';
3
+ type ConfigSetter = (config: ConfigFromHook) => void;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export { useConfig };
2
+ import type { ConfigSetter } from './shared.js';
3
+ /**
4
+ * Set configurations inside React components and Vike hooks.
5
+ *
6
+ * https://vike.dev/useConfig
7
+ */
8
+ declare function useConfig(): ConfigSetter;
@@ -0,0 +1,41 @@
1
+ export { useConfig };
2
+ import { usePageContext } from '../usePageContext.js';
3
+ import { getPageContext } from 'vike/getPageContext';
4
+ /**
5
+ * Set configurations inside React components and Vike hooks.
6
+ *
7
+ * https://vike.dev/useConfig
8
+ */
9
+ function useConfig() {
10
+ const configSetter = (config) => setConfigOverPageContext(config, pageContext);
11
+ // Vike hook
12
+ let pageContext = getPageContext();
13
+ if (pageContext)
14
+ return configSetter;
15
+ // React component
16
+ pageContext = usePageContext();
17
+ return (config) => {
18
+ if (!('_headAlreadySet' in pageContext)) {
19
+ configSetter(config);
20
+ }
21
+ else {
22
+ sideEffect(config);
23
+ }
24
+ };
25
+ }
26
+ const configsClientSide = ['title'];
27
+ function setConfigOverPageContext(config, pageContext) {
28
+ pageContext._configFromHook ?? (pageContext._configFromHook = {});
29
+ configsClientSide.forEach((configName) => {
30
+ const configValue = config[configName];
31
+ if (!configValue)
32
+ return;
33
+ pageContext._configFromHook[configName] = configValue;
34
+ });
35
+ }
36
+ function sideEffect(config) {
37
+ const { title } = config;
38
+ if (title) {
39
+ window.document.title = title;
40
+ }
41
+ }
@@ -0,0 +1,8 @@
1
+ export { useConfig };
2
+ import type { ConfigSetter } from './shared.js';
3
+ /**
4
+ * Set configurations inside React components and Vike hooks.
5
+ *
6
+ * https://vike.dev/useConfig
7
+ */
8
+ declare function useConfig(): ConfigSetter;
@@ -0,0 +1,62 @@
1
+ export { useConfig };
2
+ import { usePageContext } from '../usePageContext.js';
3
+ import { getPageContext } from 'vike/getPageContext';
4
+ import { useStream } from 'react-streaming';
5
+ /**
6
+ * Set configurations inside React components and Vike hooks.
7
+ *
8
+ * https://vike.dev/useConfig
9
+ */
10
+ function useConfig() {
11
+ const configSetter = (config) => setConfigOverPageContext(config, pageContext);
12
+ // Vike hook
13
+ let pageContext = getPageContext();
14
+ if (pageContext)
15
+ return configSetter;
16
+ // React component
17
+ pageContext = usePageContext();
18
+ const stream = useStream();
19
+ return (config) => {
20
+ if (!pageContext._headAlreadySet) {
21
+ configSetter(config);
22
+ }
23
+ else {
24
+ // <head> already sent to the browser => send DOM-manipulating scripts during HTML streaming
25
+ sideEffect(config, stream);
26
+ }
27
+ };
28
+ }
29
+ const configsHtmlOnly = ['Head', 'description', 'image'];
30
+ const configsCumulative = ['Head'];
31
+ const configsOverridable = ['title', 'description', 'image'];
32
+ function setConfigOverPageContext(config, pageContext) {
33
+ pageContext._configFromHook ?? (pageContext._configFromHook = {});
34
+ if (pageContext.isClientSideNavigation) {
35
+ // Remove HTML only configs which the client-side doesn't need (also avoiding serialization errors)
36
+ for (const configName of configsHtmlOnly)
37
+ delete config[configName];
38
+ }
39
+ // Cumulative values
40
+ configsCumulative.forEach((configName) => {
41
+ var _a;
42
+ const configValue = config[configName];
43
+ if (!configValue)
44
+ return;
45
+ (_a = pageContext._configFromHook)[configName] ?? (_a[configName] = []);
46
+ pageContext._configFromHook[configName].push(configValue);
47
+ });
48
+ // Overridable values
49
+ configsOverridable.forEach((configName) => {
50
+ const configValue = config[configName];
51
+ if (!configValue)
52
+ return;
53
+ pageContext._configFromHook[configName] = configValue;
54
+ });
55
+ }
56
+ function sideEffect(config, stream) {
57
+ const { title } = config;
58
+ if (title) {
59
+ const htmlSnippet = `<script>document.title = ${JSON.stringify(title)}</script>`;
60
+ stream.injectToStream(htmlSnippet);
61
+ }
62
+ }
@@ -1,3 +1,4 @@
1
1
  export { getHeadSetting };
2
- import type { PageContext } from 'vike/types';
3
- declare function getHeadSetting(headSetting: 'title' | 'favicon' | 'lang', pageContext: PageContext): undefined | null | string;
2
+ import type { PageContextInternal } from '../types/PageContext.js';
3
+ type HeadSetting = 'favicon' | 'lang' | 'title' | 'description' | 'image';
4
+ declare function getHeadSetting(headSetting: HeadSetting, pageContext: PageContextInternal): undefined | null | string;
@@ -1,6 +1,11 @@
1
1
  export { getHeadSetting };
2
2
  import { isCallable } from '../utils/isCallable.js';
3
3
  function getHeadSetting(headSetting, pageContext) {
4
+ {
5
+ const val = pageContext._configFromHook?.[headSetting];
6
+ if (val !== undefined)
7
+ return val;
8
+ }
4
9
  const config = pageContext.configEntries[headSetting]?.[0];
5
10
  if (!config)
6
11
  return undefined;
@@ -41,14 +41,15 @@ const onRenderClient = (pageContext) => {
41
41
  updateDocument(pageContext);
42
42
  }
43
43
  // Use cases:
44
- // - User-land document settings (e.g. user-land implementation of `config.favicon`)
45
- // - Testing tools https://github.com/vikejs/vike-react/issues/95
44
+ // - Custom user settings: https://vike.dev/head#custom-settings
45
+ // - Testing tools: https://github.com/vikejs/vike-react/issues/95
46
46
  pageContext.config.onAfterRenderClient?.(pageContext);
47
47
  };
48
48
  function updateDocument(pageContext) {
49
+ ;
50
+ pageContext._headAlreadySet = true;
49
51
  const title = getHeadSetting('title', pageContext);
50
52
  const lang = getHeadSetting('lang', pageContext);
51
- const favicon = getHeadSetting('favicon', pageContext);
52
53
  // - We skip if `undefined` as we shouldn't remove values set by the Head setting.
53
54
  // - Setting a default prevents the previous value to be leaked: upon client-side navigation, the value set by the previous page won't be removed if the next page doesn't override it.
54
55
  // - Most of the time, the user sets a default himself (i.e. a value defined at /pages/+config.js)
@@ -56,22 +57,4 @@ function updateDocument(pageContext) {
56
57
  document.title = title || '';
57
58
  if (lang !== undefined)
58
59
  document.documentElement.lang = lang || 'en';
59
- if (favicon !== undefined)
60
- setFavicon(favicon);
61
- }
62
- // TODO/next-major-release: remove config.favicon and, instead, add docs showcasing how to implement a favicon setting on the user-land.
63
- // https://stackoverflow.com/questions/260857/changing-website-favicon-dynamically/260876#260876
64
- function setFavicon(faviconUrl) {
65
- let link = document.querySelector("link[rel~='icon']");
66
- if (!faviconUrl) {
67
- if (link)
68
- document.head.removeChild(link);
69
- return;
70
- }
71
- if (!link) {
72
- link = document.createElement('link');
73
- link.rel = 'icon';
74
- document.head.appendChild(link);
75
- }
76
- link.href = faviconUrl;
77
60
  }
@@ -1,3 +1,4 @@
1
1
  export { onRenderHtml };
2
2
  import type { OnRenderHtmlAsync } from 'vike/types';
3
3
  declare const onRenderHtml: OnRenderHtmlAsync;
4
+ export type Viewport = 'responsive' | number | null;
@@ -1,21 +1,27 @@
1
1
  // https://vike.dev/onRenderHtml
2
2
  export { onRenderHtml };
3
3
  import React from 'react';
4
- import { renderToString } from 'react-dom/server';
4
+ import { renderToString, renderToStaticMarkup } from 'react-dom/server';
5
5
  import { renderToStream } from 'react-streaming/server';
6
6
  import { dangerouslySkipEscape, escapeInject, version } from 'vike/server';
7
7
  import { PageContextProvider } from '../hooks/usePageContext.js';
8
8
  import { getHeadSetting } from './getHeadSetting.js';
9
9
  import { getPageElement } from './getPageElement.js';
10
+ import { isReactElement } from '../utils/isReactElement.js';
11
+ import { getTagAttributesString } from '../utils/getTagAttributesString.js';
10
12
  checkVikeVersion();
11
13
  addEcosystemStamp();
12
14
  const onRenderHtml = async (pageContext) => {
13
15
  const pageHtml = await getPageHtml(pageContext);
14
- const { headHtml, lang } = getHeadHtml(pageContext);
16
+ const headHtml = getHeadHtml(pageContext);
17
+ const { htmlAttributesString, bodyAttributesString } = getTagAttributes(pageContext);
15
18
  return escapeInject `<!DOCTYPE html>
16
- <html lang='${lang}'>
17
- <head>${headHtml}</head>
18
- <body>
19
+ <html${dangerouslySkipEscape(htmlAttributesString)}>
20
+ <head>
21
+ <meta charset="UTF-8" />
22
+ ${headHtml}
23
+ </head>
24
+ <body${dangerouslySkipEscape(bodyAttributesString)}>
19
25
  <div id="root">${pageHtml}</div>
20
26
  </body>
21
27
  </html>`;
@@ -46,25 +52,81 @@ async function getPageHtml(pageContext) {
46
52
  return pageHtml;
47
53
  }
48
54
  function getHeadHtml(pageContext) {
49
- const title = getHeadSetting('title', pageContext);
55
+ pageContext._headAlreadySet = true;
50
56
  const favicon = getHeadSetting('favicon', pageContext);
51
- const lang = getHeadSetting('lang', pageContext) || 'en';
52
- const titleTags = !title ? '' : escapeInject `<title>${title}</title><meta property="og:title" content="${title}" />`;
57
+ const title = getHeadSetting('title', pageContext);
58
+ const description = getHeadSetting('description', pageContext);
59
+ const image = getHeadSetting('image', pageContext);
53
60
  const faviconTag = !favicon ? '' : escapeInject `<link rel="icon" href="${favicon}" />`;
54
- const Head = pageContext.config.Head || (() => React.createElement(React.Fragment, null));
55
- let headElement = (React.createElement(PageContextProvider, { pageContext: pageContext },
56
- React.createElement(Head, null)));
57
- if (pageContext.config.reactStrictMode !== false) {
58
- headElement = React.createElement(React.StrictMode, null, headElement);
59
- }
60
- const headElementHtml = dangerouslySkipEscape(renderToString(headElement));
61
+ const titleTags = !title ? '' : escapeInject `<title>${title}</title><meta property="og:title" content="${title}" />`;
62
+ const descriptionTags = !description
63
+ ? ''
64
+ : escapeInject `<meta name="description" content="${description}" /><meta property="og:description" content="${description}" />`;
65
+ const imageTags = !image
66
+ ? ''
67
+ : escapeInject `<meta property="og:image" content="${image}"><meta name="twitter:card" content="summary_large_image">`;
68
+ const headElementsHtml = dangerouslySkipEscape([
69
+ // <Head> set by +Head
70
+ pageContext.config.Head,
71
+ // <Head> set by useConfig()
72
+ ...(pageContext._configFromHook?.Head ?? [])
73
+ ]
74
+ .filter((Head) => Head !== null && Head !== undefined)
75
+ .map((Head) => getHeadElementHtml(Head, pageContext))
76
+ .join('\n'));
77
+ // Not needed on the client-side, thus we remove it to save KBs sent to the client
78
+ delete pageContext._configFromHook;
79
+ const viewportTag = dangerouslySkipEscape(getViewportTag(pageContext.config.viewport));
61
80
  const headHtml = escapeInject `
62
- <meta charset="UTF-8" />
63
81
  ${titleTags}
64
- ${headElementHtml}
82
+ ${headElementsHtml}
83
+ ${viewportTag}
84
+ ${descriptionTags}
65
85
  ${faviconTag}
86
+ ${imageTags}
66
87
  `;
67
- return { headHtml, lang };
88
+ return headHtml;
89
+ }
90
+ function getHeadElementHtml(Head, pageContext) {
91
+ let headElement;
92
+ if (isReactElement(Head)) {
93
+ headElement = Head;
94
+ }
95
+ else {
96
+ headElement = (React.createElement(PageContextProvider, { pageContext: pageContext },
97
+ React.createElement(Head, null)));
98
+ }
99
+ if (pageContext.config.reactStrictMode !== false) {
100
+ headElement = React.createElement(React.StrictMode, null, headElement);
101
+ }
102
+ return renderToStaticMarkup(headElement);
103
+ }
104
+ function getTagAttributes(pageContext) {
105
+ let lang = getHeadSetting('lang', pageContext);
106
+ // Don't set `lang` to its default value if it's `null` (so that users can set it to `null` in order to remove the default value)
107
+ if (lang === undefined)
108
+ lang = 'en';
109
+ const bodyAttributes = mergeTagAttributesList(pageContext.config.bodyAttributes);
110
+ const htmlAttributes = mergeTagAttributesList(pageContext.config.htmlAttributes);
111
+ const bodyAttributesString = getTagAttributesString(bodyAttributes);
112
+ const htmlAttributesString = getTagAttributesString({ ...htmlAttributes, lang: lang ?? htmlAttributes.lang });
113
+ return { htmlAttributesString, bodyAttributesString };
114
+ }
115
+ function mergeTagAttributesList(tagAttributesList = []) {
116
+ const tagAttributes = {};
117
+ tagAttributesList.forEach((tagAttrs) => Object.assign(tagAttributes, tagAttrs));
118
+ return tagAttributes;
119
+ }
120
+ function getViewportTag(viewport) {
121
+ if (viewport === 'responsive' || viewport === undefined) {
122
+ // `user-scalable=no` isn't recommended anymore:
123
+ // - https://stackoverflow.com/questions/22354435/to-user-scalable-no-or-not-to-user-scalable-no/22544312#comment120949420_22544312
124
+ return '<meta name="viewport" content="width=device-width,initial-scale=1">';
125
+ }
126
+ if (typeof viewport === 'number') {
127
+ return `<meta name="viewport" content="width=${viewport}">`;
128
+ }
129
+ return '';
68
130
  }
69
131
  // We don't need this anymore starting from vike@0.4.173 which added the `require` setting.
70
132
  // TODO/eventually: remove this once <=0.4.172 versions become rare.
@@ -1,11 +1,17 @@
1
1
  import type { ImportString, PageContextClient, PageContext } from 'vike/types';
2
+ import type { TagAttributes } from '../utils/getTagAttributesString.js';
3
+ import type { Viewport } from '../renderer/onRenderHtml.js';
2
4
  declare global {
3
5
  namespace Vike {
4
6
  interface Config {
5
7
  /** The page's root React component */
6
8
  Page?: () => React.ReactNode;
7
- /** React element rendered and appended into &lt;head>&lt;/head> */
8
- Head?: () => React.ReactNode;
9
+ /**
10
+ * Add arbitrary `<head>` tags.
11
+ *
12
+ * https://vike.dev/Head
13
+ */
14
+ Head?: Head;
9
15
  /**
10
16
  * A component that defines the visual layout of the page common to several pages.
11
17
  *
@@ -21,25 +27,88 @@ declare global {
21
27
  */
22
28
  Wrapper?: Wrapper | ImportString;
23
29
  /**
24
- * ```js
25
- * <title>${title}</title>
26
- * <meta property="og:title" content="${title}" />
30
+ * Set the page's tilte.
31
+ *
32
+ * Generates:
33
+ * ```jsx
34
+ * <head>
35
+ * <title>{title}</title>
36
+ * <meta property="og:title" content={title} />
37
+ * </head>
27
38
  * ```
39
+ *
40
+ * https://vike.dev/title
28
41
  */
29
42
  title?: PlainOrGetter<string>;
30
43
  /**
31
- * ```js
32
- * <link rel="icon" href="${favicon}" />
44
+ * Set the page's description.
45
+ *
46
+ * Generates:
47
+ * ```jsx
48
+ * <head>
49
+ * <meta name="description" content={description}>
50
+ * <meta property="og:description" content={description}>
51
+ * </head>
33
52
  * ```
53
+ *
54
+ * https://vike.dev/description
34
55
  */
35
- favicon?: PlainOrGetter<string>;
56
+ description?: PlainOrGetter<string>;
36
57
  /**
37
- * ```js
38
- * <html lang="${lang}">
58
+ * Set the page's preview image upon URL sharing.
59
+ *
60
+ * Generates:
61
+ * ```jsx
62
+ * <head>
63
+ * <meta property="og:image" content={image}>
64
+ * <meta name="twitter:card" content="summary_large_image">
65
+ * </head>
66
+ * ```
67
+ *
68
+ * https://vike.dev/image
69
+ */
70
+ image?: PlainOrGetter<string>;
71
+ /**
72
+ * Set the page's width shown to the user on mobile/tablet devices.
73
+ *
74
+ * https://vike.dev/viewport
75
+ *
76
+ * @default "responsive"
77
+ */
78
+ viewport?: Viewport;
79
+ /**
80
+ * Set the page's favicon.
81
+ *
82
+ * Generates:
83
+ * ```jsx
84
+ * <head>
85
+ * <link rel="icon" href={favicon} />
86
+ * </head>
39
87
  * ```
88
+ *
89
+ * https://vike.dev/favicon
90
+ */
91
+ favicon?: PlainOrGetter<string>;
92
+ /**
93
+ * Set the page's language (`<html lang>`).
94
+ *
40
95
  * @default 'en'
96
+ *
97
+ * https://vike.dev/lang
98
+ */
99
+ lang?: PlainOrGetter<string> | null;
100
+ /**
101
+ * Add tag attributes such as `<html class="dark">`.
102
+ *
103
+ * https://vike.dev/htmlAttributes
41
104
  */
42
- lang?: PlainOrGetter<string>;
105
+ htmlAttributes?: TagAttributes;
106
+ /**
107
+ * Add tag attributes such as `<body class="dark">`.
108
+ *
109
+ * https://vike.dev/bodyAttributes
110
+ */
111
+ bodyAttributes?: TagAttributes;
43
112
  /**
44
113
  * If `true`, the page is rendered twice: on the server-side (to HTML) and on the client-side (hydration).
45
114
  *
@@ -97,10 +166,13 @@ declare global {
97
166
  interface ConfigResolved {
98
167
  Wrapper?: Wrapper[];
99
168
  Layout?: Layout[];
169
+ bodyAttributes?: TagAttributes[];
170
+ htmlAttributes?: TagAttributes[];
100
171
  }
101
172
  }
102
173
  }
103
174
  type PlainOrGetter<T> = T | ((pageContext: PageContext) => T);
175
+ export type Head = React.ReactNode | (() => React.ReactNode);
104
176
  type Wrapper = (props: {
105
177
  children: React.ReactNode;
106
178
  }) => React.ReactNode;
@@ -109,4 +181,14 @@ type Loading = {
109
181
  component?: () => React.ReactNode;
110
182
  layout?: () => React.ReactNode;
111
183
  };
184
+ type PickWithoutGetter<T, K extends keyof T> = {
185
+ [P in K]: Exclude<T[P], Function>;
186
+ };
187
+ export type ConfigFromHook = PickWithoutGetter<Vike.Config, 'Head' | 'title' | 'description' | 'image'>;
188
+ export type ConfigFromHookResolved = {
189
+ Head?: Head[];
190
+ title?: string;
191
+ description?: string;
192
+ image?: string;
193
+ };
112
194
  export {};
@@ -1,6 +1,8 @@
1
1
  import type React from 'react';
2
2
  import type { JSX } from 'react';
3
3
  import type ReactDOM from 'react-dom/client';
4
+ import type { ConfigFromHookResolved } from './Config.js';
5
+ import type { PageContext } from 'vike/types';
4
6
  declare global {
5
7
  namespace Vike {
6
8
  interface PageContext {
@@ -13,3 +15,7 @@ declare global {
13
15
  }
14
16
  }
15
17
  }
18
+ export type PageContextInternal = PageContext & {
19
+ _configFromHook?: ConfigFromHookResolved;
20
+ _headAlreadySet?: true;
21
+ };
@@ -0,0 +1,4 @@
1
+ export { getTagAttributesString };
2
+ export type { TagAttributes };
3
+ type TagAttributes = Record<string, string | number | boolean | undefined | null>;
4
+ declare function getTagAttributesString(tagAttributes: TagAttributes): string;
@@ -0,0 +1,15 @@
1
+ export { getTagAttributesString };
2
+ function getTagAttributesString(tagAttributes) {
3
+ const tagAttributesString = Object.entries(tagAttributes)
4
+ .filter(([_key, value]) => value !== false && value !== null && value !== undefined)
5
+ .map(([key, value]) => `${ensureIsValidAttributeName(key)}=${JSON.stringify(String(value))}`)
6
+ .join(' ');
7
+ if (tagAttributesString.length === 0)
8
+ return '';
9
+ return ` ${tagAttributesString}`;
10
+ }
11
+ function ensureIsValidAttributeName(str) {
12
+ if (/^[a-z][a-z0-9\-]*$/i.test(str) && !str.endsWith('-'))
13
+ return str;
14
+ throw new Error(`Invalid HTML tag attribute name ${JSON.stringify(str)}`);
15
+ }
@@ -0,0 +1,5 @@
1
+ export { isReactElement };
2
+ import React from 'react';
3
+ declare function isReactElement(value: ReactElement | ReactComponent): value is ReactElement;
4
+ type ReactElement = React.ReactNode;
5
+ type ReactComponent = () => ReactElement;
@@ -0,0 +1,5 @@
1
+ export { isReactElement };
2
+ import { isValidElement } from 'react';
3
+ function isReactElement(value) {
4
+ return isValidElement(value) || Array.isArray(value);
5
+ }
package/package.json CHANGED
@@ -1,13 +1,25 @@
1
1
  {
2
2
  "name": "vike-react",
3
- "version": "0.4.18-commit-5c88c7b",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "exports": {
8
8
  "./useData": "./dist/hooks/useData.js",
9
9
  "./usePageContext": "./dist/hooks/usePageContext.js",
10
+ "./useConfig": {
11
+ "browser": "./dist/hooks/useConfig/useConfig-client.js",
12
+ "default": "./dist/hooks/useConfig/useConfig-server.js"
13
+ },
10
14
  "./ClientOnly": "./dist/components/ClientOnly.js",
15
+ "./Head": {
16
+ "browser": "./dist/components/Head/Head-client.js",
17
+ "default": "./dist/components/Head/Head-server.js"
18
+ },
19
+ "./Config": {
20
+ "browser": "./dist/components/Config/Config-client.js",
21
+ "default": "./dist/components/Config/Config-server.js"
22
+ },
11
23
  "./clientOnly": "./dist/helpers/clientOnly.js",
12
24
  ".": "./dist/index.js",
13
25
  "./config": "./dist/+config.js",
@@ -26,7 +38,7 @@
26
38
  "peerDependencies": {
27
39
  "react": ">=18.0.0",
28
40
  "react-dom": ">=18.0.0",
29
- "vike": ">=0.4.178",
41
+ "vike": ">=0.4.182",
30
42
  "vite": ">=4.3.8"
31
43
  },
32
44
  "devDependencies": {
@@ -39,7 +51,7 @@
39
51
  "react-dom": "^18.2.0",
40
52
  "rimraf": "^5.0.5",
41
53
  "typescript": "^5.5.3",
42
- "vike": "^0.4.178"
54
+ "vike": "^0.4.182"
43
55
  },
44
56
  "dependencies": {
45
57
  "react-streaming": "^0.3.42"
@@ -52,6 +64,15 @@
52
64
  "usePageContext": [
53
65
  "./dist/hooks/usePageContext.d.ts"
54
66
  ],
67
+ "useConfig": [
68
+ "./dist/hooks/useConfig/useConfig-server.d.ts"
69
+ ],
70
+ "Head": [
71
+ "./dist/components/Head/Head-server.d.ts"
72
+ ],
73
+ "Config": [
74
+ "./dist/components/Config/Config-server.d.ts"
75
+ ],
55
76
  "ClientOnly": [
56
77
  "./dist/components/ClientOnly.d.ts"
57
78
  ],