Important
Sonik becomes HonoX; development of Sonik is discontinued and it is reborn as HonoX. Everyone, please use HonoX: https://github.com/honojs/honox
Sonik is a simple and fast -supersonic- meta framework for creating web APIs and websites with Server-Side Rendering. It stands on the shoulders of giants; built on Hono, Vite, and JSX-based UI libraries.
Note: Sonik is currently in a "alpha stage". There will be breaking changes without any announcement. Don't use it in production. However, feel free to try it in your hobby project and give us your feedback!
- File-based routing - Now, you can create a large app by separating concerns.
- Fast SSR - Supports only Server-Side Rendering. Rendering is ultra-fast thanks to Hono.
- No JavaScript - By default, there's no need for JavaScript. Nothing loads.
- Island hydration - If you want interaction, create an island. JavaScript is hydrated only for that island.
- UI presets - Any JSX-based UI library works with Sonik. Presets for hono/jsx, Preact, React are available.
- Easy API creation - You can create APIs using Hono's syntax.
- Middleware - It works just like Hono, so you can use many of Hono's middleware.
- Edge optimized - The bundle size is minimized, making it easy to deploy to edge platforms like Cloudflare Workers.
Give it a try:
npm create sonik@latest
// Or
yarn create sonik
// Or
pnpm create sonik@latest
By default, it can be deployed to Cloudflare Pages.
npm:
npm install
npm run dev
npm run build
npm run deploy
yarn:
yarn install
yarn dev
yarn build
yarn deploy
Below is a typical project structure for a Sonik application with Islands.
.
├── app
│ ├── client.ts // client entry file
│ ├── islands
│ │ └── counter.tsx // island component
│ ├── routes
│ │ ├── _404.tsx // not found page
│ │ ├── _error.tsx // error page
│ │ ├── _layout.tsx // layout template
│ │ ├── about
│ │ │ └── [name].tsx // matches `/about/:name`
│ │ └── index.tsx // matches `/`
│ ├── server.ts // server entry file
│ └── style.css
├── package.json
├── public
│ └── favicon.ico
├── tsconfig.json
└── vite.config.ts
A server entry file is required. The file is should be placed at src/server.ts
.
This file is first called by the Vite during the development or build phase.
In the entry file, simply initialize your app using the createApp()
function. app
will be an instance of Hono, so you can utilize Hono's middleware and the app.showRoutes()
feature.
// app/server.ts
import { createApp } from 'sonik'
const app = createApp()
app.showRoutes()
export default app
You can construct pages with the JSX syntax using your favorite UI framework. Presets for hono/jsx, Preact, React are available.
If you prefer to use the Preact presets, simply import from @sonikjs/preact
:
import { createApp } from '@sonikjs/preact'
The following presets are available:
sonik
- hono/jsx@sonikjs/preact
- Preact@sonikjs/react
- React
There are two syntaxes for creating a page.
Before introducing the two syntaxes, let you know about c.render()
.
You can use c.render()
to return a HTML content with applying the layout is applied.
The Renderer
definition is the following:
declare module 'hono' {
interface ContextRenderer {
(content: Node, head?: Partial<Pick<types.Head, 'title' | 'link' | 'meta'>>):
| Response
| Promise<Response>
}
}
Export the AppRoute
typed object with defineRoute()
as the route
.
The app
is an instance of Hono
.
// app/index.tsx
import { defineRoute } from 'sonik'
export const route = defineRoute((app) => {
app.get((c) => {
const res = c.render(<h1>Hello</h1>, {
title: 'This is a title',
meta: [{ name: 'description', content: 'This is a description' }],
})
return res
})
})
Just return JSX function as the default
:
// app/index.tsx
export default function Home() {
return <h1>Hello!</h1>
}
Or you can use the Context
instance:
import type { Context } from 'sonik'
// app/index.tsx
export default function Home(c: Context) {
return c.render(<h1>Hello!</h1>, {
title: 'My title',
})
}
You can put both syntaxes in one file:
export const route = defineRoute((app) => {
app.post((c) => {
return c.text('Created!', 201)
})
})
export default function Books(c: Context) {
return c.render(
<form method='POST'>
<input type='text' name='title' />
<input type='submit' />
</form>
)
}
You can write the API endpoints in the same syntax as Hono.
// app/routes/about/index.ts
import { Hono } from 'hono'
const app = new Hono()
// matches `/about/:name`
app.get('/:name', (c) => {
const name = c.req.param('name')
return c.json({
'your name is': name,
})
})
export default app
Files named in the following manner have designated roles:
_404.tsx
- Not found page_error.tsx
- Error page_layout.tsx
- Layout template__layout.tsx
- Template for nested layouts
To write client-side scripts that include JavaScript or stylesheets managed by Vite, craft a file and import sonik/client
as seen in app/client.ts
:
import { createClient } from '@sonikjs/preact/client'
createClient()
Also presets are avialbles for client entry file:
@sonikjs/preact/client
- Preact@sonikjs/react/client
- React
And then, import it in app/routes/_layout.tsx
:
import type { LayoutHandler } from '@sonikjs/preact'
const handler: LayoutHandler = ({ children, head }) => {
return (
<html lang='en'>
<head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
{import.meta.env.PROD ? (
<>
<link href='/static/style.css' rel='stylesheet' />
<script type='module' src='/static/client.js'></script>
</>
) : (
<>
<link href='/app/style.css' rel='stylesheet' />
<script type='module' src='/app/client.ts'></script>
</>
)}
{head.createTags()}
</head>
<body>
<div class='bg-gray-200 h-screen'>{children}</div>
</body>
</html>
)
}
export default handler
import.meta.env.PROD
is useful flag for separate tags wehere it is on dev server or production.
You should use /app/client.ts
in development and use the file built in the production.
Given that a Sonik instance is fundamentally a Hono instance, you can utilize all of Hono's middleware. If you wish to apply it before the Sonik app processes a request, create a base
variable and pass it as a constructor option for createApp()
:
const base = new Hono()
base.use('*', poweredBy())
const app = createApp({
app: base,
})
Given that Sonik is Vite-centric, if you wish to utilize Tailwind CSS, simply adhere to the official instructions.
Prepare tailwind.config.js
and postcss.config.js
:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./app/**/*.tsx'],
theme: {
extend: {},
},
plugins: [],
}
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Write app/style.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Finally, import it in client entry file:
//app/client.ts
import { createClient } from '@sonikjs/preact/client'
import './style.css'
createClient()
Integrate MDX using @mdx-js/rollup
by configuring it in vite.config.ts
:
import devServer from '@hono/vite-dev-server'
import mdx from '@mdx-js/rollup'
import { defineConfig } from 'vite'
import sonik from 'sonik/vite'
export default defineConfig({
plugins: [
devServer({
entry: './app/server.ts',
}),
sonik(),
{
...mdx({
jsxImportSource: 'preact',
}),
},
],
})
Sonik supports SSR Streaming, which, as of now, is exclusively available for React with Suspense
.
To enable is, set the streaming
as true
and pass the renderToReadableString()
method in the createApp()
:
import { renderToReadableStream } from 'react-dom/server'
const app = createApp({
streaming: true,
renderToReadableStream: renderToReadableStream,
})
Since a Sonik instance is essentially a Hono instance, it can be deployed on any platform that Hono supports.
The following adapters for deploying to the platforms are available in the Sonik package.
Setup the vite.config.ts
:
// vite.config.ts
import { defineConfig } from 'vite'
import sonik from 'sonik/vite'
import pages from 'sonik/cloudflare-pages'
export default defineConfig({
plugins: [sonik(), pages()],
})
Build command (including a client):
vite build && vite build --mode client
Deploy with the following commands after build. Ensure you have Wrangler installed:
wrangler pages deploy ./dist
// vite.config.ts
import { defineConfig } from 'vite'
import sonik from 'sonik/vite'
import vercel from 'sonik/vercel'
export default defineConfig({
plugins: [sonik(), vercel()],
})
Build command (including a client):
vite build && vite build --mode client
Ensure you have Vercel CLI installed.
vercel --prebuilt
The Cloudflare Workers adapter supports the "server" only and does not support the "client".
// vite.config.ts
import { defineConfig } from 'vite'
import sonik from 'sonik/vite'
import workers from 'sonik/cloudflare-workers'
export default defineConfig({
plugins: [sonik(), workers()],
})
Build command:
vite build
Deploy command:
wrangler deploy --compatibility-date 2023-08-01 --minify ./dist/index.js --name my-app
- Yusuke Wada https://github.com/yusukebe
MIT