Hono - HTML

Learn how to serve HTML content and use JSX/TSX with Hono

Introduction

Hono - means flame🔥 in Japanese - is a small, simple, and ultrafast web framework built on Web Standards.

Project

Let’s create a new Hono project using Bun.

A starter for Netlify is available.

Start your project with “create-hono” command selecting netlify template:

Terminal window
bun create hono --template netlify hono-html

Move into hono-html directory.

Add dependencies:

Terminal window
bun add hono

Edge function

The file netlify/edge-functions/index.ts contains the code for your edge function.

This is where you will write your Hono application.

Update the import statements (delete @jsr):

import { Hono } from 'hono'
import { handle } from 'hono/netlify'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
export default handle(app)
  • import { Hono } from 'hono' imports the core Hono class from the framework. This is the entry point for building any Hono application: it provides the methods to define routes (app.get(), app.post(), etc.) and middleware.
  • import { handle } from 'hono/netlify' imports the Netlify adapter. The handle function wraps your Hono app so it can run as a Netlify Edge Function, translating between Netlify’s request/response format and Hono’s.
  • new Hono() creates a new application instance where you register all your routes.
  • app.get('/', (c) => ...) registers a handler for GET requests to the root path /. The callback receives a context object c, which provides methods to build responses: c.text() returns a plain text response, c.html() returns HTML, c.json() returns JSON, etc.
  • export default handle(app) exports the adapted app as the default export, which is what Netlify expects from an edge function.

Server

Run the development server with Netlify CLI.

Terminal window
netlify dev

Then, access http://localhost:8888 in your Web browser.

You should see “Hello Hono!” displayed on the page.

HTML

If you update the previous code to return <h1>Hello Hono!</h1>, you will see the tags as text on the page, not as HTML.

app.get('/', (c) => {
return c.text('<h1>Hello Hono!</h1>')
})

Instead of returning a string, you can return an HTML document using the c.html() method.

app.get('/', (c) => {
return c.html('<h1>Hello Hono!</h1>')
})

This method is a helper function that allows you to write HTML using template literals, sets the Content-Type header to text/html, and sends the provided HTML string as the response body.

Parameters

Update the code to include a dynamic route that takes a username parameter and returns a personalized greeting.

app.get('/student/:username', (c) => {
const {username} = c.req.param()
return c.html(`<h1>Hello, ${username}!</h1>`)
})
  • '/student/:username' defines a route with a dynamic parameter. The :username segment acts as a placeholder — if a user visits /student/alice, Hono captures "alice" as the value of username.
  • c.req.param() returns an object with all the dynamic parameters from the URL. Here, we use destructuring ({username}) to extract the username value directly.
  • c.html(`<h1>Hello, ${username}!</h1>`) sends an HTML response with the username value interpolated into the template literal.

Access http://localhost:8888/student/alice in your Web browser and you should see “Hello, alice!” displayed on the page.

TSX

You can write HTML with TSX syntax with hono/jsx rendering content on the server side.

Think of it like moving from writing everything on one big sheet of paper to using pre-built Lego blocks.

To use TSX, rename your file to src/index.tsx.

Note

You should additionally modify the wrangler.jsonc file to reflect that change: Update "main" entry of "src/index.tsx"

// A separate View component
const View = () => {
return (
<html>
<body>
<h1>Hello Hono!</h1>
<p>This is rendered using TSX components.</p>
</body>
</html>
)
}
app.get('/page', (c) => {
// We can just return the component inside c.html()
return c.html(<View />)
})
  • Components are like custom tags. View is a function that returns the structure of our page.
  • JSX (the HTML-like code) allows us to write our interface naturally within JavaScript.
  • c.html(<View />) automatically renders the component into a string and sets the correct Content-Type header for the browser.

Usage

import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
const Layout: FC = (props) => {
return (
<html>
<body>{props.children}</body>
</html>
)
}
const Top: FC<{ messages: string[] }> = (props: {
messages: string[]
}) => {
return (
<Layout>
<h1>Hello Hono!</h1>
<ul>
{props.messages.map((message) => {
return <li>{message}!!</li>
})}
</ul>
</Layout>
)
}
app.get('/message', (c) => {
const messages = ['Good Morning', 'Good Evening', 'Good Night']
return c.html(<Top messages={messages} />)
})
export default app
  • Layouts can be created by accepting children. This allows you to have a consistent look and feel across different pages.
  • FC is a type that stands for “Function Component”, making it easy to define your components with TypeScript.
  • Props allow you to pass data into your components, like the list of messages in the example.

PropsWithChildren

Imagine a component is a box. PropsWithChildren is a special type that ensures the box has a designated spot for “extras”—any content you put between the opening and closing tags of your component.

import { PropsWithChildren } from 'hono/jsx'
type PostProps = {
id: number
title: string
}
function Post({ title, children }: PropsWithChildren<PostProps>) {
return (
<div>
<h1>{title}</h1>
{children}
</div>
)
}
  • PropsWithChildren is a helper type that automatically adds the children property to your props.
  • It makes your components reusable wrappers. You can put anything inside the <Post> tags, and it will show up where {children} is placed.

Memoization

If you have a component that does a lot of work but usually returns the same result, you can use memo. It’s like a student who memorizes the answer to a hard math problem so they don’t have to calculate it again next time.

import { memo } from 'hono/jsx'
const Header = memo(() => (
<header>
<h1>Hono Framework</h1>
</header>
))
  • memo stores the rendered output of a component.
  • If the props don’t change, Hono will reuse the stored output instead of re-rendering, making your app faster.

Metadata hoisting

You can write document metadata tags such as <title>, <link>, and <meta> directly inside your components. These tags will be automatically hoisted to the <head> section of the document. This is especially useful when the <head> element is rendered far from the component that determines the appropriate metadata.

import { Hono } from 'hono'
const app = new Hono()
app.use('*', async (c, next) => {
c.setRenderer((content) => {
return c.html(
<html>
<head></head>
<body>{content}</body>
</html>
)
})
await next()
})
app.get('/about', (c) => {
return c.render(
<>
<title>About Page</title>
<meta name='description' content='This is the about page.' />
about page content
</>
)
})
export default app
Note

When hoisting occurs, existing elements are not removed. Elements appearing later are added to the end. For example, if you have <title>Default</title> in your <head/> and a component renders <title>Page Title</title>, both titles will appear in the head.

Fragment

Use Fragment to group multiple elements without adding extra nodes:

import { Fragment } from 'hono/jsx'
const List = () => (
<Fragment>
<p>first child</p>
<p>second child</p>
<p>third child</p>
</Fragment>
)

Or you can write it with <></> if it sets up properly.

const List = () => (
<>
<p>first child</p>
<p>second child</p>
<p>third child</p>
</>
)

Context

Context is like a radio broadcast. You broadcast information (the Provider) and any component in your app can “tune in” to hear it (the Consumer), no matter how deep it is in the component tree.

import { createContext, useContext } from 'hono/jsx'
const ThemeContext = createContext('light')
const Button = () => {
const theme = useContext(ThemeContext)
return <button class={theme}>Click Me</button>
}
app.get('/context', (c) => {
return c.html(
<ThemeContext.Provider value="dark">
<Button />
</ThemeContext.Provider>
)
})
  • createContext creates the “radio station”.
  • Provider sets the value being broadcast.
  • useContext is how a component “listens” to the broadcast.

Context

You can access the Netlify’s Context through c.env:

import { Hono } from 'hono'
import { handle } from 'hono/netlify'
// Import the type definition
import type { Context } from 'https://edge.netlify.com/'
export type Env = {
Bindings: {
context: Context
}
}
const app = new Hono<Env>()
app.get('/country', (c) =>
c.json({
'You are in': c.env.context.geo.country?.name,
})
)
export default handle(app)

Deploy

You can deploy with a netlify deploy command.

Terminal window
netlify deploy --prod

Exercises

Now it’s your turn to be the senior developer!

Task

Create a route /welcome/:name that returns an HTML page with:

  • An h1 header saying “Welcome, [name]!”
  • A background color that is different from white (use inline CSS).
  • A link a back to the home page /.
The Magic Box (PropsWithChildren)

Create a Box component using PropsWithChildren that:

  • Accepts a title prop (string).
  • Wraps its children in a section tag with a border.
  • Use it in a route /box to wrap some “Secret Information”.
The Theme Radio (Context)

Use Context to pass a “Theme” value (“light” or “dark”) down to a button.

  • Create a ThemeContext.
  • Create a ThemeButton component that consumes this context to set its class name.
  • In a route /theme, provide the value “dark” and render the button.
The SEO Specialist (Hoisting and Fragment)

Create a component that renders a title tag and a meta description, then use it inside a Fragment with some text.

  • Verify that the title appears in the head even if it’s deeply nested.
Safety and Raw Check

Try to pass some HTML tags into your /welcome/:name route via the URL.

  • Verify that Hono safely escapes them.
  • Now, create a route that uses the raw() helper to render that same input. What’s the risk?
The Speed Demon (Memoization)

Wrap a StaticFooter component in memo.

  • When should you use memo? Why is it useful for components that never change?

Final Project: The Student Portal

Now, let’s put everything together. We’re going to build a small Student Directory. This project will use layouts, components, dynamic routes, and metadata hoisting.

Student Directory
  1. The Layout: Create a MainLayout component that takes children and wraps them in a consistent HTML structure.
  2. The Data: Create a list of student objects (id, name, bio).
  3. The List Page: Create a route /students that displays a list of all students as links.
  4. The Profile Page: Create a dynamic route /students/:id that displays the student’s bio and uses metadata hoisting to set the page title to the student’s name.