diff --git a/lerna.json b/lerna.json index b3c185f583966..1a677469b921f 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "12.0.11-canary.7" + "version": "12.0.11-canary.8" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index a2c48ce8f7bbc..e0983013dfe25 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index a65f1e7fd2d07..be96e18ef2f66 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "ESLint configuration used by NextJS.", "main": "index.js", "license": "MIT", @@ -9,7 +9,7 @@ "directory": "packages/eslint-config-next" }, "dependencies": { - "@next/eslint-plugin-next": "12.0.11-canary.7", + "@next/eslint-plugin-next": "12.0.11-canary.8", "@rushstack/eslint-patch": "^1.0.8", "@typescript-eslint/parser": "^5.0.0", "eslint-import-resolver-node": "^0.3.4", diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 4e494fb09f5ef..763188e88942f 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "ESLint plugin for NextJS.", "main": "lib/index.js", "license": "MIT", diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index 616c2514501cd..941319a06ab75 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index 107c0a3b9a805..fabd3a44ff21d 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "license": "MIT", "dependencies": { "chalk": "4.1.0", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 23712ae4028ea..131f36a4d1d1a 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 2ea4c057ccaf1..6baa4ae1fc413 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index bcae4d0f04f32..78a3df568ab77 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index d63c29f85159f..f1aee44109349 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 791b6d8901d96..9ab511183f760 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index fdcc3cfc78302..8a83bd9d23ef1 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "private": true, "scripts": { "build-native": "napi build --platform --cargo-name next_swc_napi native", diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index b3b4fe2d62dc1..d72e3b461b199 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -554,7 +554,14 @@ export default async function build( .traceChild('generate-required-server-files') .traceFn(() => ({ version: 1, - config: { ...config, configFile: undefined }, + config: { + ...config, + configFile: undefined, + experimental: { + ...config.experimental, + trustHostHeader: ciEnvironment.hasNextSupport, + }, + }, appDir: dir, files: [ ROUTES_MANIFEST, diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 4bd285307154d..bb88e23be4a52 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -434,7 +434,7 @@ export async function initNext( render(renderCtx) } -export async function render(renderingProps: RenderRouteInfo): Promise { +async function render(renderingProps: RenderRouteInfo): Promise { if (renderingProps.err) { await renderError(renderingProps) return @@ -462,7 +462,7 @@ export async function render(renderingProps: RenderRouteInfo): Promise { // This method handles all runtime and debug errors. // 404 and 500 errors are special kind of errors // and they are still handle via the main render method. -export function renderError(renderErrorProps: RenderErrorProps): Promise { +function renderError(renderErrorProps: RenderErrorProps): Promise { const { App, err } = renderErrorProps // In development runtime errors are caught by our overlay diff --git a/packages/next/client/next-dev.js b/packages/next/client/next-dev.js index 8f1dad083d2f1..65f2c08c6cc77 100644 --- a/packages/next/client/next-dev.js +++ b/packages/next/client/next-dev.js @@ -1,4 +1,4 @@ -import { initNext, version, router, emitter, render, renderError } from './' +import { initNext, version, router, emitter } from './' import initOnDemandEntries from './dev/on-demand-entries-client' import initWebpackHMR from './dev/webpack-hot-middleware-client' import initializeBuildWatcher from './dev/dev-build-watcher' @@ -44,8 +44,6 @@ window.next = { return router }, emitter, - render, - renderError, } initNext({ webpackHMR, beforeRender: displayContent }) .then(() => { diff --git a/packages/next/client/next.js b/packages/next/client/next.js index 8a0f8bad9e4a9..e17fdc100dd8f 100644 --- a/packages/next/client/next.js +++ b/packages/next/client/next.js @@ -1,4 +1,4 @@ -import { initNext, version, router, emitter, render, renderError } from './' +import { initNext, version, router, emitter } from './' window.next = { version, @@ -7,8 +7,6 @@ window.next = { return router }, emitter, - render, - renderError, } initNext().catch(console.error) diff --git a/packages/next/package.json b/packages/next/package.json index 5fa1fb4021bf0..716d182a10779 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -69,7 +69,7 @@ ] }, "dependencies": { - "@next/env": "12.0.11-canary.7", + "@next/env": "12.0.11-canary.8", "caniuse-lite": "^1.0.30001283", "postcss": "8.4.5", "styled-jsx": "5.0.0", @@ -117,11 +117,11 @@ "@hapi/accept": "5.0.2", "@napi-rs/cli": "1.2.1", "@napi-rs/triples": "1.0.3", - "@next/polyfill-module": "12.0.11-canary.7", - "@next/polyfill-nomodule": "12.0.11-canary.7", - "@next/react-dev-overlay": "12.0.11-canary.7", - "@next/react-refresh-utils": "12.0.11-canary.7", - "@next/swc": "12.0.11-canary.7", + "@next/polyfill-module": "12.0.11-canary.8", + "@next/polyfill-nomodule": "12.0.11-canary.8", + "@next/react-dev-overlay": "12.0.11-canary.8", + "@next/react-refresh-utils": "12.0.11-canary.8", + "@next/swc": "12.0.11-canary.8", "@peculiar/webcrypto": "1.1.7", "@taskr/clear": "1.1.0", "@taskr/esnext": "1.1.0", diff --git a/packages/next/server/api-utils.ts b/packages/next/server/api-utils.ts index a8bc956f4f6bf..d5244556a4319 100644 --- a/packages/next/server/api-utils.ts +++ b/packages/next/server/api-utils.ts @@ -26,7 +26,11 @@ export async function apiResolver( res: ServerResponse, query: any, resolverModule: any, - apiContext: __ApiPreviewProps, + apiContext: __ApiPreviewProps & { + trustHostHeader?: boolean + hostname?: string + port?: number + }, propagateError: boolean, dev?: boolean, page?: string @@ -95,6 +99,8 @@ export async function apiResolver( apiRes.setPreviewData = (data, options = {}) => setPreviewData(apiRes, data, Object.assign({}, apiContext, options)) apiRes.clearPreviewData = () => clearPreviewData(apiRes) + apiRes.unstable_revalidate = (urlPath: string) => + unstable_revalidate(urlPath, req, apiContext) const resolver = interopDefault(resolverModule) let wasPiped = false @@ -334,6 +340,42 @@ export function sendJson(res: NextApiResponse, jsonBody: any): void { res.send(jsonBody) } +const PRERENDER_REVALIDATE_HEADER = 'x-prerender-revalidate' + +export function checkIsManualRevalidate( + req: IncomingMessage | BaseNextRequest, + previewProps: __ApiPreviewProps +): boolean { + return req.headers[PRERENDER_REVALIDATE_HEADER] === previewProps.previewModeId +} + +async function unstable_revalidate( + urlPath: string, + req: IncomingMessage | BaseNextRequest, + context: { + hostname?: string + port?: number + previewModeId: string + trustHostHeader?: boolean + } +) { + if (!context.trustHostHeader && (!context.hostname || !context.port)) { + throw new Error( + `"hostname" and "port" must be provided when starting next to use "unstable_revalidate". See more here https://nextjs.org/docs/advanced-features/custom-server` + ) + } + + const baseUrl = context.trustHostHeader + ? `https://${req.headers.host}` + : `http://${context.hostname}:${context.port}` + + return fetch(`${baseUrl}${urlPath}`, { + headers: { + [PRERENDER_REVALIDATE_HEADER]: context.previewModeId, + }, + }) +} + const COOKIE_NAME_PRERENDER_BYPASS = `__prerender_bypass` const COOKIE_NAME_PRERENDER_DATA = `__next_preview_data` diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 7b492fb4d3d74..2541d3faec021 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -58,6 +58,7 @@ import { MIDDLEWARE_ROUTE } from '../lib/constants' import { addRequestMeta, getRequestMeta } from './request-meta' import { createHeaderRoute, createRedirectRoute } from './server-route-utils' import { PrerenderManifest } from '../build' +import { checkIsManualRevalidate } from '../server/api-utils' export type FindComponentsResult = { components: LoadComponentsReturnType @@ -1173,6 +1174,15 @@ export default abstract class Server { isPreviewMode = previewData !== false } + let isManualRevalidate = false + + if (isSSG) { + isManualRevalidate = checkIsManualRevalidate( + req, + this.renderOpts.previewProps + ) + } + // Compute the iSSG cache key. We use the rewroteUrl since // pages with fallback: false are allowed to be rewritten to // and we need to look up the path by the rewritten path @@ -1238,7 +1248,7 @@ export default abstract class Server { let ssgCacheKey = isPreviewMode || !isSSG || this.minimalMode || opts.supportsDynamicHTML - ? null // Preview mode bypasses the cache + ? null // Preview mode and manual revalidate bypasses the cache : `${locale ? `/${locale}` : ''}${ (pathname === '/' || resolvedUrlPathname === '/') && locale ? '' @@ -1364,7 +1374,7 @@ export default abstract class Server { const cacheEntry = await this.responseCache.get( ssgCacheKey, - async (hasResolved) => { + async (hasResolved, hadCache) => { const isProduction = !this.renderOpts.dev const isDynamicPathname = isDynamicRoute(pathname) const didRespond = hasResolved || res.sent @@ -1380,6 +1390,12 @@ export default abstract class Server { fallbackMode = 'blocking' } + // only allow manual revalidate for fallback: true/blocking + // or for prerendered fallback: false paths + if (isManualRevalidate && (fallbackMode !== false || hadCache)) { + fallbackMode = 'blocking' + } + // When we did not respond from cache, we need to choose to block on // rendering or return a skeleton. // @@ -1464,6 +1480,9 @@ export default abstract class Server { ? result.revalidate : /* default to minimum revalidate (this should be an invariant) */ 1, } + }, + { + isManualRevalidate, } ) diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index a914c36cbdf29..b9759dc867365 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -452,7 +452,13 @@ export default class NextNodeServer extends BaseServer { res.originalResponse, query, pageModule, - this.renderOpts.previewProps, + { + ...this.renderOpts.previewProps, + port: this.port, + hostname: this.hostname, + // internal config so is not typed + trustHostHeader: (this.nextConfig.experimental as any).trustHostHeader, + }, this.minimalMode, this.renderOpts.dev, page diff --git a/packages/next/server/response-cache.ts b/packages/next/server/response-cache.ts index 2279733e721e4..5cdc23fae7b36 100644 --- a/packages/next/server/response-cache.ts +++ b/packages/next/server/response-cache.ts @@ -20,7 +20,8 @@ export type ResponseCacheEntry = { } type ResponseGenerator = ( - hasResolved: boolean + hasResolved: boolean, + hadCache: boolean ) => Promise export default class ResponseCache { @@ -34,7 +35,8 @@ export default class ResponseCache { public get( key: string | null, - responseGenerator: ResponseGenerator + responseGenerator: ResponseGenerator, + context: { isManualRevalidate?: boolean } ): Promise { const pendingResponse = key ? this.pendingResponses.get(key) : null if (pendingResponse) { @@ -71,7 +73,11 @@ export default class ResponseCache { ;(async () => { try { const cachedResponse = key ? await this.incrementalCache.get(key) : null - if (cachedResponse) { + if ( + cachedResponse && + (!context.isManualRevalidate || + cachedResponse.revalidateAfter === false) + ) { resolve({ revalidate: cachedResponse.curRevalidate, value: @@ -90,7 +96,7 @@ export default class ResponseCache { } } - const cacheEntry = await responseGenerator(resolved) + const cacheEntry = await responseGenerator(resolved, !!cachedResponse) resolve(cacheEntry) if (key && cacheEntry && typeof cacheEntry.revalidate !== 'undefined') { diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 6c24e27fcff61..8bf0230b4841e 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -293,6 +293,8 @@ export type NextApiResponse = ServerResponse & { } ) => NextApiResponse clearPreviewData: () => NextApiResponse + + unstable_revalidate: (urlPath: string) => Promise } /** diff --git a/packages/react-dev-overlay/package.json b/packages/react-dev-overlay/package.json index ecd62875a4f90..40e829934bbbc 100644 --- a/packages/react-dev-overlay/package.json +++ b/packages/react-dev-overlay/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-dev-overlay", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "A development-only overlay for developing React applications.", "repository": { "url": "vercel/next.js", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 5ac6d2e08320a..1322bf67bc246 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "12.0.11-canary.7", + "version": "12.0.11-canary.8", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/test/e2e/prerender.test.ts b/test/e2e/prerender.test.ts index 9e82de16ddf2e..db468d359955e 100644 --- a/test/e2e/prerender.test.ts +++ b/test/e2e/prerender.test.ts @@ -1890,6 +1890,147 @@ describe('Prerender', () => { } }) } + + if (!(global as any).isNextDev) { + it('should handle manual revalidate for fallback: blocking', async () => { + const html = await renderViaHTTP( + next.url, + '/blocking-fallback/test-manual-1' + ) + const $ = cheerio.load(html) + const initialTime = $('#time').text() + + expect($('p').text()).toMatch(/Post:.*?test-manual-1/) + + const html2 = await renderViaHTTP( + next.url, + '/blocking-fallback/test-manual-1' + ) + const $2 = cheerio.load(html2) + + expect(initialTime).toBe($2('#time').text()) + + const res = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/blocking-fallback/test-manual-1', + }, + { redirect: 'manual' } + ) + + expect(res.status).toBe(200) + const revalidateData = await res.json() + const revalidatedText = revalidateData.text + const $3 = cheerio.load(revalidatedText) + expect(revalidateData.status).toBe(200) + expect($3('#time').text()).not.toBe(initialTime) + + const html4 = await renderViaHTTP( + next.url, + '/blocking-fallback/test-manual-1' + ) + const $4 = cheerio.load(html4) + expect($4('#time').text()).not.toBe(initialTime) + expect($3('#time').text()).toBe($4('#time').text()) + }) + + it('should not manual revalidate for revalidate: false', async () => { + const html = await renderViaHTTP( + next.url, + '/blocking-fallback-once/test-manual-1' + ) + const $ = cheerio.load(html) + const initialTime = $('#time').text() + + expect($('p').text()).toMatch(/Post:.*?test-manual-1/) + + const html2 = await renderViaHTTP( + next.url, + '/blocking-fallback-once/test-manual-1' + ) + const $2 = cheerio.load(html2) + + expect(initialTime).toBe($2('#time').text()) + + const res = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/blocking-fallback-once/test-manual-1', + }, + { redirect: 'manual' } + ) + + expect(res.status).toBe(200) + const revalidateData = await res.json() + const revalidatedText = revalidateData.text + const $3 = cheerio.load(revalidatedText) + expect(revalidateData.status).toBe(200) + expect($3('#time').text()).toBe(initialTime) + + const html4 = await renderViaHTTP( + next.url, + '/blocking-fallback-once/test-manual-1' + ) + const $4 = cheerio.load(html4) + expect($4('#time').text()).toBe(initialTime) + expect($3('#time').text()).toBe($4('#time').text()) + }) + + it('should handle manual revalidate for fallback: false', async () => { + const res = await fetchViaHTTP( + next.url, + '/catchall-explicit/test-manual-1' + ) + expect(res.status).toBe(404) + + // fallback: false pages should only manually revalidate + // prerendered paths + const res2 = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/catchall-explicity/test-manual-1', + }, + { redirect: 'manual' } + ) + + expect(res2.status).toBe(200) + const revalidateData = await res2.json() + expect(revalidateData.status).toBe(404) + + const res3 = await fetchViaHTTP( + next.url, + '/catchall-explicit/test-manual-1' + ) + expect(res3.status).toBe(404) + + const res4 = await fetchViaHTTP(next.url, '/catchall-explicit/first') + expect(res4.status).toBe(200) + const html = await res4.text() + const $ = cheerio.load(html) + const initialTime = $('#time').text() + + const res5 = await fetchViaHTTP( + next.url, + '/api/manual-revalidate', + { + pathname: '/catchall-explicit/first', + }, + { redirect: 'manual' } + ) + expect(res5.status).toBe(200) + expect((await res5.json()).status).toBe(200) + + const res6 = await fetchViaHTTP(next.url, '/catchall-explicit/first') + expect(res6.status).toBe(200) + const html2 = await res6.text() + const $2 = cheerio.load(html2) + + expect(initialTime).not.toBe($2('#time').text()) + }) + } } runTests((global as any).isNextDev) }) diff --git a/test/e2e/prerender/pages/api/manual-revalidate.js b/test/e2e/prerender/pages/api/manual-revalidate.js new file mode 100644 index 0000000000000..c1d9479c3292e --- /dev/null +++ b/test/e2e/prerender/pages/api/manual-revalidate.js @@ -0,0 +1,10 @@ +export default async function handler(req, res) { + // WARNING: don't use user input in production + // make sure to use trusted value for revalidating + const revalidateRes = await res.unstable_revalidate(req.query.pathname) + res.json({ + revalidated: true, + status: revalidateRes.status, + text: await revalidateRes.text(), + }) +} diff --git a/test/e2e/prerender/pages/blocking-fallback-once/[slug].js b/test/e2e/prerender/pages/blocking-fallback-once/[slug].js index 5b047ded890c0..4ca33a678ebee 100644 --- a/test/e2e/prerender/pages/blocking-fallback-once/[slug].js +++ b/test/e2e/prerender/pages/blocking-fallback-once/[slug].js @@ -32,7 +32,7 @@ export default ({ post, time, params }) => { return ( <>

Post: {post}

- time: {time} + time: {time}
{JSON.stringify(params)}
{JSON.stringify(useRouter().query)}
diff --git a/test/e2e/prerender/pages/blocking-fallback/[slug].js b/test/e2e/prerender/pages/blocking-fallback/[slug].js index 0b39f579352ca..aa6aeec99ac98 100644 --- a/test/e2e/prerender/pages/blocking-fallback/[slug].js +++ b/test/e2e/prerender/pages/blocking-fallback/[slug].js @@ -32,7 +32,7 @@ export default ({ post, time, params }) => { return ( <>

Post: {post}

- time: {time} + time: {time}
{JSON.stringify(params)}
{JSON.stringify(useRouter().query)}
diff --git a/test/e2e/prerender/pages/catchall-explicit/[...slug].js b/test/e2e/prerender/pages/catchall-explicit/[...slug].js index dcdd57e35e570..103b5650af58b 100644 --- a/test/e2e/prerender/pages/catchall-explicit/[...slug].js +++ b/test/e2e/prerender/pages/catchall-explicit/[...slug].js @@ -8,6 +8,7 @@ export async function getStaticProps({ params: { slug } }) { return { props: { slug, + time: Date.now(), }, revalidate: 1, } @@ -27,12 +28,13 @@ export async function getStaticPaths() { } } -export default function Page({ slug }) { +export default function Page({ slug, time }) { // Important to not check for `slug` existence (testing that build does not // render fallback version and error) return ( <> -

Hi {slug.join(' ')}

{' '} +

Hi {slug.join(' ')}

+

time: {time}

to home