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:
Move into hono-html directory.
Add dependencies:
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):
const app =
app. return c.
})
import { Hono } from 'hono'imports the coreHonoclass 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. Thehandlefunction 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 forGETrequests to the root path/. The callback receives a context objectc, 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.
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. return c.
})Instead of returning a string, you can return an HTML document using the c.html() method.
app. return c.
})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. const = c..
return c.
})'/student/:username'defines a route with a dynamic parameter. The:usernamesegment acts as a placeholder – if a user visits/student/alice, Hono captures"alice"as the value ofusername.c.req.param()returns an object with all the dynamic parameters from the URL. Here, we use destructuring ({username}) to extract theusernamevalue directly.c.html(`<h1>Hello, ${username}!</h1>`)sends an HTML response with theusernamevalue 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.
You should additionally modify the wrangler.jsonc file to reflect that change: Update "main" entry of "src/index.tsx"
// A separate View component
const return <html>
<body>
<h1>Hello Hono!</h1>
<p>This is rendered using TSX components.</p>
</body>
</html>
)
}
app. // We can just return the component inside c.html()
return c.})- Components are like custom tags.
Viewis 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 correctContent-Typeheader for the browser.
Usage
from 'hono/jsx'
const app =
const Layout: return <html>
<body></body>
</html>
)
}
const Top: FC<> = messages: string
}) => return <Layout>
<h1>Hello Hono!</h1>
<ul>
return <li>!!</li>
})}
</ul>
</Layout>
)
}
app. const messages =
return c.})
- 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.
type PostProps = id: number
title: string
}
return <div>
<h1></h1>
</div>
)
}PropsWithChildrenis a helper type that automatically adds thechildrenproperty 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.
const Header = <header>
<h1>Hono Framework</h1>
</header>
))memostores 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.
const app =
app. c. return c. <html>
<head></head>
<body></body>
</html>
)
})
await
})
app. return c. <>
<title>About Page</title>
<meta name='description' content='This is the about page.' />
about page content
</>
)
})
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:
const <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 <>
<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.
const ThemeContext =
const const theme =
return <button >Click Me</button>
}
app. return c. <ThemeContext. value="dark">
<Button />
</ThemeContext.Provider>
)
})createContextcreates the “radio station”.Providersets the value being broadcast.useContextis how a component “listens” to the broadcast.
Context
You can access the Netlify’s Context through c.env:
// Import the type definition
from 'https://edge.netlify.com/'
Env = Bindings: context: Context
}
}
const app = <Env>
app. c. 'You are in': c....?.,
})
)
Deploy
You can deploy with a netlify deploy command.
Exercises
Now it’s your turn to be the senior developer!
Create a route /welcome/:name that returns an HTML page with:
- An
h1header saying “Welcome, [name]!” - A background color that is different from white (use inline CSS).
- A link
aback to the home page/.
Show solution
app. const = c..
return c. html`<!doctype html>
<body style="background-color: lightblue;">
<h1>Welcome, $!</h1>
<a href="/">Go Home</a>
</body>`
)
})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”.
Show solution
const <section style=}>
<h2></h2>
</section>
)
app. return c. <Box title="Secret Information">
<p>The password is 'hono-is-awesome'.</p>
</Box>
)
})Use Context to pass a “Theme” value (“light” or “dark”) down to a button.
- Create a
ThemeContext. - Create a
ThemeButtoncomponent that consumes this context to set its class name. - In a route
/theme, provide the value “dark” and render the button.
Show solution
const ThemeContext =
const const theme =
return <button >I am !</button>
}
app. return c. <ThemeContext. value="dark">
<ThemeButton />
</ThemeContext.Provider>
)
})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.
Show solution
const <>
<title>Hono SEO Mastery</title>
<meta name="description" content="Mastering Hono metadata hoisting!" />
</>
)
app. return c. <>
<SEO />
<h1>Check my head!</h1>
<p>The title and description should be hoisted.</p>
</>
)
})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?
Show solution
- Hono escapes values by default, so tags show as literal text in the browser.
- If you use raw(), the browser will render the tags.
- The Risk: If a user sends a script tag, the script will run in the browser of anyone visiting that URL. Never use raw() with untrusted user input!
Wrap a StaticFooter component in memo.
- When should you use memo? Why is it useful for components that never change?
Show solution
const StaticFooter = <footer>
<p>© 2024 Hono Academy</p>
</footer>
))- When: Use it for components that render the same output for the same props.
- Why: It skips the rendering phase and reuses the previous output, saving CPU cycles on the server.
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.
- The Layout: Create a MainLayout component that takes children and wraps them in a consistent HTML structure.
- The Data: Create a list of student objects (id, name, bio).
- The List Page: Create a route /students that displays a list of all students as links.
- 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.
Show solution
from 'hono/jsx'
const app =
// 1. The Layout Component
const MainLayout: return <html>
<head>
<title>School Portal</title>
</head>
<body style=}>
<header style=}>
<h1>School Portal</h1>
</header>
<main></main>
<footer><hr/><p>Powered by Hono</p></footer>
</body>
</html>
)
}
// 2. Sample Data
const students = ,
,
]
// 3. List Page
app. return c. <MainLayout>
<h2>All Students</h2>
<ul>
<li key=><a href=`}>{s.name}</a></li>
))}
</ul>
</MainLayout>
)
} )
// 4. Profile Page
app. const id = c..
const student = students.
return c.
return c. <MainLayout>
<title>'s Profile</title>
<div style=}>
<h2></h2>
<p></p>
<a href="/students">Back to list</a>
</div>
</MainLayout>
)
})