diff --git a/.github/workflows/readme-link-check.yml b/.github/workflows/readme-link-check.yml index 919fd97441..5722049bf9 100644 --- a/.github/workflows/readme-link-check.yml +++ b/.github/workflows/readme-link-check.yml @@ -20,4 +20,4 @@ jobs: - name: Check links in README.md with awesome_bot run: | gem install awesome_bot - awesome_bot --request-delay 1 --allow-dupe --white-list http://localhost:8080/Plone,http://localhost:3000,https://github.com/kitconcept/volto-blocks-grid.git --files README.md,packages/client/README.md,packages/components/README.md,packages/generator-volto/README.md,packages/registry/README.md,packages/scripts/README.md,packages/types/README.md,packages/volto-slate/README.md + awesome_bot --request-delay 1 --allow-dupe --white-list http://localhost:8080/Plone,http://localhost:3000,https://github.com/kitconcept/volto-blocks-grid.git,https://my-server-DNS-name.tld/api --files README.md,packages/client/README.md,packages/components/README.md,packages/generator-volto/README.md,packages/registry/README.md,packages/scripts/README.md,packages/types/README.md,packages/volto-slate/README.md,apps/nextjs/README.md,apps/vite-ssr/README.md diff --git a/Makefile b/Makefile index 3ee758c4d0..e4e8146a09 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,8 @@ include variables.mk # You can set these variables from the command line. SPHINXOPTS ?= # Internal variables. -SPHINXBUILD = $(realpath bin/sphinx-build) -SPHINXAUTOBUILD = $(realpath bin/sphinx-autobuild) +SPHINXBUILD = "$(realpath bin/sphinx-build)" +SPHINXAUTOBUILD = "$(realpath bin/sphinx-autobuild)" DOCS_DIR = ./docs/source/ BUILDDIR = ../_build/ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . @@ -26,10 +26,10 @@ VALEFILES := $(shell find $(DOCS_DIR) -type f -name "*.md" -print) # Recipe snippets for reuse -CHECKOUT_BASENAME=$(shell basename $(shell realpath ./)) +CHECKOUT_BASENAME="$(shell basename $(shell realpath ./))" CHECKOUT_BRANCH=$(shell git branch --show-current) CHECKOUT_TMP=../$(CHECKOUT_BASENAME).tmp -CHECKOUT_TMP_ABS=$(shell realpath $(CHECKOUT_TMP)) +CHECKOUT_TMP_ABS="$(shell realpath $(CHECKOUT_TMP))" # We like colors # From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects @@ -189,6 +189,10 @@ copyreleasenotestodocs: start-backend-docker: docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' $(DOCKER_IMAGE) +.PHONY: start-backend-docker-no-cors +start-backend-docker-nocors: + docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone -e ADDONS='$(KGS)' -e CORS_=true $(DOCKER_IMAGE) + .PHONY: start-frontend-docker start-frontend-docker: docker run -it --rm --name=volto --link backend -p 3000:3000 -e RAZZLE_INTERNAL_API_PATH=http://backend:8080/Plone -e RAZZLE_DEV_PROXY_API_PATH=http://backend:8080/Plone plone/plone-frontend:latest @@ -212,7 +216,7 @@ start-test-acceptance-frontend-dev: build-deps ## Start the Core Acceptance Fron .PHONY: start-test-acceptance-server test-acceptance-server start-test-acceptance-server test-acceptance-server: ## Start Test Acceptance Server Main Fixture (docker container) - docker run -i --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + docker run -it --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) .PHONY: start-test-acceptance-frontend start-test-acceptance-frontend: build-deps ## Start the Core Acceptance Frontend Fixture @@ -258,7 +262,7 @@ start-test-acceptance-frontend-project: build-deps ## Start the Project Acceptan .PHONY: start-test-acceptance-server-coresandbox test-acceptance-server-coresandbox start-test-acceptance-server-coresandbox test-acceptance-server-coresandbox: ## Start CoreSandbox Test Acceptance Server Fixture (docker container) - docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage,plone.volto:coresandbox -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors,plone.volto.coresandbox $(DOCKER_IMAGE_ACCEPTANCE) + docker run -it --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage,plone.volto:coresandbox -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors,plone.volto.coresandbox $(DOCKER_IMAGE_ACCEPTANCE) # ZSERVER_PORT=55001 CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors,plone.volto.coresandbox APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage,plone.volto:coresandbox ./api/bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING .PHONY: start-test-acceptance-frontend-coresandbox @@ -285,7 +289,7 @@ full-test-acceptance-coresandbox: ## Runs CoreSandbox Full Acceptance Testing in .PHONY: start-test-acceptance-server-multilingual test-acceptance-server-multilingual start-test-acceptance-server-multilingual test-acceptance-server-multilingual: ## Start Multilingual Acceptance Server Multilingual Fixture (docker container) - docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) + docker run -it --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) .PHONY: start-test-acceptance-frontend-multilingual start-test-acceptance-frontend-multilingual: build-deps ## Start the Multilingual Acceptance Frontend Fixture @@ -307,7 +311,7 @@ full-test-acceptance-multilingual: ## Runs Multilingual Full Acceptance Testing .PHONY: start-test-acceptance-server-seamless-multilingual test-acceptance-server-seamless-multilingual start-test-acceptance-server-seamless-multilingual test-acceptance-server-seamless-multilingual: ## Start Seamless Multilingual Acceptance Server Multilingual Fixture (docker container) - docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) + docker run -it --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:multilingual $(DOCKER_IMAGE_ACCEPTANCE) .PHONY: start-test-acceptance-frontend-seamless-multilingual start-test-acceptance-frontend-seamless-multilingual: build-deps ## Start the Seamless Multilingual Acceptance Frontend Fixture @@ -329,7 +333,7 @@ full-test-acceptance-seamless-multilingual: ## Runs Seamless Multilingual Full A .PHONY: start-test-acceptance-server-workingcopy test-acceptance-server-workingcopy start-test-acceptance-server-workingcopy test-acceptance-server-workingcopy : ## Start the WorkingCopy Acceptance Server Fixture (docker container) - docker run -i --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.app.iterate:default,plone.volto:default-homepage $(DOCKER_IMAGE_ACCEPTANCE) + docker run -it --rm -p 55001:55001 -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.app.iterate:default,plone.volto:default-homepage $(DOCKER_IMAGE_ACCEPTANCE) # ZSERVER_PORT=55001 CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.app.iterate,plone.volto,plone.volto.cors APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.app.iterate:default,plone.volto:default-homepage ./api/bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING .PHONY: start-test-acceptance-frontend-workingcopy @@ -374,7 +378,7 @@ full-test-acceptance-guillotina: ## Runs the Guillotina Full Acceptance Testing .PHONY: start-test-acceptance-server-5 start-test-acceptance-server-5: ## Start Test Acceptance Server Main Fixture Plone 5 (docker container) - docker run -i --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e ADDONS='$(KGS5) $(TESTING_ADDONS)' -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors $(DOCKER_IMAGE5) ./bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING + docker run -it --rm -e ZSERVER_HOST=0.0.0.0 -e ZSERVER_PORT=55001 -p 55001:55001 -e ADDONS='$(KGS5) $(TESTING_ADDONS)' -e APPLY_PROFILES=plone.app.contenttypes:plone-content,plone.restapi:default,plone.volto:default-homepage -e CONFIGURE_PACKAGES=plone.app.contenttypes,plone.restapi,plone.volto,plone.volto.cors $(DOCKER_IMAGE5) ./bin/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING ######### @plone/client diff --git a/apps/nextjs/README.md b/apps/nextjs/README.md index ff8389a364..c881b283fc 100644 --- a/apps/nextjs/README.md +++ b/apps/nextjs/README.md @@ -21,7 +21,11 @@ make start-backend-docker We introduce an environment variable `API_SERVER_URL`. -We have to create this environment variable in the Vercel deployment's control panel, specifying the URL where your backend API server is deployed and the route where the API is located, such as `API_SERVER_URL=https://my_server_DNS_name/api`. +You need to create this environment variable in the Vercel deployment's control panel, specifying the URL where your backend API server is deployed, and the route where the API is located, as shown. + +```shell +API_SERVER_URL=https://my-server-DNS-name.tld/api +``` ### Application rewrite configuragtion @@ -92,8 +96,6 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - ## Learn More To learn more about Next.js, take a look at the following resources: @@ -107,4 +109,4 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. +Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. diff --git a/apps/vite-ssr/.gitignore b/apps/vite-ssr/.gitignore new file mode 100644 index 0000000000..d451ff16c1 --- /dev/null +++ b/apps/vite-ssr/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local diff --git a/apps/vite-ssr/.prettierignore b/apps/vite-ssr/.prettierignore new file mode 100644 index 0000000000..083bdb7c4c --- /dev/null +++ b/apps/vite-ssr/.prettierignore @@ -0,0 +1 @@ +src/routeTree.gen.ts diff --git a/apps/vite-ssr/README.md b/apps/vite-ssr/README.md new file mode 100644 index 0000000000..2894982c36 --- /dev/null +++ b/apps/vite-ssr/README.md @@ -0,0 +1,6 @@ +# Plone on Vite with SSR mode + +This is a proof of concept of a [Vite](https://vitejs.dev) build, using `@plone/client` and `@plone/components` libraries. +This is intended to serve as both a playground for the development of both packages and as a demo of Plone using Vite built with server side rendering (SSR). + +It also uses [TanStack Router](https://tanstack.com/router/latest/docs/framework/react/overview) for its routing library. diff --git a/apps/vite-ssr/index.html b/apps/vite-ssr/index.html new file mode 100644 index 0000000000..eecb058a61 --- /dev/null +++ b/apps/vite-ssr/index.html @@ -0,0 +1,12 @@ + + + + + + + + +
+ + + diff --git a/apps/vite-ssr/package.json b/apps/vite-ssr/package.json new file mode 100644 index 0000000000..314a5bebe6 --- /dev/null +++ b/apps/vite-ssr/package.json @@ -0,0 +1,45 @@ +{ + "name": "plone-vite-ssr", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "node server", + "build": "npm run build:client && npm run build:server", + "build:client": "vite build --outDir dist/client", + "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", + "serve": "NODE_ENV=production node server", + "debug": "node --inspect-brk server" + }, + "dependencies": { + "@plone/client": "workspace:*", + "@tanstack/react-query": "5.0.5", + "@tanstack/react-router": "^1.16.0", + "@tanstack/react-router-server": "^1.16.0", + "@tanstack/router-devtools": "^1.16.0", + "@tanstack/router-vite-plugin": "^1.16.1", + "axios": "^1.6.5", + "get-port": "^7.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sirv": "^2.0.4" + }, + "devDependencies": { + "@babel/core": "^7.23.7", + "@babel/generator": "^7.23.6", + "@rollup/plugin-babel": "^6.0.4", + "@tanstack/react-query-devtools": "^5.20.1", + "@types/express": "^4.17.21", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4", + "compression": "^1.7.4", + "express": "^4.18.2", + "isbot": "^4.3.0", + "node-fetch": "^3.3.2", + "serve-static": "^1.15.0", + "typescript": "^5.3.3", + "vite": "^5.0.12", + "vite-plugin-babel": "^1.2.0" + } +} diff --git a/apps/vite-ssr/server.js b/apps/vite-ssr/server.js new file mode 100644 index 0000000000..d073028c17 --- /dev/null +++ b/apps/vite-ssr/server.js @@ -0,0 +1,100 @@ +import fs from 'node:fs/promises'; +import express from 'express'; +import getPort, { portNumbers } from 'get-port'; + +const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD; + +export async function createServer( + root = process.cwd(), + isProd = process.env.NODE_ENV === 'production', + hmrPort, +) { + const app = express(); + + const prodIndexHtml = isProd + ? await fs.readFile('./dist/client/index.html', 'utf-8') + : ''; + + /** + * @type {import('vite').ViteDevServer} + */ + let vite; + if (!isProd) { + vite = await ( + await import('vite') + ).createServer({ + root, + logLevel: isTest ? 'error' : 'info', + server: { + middlewareMode: true, + watch: { + // During tests we edit the files too fast and sometimes chokidar + // misses change events, so enforce polling for consistency + usePolling: true, + interval: 100, + }, + hmr: { + port: hmrPort, + }, + }, + appType: 'custom', + }); + // use vite's connect instance as middleware + app.use(vite.middlewares); + } else { + const sirv = (await import('sirv')).default; + app.use((await import('compression')).default()); + app.use('/', sirv('./dist/client', { extensions: [] })); + } + + app.use('*', async (req, res) => { + try { + const url = req.originalUrl; + + if (url.includes('.')) { + console.warn(`${url} is not valid router path`); + res.status(404); + res.end(`${url} is not valid router path`); + return; + } + + // Extract the head from vite's index transformation hook + let viteHead = !isProd + ? await vite.transformIndexHtml( + url, + ``, + ) + : prodIndexHtml; + + viteHead = viteHead.substring( + viteHead.indexOf('') + 6, + viteHead.indexOf(''), + ); + + const entry = await (async () => { + if (!isProd) { + return vite.ssrLoadModule('/src/entry-server.tsx'); + } else { + return import('./dist/server/entry-server.js'); + } + })(); + + console.log('Rendering: ', url, '...'); + entry.render({ req, res, url, head: isProd ? viteHead : '' }); + } catch (e) { + !isProd && vite.ssrFixStacktrace(e); + console.log(e.stack); + res.status(500).end(e.stack); + } + }); + + return { app, vite }; +} + +if (!isTest) { + createServer().then(async ({ app }) => + app.listen(await getPort({ port: portNumbers(3000, 3100) }), () => { + console.log('Client Server: http://localhost:3000'); + }), + ); +} diff --git a/apps/vite-ssr/src/config.ts b/apps/vite-ssr/src/config.ts new file mode 100644 index 0000000000..c2f3c47921 --- /dev/null +++ b/apps/vite-ssr/src/config.ts @@ -0,0 +1,9 @@ +const settings = { + apiPath: 'http://localhost:8080/Plone', +}; + +const config = { + settings, +}; + +export default config; diff --git a/apps/vite-ssr/src/entry-client.tsx b/apps/vite-ssr/src/entry-client.tsx new file mode 100644 index 0000000000..ec79c6f65b --- /dev/null +++ b/apps/vite-ssr/src/entry-client.tsx @@ -0,0 +1,8 @@ +import ReactDOM from 'react-dom/client'; + +import { StartClient } from '@tanstack/react-router-server/client'; +import { createRouter } from './router'; + +const router = createRouter(); + +ReactDOM.hydrateRoot(document, ); diff --git a/apps/vite-ssr/src/entry-server.tsx b/apps/vite-ssr/src/entry-server.tsx new file mode 100644 index 0000000000..15d6952b19 --- /dev/null +++ b/apps/vite-ssr/src/entry-server.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import ReactDOMServer from 'react-dom/server'; +import { createMemoryHistory } from '@tanstack/react-router'; +import { ServerResponse } from 'http'; +import express from 'express'; +import { StartServer } from '@tanstack/react-router-server/server'; +import { createRouter } from './router'; + +// index.js +import './fetch-polyfill'; + +export async function render(opts: { + url: string; + head: string; + req: express.Request; + res: ServerResponse; +}) { + const router = createRouter(); + + const memoryHistory = createMemoryHistory({ + initialEntries: [opts.url], + }); + + // Update the history and context + router.update({ + history: memoryHistory, + context: { + ...router.options.context, + head: opts.head, + }, + }); + + // Since we're using renderToString, Wait for the router to finish loading + await router.load(); + + // Render the app + const appHtml = ReactDOMServer.renderToString( + , + ); + + opts.res.statusCode = 200; + opts.res.setHeader('Content-Type', 'text/html'); + opts.res.end(`${appHtml}`); +} diff --git a/apps/vite-ssr/src/fetch-polyfill.js b/apps/vite-ssr/src/fetch-polyfill.js new file mode 100644 index 0000000000..555ffcb254 --- /dev/null +++ b/apps/vite-ssr/src/fetch-polyfill.js @@ -0,0 +1,20 @@ +// fetch-polyfill.js +import fetch, { + Blob, + blobFrom, + blobFromSync, + File, + fileFrom, + fileFromSync, + FormData, + Headers, + Request, + Response, +} from 'node-fetch'; + +if (!globalThis.fetch) { + globalThis.fetch = fetch; + globalThis.Headers = Headers; + globalThis.Request = Request; + globalThis.Response = Response; +} diff --git a/apps/vite-ssr/src/routeTree.gen.ts b/apps/vite-ssr/src/routeTree.gen.ts new file mode 100644 index 0000000000..d521ff1cd8 --- /dev/null +++ b/apps/vite-ssr/src/routeTree.gen.ts @@ -0,0 +1,93 @@ +/* prettier-ignore-start */ + +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file is auto-generated by TanStack Router + +// Import Routes + +import { Route as rootRoute } from './routes/__root'; +import { Route as PostsImport } from './routes/posts'; +import { Route as ErrorImport } from './routes/error'; +import { Route as IndexImport } from './routes/index'; +import { Route as PostsIndexImport } from './routes/posts/index'; +import { Route as PostsPostIdImport } from './routes/posts/$postId'; +import { Route as SplatEditImport } from './routes/$.edit'; + +// Create/Update Routes + +const PostsRoute = PostsImport.update({ + path: '/posts', + getParentRoute: () => rootRoute, +} as any); + +const ErrorRoute = ErrorImport.update({ + path: '/error', + getParentRoute: () => rootRoute, +} as any); + +const IndexRoute = IndexImport.update({ + path: '/', + getParentRoute: () => rootRoute, +} as any); + +const PostsIndexRoute = PostsIndexImport.update({ + path: '/', + getParentRoute: () => PostsRoute, +} as any); + +const PostsPostIdRoute = PostsPostIdImport.update({ + path: '/$postId', + getParentRoute: () => PostsRoute, +} as any); + +const SplatEditRoute = SplatEditImport.update({ + path: '/$/edit', + getParentRoute: () => rootRoute, +} as any); + +// Populate the FileRoutesByPath interface + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/': { + preLoaderRoute: typeof IndexImport; + parentRoute: typeof rootRoute; + }; + '/error': { + preLoaderRoute: typeof ErrorImport; + parentRoute: typeof rootRoute; + }; + '/posts': { + preLoaderRoute: typeof PostsImport; + parentRoute: typeof rootRoute; + }; + '/$/edit': { + preLoaderRoute: typeof SplatEditImport; + parentRoute: typeof rootRoute; + }; + '/posts/$postId': { + preLoaderRoute: typeof PostsPostIdImport; + parentRoute: typeof PostsImport; + }; + '/posts/': { + preLoaderRoute: typeof PostsIndexImport; + parentRoute: typeof PostsImport; + }; + } +} + +// Create and export the route tree + +export const routeTree = rootRoute.addChildren([ + IndexRoute, + ErrorRoute, + PostsRoute.addChildren([PostsPostIdRoute, PostsIndexRoute]), + SplatEditRoute, +]); + +/* prettier-ignore-end */ diff --git a/apps/vite-ssr/src/router.tsx b/apps/vite-ssr/src/router.tsx new file mode 100644 index 0000000000..d249e7521e --- /dev/null +++ b/apps/vite-ssr/src/router.tsx @@ -0,0 +1,60 @@ +import { + QueryClient, + QueryClientProvider, + dehydrate, + hydrate, +} from '@tanstack/react-query'; +import PloneClient from '@plone/client'; +import { createRouter as createReactRouter } from '@tanstack/react-router'; + +import { routeTree } from './routeTree.gen'; +import { PloneClientProvider } from '@plone/client/provider'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // With SSR, we usually want to set some default staleTime + // above 0 to avoid refetching immediately on the client + staleTime: 60 * 1000, + }, + }, +}); + +const ploneClient = PloneClient.initialize({ + apiPath: 'http://localhost:8080/Plone', +}); + +export function createRouter() { + return createReactRouter({ + routeTree, + context: { + head: '', + queryClient, + ploneClient, + }, + defaultPreload: 'intent', + dehydrate: () => { + return { + queryClientState: dehydrate(queryClient), + }; + }, + hydrate: (dehydrated) => { + hydrate(queryClient, dehydrated.queryClientState); + }, + Wrap: ({ children }: { children: any }) => { + return ( + + + {children} + + + ); + }, + }); +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType; + } +} diff --git a/apps/vite-ssr/src/routerContext.tsx b/apps/vite-ssr/src/routerContext.tsx new file mode 100644 index 0000000000..de5df3de21 --- /dev/null +++ b/apps/vite-ssr/src/routerContext.tsx @@ -0,0 +1,8 @@ +import { QueryClient } from '@tanstack/react-query'; +import PloneClient from '@plone/client'; + +export type RouterContext = { + head: string; + queryClient: QueryClient; + ploneClient: PloneClient; +}; diff --git a/apps/vite-ssr/src/routes/$.edit.tsx b/apps/vite-ssr/src/routes/$.edit.tsx new file mode 100644 index 0000000000..db957eb8d4 --- /dev/null +++ b/apps/vite-ssr/src/routes/$.edit.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { createFileRoute, Link, Outlet } from '@tanstack/react-router'; + +export type PostType = { + id: string; + title: string; + body: string; +}; + +export const Route = createFileRoute('/$/edit')({ + loader: async (params) => { + console.log(params); + console.log('Fetching posts...'); + await new Promise((r) => + setTimeout(r, 300 + Math.round(Math.random() * 300)), + ); + return fetch('https://jsonplaceholder.typicode.com/posts') + .then((d) => d.json() as Promise) + .then((d) => d.slice(0, 10)); + }, + component: PostsComponent, +}); + +function PostsComponent() { + const posts = Route.useLoaderData(); + + return ( +
+
    + {posts?.map((post) => { + return ( +
  • + +
    {post.title.substring(0, 20)}
    + +
  • + ); + })} +
+
+ +
+ ); +} diff --git a/apps/vite-ssr/src/routes/__root.tsx b/apps/vite-ssr/src/routes/__root.tsx new file mode 100644 index 0000000000..b2b411e763 --- /dev/null +++ b/apps/vite-ssr/src/routes/__root.tsx @@ -0,0 +1,90 @@ +import { TanStackRouterDevtools } from '@tanstack/router-devtools'; +import * as React from 'react'; +import { + Link, + Outlet, + createRootRouteWithContext, + useRouter, +} from '@tanstack/react-router'; +import { DehydrateRouter } from '@tanstack/react-router-server/client'; +import { RouterContext } from '../routerContext'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +export const Route = createRootRouteWithContext()({ + component: RootComponent, +}); + +function RootComponent() { + const router = useRouter(); + + return ( + + {router.options.context.head ? ( + + ) : ( + + + + Vite App +