This website was made using futuristic internet technologies.
<= Back
)
```
The default layout automatically includes `src/css/main.css` and `src/client/main.ts`, wraps your page content in ``, `
`, and `...`.
### Accessing the request
Page components receive `c` (the Hono context) and `req` (the Hono request) as props:
```tsx
// src/pages/greet.tsx
export default ({ req }) => (
Hello, {req.query('name') ?? 'stranger'}!
)
```
Visit `/greet?name=Chris` to see `Hello, Chris!`.
For the full Hono context:
```tsx
// src/pages/debug.tsx
export default ({ c, req }) => {
const ua = req.header('user-agent')
return (
Request Info
URL: {req.url}
User-Agent: {ua}
)
}
```
---
## SPA Apps
For a single-page app with client-side rendering, disable the default layout and provide your own HTML shell:
```ts
// src/server/index.ts
import { Hype } from '@because/hype'
const app = new Hype({ layout: false })
export default app.defaults
```
```tsx
// src/pages/index.tsx
export default () => (
My SPA
)
```
```tsx
// src/client/app.tsx
import { render } from 'hono/jsx/dom'
import { useState } from 'hono/jsx/dom'
function App() {
const [count, setCount] = useState(0)
return (
<>
My SPA
Count: {count}
>
)
}
render(, document.getElementById('root')!)
```
The client-side app uses `hono/jsx/dom` for React-like rendering with hooks (`useState`, `useEffect`, etc.). Hype automatically transpiles and bundles `src/client/app.tsx` when the browser requests `/client/app.js`.
### Cache-busting in SPAs
Use the git hash to bust asset caches:
```tsx
// src/pages/index.tsx
import { $ } from 'bun'
const GIT_HASH = await $`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => 'dev')
export default () => (
)
```
### Passing server data to the client
Inject globals via an inline script:
```tsx
```
---
## Routing
Hype supports two routing layers that work together.
### File-based routing
Files in `src/pages/` are automatically mapped to GET routes:
| File | URL |
|------|-----|
| `src/pages/index.tsx` | `/` |
| `src/pages/about.tsx` | `/about` |
| `src/pages/contact.tsx` | `/contact` |
Files prefixed with `_` are private and won't be served (returns 404). This is used for layouts and other internal files.
### Custom routes
Since Hype extends Hono, you can define any route directly:
```ts
// src/server/index.ts
import { Hype } from '@because/hype'
const app = new Hype()
// JSON API
app.get('/api/users/:id', (c) => {
return c.json({ id: c.req.param('id'), name: 'Chris' })
})
// Form handling
app.post('/api/contact', async (c) => {
const body = await c.req.parseBody()
console.log('Message:', body.message)
return c.redirect('/thanks')
})
// Custom HTML
app.get('/custom', (c) => {
return c.html('
Custom page
')
})
export default app.defaults
```
Custom routes are defined before the file-based routes, so they take priority.
---
## Layouts
### Default layout
By default, Hype wraps every page in a simple HTML5 layout that includes:
- ``
- `` for responsive design
- `` for automatic dark mode
- `` to `/css/main.css` (your `src/css/main.css`)
- `
{children}
)
export default Layout
```
The layout receives:
- `children` — the rendered page content
- `title` — page title (defaults to `'hype'`)
- `props` — the `HypeProps` passed to the constructor (useful for conditional CSS)
### No layout
Disable the layout entirely for full control (used in SPA mode):
```ts
const app = new Hype({ layout: false })
```
---
## Pages
Pages are `.tsx` files in `src/pages/` that export a default function (or raw JSX).
### Function export (recommended)
```tsx
// src/pages/index.tsx
export default ({ c, req }) => (
)
```
### Async data in pages
Since pages render on the server, you can use top-level await:
```tsx
// src/pages/users.tsx
const users = await fetch('https://api.example.com/users').then(r => r.json())
export default () => (
Users
{users.map(u =>
{u.name}
)}
)
```
Note: top-level data is fetched once at import time and cached. For per-request data, use the `c` context:
```tsx
// src/pages/profile.tsx
export default async ({ c, req }) => {
const userId = req.query('id')
const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json())
return (
{user.name}
)
}
```
### Private pages
Prefix a file with `_` to prevent it from being served:
```
src/pages/_layout.tsx # not a route — used as the layout
src/pages/_helpers.tsx # not a route — internal helpers
src/pages/index.tsx # GET /
```
---
## Client-Side JavaScript
### Automatic transpilation
TypeScript files in `src/client/` and `src/shared/` are automatically transpiled and bundled by Bun when requested by the browser. The URL maps directly to the file path:
| File | URL |
|------|-----|
| `src/client/main.ts` | `/client/main.ts` |
| `src/client/app.tsx` | `/client/app.js` |
| `src/shared/utils.ts` | `/shared/utils.ts` |
You can request `.ts` or `.js` extensions — Hype resolves `.ts` and `.tsx` files automatically.
### Module imports
Client-side files can import from each other using relative paths:
```ts
// src/client/main.ts
import { initBurger } from './burger'
initBurger()
```
```ts
// src/client/burger.ts
export function initBurger() {
document.addEventListener('click', (ev) => {
const el = (ev?.target as HTMLElement).closest('.burger') as HTMLImageElement
if (!el) return
el.src = '/img/bite.png'
})
}
```
Imports are bundled — the full dependency graph is included in the output, so the browser only needs one request.
### The default layout auto-includes `main.ts`
When using the default layout, `src/client/main.ts` is automatically loaded as a module. Just create the file and it works.
---
## Styling
### External CSS
Put your styles in `src/css/main.css`. The default layout auto-includes it:
```css
/* src/css/main.css */
section {
max-width: 500px;
margin: 0 auto;
}
```
You can also serve additional CSS files from `src/css/`:
```html
```
### Pico CSS
Enable the bundled [Pico CSS](https://picocss.com) for classless styling:
```ts
const app = new Hype({ pico: true })
```
Or include it in a custom layout:
```html
```
### CSS Reset
Enable the bundled CSS reset (Josh W. Comeau's reset):
```ts
const app = new Hype({ reset: true })
```
Or include it in a custom layout:
```html
```
### Combining options
```ts
const app = new Hype({ pico: true, reset: true })
```
---
## Server-Sent Events (SSE)
Hype provides `app.sse()` for streaming data to the browser.
### Server
```ts
// src/server/index.ts
import { Hype } from '@because/hype'
const app = new Hype()
// Stream the current time every second
app.sse('/api/time', (send) => {
send({ time: Date.now() })
const interval = setInterval(() => send({ time: Date.now() }), 1000)
return () => clearInterval(interval) // cleanup on disconnect
})
export default app.defaults
```
The `send` function:
- Automatically `JSON.stringify`s objects; strings are sent as-is
- Accepts an optional second argument for named events: `send(data, 'eventName')`
The handler receives `(send, c)` where `c` is the Hono context:
```ts
app.sse('/api/user-events', (send, c) => {
const userId = c.req.query('userId')
// subscribe to user-specific events...
})
```
Return a cleanup function to handle client disconnection.
### Client
```tsx
// src/pages/sse.tsx
export default () => (
SSE Demo
)
```
```ts
// src/client/main.ts
const timeEl = document.getElementById('time')
const events = new EventSource('/api/time')
events.onmessage = (e) => {
const data = JSON.parse(e.data)
timeEl!.textContent = new Date(data.time).toLocaleTimeString()
}
events.onerror = () => {
timeEl!.textContent = 'Disconnected'
}
```
### Named events
```ts
// Server
app.sse('/api/feed', (send) => {
send({ type: 'user_joined', name: 'Chris' }, 'activity')
send({ message: 'Hello everyone!' }, 'chat')
})
```
```js
// Client
const events = new EventSource('/api/feed')
events.addEventListener('activity', (e) => {
console.log('Activity:', JSON.parse(e.data))
})
events.addEventListener('chat', (e) => {
console.log('Chat:', JSON.parse(e.data))
})
```
### Test with curl
```sh
curl -N http://localhost:3000/api/time
```
---
## Custom API Routes
Since Hype extends Hono, use any Hono routing method:
```ts
import { Hype } from '@because/hype'
const app = new Hype()
// GET with params
app.get('/api/users/:id', (c) => {
return c.json({ id: c.req.param('id') })
})
// POST with body parsing
app.post('/api/users', async (c) => {
const body = await c.req.json()
return c.json({ created: true, name: body.name }, 201)
})
// Form submission
app.post('/contact', async (c) => {
const form = await c.req.parseBody()
console.log(form.email, form.message)
return c.redirect('/')
})
// Delete
app.delete('/api/users/:id', (c) => {
return c.json({ deleted: true })
})
export default app.defaults
```
### Redirect back
Use `redirectBack()` to redirect to the referrer:
```ts
import { Hype, redirectBack } from '@because/hype'
const app = new Hype()
app.post('/api/like', async (c) => {
// ... handle the like
return redirectBack(c, '/') // falls back to '/' if no referrer
})
```
---
## Sub-Routers
Use `Hype.router()` to create sub-routers without duplicate middleware:
```ts
// src/server/index.ts
import { Hype } from '@because/hype'
import api from './api'
const app = new Hype()
app.route('/api', api)
export default app.defaults
```
```ts
// src/server/api.ts
import { Hype } from '@because/hype'
const api = Hype.router()
api.get('/users', (c) => c.json([{ id: 1, name: 'Chris' }]))
api.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
api.post('/users', async (c) => {
const body = await c.req.json()
return c.json(body, 201)
})
export default api
```
`Hype.router()` creates a Hype instance that skips middleware registration (no duplicate logging, static file serving, etc.).
---
## Static Files
Files in `pub/` are served at the root URL:
```
pub/img/logo.png => /img/logo.png
pub/favicon.ico => /favicon.ico
pub/robots.txt => /robots.txt
```
Use them in pages:
```tsx
export default () => (
)
```
---
## Configuration
### Constructor options
```ts
const app = new Hype({
pico: true, // Include Pico CSS (default: false)
reset: true, // Include CSS reset (default: false)
prettyHTML: true, // Pretty-print HTML in dev (default: true in dev, false in prod)
layout: true, // Wrap pages in layout (default: true)
logging: true, // HTTP request logging (default: true)
ok: true, // Add GET /ok healthcheck (default: false)
})
```
### `app.defaults`
Always export `app.defaults` as the default export. It returns a Bun.serve-compatible config object:
```ts
export default app.defaults
// => { port, fetch, idleTimeout: 255 }
```
This triggers lazy route registration and auto-port selection. Bun picks it up as the server config when the file is the entry point.
---
## Environment Variables
| Variable | Effect |
|----------|--------|
| `PORT` | Server port (default: `3000`) |
| `NODE_ENV` | Set to `production` to disable auto-port, HTML prettification, and enable transpile caching |
| `NO_AUTOPORT` | Disable auto-port selection even in dev |
In dev mode, if port 3000 is busy, Hype automatically tries 3001, 3002, etc. (up to 100 attempts).
---
## Path Aliases
The recommended `tsconfig.json` defines three path aliases:
| Alias | Maps to | Use for |
|-------|---------|---------|
| `$*` | `src/server/*` | Server-only imports |
| `#*` | `src/client/*` | Client-only imports |
| `@*` | `src/shared/*` | Isomorphic imports |
```ts
// In server code:
import { db } from '$db' // => src/server/db.ts
// In client code:
import { format } from '@utils' // => src/shared/utils.ts
```
---
## Utility Functions
Hype exports a collection of helpers from `@because/hype` (or `@because/hype/utils`):
### Randomness
```ts
import { rand, randRange, randItem, randIndex, shuffle, weightedRand, randomId } from '@because/hype'
rand() // 1 or 2 (coin flip)
rand(6) // 1-6 (roll a die)
rand(20) // 1-20 (d20)
randRange(5, 10) // 5-10 inclusive
randItem(['a', 'b', 'c']) // random element
randIndex(['a', 'b', 'c']) // random index (0, 1, or 2)
shuffle([1, 2, 3, 4, 5]) // shuffled copy
weightedRand() // 1-10, lower numbers more likely
randomId() // e.g. "k7x2m1"
```
### Arrays
```ts
import { times, unique } from '@because/hype'
times(5) // [1, 2, 3, 4, 5]
unique([1, 1, 2, 2, 3, 3]) // [1, 2, 3]
```
### Colors
```ts
import { lightenColor, darkenColor } from '@because/hype'
lightenColor('#3498db', 0.5) // blend halfway to white
darkenColor('#3498db', 0.5) // blend halfway to black
```
### Strings
```ts
import { capitalize } from '@because/hype'
capitalize('hello') // "Hello"
```
### Dark mode detection (client-side)
```ts
import { isDarkMode } from '@because/hype'
if (isDarkMode()) {
// user prefers dark mode
}
```
### Transpilation
```ts
import { transpile } from '@because/hype'
const js = await transpile('./src/client/app.tsx')
// => bundled ESM JavaScript string
```
---
## Recipes
### Form with POST handler
```ts
// src/server/index.ts
import { Hype, redirectBack } from '@because/hype'
const app = new Hype()
const messages: string[] = []
app.post('/api/message', async (c) => {
const { message } = await c.req.parseBody()
if (typeof message === 'string') messages.push(message)
return redirectBack(c)
})
app.get('/api/messages', (c) => c.json(messages))
export default app.defaults
```
```tsx
// src/pages/index.tsx
export default () => (
Guestbook
)
```
### Healthcheck endpoint
```ts
const app = new Hype({ ok: true })
// GET /ok => "ok" (200)
```
### SSR with Pico CSS
```ts
const app = new Hype({ pico: true, reset: true })
```
All your pages instantly get clean, classless styling. Just write semantic HTML.
### Multiple SSE channels
```ts
const app = new Hype()
// Chat messages
app.sse('/api/chat', (send, c) => {
const room = c.req.query('room') ?? 'general'
// subscribe to chat room...
return () => { /* unsubscribe */ }
})
// Live notifications
app.sse('/api/notifications', (send, c) => {
send({ type: 'connected' })
const check = setInterval(async () => {
// check for new notifications...
send({ count: 5 }, 'notification')
}, 5000)
return () => clearInterval(check)
})
```
### Mixing SSR pages with a JSON API
```ts
import { Hype } from '@because/hype'
const app = new Hype({ pico: true })
// API routes (JSON)
app.get('/api/data', (c) => c.json({ items: [1, 2, 3] }))
// SSR pages still work via src/pages/
// Custom SSR route:
app.get('/dashboard', (c) => {
return c.html(