Mari, Enzo (1957). Il Gioco delle Favole. Available at: https://www.enzomari.com/

Cleo

is a Vite plugin for building server rendered websites with Fastify and Preact.

Version 0.7.0 Alpha
View on GitHub

§Features

§Why

Isomorphic JavaScript frameworks (e.g. Next.js, Nuxt, SvelteKit) have greatly improved the tools for building dynamic web applications. However, not all projects need the pros and cons associated with universal rendering.

Cleo combines a modern developer experience with the simplicity of only rendering on the server. TSX is used to build components and pages with type-safe data binding. Vite provides an extensible build system and hot module reloading in development. Routes are registered automatically from the file system and allow the full use of the Fastify ecosystem with type-safe route schemas.

§Getting Started

Run npm create cleo@latest <project-name>

Inside the new directory, install dependencies using your preferred package manager. Run the dev script to start the Vite development server. Use the build script to create a production build. The start script will load the production server.

§Imports

## is aliased to the root directory of your project. Cleo also auto-imports useful functions including TypeBox for creating type-safe route schemas, createRequestHandler for registering routes, getHref for generating links, and Helmet for customizing the <head> element.

§Routing

Files in the ##/routes/ directory are crawled for route definitions. Named exports of HTTP verbs are registered with the Fastify server. A name export can be used to provide a custom name for the given route.

The createRequestHandler method is auto-imported and is needed for type-safe route schemas.

// ##/routes/blog/[slug].tsx
// => GET /blog/:slug

export const name = 'BlogDetail'
export const get = createRequestHandler({
  schema: {
    params: TypeBox.Object({
      slug: TypeBox.String(),
    }),
  },
  async handler(req, res) {
    let { slug } = req.params // slug: string

    // Fetch the blog post

    return res.render({
      component: () => <h1>A blog post with slug: {slug}</h1>,
    })
  },
})

The getHref method is auto-imported and can be used to generate links to routes. The name argument will be limited to detected and generated route names. The params argument will include required route params based on the route's path.

// Given the above BlogDetail route, use this anywhere in your app
const linkToBlogPost = getHref({
  name: 'BlogDetail',
  params: { slug: 'foo' },
})

§Configuration

The ##/cleo.config.ts file can be used to customize the server's behavior.

import { defineCleoConfig } from '@ordinal/cleo'
export default defineCleoConfig(async ({ isDev, prerender }) => {
  return {
    hooks: {
      // Hooks will run while creating the Fastify server
      fastifyHooks: [
        async (app, { isDev, prerender }) => {
          app.addHook('onRequest', async (req, res) => {
            // ...
          })
        },
      ],
      // Hooks will run before rendering the route's component.
      // "options" includes anything passed to "res.render"
      beforeRenderPage: [
        async (req, res, options) => {
          // ...
        },
      ],
      // Hooks will run after rendering the route's component,
      // before sending the HTML string "template". Useful for
      // replacing placeholders in "index.html" for e.g. a CSP nonce
      afterRenderPage: [
        (req, res, template) => {
          // ...
          return template
        },
      ],
    },
    // FastifyServerOptions passed when creating the server
    fastifyOpts: {
      // e.g.
      // ignoreTrailingSlash: true,
      // logger: true,
    },
  }
})

§Prerendering

In your vite.config.ts

import { defineConfig } from 'vite'
import { cleo } from '@ordinal/cleo'
export default defineConfig(() => {
  return {
plugins: [cleo({ prerender: true })],
} })

Then, run the build command. Static routes will automatically be generated and the ##/dist/ folder can be served statically.

Dynamic routes can be added for generation using the generate.addPaths option in your cleo.config.ts.

import { defineCleoConfig } from '@ordinal/cleo'
export default defineCleoConfig(async ({ isDev, prerender }) => {
  return {
    generate: {
      // An array or a function that returns an array of paths to generate during prerendering
addPaths: async () => {
// ...
return []
}, }, } })

§Helmet

Cleo supports and auto-imports react-helmet via preact-compat

// ##/components/About.tsx
import { FunctionComponent } from 'preact'

export const About: FunctionComponent = ({ children }) => (
  <div>
    <Helmet>
      <title>About</title>
      <meta name="description" content="An about page description using React Helmet" />
    </Helmet>
  </div>
)

§Layouts

Cleo will automatically load the files ##/layouts/default.tsx and ##/layouts/error.tsx to override the built-in default and error layouts. A default export must be used to provide a layout override.

// ##/layouts/default.tsx
import { FunctionComponent } from 'preact'

const DefaultLayout: FunctionComponent = ({ children }) => (
  <>
    <header>Cleo - default layout override</header>
    <main>{children}</main>
  </>
)

export default DefaultLayout

Layouts can also be specified when calling the res.render function using the layout property. The layoutProps property will be typed based on the provided layout.

§Type provider

Cleo uses @sinclair/typebox with ajv by default, as recommended by Fastify. You can also use fastify-type-provider-zod by modifying your vite.config.ts.

import { defineConfig } from 'vite'
import { cleo } from '@ordinal/cleo'
export default defineConfig(() => {
  return {
plugins: [cleo({ typeProvider: 'zod' })],
} })

The createRequestHandler type will be updated to use ZodTypeProvider. Then, add the validator and serializer compilers in your cleo.config.ts

import { defineCleoConfig } from '@ordinal/cleo'
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'

export default defineCleoConfig(async ({ isDev, prerender }) => ({
  hooks: {
    fastifyHooks: [
(app) => {
app.setValidatorCompiler(validatorCompiler)
app.setSerializerCompiler(serializerCompiler)
},
], }, }))

zod will be auto-imported and can be used when defining schemas within createRequestHandler

export const get = async (app) =>
  createRequestHandler({
    schema: {
      querystring: zod.object({
        name: zod.optional(zod.string()),
      }),
    },
    async handler(req, res) {
      return await res.render({
        component: () => <h1>Hello world! {req.query.name ?? ''}</h1>,
      })
    },
  })