vike-react 0.4.18 → 0.5.1

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
@@ -16,6 +16,7 @@ declare const _default: {
16
16
  env: {
17
17
  server: true;
18
18
  };
19
+ cumulative: true;
19
20
  };
20
21
  Layout: {
21
22
  env: {
@@ -30,11 +31,26 @@ declare const _default: {
30
31
  client: true;
31
32
  };
32
33
  };
34
+ description: {
35
+ env: {
36
+ server: true;
37
+ };
38
+ };
39
+ image: {
40
+ env: {
41
+ server: true;
42
+ };
43
+ };
44
+ viewport: {
45
+ env: {
46
+ server: true;
47
+ };
48
+ };
33
49
  favicon: {
34
50
  env: {
35
51
  server: true;
36
- client: true;
37
52
  };
53
+ global: true;
38
54
  };
39
55
  lang: {
40
56
  env: {
@@ -42,6 +58,20 @@ declare const _default: {
42
58
  client: true;
43
59
  };
44
60
  };
61
+ htmlAttributes: {
62
+ env: {
63
+ server: true;
64
+ };
65
+ global: true;
66
+ cumulative: true;
67
+ };
68
+ bodyAttributes: {
69
+ env: {
70
+ server: true;
71
+ };
72
+ global: true;
73
+ cumulative: true;
74
+ };
45
75
  ssr: {
46
76
  env: {
47
77
  config: true;
@@ -62,11 +92,13 @@ declare const _default: {
62
92
  env: {
63
93
  client: true;
64
94
  };
95
+ cumulative: true;
65
96
  };
66
97
  onAfterRenderClient: {
67
98
  env: {
68
99
  client: true;
69
100
  };
101
+ cumulative: true;
70
102
  };
71
103
  Wrapper: {
72
104
  cumulative: true;
@@ -75,16 +107,6 @@ declare const _default: {
75
107
  server: true;
76
108
  };
77
109
  };
78
- name: {
79
- env: {
80
- config: true;
81
- };
82
- };
83
- require: {
84
- env: {
85
- config: true;
86
- };
87
- };
88
110
  reactStrictMode: {
89
111
  env: {
90
112
  client: 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),
@@ -22,7 +23,8 @@ export default {
22
23
  // https://vike.dev/meta
23
24
  meta: {
24
25
  Head: {
25
- env: { server: true }
26
+ env: { server: true },
27
+ cumulative: true
26
28
  },
27
29
  Layout: {
28
30
  env: { server: true, client: true },
@@ -31,12 +33,32 @@ export default {
31
33
  title: {
32
34
  env: { server: true, client: true }
33
35
  },
36
+ description: {
37
+ env: { server: true }
38
+ },
39
+ image: {
40
+ env: { server: true }
41
+ },
42
+ viewport: {
43
+ env: { server: true }
44
+ },
34
45
  favicon: {
35
- env: { server: true, client: true }
46
+ env: { server: true },
47
+ global: true
36
48
  },
37
49
  lang: {
38
50
  env: { server: true, client: true }
39
51
  },
52
+ htmlAttributes: {
53
+ env: { server: true },
54
+ global: true,
55
+ cumulative: true // for Vike extensions
56
+ },
57
+ bodyAttributes: {
58
+ env: { server: true },
59
+ global: true,
60
+ cumulative: true // for Vike extensions
61
+ },
40
62
  ssr: {
41
63
  env: { config: true },
42
64
  effect: ssrEffect
@@ -48,23 +70,17 @@ export default {
48
70
  env: { server: true }
49
71
  },
50
72
  onBeforeRenderClient: {
51
- env: { client: true }
73
+ env: { client: true },
74
+ cumulative: true
52
75
  },
53
76
  onAfterRenderClient: {
54
- env: { client: true }
77
+ env: { client: true },
78
+ cumulative: true
55
79
  },
56
80
  Wrapper: {
57
81
  cumulative: true,
58
82
  env: { client: true, server: true }
59
83
  },
60
- // Vike already defines the setting 'name', but we redundantly define it here for older Vike versions (otherwise older Vike versions will complain that 'name` is an unknown config). TODO/eventually: remove this once <=0.4.172 versions become rare (also because we use the `require` setting starting from `0.4.173`).
61
- name: {
62
- env: { config: true }
63
- },
64
- // Vike already defines the setting 'require', but we redundantly define it here for older Vike versions (otherwise older Vike versions will complain that 'require` is an unknown config). TODO/eventually: remove this once <=0.4.172 versions become rare (also because we use the `require` setting starting from `0.4.173`).
65
- require: {
66
- env: { config: true }
67
- },
68
84
  reactStrictMode: {
69
85
  env: { client: true, server: true }
70
86
  },
@@ -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,8 @@
1
+ export { useConfig };
2
+ import type { ConfigFromHook } from '../../types/Config.js';
3
+ /**
4
+ * Set configurations inside React components and Vike hooks.
5
+ *
6
+ * https://vike.dev/useConfig
7
+ */
8
+ declare function useConfig(): (config: ConfigFromHook) => void;
@@ -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 { ConfigFromHook } from '../../types/Config.js';
3
+ /**
4
+ * Set configurations inside React components and Vike hooks.
5
+ *
6
+ * https://vike.dev/useConfig
7
+ */
8
+ declare function useConfig(): (config: ConfigFromHook) => void;
@@ -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,5 @@
1
1
  export { getHeadSetting };
2
2
  import type { PageContext } from 'vike/types';
3
- declare function getHeadSetting(headSetting: 'title' | 'favicon' | 'lang', pageContext: PageContext): undefined | null | string;
3
+ import type { PageContextInternal } from '../types/PageContext.js';
4
+ type HeadSetting = 'favicon' | 'lang' | 'title' | 'description' | 'image';
5
+ declare function getHeadSetting(headSetting: HeadSetting, pageContext: 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;
@@ -1,4 +1,4 @@
1
1
  export { onRenderClient };
2
- import type { OnRenderClientSync } from 'vike/types';
2
+ import type { OnRenderClientAsync } from 'vike/types';
3
3
  import './styles.css';
4
- declare const onRenderClient: OnRenderClientSync;
4
+ declare const onRenderClient: OnRenderClientAsync;
@@ -4,23 +4,23 @@ import ReactDOM from 'react-dom/client';
4
4
  import { getHeadSetting } from './getHeadSetting.js';
5
5
  import { getPageElement } from './getPageElement.js';
6
6
  import './styles.css';
7
+ import { callCumulativeHooks } from '../utils/callCumulativeHooks.js';
7
8
  let root;
8
- const onRenderClient = (pageContext) => {
9
+ const onRenderClient = async (pageContext) => {
9
10
  // Use case:
10
11
  // - Store hydration https://github.com/vikejs/vike-react/issues/110
11
- pageContext.config.onBeforeRenderClient?.(pageContext);
12
+ await callCumulativeHooks(pageContext.config.onBeforeRenderClient, pageContext);
12
13
  const page = getPageElement(pageContext);
14
+ pageContext.page = page;
13
15
  // TODO: implement this? So that, upon errors, onRenderClient() throws an error and Vike can render the error. As of April 2024 it isn't released yet.
14
16
  // - https://react-dev-git-fork-rickhanlonii-rh-root-options-fbopensource.vercel.app/reference/react-dom/client/createRoot#show-a-dialog-for-uncaught-errors
15
17
  // - https://react-dev-git-fork-rickhanlonii-rh-root-options-fbopensource.vercel.app/reference/react-dom/client/hydrateRoot#show-a-dialog-for-uncaught-errors
16
18
  const onUncaughtError = (_error, _errorInfo) => { };
17
19
  const container = document.getElementById('root');
18
- if (
19
- // Whether the page was rendered to HTML. (I.e. whether the user set the [`ssr`](https://vike.dev/ssr) setting to `false`.)
20
- container.innerHTML !== '' &&
21
- // Whether the page was already rendered to HTML. (I.e. whether this is the first client-side rendering.)
22
- pageContext.isHydration) {
23
- // Hydration
20
+ if (pageContext.isHydration &&
21
+ // Whether the page was [Server-Side Rendered](https://vike.dev/ssr).
22
+ container.innerHTML !== '') {
23
+ // First render while using SSR, i.e. [hydration](https://vike.dev/hydration)
24
24
  root = ReactDOM.hydrateRoot(container, page, {
25
25
  // @ts-expect-error
26
26
  onUncaughtError
@@ -28,46 +28,34 @@ const onRenderClient = (pageContext) => {
28
28
  }
29
29
  else {
30
30
  if (!root) {
31
- // First render (not hydration)
31
+ // First render without SSR
32
32
  root = ReactDOM.createRoot(container, {
33
33
  // @ts-expect-error
34
34
  onUncaughtError
35
35
  });
36
36
  }
37
- else {
38
- // Client-side navigation
39
- const title = getHeadSetting('title', pageContext) || '';
40
- const lang = getHeadSetting('lang', pageContext) || 'en';
41
- const favicon = getHeadSetting('favicon', pageContext);
42
- // We skip if the value is undefined because we shouldn't remove values set in HTML (by the Head setting).
43
- // - This also means that previous values will leak: upon client-side navigation, the title set by the previous page won't be removed if the next page doesn't override it. But that's okay because usually pages always have a favicon and title, which means that previous values are always overriden. Also, as a workaround, the user can set the value to `null` to ensure that previous values are overriden.
44
- if (title !== undefined)
45
- document.title = title;
46
- if (lang !== undefined)
47
- document.documentElement.lang = lang;
48
- if (favicon !== undefined)
49
- setFavicon(favicon);
50
- }
51
37
  root.render(page);
52
38
  }
53
- pageContext.page = page;
54
39
  pageContext.root = root;
55
- // Use case:
56
- // - Testing tool https://github.com/vikejs/vike-react/issues/95
57
- pageContext.config.onAfterRenderClient?.(pageContext);
58
- };
59
- // https://stackoverflow.com/questions/260857/changing-website-favicon-dynamically/260876#260876
60
- function setFavicon(faviconUrl) {
61
- let link = document.querySelector("link[rel~='icon']");
62
- if (!faviconUrl) {
63
- if (link)
64
- document.head.removeChild(link);
65
- return;
66
- }
67
- if (!link) {
68
- link = document.createElement('link');
69
- link.rel = 'icon';
70
- document.head.appendChild(link);
40
+ if (!pageContext.isHydration) {
41
+ // E.g. document.title
42
+ updateDocument(pageContext);
71
43
  }
72
- link.href = faviconUrl;
44
+ // Use cases:
45
+ // - Custom user settings: https://vike.dev/head-tags#custom-settings
46
+ // - Testing tools: https://github.com/vikejs/vike-react/issues/95
47
+ await callCumulativeHooks(pageContext.config.onAfterRenderClient, pageContext);
48
+ };
49
+ function updateDocument(pageContext) {
50
+ pageContext._headAlreadySet = true;
51
+ const title = getHeadSetting('title', pageContext);
52
+ const lang = getHeadSetting('lang', pageContext);
53
+ // - We skip if `undefined` as we shouldn't remove values set by the Head setting.
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.
55
+ // - Most of the time, the user sets a default himself (i.e. a value defined at /pages/+config.js)
56
+ // - If he doesn't have a default then he can use `null` to opt into Vike's defaults
57
+ if (title !== undefined)
58
+ document.title = title || '';
59
+ if (lang !== undefined)
60
+ document.documentElement.lang = lang || 'en';
73
61
  }
@@ -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,26 @@
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
- import { dangerouslySkipEscape, escapeInject, version } from 'vike/server';
6
+ import { dangerouslySkipEscape, escapeInject } 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
- checkVikeVersion();
10
+ import { isReactElement } from '../utils/isReactElement.js';
11
+ import { getTagAttributesString } from '../utils/getTagAttributesString.js';
11
12
  addEcosystemStamp();
12
13
  const onRenderHtml = async (pageContext) => {
13
14
  const pageHtml = await getPageHtml(pageContext);
14
- const { headHtml, lang } = getHeadHtml(pageContext);
15
+ const headHtml = getHeadHtml(pageContext);
16
+ const { htmlAttributesString, bodyAttributesString } = getTagAttributes(pageContext);
15
17
  return escapeInject `<!DOCTYPE html>
16
- <html lang='${lang}'>
17
- <head>${headHtml}</head>
18
- <body>
18
+ <html${dangerouslySkipEscape(htmlAttributesString)}>
19
+ <head>
20
+ <meta charset="UTF-8" />
21
+ ${headHtml}
22
+ </head>
23
+ <body${dangerouslySkipEscape(bodyAttributesString)}>
19
24
  <div id="root">${pageHtml}</div>
20
25
  </body>
21
26
  </html>`;
@@ -46,40 +51,81 @@ async function getPageHtml(pageContext) {
46
51
  return pageHtml;
47
52
  }
48
53
  function getHeadHtml(pageContext) {
49
- const title = getHeadSetting('title', pageContext);
54
+ pageContext._headAlreadySet = true;
50
55
  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}" />`;
56
+ const title = getHeadSetting('title', pageContext);
57
+ const description = getHeadSetting('description', pageContext);
58
+ const image = getHeadSetting('image', pageContext);
53
59
  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));
60
+ const titleTags = !title ? '' : escapeInject `<title>${title}</title><meta property="og:title" content="${title}" />`;
61
+ const descriptionTags = !description
62
+ ? ''
63
+ : escapeInject `<meta name="description" content="${description}" /><meta property="og:description" content="${description}" />`;
64
+ const imageTags = !image
65
+ ? ''
66
+ : escapeInject `<meta property="og:image" content="${image}"><meta name="twitter:card" content="summary_large_image">`;
67
+ const headElementsHtml = dangerouslySkipEscape([
68
+ // Added by +Head
69
+ ...(pageContext.config.Head ?? []),
70
+ // Added by useConfig()
71
+ ...(pageContext._configFromHook?.Head ?? [])
72
+ ]
73
+ .filter((Head) => Head !== null && Head !== undefined)
74
+ .map((Head) => getHeadElementHtml(Head, pageContext))
75
+ .join('\n'));
76
+ // Not needed on the client-side, thus we remove it to save KBs sent to the client
77
+ delete pageContext._configFromHook;
78
+ const viewportTag = dangerouslySkipEscape(getViewportTag(pageContext.config.viewport));
61
79
  const headHtml = escapeInject `
62
- <meta charset="UTF-8" />
63
80
  ${titleTags}
64
- ${headElementHtml}
81
+ ${viewportTag}
82
+ ${headElementsHtml}
65
83
  ${faviconTag}
84
+ ${descriptionTags}
85
+ ${imageTags}
66
86
  `;
67
- return { headHtml, lang };
87
+ return headHtml;
68
88
  }
69
- // We don't need this anymore starting from vike@0.4.173 which added the `require` setting.
70
- // TODO/eventually: remove this once <=0.4.172 versions become rare.
71
- function checkVikeVersion() {
72
- if (version) {
73
- const versionParts = version.split('.').map((s) => parseInt(s, 10));
74
- if (versionParts[0] > 0)
75
- return;
76
- if (versionParts[1] > 4)
77
- return;
78
- if (versionParts[2] >= 173)
79
- return;
89
+ function getHeadElementHtml(Head, pageContext) {
90
+ let headElement;
91
+ if (isReactElement(Head)) {
92
+ headElement = Head;
93
+ }
94
+ else {
95
+ headElement = (React.createElement(PageContextProvider, { pageContext: pageContext },
96
+ React.createElement(Head, null)));
97
+ }
98
+ if (pageContext.config.reactStrictMode !== false) {
99
+ headElement = React.createElement(React.StrictMode, null, headElement);
100
+ }
101
+ return renderToStaticMarkup(headElement);
102
+ }
103
+ function getTagAttributes(pageContext) {
104
+ let lang = getHeadSetting('lang', pageContext);
105
+ // 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)
106
+ if (lang === undefined)
107
+ lang = 'en';
108
+ const bodyAttributes = mergeTagAttributesList(pageContext.config.bodyAttributes);
109
+ const htmlAttributes = mergeTagAttributesList(pageContext.config.htmlAttributes);
110
+ const bodyAttributesString = getTagAttributesString(bodyAttributes);
111
+ const htmlAttributesString = getTagAttributesString({ ...htmlAttributes, lang: lang ?? htmlAttributes.lang });
112
+ return { htmlAttributesString, bodyAttributesString };
113
+ }
114
+ function mergeTagAttributesList(tagAttributesList = []) {
115
+ const tagAttributes = {};
116
+ tagAttributesList.forEach((tagAttrs) => Object.assign(tagAttributes, tagAttrs));
117
+ return tagAttributes;
118
+ }
119
+ function getViewportTag(viewport) {
120
+ if (viewport === 'responsive' || viewport === undefined) {
121
+ // `user-scalable=no` isn't recommended anymore:
122
+ // - https://stackoverflow.com/questions/22354435/to-user-scalable-no-or-not-to-user-scalable-no/22544312#comment120949420_22544312
123
+ return '<meta name="viewport" content="width=device-width,initial-scale=1">';
124
+ }
125
+ if (typeof viewport === 'number') {
126
+ return `<meta name="viewport" content="width=${viewport}">`;
80
127
  }
81
- // We can leave it 0.4.173 until we entirely remove checkVikeVersion() (because starting vike@0.4.173 we use the new `require` setting).
82
- throw new Error('Update Vike to 0.4.173 or above');
128
+ return '';
83
129
  }
84
130
  // For improving error messages of:
85
131
  // - react-streaming https://github.com/brillout/react-streaming/blob/6a43dd20c27fb5d751dca41466b06ee3f4f35462/src/server/useStream.ts#L21
@@ -1,13 +1,23 @@
1
- import type { ImportString, PageContextClient, PageContext } from 'vike/types';
1
+ import type { ImportString, PageContextClient, PageContext as PageContext_, PageContextServer } 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
- /** The page's root React component */
7
+ /**
8
+ * The page's root React component.
9
+ *
10
+ * https://vike.dev/Page
11
+ */
6
12
  Page?: () => React.ReactNode;
7
- /** React element rendered and appended into &lt;head>&lt;/head> */
8
- Head?: () => React.ReactNode;
9
13
  /**
10
- * A component that defines the visual layout of the page common to several pages.
14
+ * Add arbitrary `<head>` tags.
15
+ *
16
+ * https://vike.dev/Head
17
+ */
18
+ Head?: Head;
19
+ /**
20
+ * A component that defines the visual layout common to several pages.
11
21
  *
12
22
  * Technically: the `<Layout>` component wraps the root component `<Page>`.
13
23
  *
@@ -21,34 +31,96 @@ declare global {
21
31
  */
22
32
  Wrapper?: Wrapper | ImportString;
23
33
  /**
24
- * ```js
25
- * <title>${title}</title>
26
- * <meta property="og:title" content="${title}" />
34
+ * Set the page's tilte.
35
+ *
36
+ * Generates:
37
+ * ```jsx
38
+ * <head>
39
+ * <title>{title}</title>
40
+ * <meta property="og:title" content={title} />
41
+ * </head>
27
42
  * ```
43
+ *
44
+ * https://vike.dev/title
28
45
  */
29
- title?: PlainOrGetter<string>;
46
+ title?: string | ((pageContext: PageContext_) => string);
30
47
  /**
31
- * ```js
32
- * <link rel="icon" href="${favicon}" />
48
+ * Set the page's description.
49
+ *
50
+ * Generates:
51
+ * ```jsx
52
+ * <head>
53
+ * <meta name="description" content={description}>
54
+ * <meta property="og:description" content={description}>
55
+ * </head>
33
56
  * ```
57
+ *
58
+ * https://vike.dev/description
34
59
  */
35
- favicon?: PlainOrGetter<string>;
60
+ description?: string | ((pageContext: PageContextServer) => string);
36
61
  /**
37
- * ```js
38
- * <html lang="${lang}">
62
+ * Set the page's preview image upon URL sharing.
63
+ *
64
+ * Generates:
65
+ * ```jsx
66
+ * <head>
67
+ * <meta property="og:image" content={image}>
68
+ * <meta name="twitter:card" content="summary_large_image">
69
+ * </head>
39
70
  * ```
71
+ *
72
+ * https://vike.dev/image
73
+ */
74
+ image?: string | ((pageContext: PageContextServer) => string);
75
+ /**
76
+ * Set the page's width shown to the user on mobile/tablet devices.
77
+ *
78
+ * @default "responsive"
79
+ *
80
+ * https://vike.dev/viewport
81
+ */
82
+ viewport?: Viewport;
83
+ /**
84
+ * Set the page's favicon.
85
+ *
86
+ * Generates:
87
+ * ```jsx
88
+ * <head>
89
+ * <link rel="icon" href={favicon} />
90
+ * </head>
91
+ * ```
92
+ *
93
+ * https://vike.dev/favicon
94
+ */
95
+ favicon?: string | ((pageContext: PageContextServer) => string);
96
+ /**
97
+ * Set the page's language (`<html lang>`).
98
+ *
40
99
  * @default 'en'
100
+ *
101
+ * https://vike.dev/lang
102
+ */
103
+ lang?: string | ((pageContext: PageContext_) => string);
104
+ /**
105
+ * Add tag attributes such as `<html class="dark">`.
106
+ *
107
+ * https://vike.dev/htmlAttributes
108
+ */
109
+ htmlAttributes?: TagAttributes;
110
+ /**
111
+ * Add tag attributes such as `<body class="dark">`.
112
+ *
113
+ * https://vike.dev/bodyAttributes
41
114
  */
42
- lang?: PlainOrGetter<string>;
115
+ bodyAttributes?: TagAttributes;
43
116
  /**
44
117
  * If `true`, the page is rendered twice: on the server-side (to HTML) and on the client-side (hydration).
45
118
  *
46
119
  * If `false`, the page is rendered only once in the browser.
47
120
  *
48
- * https://vike.dev/ssr
49
- *
50
121
  * @default true
51
122
  *
123
+ * https://vike.dev/ssr
52
124
  */
53
125
  ssr?: boolean;
54
126
  /**
@@ -63,7 +135,6 @@ declare global {
63
135
  * @default false
64
136
  *
65
137
  * https://vike.dev/stream
66
- *
67
138
  */
68
139
  stream?: boolean | 'node' | 'web';
69
140
  /**
@@ -75,9 +146,9 @@ declare global {
75
146
  /**
76
147
  * Whether to use `<StrictMode>`.
77
148
  *
78
- * https://vike.dev/reactStrictMode
79
- *
80
149
  * @default true
150
+ *
151
+ * https://vike.dev/reactStrictMode
81
152
  */
82
153
  reactStrictMode?: boolean;
83
154
  /**
@@ -92,15 +163,25 @@ declare global {
92
163
  * https://vike.dev/onAfterRenderClient
93
164
  */
94
165
  onAfterRenderClient?: (pageContext: PageContextClient) => void;
166
+ /**
167
+ * Define loading animations.
168
+ *
169
+ * https://vike.dev/Loading
170
+ */
95
171
  Loading?: Loading | ImportString;
96
172
  }
97
173
  interface ConfigResolved {
98
174
  Wrapper?: Wrapper[];
99
175
  Layout?: Layout[];
176
+ Head?: Head[];
177
+ bodyAttributes?: TagAttributes[];
178
+ htmlAttributes?: TagAttributes[];
179
+ onBeforeRenderClient?: Function[];
180
+ onAfterRenderClient?: Function[];
100
181
  }
101
182
  }
102
183
  }
103
- type PlainOrGetter<T> = T | ((pageContext: PageContext) => T);
184
+ export type Head = React.ReactNode | (() => React.ReactNode);
104
185
  type Wrapper = (props: {
105
186
  children: React.ReactNode;
106
187
  }) => React.ReactNode;
@@ -109,4 +190,14 @@ type Loading = {
109
190
  component?: () => React.ReactNode;
110
191
  layout?: () => React.ReactNode;
111
192
  };
193
+ type PickWithoutGetter<T, K extends keyof T> = {
194
+ [P in K]: Exclude<T[P], Function>;
195
+ };
196
+ export type ConfigFromHook = PickWithoutGetter<Vike.Config, 'Head' | 'title' | 'description' | 'image'>;
197
+ export type ConfigFromHookResolved = {
198
+ Head?: Head[];
199
+ title?: string;
200
+ description?: string;
201
+ image?: string;
202
+ };
112
203
  export {};
@@ -1,6 +1,7 @@
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';
4
5
  declare global {
5
6
  namespace Vike {
6
7
  interface PageContext {
@@ -13,3 +14,7 @@ declare global {
13
14
  }
14
15
  }
15
16
  }
17
+ export type PageContextInternal = {
18
+ _configFromHook?: ConfigFromHookResolved;
19
+ _headAlreadySet?: true;
20
+ };
@@ -0,0 +1 @@
1
+ export declare function callCumulativeHooks(values: undefined | unknown[], pageContext: unknown): Promise<unknown[]>;
@@ -0,0 +1,16 @@
1
+ export async function callCumulativeHooks(values, pageContext) {
2
+ if (!values)
3
+ return [];
4
+ const valuesPromises = values.map((val) => {
5
+ if (typeof val === 'function') {
6
+ // Hook
7
+ return val(pageContext);
8
+ }
9
+ else {
10
+ // Plain value
11
+ return val;
12
+ }
13
+ });
14
+ const valuesResolved = await Promise.all(valuesPromises);
15
+ return valuesResolved;
16
+ }
@@ -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",
3
+ "version": "0.5.1",
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
  ],