vite-plugin-ssr

website·Indexed 33 days ago

https://cn.vite-plugin-ssr.com/

A plugin for Vite that provides server-side rendering (SSR) capabilities. It supports multiple render modes including SPA, SSR, SSG, and HTML-only, and is UI framework independent with dedicated guides for Vue and React. Key features include data fetching via onBeforeRender(), routing precedence, HTML streaming, and a flexible pageContext for state management.

Tokens
18.8K
Snippets
87
Records
107
Agent score
49%

What's inside vite-plugin-ssr

  1. Compare vite-plugin-ssr (VPS) with Next.js

    vite-plugin-ssr (VPS) differs from Next.js in three primary architectural areas:

    1. UI Framework Independence: VPS has zero dependencies on React. It can be used with any UI framework (Vue, Preact, Solid, etc.), allowing developers to control the integration of frontend tools like Relay for render-as-you-fetch streaming.
    2. Server and Deployment Independence: VPS does not lock users into a specific hosting provider or server architecture. It is integrated as middleware into any server (e.g., Express.js, Fastify, Edge Workers, Serverless Functions).
    3. Minimal Dependency Footprint: VPS shares dependencies with Vite (e.g., fast-glob) or uses minimal transitive dependencies, avoiding the addition of unnecessary bloat to the project.

    Key advantages include faster HMR (via Vite), first-class SPA support, domain-driven file structure, and advanced code splitting via Rollup.

  2. Organize a basic vite-plugin-ssr project structure

    For basic applications, it is recommended to use a single pages/ directory. To keep the project organized, create a dedicated directory for each page rather than using a single file (e.g., use /pages/about/index.page.js instead of /pages/about.page.js). This allows you to keep page-specific components and assets close to the page definition.
    # Landing page (URL: `/`)
    /pages/index/index.page.js
    /pages/index/SomeComponentForLandingPage.js
    
    # About page (URL: `/about`)
    /pages/about/index.page.js
    /pages/about/SomeComponentForAboutPage.js
    
    # Shared Components
    /components/*
    
    # Rendering logic
    /renderer/_default.page.server.js
    /renderer/_default.page.client.js
    /renderer/_error.page.js
    /renderer/PageShell.js
    
    # Server code (Express.js/Fastify/etc)
    /server/*
  3. Compare render modes in vite-plugin-ssr

    vite-plugin-ssr supports four primary render modes. Choosing the right one depends on requirements for SEO, mobile performance, and deployment targets:

    • SSR (Server-Side Rendering): The page is rendered twice—once to HTML on the server and once (hydrated) in the browser. Best for high SEO requirements and mobile performance (content is visible before JS loads).
    • SPA (Single Page Application): The page is rendered only in the browser. Best for apps that don't need SEO (e.g., Admin Panels) or use UI libraries that are incompatible with Node.js.
    • Pre-rendering (SSG): The page is rendered to HTML at build-time rather than request-time. This allows the application to be deployed to static hosts (e.g., GitHub Pages).
    • HTML-only: The page is rendered only on the server to HTML and has minimal to no browser-side JavaScript. Best for content-centric pages with little interactivity (e.g., blogs, documentation, portfolios) to achieve maximum loading speed.
  4. Understand the `pageContext` object in vite-plugin-ssr

    pageContext is the core of vite-plugin-ssr, containing all information about the page currently being rendered. It is discarded and recreated every time a new page is rendered. It consists of built-in properties and can be extended with custom properties.
  5. Add custom properties to `pageContext`

    You can extend pageContext with custom data in three primary locations: the onBeforeRender hook, the render hook, or the server middleware.
    // 1. In onBeforeRender hook
    export function onBeforeRender() {
      return {
        pageContext: {
          customData: 'value'
        },
      };
    }
    
    // 2. In render hook
    export function render() {
      return {
        documentHtml: escapeInject`<html><!--...--></html>`,
        pageContext: {
          customData: 'value'
        },
      };
    }
    
    // 3. In Server middleware
    app.get("*", async (req, res, next) => {
      const pageContextInit = {
        urlOriginal: req.url,
        customData: 'value' // Custom property added here
      };
      const pageContext = await renderPage(pageContextInit);
      /* ... */
    });
  6. Define global HTML head tags in the render hook

    The primary way to define <head> tags (like <title> or <meta>) is within the server-side render() hook. Use escapeInject to prevent XSS when interpolating values into the HTML template.
    // /renderer/_default.page.server.js
    import { escapeInject, dangerouslySkipEscape } from "vite-plugin-ssr";
    import { renderToHtml } from "some-ui-framework";
    
    export { render };
    
    async function render(pageContext) {
      return escapeInject`<html>
        <head>
          <title>SpaceX</title>
          <meta name="description" content="We deliver payload to space.">
        </head>
        <body>
          <div id="root">
            ${dangerouslySkipEscape(await renderToHtml(pageContext.Page))}
          </div>
        </body>
      </html>`;
    }
  7. Enable client-side routing in vite-plugin-ssr

    By default, vite-plugin-ssr performs server-side routing. To enable client-side routing, you must modify the /renderer/_default.client.js file. This allows the browser to handle navigation without full page reloads. When enabled, the client router automatically intercepts clicks on <a> elements unless they have the rel="external" attribute.
    // /renderer/_default.page.client.js
    export const clientRouting = true
  8. Implement SPA (Single Page Application) rendering mode

    To configure a page as an SPA, where it is only loaded and rendered in the browser, you must use a .page.client.js file instead of a .page.js file. This ensures the page component is not executed on the server. If you need to provide metadata (like titles or descriptions) for the server to render in the <head>, you should define a corresponding .page.js file for the exports and a .page.client.js file for the Page component.
    // /pages/about.page.js
    // This file is used by the server to access metadata
    export const meta = {
      title: "About | My App",
      description: "My App is ...",
    };
    
    // /pages/about.page.client.js
    // This file contains the actual page component, rendered only in the browser
    export default PageComponent;
  9. Integrate browser-side tools and libraries

    The _default.page.client.js file is the ideal location to initialize browser-only tools such as error tracking (Sentry, Bugsnag), Service Workers, or vanilla JS component libraries (tooltips, modals) after hydration has occurred.
    // /renderer/_default.page.client.js
    // Environment: Browser
    import { hydrateDom } from 'some-ui-framework'
    
    export { render }
    
    // Initialize error tracking or Service Workers
    Sentry.init()
    navigator.serviceWorker.register(/* ... */)
    
    async render(pageContext) {
      const { Page } = pageContext
      await hydrateDom(Page)
      init()
    }
    
    function init() {
      // Initialize vanilla JS libraries after hydration
      tooltip.init(document.querySelectorAll('.tooltip'))
      $('.my-modals').modal()
    }
  10. Manage head tags from deeply nested components

    To allow nested UI components to modify the <head>, follow these steps:

    1. Add documentProps to the passToClient list in the server-side logic.
    2. Pass pageContext.documentProps down to components (e.g., via React Context or Vue global properties).
    3. Modify the documentProps object within the component.
    4. The render() hook will then use the modified pageContext.documentProps to generate the final HTML.
    // /renderer/_default.page.server.js
    export async function render(pageContext) {
      // Pass documentProps to the UI framework's context provider
      const pageHtml = await renderToHtml(
        <ContextProvider documentProps={pageContext.documentProps}>
          <Page />
        </ContextProvider>
      );
    
      return escapeInject`<html>
        <head>
          <title>${pageContext.documentProps.title}</title>
          <meta name="description" content="${pageContext.documentProps.description}">
        </head>
        <body>
          <div id="app">
            ${dangerouslySkipEscape(pageHtml)}
          </div>
        </body>
      </html>`;
    }
  11. Use Custom Exports to pass data from pages to the renderer

    In vite-plugin-ssr, any named export from a .page.js, .page.server.js, or .page.client.js file (including _default.page.* files) is automatically available in the pageContext.exports object. This allows you to define page-specific configuration, metadata, or components that the global renderer can access.

    Environment Constraints:

    • Exports from .page.js are available in both Node.js and the Browser.
    • Exports from .page.server.js are available only in Node.js.
    • Exports from .page.client.js are available only in the Browser.
    // /pages/countries.page.js
    export const title = "Earth Countries"; // Available in Node & Browser
    
    // /pages/countries.page.server.js
    export const dataUrl = "https://restcountries.com/v3.1/all"; // Available ONLY in Node
    
    // /renderer/_default.page.server.js
    async function onBeforeRender(pageContext) {
      const title = pageContext.exports.title; // Works
      const dataUrl = pageContext.exports.dataUrl; // Works
    }
  12. Best practices for the prerender() hook

    The prerender() hook should be used exclusively to speed up the pre-rendering build. To maintain consistency between development (where prerender() is not called) and production, ensure that all necessary pageContext is also provided by the onBeforeRender() hook. Avoid using prerender() to inject logic or data that is not also handled by onBeforeRender().