Why vite-plugin-ssr-ssg

The motivation

It all started when I wanted to use vite to create a static site such as a blog. The overwhelming speed that static site generators haven't had has made me irreversible to other tools.

There are few tools for doing SSR and SSG with Vite. However, in most cases, it provides only wrapper functions and commands and is so abstract that you don't know what's going on inside. This is good for achieving a particular purpose, but it's confusing to customize and feels a bit overkill.

On the other hand, in this project aim for a simpler and easier-to-understand structure.

Client-side and server-side entry points are not intentionally hidden and are forced to declare. This is due to the fact that common parts are not yet been found between frameworks to support cross-framework. Of course, the entry point can be overridden and aim to make it nonexistent by default. Instead of reducing the level of abstraction, provide a script that makes it easy to set up your project with vite-ssrg init.

Release Notes

Latest version: npm

Detailed release notes for each version are available on GitHub.

Getting Started

with Yarn

yarn add -D vite-plugin-ssr-ssg

with NPM

npm i -D vite-plugin-ssr-ssg

Quick start

vite-plugin-ssr-ssg provides initialize command. If you

In your vite project:

npx run vite-ssrg init
or
yarn vite-ssrg init

That's all.

Refer to examples for manual setup.

Commands

vite-ssrg [command]

CommandDescription
initSetup SSR and SSG development environment
devSetup SSR develop server
buildOutput client side code and server side code
generateOutput pre-rendered client side code.
previewSetup node server for builded or generated output

Entrypoint

Entry points may no longer be needed in the future.

You need to have a placeholder <!-App-html-> in index.html to replace the result rendered in Server Side.

index.html

<body>
  <div id="app"><!--app-html--></div>
  <script type="module" src="/src/entry-client.tsx"></script>
</body>

Client Side

The Client Side entry point is specified by <script type =" module "/> in index.html. Also, on the Client Side, you need to call the rendering function for hydration.

React

import { hydrate } from 'react-dom'

hydrate(<App />, document.getElementById('app'))

Vue

createRouter()
  .isReady()
  .then(() => {
    app.mount('#app')
  })

Server Side

You need the src/entry-server.(t|j)sx? File. And you need to expose the renderer function as default export.

There is a type definition for the renderer function.

type ServerRender = (
  url: string,
  manifest: Record<string, string[] | undefined>
) => Promise<{
  bodyTags: string
  headTags?: string
  htmlAttrs?: string
  bodyAttrs?: string
}>

React
src/entry-server.tsx

import React from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import App from './App'
import type { ServerRenderer } from 'vite-plugin-ssr-ssg'

const render: ServerRenderer = async (url, manifest) => {
  const headTags = [] as any
  const context = {} as { modules: Set<string> }

  const body = renderToString(
    <HeadProvider headTags={headTags}>
      <StaticRouter location={url} context={context}>
        <App />
      </StaticRouter>
    </HeadProvider>
  )

  return { bodyTags: body, headTags: renderToString(headTags) }
}

export default render

Vue
src/entry-server.ts

import { ServerRenderer } from 'vite-plugin-ssr-ssg'
import { renderToString } from '@vue/server-renderer'
import { renderHeadToString } from '@vueuse/head'

const render: ServerRenderer = async (url, manifest) => {
  const { app, router, head } = createApp() // app factory

  router.push(url)
  await router.isReady()

  const context = {} as { modules: Set<string> }
  const html = await renderToString(app, context)
  const { headTags, htmlAttrs, bodyAttrs } = renderHeadToString(head)

  return {
    bodyTags: html,
    headTags,
    htmlAttrs,
    bodyAttrs
  }
}

export default render

The return value of default export is finally rendered in index.html as follows:

<!DOCTYPE html>
<html>
  <head ${headTags}>
    <>Other tags</>
    ${headTags}
  </head>
  <body ${bodyAttrs}>
    <div id="app"><!--app-html--> -> ${bodyTags}</div>
  </body>
</html>

Page generation

The files under src/pages are automatically used to generate static sites. At that time, it is recognized as Path according to the following rules.

File NamePath
index.*/
[param].*/param
nested/index.*/nested
nested/page.*/nested/page

Also, if you want to generate routes with dynamic parameters, see generate.routes.

API

Named exports has JavaScript API.

JavaScript API

Vite's JavaScript APIs are fully typed, and it's recommended to use TypeScript or enable JS type checking in VSCode to leverage the intellisense and validation.

getRoutes

Helper for getting routes.

Type Signature

const getRoutes: <T extends Framework>(
  pages: Record<
    string,
    {
      [key: string]: any
    }
  >,
  routeOptions?: Partial<RouteOptions> | undefined
) => Route<T>[]

Return value:

keynamepath
index.*index/
[param].*param/:param
nested/index.*nested/nested
nested/page.*nested-page/nested/page

Example Usage

React

import { Route, Switch } from 'react-router-dom'
import { getRoutes } from 'vite-plugin-ssr-ssg'

const pages = import.meta.globEager('./pages/**/*.tsx')
const routes = getRoutes<'react'>(pages)
// {
  name: string,
  path: string,
  Component: React.ComponentType
}[]

() => <Switch>
  {routes.map(({ path, name, Component }) => {
    return (
      <Route exact path={path} key={name}>
        <Component />
      </Route>
    )
  })}
</Switch>

Vue3

import { createRouter } from 'vue-router'
import { getRoutes } from 'vite-plugin-ssr-ssg'

const pages = import.meta.globEager('./pages/**/*.vue')
const routes = getRoutes<'vue'>(pages)
// RouteRecordRaw[]
const router = createRouter({
  routes
})