diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 1dadd6d0..e2b8cb09 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 00000000..3c041807 --- /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 ab339a9b..08ccb75b 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 00000000..8af85bd2 --- /dev/null +++ b/docs/src/content/docs/reference/sdk-client.mdx @@ -0,0 +1,15 @@ +--- +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. diff --git a/sdk/src/runtime/clientNavigation.ts b/sdk/src/runtime/clientNavigation.ts new file mode 100644 index 00000000..eb013dcc --- /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 334806f2..82f075da 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";