Giter Site home page Giter Site logo

vite-plugin-ssr's Introduction

vite-plugin-ssr

vite-plugin-ssr

Do-One-Thing-Do-It-Well, Flexible, Simple.

vite-plugin-ssr discord

Introduction
Vue Tour
React Tour
Get Started
   •  Boilerplates
   •  Manual Installation
Guides
   •  Data Fetching
   •  Routing
   •  Auth Data
   •  Store
   •  Markdown
   •  Pre-rendering
   •  Page Redirection
API
   •  *.page.js
   •  *.page.client.js
        •  import { getPage } from 'vite-plugin-ssr/client'
   •  *.page.route.js
        •  Route String
        •  Route Function
   •  *.page.server.js
        •  export { addContextProps }
        •  export { setPageProps }
        •  export { prerender }
        •  export { render }
        •  import { html } from 'vite-plugin-ssr'
   •  _default.*
   •  _404.page.js
   •  Filesystem Routing
   •  import { createRender } from 'vite-plugin-ssr'
   •  import vitePlugin from 'vite-plugin-ssr'


Introduction

vite-plugin-ssr gives you a similar experience than Nuxt/Next.js, but with Vite's wonderful DX, and as a do-one-thing-do-it-well tool: vite-plugin-ssr doesn't interfere with your stack and can be used with any tool you want.

  • Do-One-Thing-Do-It-Well. Only takes care of SSR and works with: other Vite plugins, any view framework (Vue 3, Vue 2, React, Svelte, Preact, ...), and any server framework (Express, Koa, Hapi, Fastify, ...).
  • Render Control. You control how your pages are rendered enabling you to easily and naturally integrate tools such as Vuex and Redux.
  • Routing. Supports Filesystem Routing for basic needs, Route Strings for simple parameterized routes, Route Functions for full flexibility, and can be used with Vue Router or React Router for client-side dynamic nested routes.
  • Pre-render / SSG / Static Websites. Deploy your app to a static host by pre-rendering your pages.
  • Scalable. Thanks to Vite's lazy transpiling, Vite apps can scale to thousands of modules with no hit on dev speed.
  • Fast Production Cold Start. Your pages' server-side code is lazy loaded so that adding pages doesn't increase cold start.
  • Code Splitting. Each page loads only the browser-side code it needs.
  • Simple Design. Simple overall design resulting in a small & robust tool that is easy to use.

To get an idea of what it's like to use vite-plugin-ssr, checkout the Vue Tour or React Tour.



Vue Tour

Similarly to SSR frameworks, pages are defined by page files.

<!-- /pages/index.page.vue -->
<!-- Environment: Browser, Node.js -->

<template>
  This page is rendered to HTML and interactive:
  <button @click="state.count++">Counter {{ state.count }}</button>
</template>

<script>
import { reactive } from 'vue'
export default {
  setup() {
    const state = reactive({ count: 0 })
    return { state }
  }
}
</script>

By default, vite-plugin-ssr does filesystem routing:

FILESYSTEM                  URL
pages/index.page.vue        /
pages/about.page.vue        /about

You can also use Route Strings (for parameterized routes such as /movies/:id) and Route Functions (for full programmatic flexibility).

// /pages/index.page.route.js
// Environment: Node.js

export default '/'

Unlike SSR frameworks, you define how your pages are rendered.

// /pages/_default.page.server.js
// Environment: Node.js

import { createSSRApp, h } from 'vue'
import { renderToString } from '@vue/server-renderer'
import { html } from 'vite-plugin-ssr'

export { render }

async function render({ Page, pageProps }) {
  const app = createSSRApp({
    render: () => h(Page, pageProps)
  })

  const appHtml = await renderToString(app)

  return html`<!DOCTYPE html>
    <html>
      <head>
        <title>Vite w/ SSR Demo</title>
      </head>
      <body>
        <div id="app">${html.dangerouslySetHtml(appHtml)}</div>
      </body>
    </html>`
}
// /pages/_default.page.client.js
// Environment: Browser

import { createSSRApp, h } from 'vue'
import { getPage } from 'vite-plugin-ssr/client'

hydrate()

async function hydrate() {
  // (In production, the page is `<link rel="preload">`'d.)
  const { Page, pageProps } = await getPage()

  const app = createSSRApp({
    render: () => h(Page, pageProps)
  })

  app.mount('#app')
}

The render() hook in pages/_default.page.server.js gives you full control over how your pages are rendered, and pages/_default.page.client.js gives you full control over the browser-side code. This control enables you to easily and naturally:

  • Use any tool you want such as Vue Router and Vuex.
  • Use any Vue version you want.

Note how the files we created so far end with .page.vue, .page.route.js, .page.server.js, and .page.client.js.

  • .page.js: defines the page's view that is rendered to HTML / the DOM.
  • .page.client.js: defines the page's browser-side code.
  • .page.server.js: defines the page's hooks (always run in Node.js).
  • .page.route.js: defines the page's Route String or Route function.

Using vite-plugin-ssr consists simply of writing these four types of files.

Instead of creating a .page.client.js and .page.server.js file for each page, you can create _default.page.client.js and _default.page.server.js which apply as default for all pages.

We already defined our _default.* files above, which means that we can now create a new page simply by defining a new .page.vue file. (The .page.route.js file is optional and only needed if we want to define a parameterized route.)

The _default.* files can be overridden. For example, you can create a page with a different browser-side code than your other pages.

// /pages/about.page.client.js

// This file is empty which means that the `/about` page has zero browser-side JavaScript.
<!-- /pages/about.page.vue -->

<template>
  This page is only rendered to HTML.
</template>

By overriding _default.page.server.js you can even render some of your pages with an entire different view framework such as React.

Note how files are collocated and share the same base /pages/about.page.*; this is how you tell vite-plugin-ssr that /pages/about.page.client.js is the browser-side code of /pages/about.page.vue.

Let's now have a look at how to fetch data for a page that has a parameterized route.

<!-- /pages/star-wars/movie.page.vue -->
<!-- Environment: Browser, Node.js -->

<template>
  <h1>{{movie.title}}</h1>
  <p>Release Date: {{movie.release_date}}</p>
  <p>Director: {{movie.director}}</p>
</template>

<script lang="ts">
const pageProps = ['movie']
export default { props: pageProps }
</script>
// /pages/star-wars/movie.page.route.js
// Environment: Node.js

export default '/star-wars/:movieId'
// /pages/star-wars/movie.page.server.js
// Environment: Node.js

import fetch from 'node-fetch'

export { addContextProps }
export { setPageProps }

async function addContextProps({ contextProps }) {
  // Route parameters are available at `contextProps`
  const { movieId } = contextProps
  // We could also use SQL/ORM queries here
  const response = await fetch(`https://swapi.dev/api/films/${movieId}`)
  const movie = await response.json()
  return { movie }
}

// The `contextProps` are available only on the server, and only the `pageProps` are
// serialized and passed to the browser.
function setPageProps({ contextProps }) {
  // We select only the data we need in order to minimize what it sent over the network
  const { title, release_date, director } = contextProps.movie
  const movie = { title, release_date, director }
  const pageProps = { movie }
  return pageProps
}

The addContextProps() hook always runs in Node.js, which means SQL/ORM queries can be used to fetch data.

That's it, and we have actually already seen most of vite-plugin-ssr's interface.

Thanks to the render() hook you keep full control over how your pages are rendered, and thanks to *.page.client.js, you keep full control over the entire browser-side code. This makes it easy and natural to use vite-plugin-ssr with any tool you want.

In short: vite-plugin-ssr is not only the most flexible, but also the easiest SSR tool out there.



React Tour

Similarly to SSR frameworks, pages are defined by page files.

// /pages/index.page.jsx
// Environment: Browser, Node.js

import React, { useState } from "react";

export { Page };

function Page() {
  return <>
    This page is rendered to HTML and interactive: <Counter />
  </>;
}

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((count) => count + 1)}>
      Counter {count}
    </button>
  );
}

By default, vite-plugin-ssr does filesystem routing:

FILESYSTEM                  URL
pages/index.page.jsx        /
pages/about.page.jsx        /about

You can also use Route Strings (for parameterized routes such as /movies/:id) and Route Functions (for full programmatic flexibility).

// /pages/index.page.route.js
// Environment: Node.js

export default "/";

Unlike SSR frameworks, you define how your pages are rendered.

// /pages/_default.page.server.jsx
// Environment: Node.js

import ReactDOMServer from "react-dom/server";
import React from "react";
import { html } from "vite-plugin-ssr";

export { render };

async function render({ Page, pageProps }) {
  const viewHtml = ReactDOMServer.renderToString(
    <Page {...pageProps} />
  );

  return html`<!DOCTYPE html>
    <html>
      <head>
        <title>Vite w/ SSR Demo</title>
      </head>
      <body>
        <div id="page-view">${html.dangerouslySetHtml(viewHtml)}</div>
      </body>
    </html>`;
}
// /pages/_default.page.client.jsx
// Environment: Browser

import ReactDOM from "react-dom";
import React from "react";
import { getPage } from "vite-plugin-ssr/client";

hydrate();

async function hydrate() {
  // (In production, the page is `<link rel="preload">`'d.)
  const { Page, pageProps } = await getPage();

  ReactDOM.hydrate(
    <Page {...pageProps} />,
    document.getElementById("page-view")
  );
}

The render() hook in pages/_default.page.server.jsx gives you full control over how your pages are rendered, and pages/_default.page.client.jsx gives you full control over the browser-side code. This control enables you to easily and naturally:

  • Use any tool you want such as React Router or Redux.
  • Use Preact, Inferno, Solid or any other React-like alternative.

Note how the files we created so far end with .page.jsx, .page.route.js, .page.server.jsx, and .page.client.jsx.

  • .page.js: defines the page's view that is rendered to HTML / the DOM.
  • .page.client.js: defines the page's browser-side code.
  • .page.server.js: defines the page's hooks (always run in Node.js).
  • .page.route.js: defines the page's Route String or Route function.

Using vite-plugin-ssr consists simply of writing these four types of files.

Instead of creating a .page.client.js and .page.server.js file for each page, you can create _default.page.client.js and _default.page.server.js which apply as default for all pages.

We already defined our _default.* files above, which means that we can now create a new page simply by defining a new .page.jsx file. (The .page.route.js file is optional and only needed if we want to define a parameterized route.)

The _default.* files can be overridden. For example, you can create a page with a different browser-side code than your other pages.

// /pages/about.page.client.js

// This file is empty which means that the `/about` page has zero browser-side JavaScript.
// /pages/about.page.jsx

export { Page };

function Page() {
  return <>This page is only rendered to HTML.<>;
}

By overriding _default.page.server.js you can even render some of your pages with an entire different view framework such as Vue.

Note how files are collocated and share the same base /pages/about.page.*; this is how you tell vite-plugin-ssr that /pages/about.page.client.js is the browser-side code of /pages/about.page.jsx.

Let's now have a look at how to fetch data for a page that has a parameterized route.

// /pages/star-wars/movie.page.jsx
// Environment: Browser, Node.js

import React from "react";

export { Page };

function Page(pageProps) {
  const { movie } = pageProps;
  return <>
    <h1>{movie.title}</h1>
    <p>Release Date: {movie.release_date}</p>
    <p>Director: {movie.director}</p>
  </>;
}
// /pages/star-wars/movie.page.route.js
// Environment: Node.js

export default "/star-wars/:movieId";
// /pages/star-wars/movie.page.server.js
// Environment: Node.js

import fetch from "node-fetch";

export { addContextProps };
export { setPageProps };

async function addContextProps({ contextProps }) {
  // Route parameters are available at `contextProps`
  const { movieId } = contextProps;
  // We could also use SQL/ORM queries here
  const response = await fetch(`https://swapi.dev/api/films/${movieId}`);
  const movie = await response.json();
  return { movie };
}

// The `contextProps` are available only on the server, and only the `pageProps` are
// serialized and passed to the browser.
function setPageProps({ contextProps }) {
  // We select only the data we need in order to minimize what it sent over the network
  const { title, release_date, director } = contextProps.movie;
  const movie = { title, release_date, director };
  const pageProps = { movie };
  return pageProps;
}

The addContextProps() hook always runs in Node.js, which means SQL/ORM queries can be used to fetch data.

That's it, and we have actually already seen most of vite-plugin-ssr's interface.

Thanks to the render() hook you keep full control over how your pages are rendered, and thanks to *.page.client.js, you keep full control over the entire browser-side code. This makes it easy and natural to use vite-plugin-ssr with any tool you want.

In short: vite-plugin-ssr is not only the most flexible, but also the easiest SSR tool out there.



Boilerplates

Scaffold a Vite app that uses vite-plugin-ssr.

With NPM:

npm init vite-plugin-ssr

With Yarn:

yarn create vite-plugin-ssr

Then choose between vue, vue-ts, react, and react-ts.



Manual Installation

If you already have an existing Vite app:

  1. Add vite-plugin-ssr to your vite.config.js.

  2. Integrate createRender() with your server (Express.js, Koa, Hapi, Fastify, ...).

  3. Define your _default.page.client.js and _default.page.server.js.

  4. Create your first index.page.js.

  5. Add the dev and build scripts to your package.json.



Data Fetching

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

You fech data by using two hooks: addContextProps() and setPageProps(). The async function addContextProps() fetches data, while the function setPageProps() (not async) specifies what data is serialized and passed to the browser.

Hooks are called in Node.js, which means that you can use ORM/SQL database queries in your addcontextprops() hook.

Hooks are defined in .page.server.js.

// /pages/movies.page.server.js
// Environment: Node.js

import fetch from "node-fetch";

export { addContextProps }
export { setPageProps }

async function addContextProps({ contextProps }) {
  const response = await fetch("https://api.imdb.com/api/movies/")
  const { movies } = await response.json()
  return { movies }
}

function setPageProps({ contextProps: { movies } }) {
  // We only select data we need: `vite-plugin-ssr` serializes and passes `pageProps`
  // to the client and we want to minimize what it sent over the network.
  movies = movies.map(({ title, release_date }) => ({title, release_date}))
  const pageProps = { movies }
  return pageProps
}

The pageProps are:

  1. Passed to your render() hook.
  2. Serialized and passed to the client-side.
// /pages/_default.page.server.js
// Environment: Node.js

import { html } from 'vite-plugin-ssr'
import { renderView } from 'some-view-library'

export { render }

async function render({ Page, pageProps }) {
  // `Page` is defined below in `/pages/movies.page.js`.
  const pageHtml = await renderView(<Page {...pageProps} />)
  return html`<html>
    <div id='view-root'>
      ${html.dangerouslySetHtml(pageHtml)}
    </div>
  </html>`
}
// /pages/_default.page.client.js
// Environment: Browser

import { getPage } from 'vite-plugin-ssr/client'
import { hydrateView } from 'some-view-library'

hydrate()

async function hydrate() {
  const { Page, pageProps } = await getPage()
  await hydrateView(<Page {...pageProps} />, document.getElementById('view-root'))
}
// /pages/movies.page.js
// Environment: Browser, Node.js

export { Page }

function Page(pageProps) {
  const { movies } = pageProps
  /* ... */
}



Routing

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

By default vite-plugin-ssr does Filesystem Routing.

FILESYSTEM                  URL
pages/index.page.js         /
pages/about.page.js         /about
pages/faq/index.page.js     /faq

For more control, you can define route strings in .page.route.js files.

// /pages/product.page.route.js

export default '/product/:productId'

The productId value is available at contextProps.productId so that you can fetch data in async addContextProps({contextProps}) which is explained at Data Fetching.

For full programmatic flexibility, you can define route functions.

// /pages/admin.page.route.js

// Route functions allow us to implement advanced routing such as route guards.
export default async ({ url, contextProps }) => {
  if (url==='/admin' && contextProps.user.isAdmin) {
    return { match: true }
  }
}

For detailed informations about Filesystem Routing, route strings, and route functions:



Auth Data

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

Information about the authenticated user can be added to contextProps at the server integration point createRender(). The contextProps are available to all hooks and route functions.

const render = createRender(/*...*/)

app.get('*', async (req, res, next) => {
  const url = req.originalUrl
  // The `user` object, which holds user information, is provided by your
  // authentication middleware, for example the Express.js Passport middleware.
  const { user } = req
  const contextProps = { user }
  const html = await render({ url, contextProps })
  if (!html) return next()
  res.send(html)
})



Store

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

Even complex integrations, such as Vuex or Redux, are simple and straightforward to implement. Because you control how your pages are rendered, integration is just a matter of following the official guide of the tool you want to integrate.

While you can follow the official guides exactly as they are (including serializing and injecting the initial state into HTML), you can also leverage vite-plugin-ssr's pageProps to make your life slightly easier, as shown in the following examples.



Markdown

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

You can use vite-plugin-ssr with any Vite markdown plugin.

For Vue you can use vite-plugin-md:

For React you can use @brillout/vite-plugin-mdx:



Pre-rendering

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

⚠️ Pre-rendering is currenlty being worked on. If you want an ETA, open a GitHub issue.

*️⃣ What is pre-rendering? Pre-rendering means to render the HTML of all your pages at once. Normally, the HTML of a page is rendered at request-time (when your user goes to your website). With pre-rendering, the HTML of a page is rendered at build-time instead (when yun run vite-plugin-ssr prerender). Your app then consists only of static assets (HTML, JS, CSS, images, ...) and you can deploy your app to so-called "static hosts" such as GitHub Pages or Netlify. Without pre-rendering, you need to use a Node.js server that will render your pages' HTML at request-time.

To pre-render your pages, run npx vite && npx vite --ssr && npx vite-plugin-ssr prerender. (Or with Yarn: yarn vite && yarn vite --ssr && yarn vite-plugin-ssr prerender.)

For pages with a parameterized route (e.g. /movie/:movieId), you'll have to use the prerender() hook.

The prerender() hook can also be used to prefetch data for multiple pages at once.



Page Redirection

⚠️ We recommend reading the Vue Tour or React Tour before proceeding with guides.

Your render() hook doesn't have to return HTML and can, for example, return { redirectTo: '/some/url' } in order to do a URL redirect.

export { render }

function render({ contextProps }) {
  // If the user goes to `/movie/42` but there is no movie with ID `42` then
  // we redirect the user to `/movie/add` so he can add a new movie.
  if (contextProps.movieId === null) {
    return { redirectTo: '/movie/add' }
  } else {
    // The usual render stuff
    // ...
  }
}
const render = createRender(/*...*/)

app.get('*', async (req, res, next) => {
  const url = req.originalUrl
  const contextProps = {}
  const renderResult = await render({ url, contextProps })
  if (renderResult?.redirectTo) {
    res.redirect(307, '/movie/add')
  } else if (typeof renderResult === 'string') {
    res.send(renderResult)
  } else {
    next()
  }
})



*.page.js

Environment: Browser, Node.js
Ext Glob: /**/*.page.*([a-zA-Z0-9])

A *.page.js file should have a export { Page }. (Or a export default.)

Page represents the page's view that is rendered to HTML / the DOM.

vite-plugin-ssr doesn't do anything with Page and just passes it untouched to:

  • Your render({ Page }) hook.
  • The client-side.
// *.page.js
// Environment: Browser, Node.js

export { Page }

// We export a JSX component, but we could as well export a Vue/Svelte/... component,
// or even export some totally custom object since vite-plugin-ssr doesn't do anything
// with `Page`: it just passes it to your `render()` hook and to the client-side.
function Page() {
  return <>Hello</>
}
// *.page.server.js
// Environment: Node.js

import { html } from 'vite-plugin-ssr'
import renderToHtml from 'some-view-library'

export { render }

// `Page` is passed to the `render()` hook
async function render({ Page }) {
  const pageHtml = await renderToHtml(Page)

  return html`<html>
    <body>
      <div id="root">
        ${html.dangerouslySetHtml(pageHtml)}
      </div>
    </body>
  </html>`
}
// *.page.client.js
// Environment: Browser

import { getPage } from 'vite-plugin-ssr/client'
import { hydrateToDom } from 'some-view-library'

hydrate()

async function hydrate() {
  // `Page` is available in the browser.
  const { Page } = await getPage()
  await hydrateToDom(Page)
}

The *.page.js file is lazy-loaded only when needed, that is when an HTTP request matches the page's route.



*.page.client.js

Environment: Browser
Ext Glob: /**/*.page.client.*([a-zA-Z0-9])

A .page.client.js file is a .page.js-adjacent file that defines the page's browser-side code.

It represents the entire browser-side code. This means that if you create an empty .page.client.js file, then the page has zero browser-side JavaScript. (Except of Vite's dev code when not in production.)

This also means that you have full control over the browser-side code: not only can you render/hydrate your pages as you wish, but you can also easily integrate browser libraries.

// *.page.client.js

import { getPage } from 'vite-plugin-ssr/client'
import { hydrateView } from 'some-view-library'
import GoogleAnalytics from '@brillout/google-analytics'

main()

async function main() {
  analytics_init()
  analytics.event('[hydration] begin')
  await hydrate()
  analytics.event('[hydration] end')
}

async function hydrate() {
  const { Page, pageProps } = await getPage()
  await hydrateView(<Page {...pageProps} />, document.getElementById('view-root'))
}

let analytics
function analytics_init() {
  analytics = new GoogleAnalytics('UA-121991291')
}

import { getPage } from 'vite-plugin-ssr/client'

Environment: Browser

The async getPage() function provides Page and pageProps for the browser-side code .page.client.js.

// /pages/demo.page.client.js

import { getPage } from 'vite-plugin-ssr/client'

hydrate()

async function hydrate() {
  const { Page, pageProps } = await getPage()
  /* ... */
}
  • Page is the export { Page } (or export default) of the /pages/demo.page.js file.
  • pageProps is the value returned by your setPageProps() function (which you define and export in the adjacent pages/demo.page.server.js file).

The pageProps are serialized and passed from the server to the browser with devalue.

In development getPage() dynamically import() the page, while in production the page is preloaded (with <link rel="preload">).



*.page.route.js

Environment: Node.js
Ext Glob: /**/*.page.route.*([a-zA-Z0-9])

The *.page.route.js files enable further control over routing with:

  • Route Strings
  • Route Functions

Route String

For a page /pages/film.page.js, a route string can be defined in a /pages/film.page.route.js adjacent file.

// /pages/film.page.route.js

// Match URLs `/film/1`, `/film/2`, ...
export default '/film/:filmId'

If the URL matches, the value of filmId is available at contextProps.filmId.

The syntax of route strings is based on path-to-regexp (the most widespread route syntax in JavaScript). For user friendlier docs, check out the Express.js Routing Docs (Express.js uses path-to-regexp).


Route Function

Route functions give you full programmatic flexibility to define your routing logic.

// /pages/film/admin.page.route.js

export default async ({ url, contextProps }) {
  // Route functions allow us to implement advanced routing such as route guards.
  if (! contextProps.user.isAdmin) {
    return {match: false}
  }
  // We can use RegExp and any JavaScript tool we want.
  if (! /\/film\/[0-9]+\/admin/.test(url)) {
    return {match: false}
  }
  filmId = url.split('/')[2]
  return {
    match: true,
    // Add `filmId` to `contextProps`
    contextProps: { filmId }
  }
}

The match value can be a (negative) number which enables you to resolve route conflicts. The higher the number, the higher the priority. For example, vite-plugin-ssr internally defines _404.page.js's route as:

// node_modules/vite-plugin-ssr/.../_404.page.route.js

// Ensure lowest priority for the 404 page
export default () => ({match: -Infinity})



*.page.server.js

Environment: Node.js
Ext Glob: /**/*.page.server.*([a-zA-Z0-9])

A .page.server.js file is a .page.js-adjacent file that exports the page's hooks:

  • export { addContextProps }
  • export { setPageProps }
  • export { render }
  • export { prerender }

The *.page.server.js file is lazy-loaded only when needed.


export { addContextProps }

The addContextProps() hook is used to provide further contextProps values.

The contextProps are passed to all hooks (which are defined in .page.server.js) and to the route function (if there is one defined in .page.route.js).

You can provide initial contextProps values at your server integration point: const render = createRender(/*...*/); render({ url, contextProps }). Which you usually use to pass information about the authenticated user, see Auth Data guide.

The addContextProps() hook is usually used in conjunction with the setPageProps() hook to fetch data, see Data Fetching guide.

Since addContextProps() is always called in Node.js, ORM/SQL database queries can be used.

// /pages/movies.page.server.js

import fetch from "node-fetch";

export { addContextProps }

async function addContextProps({ contextProps, Page }){
  const response = await fetch("https://api.imdb.com/api/movies/")
  const { movies } = await response.json()
  /* Or with an ORM:
  const movies = Movie.findAll() */
  /* Or with SQL:
  const movies = sql`SELECT * FROM movies;` */
  return { movies }
}
  • Page is the export { Page } (or export default) of the .page.js file.
  • contextProps is the initial accumulation of:
    1. The contextProps you provided in your the server integration point createRender().
    2. The route parameters (such as contextProps.movieId for a page with a route string /movie/:movieId).

export { setPageProps }

The setPageProps() hook provides the pageProps which are consumed by Page.

The pageProps are serialized and passed from the server to the browser with devalue.

It is usally used in conjunction with the addContextProps() hook: data is fetched in addContextProps() and then made available to Page with setPageProps().

// /pages/movies.page.server.js
// Environment: Node.js

import fetch from "node-fetch";

async function addContextProps({ contextProps }) {
  const response = await fetch("https://api.imdb.com/api/movies/")
  const { movies } = await response.json()
  return { movies }
}

function setPageProps({ contextProps: { movies } }) {
  // We remove data we don't need: `vite-plugin-ssr` serializes and passes `pageProps`
  // to the client and we want to minimize what it sent over the network.
  movies = movies.map(({ title, release_date }) => ({title, release_date}))
  const pageProps = { movies }
  return pageProps
}
// /pages/movies.page.js
// Environment: Browser, Node.js

export { Page }

function Page(pageProps) {
  const { movies } = pageProps
  /* ... */
}

export { prerender }

*️⃣ Check out the Pre-rendering Guide to get an overview about pre-rendering.

The prerender() hook enables parameterized routes (e.g. /movie/:movieId) to be pre-rendered: by defining the prerender() hook you provide the list of URLs (/movie/1, /movie/2, ...) and (optionally) the contextProps of each URL.

If you don't have any parameterized route, then you can prerender your app without defining any prerender() hook. You can, however, still use the prerender() hook to increase the effeciency of pre-rendering as it enables you to fetch data for multiple pages at once.

// /pages/movie.page.route.js

export default '/movie/:movieId`
// /pages/movie.page.server.js

export { prerender }

async function prerender() {
  const movies = await Movie.findAll()

  const moviePages = (
    movies
    .map(movie => {
      const url = `/movie/${movie.id}`
      const contextProps = { movie }
      return {
        url,
        // Beacuse we already provide the `contextProps`, vite-plugin-ssr will *not* call
        // the `addContextProps()` hook.
        contextProps
      }
      // We could also return `url` wtihout `contextProps`. In that case vite-plugin-ssr would
      // call `addContextProps()`. But that would be wasteful since we already have all the data
      // of all movies from our `await Movie.findAll()` call.
      // return { url }
    })
  )

  // We can also return URLs that don't match the page's route.
  // That way we can provide the `contextProps` of other pages.
  // Here we provide the `contextProps` of the `/movies` page since
  // we already have the data.
  const movieListPage = {
    url: '/movies', // The `/movies` URL doesn't belong to the page's route `/movie/:movieId`
    contextProps: {
      movieList: movies.map(({id, title}) => ({id, title})
    }
  }

  return [movieListPage, ...moviePages]
}

The prerender() hook is only used when pre-rendering: if you don't call vite-plugin-ssr prerender then no prerender() hook is called.


export { render }

The render() hook renders Page to an HTML string.

Note that the render() hook can also return something else than HTML, for example an object { redirectTo: '/some/url' } in order to do Page Redirection.

// *.page.server.js

import { html } from 'vite-plugin-ssr'
import renderToHtml from 'some-view-library'

export { render }

async function render({ Page, pageProps, contextProps }){
  const pageHtml = await renderToHtml(<Page {...pageProps} />)

  const title = contextProps.title || 'My SSR App'

  return html`<!DOCTYPE html>
    <html>
      <head>
        <title>${title}</title>
      </head>
      <body>
        <div id="page-root">${html.dangerouslySetHtml(pageHtml)}</div>
      </body>
    </html>`
}
  • Page is the export { Page } (or export default) of the .page.js file.
  • pageProps is the value returned by the setPageProps() hook.
  • contextProps is the accumulation of:
    1. The contextProps you passed to const render = createRender(/*...*/); render({ url, contextProps }).
    2. The route parameters (such as contextProps.movieId for a page with a route string /movie/:movieId).
    3. The contextProps you returned in your addContextProps() hook (if you defined one).

import { html } from 'vite-plugin-ssr'

Environment: Node.js

The html tag sanitizes HTML (to prevent XSS injections). It is usually used in your render() hook defined in .page.server.js.

// *.page.server.js

import { html } from 'vite-plugin-ssr'

export { render }

async function render() {
  const title = 'Hello<script src="https://devil.org/evil-code"></script>'
  const pageHtml = "<div>I'm already <b>sanitized</b>, e.g. by Vue/React</div>"

  // We're safe because `html` sanitizes `title`
  return html`<!DOCTYPE html>
    <html>
      <head>
        <title>${title}</title>
      </head>
      <body>
        <div id="page-root">${html.dangerouslySetHtml(pageHtml)}</div>
      </body>
    </html>`
}

All strings, e.g. title, are automatically sanitized (technically speaking: HTML-escaped) so that you can safely inject untrusted strings such as user-generated text.

The html.dangerouslySetHtml(str) function injects the string str as-is without sanitizing. It should be used with caution and only for HTML strings that are guaranteed to be already sanitized. It is usually used to include the HTML generated by React/Vue/Solid/... as these frameworks generate sanitized HTML. If you find yourself using html.dangerouslySetHtml() in other situations be extra careful as you run into the risk of creating a security breach.



_default.*

The _default.page.server.js and _default.page.client.js files are like regular .page.server.js and .page.client.js files, but they are special in the sense that they don't apply to a single page file; instead, they apply as a default to all pages.

There can be several _default.* files.

marketing/_default.page.server.js
marketing/_default.page.client.js
marketing/index.page.js
marketing/about.page.js
marketing/jobs.page.js
admin-panel/_default.page.server.js
admin-panel/_default.page.client.js
admin-panel/index.page.js

The marketing/_default.* files apply to the marketing/*.page.js files, while the admin-panel/_default.* files apply to the admin-panel/*.page.js files.

The _default.page.server.js and _default.page.client.js files are not adjacent to any .page.js file, and defining _default.page.js or _default.page.route.js is forbidden.



_404.page.js

The _404.page.js page is like any other page with the exception that it has a predefined route.

// node_modules/vite-plugin-ssr/.../_404.page.route.js

// Ensure lowest priority for the 404 page
export default () => ({match: -Infinity})



Filesystem Routing

By default a page is mapped to a URL based on where its .page.js file is located.

FILESYSTEM                        URL              COMMENT
pages/about.page.js               /about
pages/index/index.page.js         /                (`index` is mapped to the empty string)
pages/HELLO.page.js               /hello           (Mapping is done lower case)

The pages/ directory is optional and you can save your .page.js files wherever you want.

FILESYSTEM                        URL
user/list.page.js                 /user/list
user/create.page.js               /user/create
todo/list.page.js                 /todo/list
todo/create.page.js               /todo/create

The directory common to all your *.page.js files is considered the routing root.

For more control over routing, define route strings or route functions in *.page.route.js.



import { createRender } from 'vite-plugin-ssr'

Environment: Node.js

The createRender() is the integration point between your server and vite-plugin-ssr.

const render = createRender({ viteDevServer, isProduction, root })

app.get('*', async (req, res, next) => {
  const url = req.originalUrl
  const contextProps = {}
  const html = await render({ url, contextProps })
  if (!html) return next()
  res.send(html)
})
  • isProduction is a boolean. When set to true, vite-plugin-ssr loads already-transpiled code from dist/ instead of on-the-fly transpiling code.
  • root is a string holding the absolute path of your app's root directory. All your .page.js files should be a descendent of the root directory.
  • viteDevServer is the value returned by const viteDevServer = await vite.createServer(/*...*/).

Since render({ url, contextProps}) is agnostic to Express.js, you can use vite-plugin-ssr with any server framework such as Koa, Hapi, Fastify, or vanilla Node.js.

Examples:



import vitePlugin from 'vite-plugin-ssr'

Environment: Node.js

The Vite plugin has no options.

// vite.config.js

const ssr = require("vite-plugin-ssr");

module.exports = {
  plugins: [ssr()]
};



vite-plugin-ssr's People

Contributors

brillout avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.