From dd8f19d1a271a559373abb8476b380520d302471 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Wed, 18 Jun 2025 11:49:14 +0200 Subject: [PATCH 1/3] Add client side navigation library. --- sdk/src/runtime/clientNavigation.ts | 65 +++++++++++++++++++++++++++++ sdk/src/runtime/entries/client.ts | 1 + 2 files changed, 66 insertions(+) create mode 100644 sdk/src/runtime/clientNavigation.ts diff --git a/sdk/src/runtime/clientNavigation.ts b/sdk/src/runtime/clientNavigation.ts new file mode 100644 index 000000000..eb013dcca --- /dev/null +++ b/sdk/src/runtime/clientNavigation.ts @@ -0,0 +1,65 @@ +export function initClientNavigation( + opts: { + onNavigate: () => void; + } = { + onNavigate: async function onNavigate() { + // @ts-expect-error + await globalThis.__rsc_callServer(); + }, + }, +) { + // Intercept all anchor tag clicks + document.addEventListener( + "click", + async function handleClickEvent(event: MouseEvent) { + // should this only work for left click? + if (event.button !== 0) { + return; + } + + if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) { + return; + } + + const target = event.target as HTMLElement; + const link = target.closest("a"); + + if (!link) { + return; + } + + const href = link.getAttribute("href"); + if (!href) { + return; + } + + // Skip if target="_blank" or similar + if (link.target && link.target !== "_self") { + return; + } + + // Skip if download attribute + if (link.hasAttribute("download")) { + return; + } + + // Prevent default navigation + event.preventDefault(); + + // push this to the history stack. + window.history.pushState( + { path: href }, + "", + window.location.origin + href, + ); + + await opts.onNavigate(); + }, + true, + ); + + // Handle browser back/forward buttons + window.addEventListener("popstate", async function handlePopState() { + await opts.onNavigate(); + }); +} diff --git a/sdk/src/runtime/entries/client.ts b/sdk/src/runtime/entries/client.ts index 334806f21..82f075da0 100644 --- a/sdk/src/runtime/entries/client.ts +++ b/sdk/src/runtime/entries/client.ts @@ -1,3 +1,4 @@ export * from "../client"; export * from "../register/client"; export * from "../lib/streams/consumeEventStream"; +export * from "../clientNavigation"; From bb19fc994c443512b7502512eea84f018c3b8eaa Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Wed, 18 Jun 2025 14:17:18 +0200 Subject: [PATCH 2/3] Update docs. --- docs/astro.config.mjs | 29 +++++++++---- .../docs/guides/frontend/client-side-nav.mdx | 38 +++++++++++++++++ .../content/docs/guides/frontend/tailwind.mdx | 42 +++++++++---------- .../src/content/docs/reference/sdk-client.mdx | 19 +++++++++ 4 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 docs/src/content/docs/guides/frontend/client-side-nav.mdx create mode 100644 docs/src/content/docs/reference/sdk-client.mdx diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 1dadd6d00..e2b8cb095 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -41,7 +41,7 @@ export default defineConfig({ label: "Core", items: [ { slug: "core/overview" }, - { label: "Request Handling", slug: "core/routing" }, + { label: "Request Handling & Routing", slug: "core/routing" }, { slug: "core/react-server-components" }, { slug: "core/database" }, { slug: "core/storage" }, @@ -102,16 +102,19 @@ export default defineConfig({ label: "Guides", items: [ { - label: 'Email', - collapsed: true, + label: "Email", + collapsed: false, items: [ - { label: 'Sending Email', slug: 'guides/email/sending-email' }, - { label: 'Email Templates', slug: 'guides/email/email-templates' }, - ] + { label: "Sending Email", slug: "guides/email/sending-email" }, + { + label: "Email Templates", + slug: "guides/email/email-templates", + }, + ], }, { - label: 'Frontend Development', - collapsed: true, + label: "Frontend Development", + collapsed: false, items: [ { label: "Tailwind CSS", slug: "guides/frontend/tailwind" }, { label: "Storybook", slug: "guides/frontend/storybook" }, @@ -123,7 +126,14 @@ export default defineConfig({ slug: "guides/frontend/public-assets", }, { label: "Metadata", slug: "guides/frontend/metadata" }, - { label: "Dynamic OG Images", slug: "guides/frontend/og-images" }, + { + label: "Dynamic OG Images", + slug: "guides/frontend/og-images", + }, + { + label: "Client Side Navigation (SPA)", + slug: "guides/frontend/client-side-nav", + }, ], }, { label: "Server Function Streams", slug: "guides/rsc-streams" }, @@ -136,6 +146,7 @@ export default defineConfig({ { slug: "reference/create-rwsdk" }, { slug: "reference/sdk-worker" }, { slug: "reference/sdk-router" }, + { slug: "reference/sdk-client" }, ], collapsed: true, }, diff --git a/docs/src/content/docs/guides/frontend/client-side-nav.mdx b/docs/src/content/docs/guides/frontend/client-side-nav.mdx new file mode 100644 index 000000000..3c041807d --- /dev/null +++ b/docs/src/content/docs/guides/frontend/client-side-nav.mdx @@ -0,0 +1,38 @@ +--- +title: Client Side Navigation (Single Page Apps) +description: Implement client side navigation in your RedwoodSDK project +--- + +## What is Client Side Navigation? + +Client-side navigation is a technique that allows users to move between pages without a full-page reload. Instead of the browser reloading the entire HTML document, the JavaScript runtime intercepts navigation events (like link clicks), fetches the next page's content (usually as JavaScript modules or RSC payload), and updates the current view. + +This approach is commonly referred to as a Single Page App (SPA). In RedwoodSDK, you get SPA-like navigation with server-fetched React Server Components (RSC), so it's fast and dynamic, but still uses the server for rendering. + +```tsx title="src/client.tsx" ins="initClientNavigation()" +import { initClient, initClientNavigation } from "rwsdk/client"; + +initClient(); +initClientNavigation(); +``` + +Once this is initialized, internal `` links will no longer trigger full-page reloads. Instead, the SDK will: + +1. Intercept the link click, +2. Push the new URL to the browser's history, +3. Fetch the new page's RSC payload from the server, +4. And hydrate it on the client. + +RedwoodSDK keeps everything minimal and transparent. No magic routing system. No nested router contexts. You get the benefits of a modern SPA without giving up control. + +## Transitions and View Animations + +Client-side navigation enables you to animate between pages without jank. Pair it with View Transitions in React 19 to create seamless visual transitions. + +## Caveats + +No routing system is included: RedwoodSDK doesn't provide a client-side router. You can layer your own state management or page transitions as needed. + +Only internal links are intercepted: RedwoodSDK will only handle links pointing to the same origin. External links (https://example.com) or those with `target="\_blank"` behave normally. + +Middleware still runs: Every navigation hits your server again — so auth checks, headers, and streaming behavior remain intact. diff --git a/docs/src/content/docs/guides/frontend/tailwind.mdx b/docs/src/content/docs/guides/frontend/tailwind.mdx index ab339a9b7..08ccb75b4 100644 --- a/docs/src/content/docs/guides/frontend/tailwind.mdx +++ b/docs/src/content/docs/guides/frontend/tailwind.mdx @@ -3,8 +3,8 @@ title: Tailwind CSS description: A step-by-step guide for installing and configuring Tailwind CSS v4 in Redwood SDK projects, including customization options and font integration techniques. --- -import { Aside } from '@astrojs/starlight/components'; -import { Steps } from '@astrojs/starlight/components'; +import { Aside } from "@astrojs/starlight/components"; +import { Steps } from "@astrojs/starlight/components"; import { PackageManagers } from "starlight-package-managers"; @@ -14,40 +14,41 @@ Since the RedwoodSDK is based on React and Vite, we can work through the ["Using -1. Install Tailwind CSS - +1. Install Tailwind CSS + + + +2. Configure the Vite Plugin + {" "} -2. Configure the Vite Plugin ```ts ins="import tailwindcss from '@tailwindcss/vite'" ins="tailwindcss()," {3, 12} ins={7-9} title="vite.config.mts" import { defineConfig } from "vite"; - import tailwindcss from '@tailwindcss/vite' + import tailwindcss from "@tailwindcss/vite"; import { redwood } from "rwsdk/vite"; export default defineConfig({ environments: { ssr: {}, }, - plugins: [ - redwood(), - tailwindcss(), - ], + plugins: [redwood(), tailwindcss()], }); ``` - + +3. Create a `src/app/styles.css` file, and import Tailwind CSS + ```css title="src/app/styles.css" @import "tailwindcss"; ``` -4. Import your CSS and add a `` to the `styles.css` file.
+4. Import your CSS and add a `` to the `styles.css` file.
In the `Document.tsx` file, within the `` section, add: ```tsx title="src/app/Document.tsx" add={1, 5} @@ -60,14 +61,11 @@ Since the RedwoodSDK is based on React and Vite, we can work through the ["Using ``` -5. To test that Tailwind is working, you'll need to style something in your app. Use the
Tailwind CSS docs to understand how to use the utility classes.
+5. To test that Tailwind is working, you'll need to style something in your app. Use the Tailwind CSS docs to understand how to use the utility classes.
For example, you can just pick a random element in your app and add a blue background color to it by adding `className="bg-blue-500"` to it.` -6. Now, you can run `dev` and the element you styled should look different. - +6. Now, you can run `dev` and the element you styled should look different. + diff --git a/docs/src/content/docs/reference/sdk-client.mdx b/docs/src/content/docs/reference/sdk-client.mdx new file mode 100644 index 000000000..9344963bf --- /dev/null +++ b/docs/src/content/docs/reference/sdk-client.mdx @@ -0,0 +1,19 @@ +--- +title: sdk/client +description: Client Side Functions +next: false +--- + +The `rwsdk/client` module provides a set of functions for client-side operations. + +## `initClient` + +The `initClient` function is used to initialize the React Client. This hydrates the RSC flight payload that's add at the bottom of the page. This makes the page interactive. + +## `initClientNavigation` + +The `initClientNavigation` function is used to initialize the client side navigation. An event handler is assocated to clicking the document. If the clicked element contains a link, href, and the href is a relative path, the event handler will be triggered. This will then fetch the RSC payload for the new page, and hydrate it on the client. + +## `preloadPage` + +The `preloadPage` function is used to preload a page. From 1cd0e9e36cd4e20db00d6ff6e205837ee53c1677 Mon Sep 17 00:00:00 2001 From: Peter Pistorius Date: Wed, 18 Jun 2025 16:17:13 +0200 Subject: [PATCH 3/3] Remove preload reference in docs. --- docs/src/content/docs/reference/sdk-client.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/src/content/docs/reference/sdk-client.mdx b/docs/src/content/docs/reference/sdk-client.mdx index 9344963bf..8af85bd2d 100644 --- a/docs/src/content/docs/reference/sdk-client.mdx +++ b/docs/src/content/docs/reference/sdk-client.mdx @@ -13,7 +13,3 @@ The `initClient` function is used to initialize the React Client. This hydrates ## `initClientNavigation` The `initClientNavigation` function is used to initialize the client side navigation. An event handler is assocated to clicking the document. If the clicked element contains a link, href, and the href is a relative path, the event handler will be triggered. This will then fetch the RSC payload for the new page, and hydrate it on the client. - -## `preloadPage` - -The `preloadPage` function is used to preload a page.