How This Site Was Made
March 09, 2025
By: Mitch
Introduction
Ever wanted to build a website without relying on complex frameworks? In this blog, I'll walk you through how I built this site with Bun — blending static content and dynamic routes to serve this very blog post!
Bun as a static server
Bun is a modern JavaScript runtime designed for speed. It's an alternative to Node.js that aims to simplify development with built-in features like a fast HTTP server, native file system utilities, and first-class TypeScript support. At the most basic level, this site is built mostly on top of Bun's HTTP server. Recently, as of this blog post, Bun released an awesome update to this server which added support for static imports of HTML files in order to be bundled and served by Bun automatically.
Static content with Bun
When it comes to this site, most pages are handled by Bun's static imports. This
feature simplifies serving static content by letting you import .html
files
directly into your server code, reducing boilerplate. It is pretty simple to set
this up:
import { serve } from "bun";
import html from "./my-html-page.html";
serve({
port: 3000,
routes: {
"/*": html,
},
});
What is particularly cool about this feature is that it will also automatically transpile and bundle React so you can serve a single-page app all from one route on your server! In my case though, I didn't want an SPA. I wanted static content that I could serve without needing to render on the client side. To handle this, I could manually write all of the HTML boilerplate and markup myself, which would be pretty tedious the more pages I add, or I could still use React.
Handling more than one page
Since I wanted to keep the site simple and modular, I decided to stick with React. React's component model makes it easy to build reusable and composable components that I can use across different pages. However, this posed a new challenge: how do I render React components as static HTML without running any JavaScript on the client side?
To solve this, I built a custom static content generation solution. Given the
relatively simple nature of this site, I didn't want to rely on third-party
libraries or complex tools. The core of this solution involves rendering React
components into static HTML using renderToStaticMarkup
. Once the React
components are rendered as strings, I just insert them into a basic HTML
template. From there, I write the resulting HTML to a file that Bun can serve.
This allows me to create multiple static pages, each with its own content, and
easily expand the site by adding more pages.
// build.tsx
import { renderToStaticMarkup } from "react-dom/server";
function addContentToHTML(content: string) {
const html = `
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Static Site</title>
</head>
<body>
${content}
</body>
</html>
`;
return html;
}
const markup = {
home: <div>Hello world!</div>,
page2: <div>This is another page!</div>
}
for (const [name, jsx] of Object.entries(markup)) {
// render our JSX into HTML
const content = renderToStaticMarkup(jsx);
// write that HTML to a file
await Bun.write(`./dist/${name}.html`, addContentToHTML(content));
}
Once the build script runs and generates the HTML files, we can import and serve them with Bun.
import { serve } from "bun";
import home from "./dist/home.html";
import page2 from "./dist/page2.html";
serve({
port: 3000,
routes: {
"/": home,
"page2": page2,
},
});
And with that, you have a statically generated multi-page website built with React and Bun!
Blog posts using dynamic routes
As the site grows and more pages are added, maintaining static routes becomes more tedious. For example, if I want to add a blog, I don't want to manually create a new route in my Bun server for every post. To fix this, I decided to use dynamic routing for blog posts. Additionally, I wanted to write my posts using MDX, which lets me focus on the content without worrying about the underlying HTML or JSX structure.
To integrate MDX-based blog posts into the site, I read the MDX files and
compile them into React components that can be rendered into static HTML. The
MDX content is compiled using @mdx-js/mdx
into a function, which is then
executed to produce the corresponding React component. This allows me to render
the MDX content inside my static HTML structure.
Here’s how it works:
// build.tsx
import { MDXProvider } from "@mdx-js/react";
import { compile } from "@mdx-js/mdx";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { renderToStaticMarkup } from "react-dom/server";
// This manifest is generated automatically and imported but shown here for clarity
const blogManifest = {
"my-blog": "path-to-my-blog.mdx",
}
for (const [name, path] of Object.entries(blogManifest)) {
// Read the MDX file content
const mdxContent = await Bun.file(path).text();
// Compile the MDX content into a function
const compiled = await compile(mdxContent, { outputFormat: "function-body" });
// Create a new function to run the compiled code and export the React component
const { default: Blog } = new Function("runtime", String(compiled))({
Fragment,
jsx,
jsxs,
});
const content = renderToStaticMarkup(
<MDXProvider>
<Blog />
</MDXProvider>,
);
await Bun.write(`./dist/blogs/${name}.html`, addContentToHTML(content));
}
With the MDX content compiled into static HTML files, the next step is to handle
dynamic routes for each blog post. Instead of manually adding a new route for
every blog, we use dynamic routing in Bun. This allows us to define a single
route that can serve any blog post by its name, based on the :name
parameter in
the route path.
Here’s how the dynamic routing works:
import { serve } from "bun";
import home from "./dist/home.html";
import page2 from "./dist/page2.html";
serve({
port: 3000,
routes: {
"/": home,
"page2": page2,
"/blog/:name": async (req) => {
// Bun provides typesafe named params from the route path in the `req` object
const { name } = req.params;
const blog = Bun.file(`./dist/blogs/${name}.html`);
// Make sure it exists before attempting to serve it
if (await blog.exists()) {
return new Response(blog);
}
return new Response("Not found", { status: 404 });
}
},
});
And there you have it! Dynamic routes with statically generated content. While this is the bare minimum needed to get the site up and running, adding styles and improving the design will require additional steps. I've provided the foundation for that in this blog post so I'll leave that up to you to figure out as a challenge for yourself!
Benefits and drawbacks to this approach
As with any software solution, there are benefits and trade-offs to consider. For this site, the goal was to keep things simple by avoiding the use of an existing meta-framework, which comes with both advantages and challenges.
Benefits of a custom solution
Building my own static content generator offers several advantages:
- Fewer dependencies to manage
- Minimal boilerplate and a simple setup
- Customizability for future changes
- An opportunity to learn new techniques and approaches
Drawbacks to the custom approach
While building a custom static content generator offers flexibility, there are some drawbacks compared to using a meta-framework:
- Limited hot-module reloading: Bun supports hot reloading for static pages, but issues arise with rerendering and reloading at times.
- Lack of built-in features: Bun does not automatically handle bundling, minifying, or serving markup and CSS for dynamic pages (it does for the static imports though), requiring manual setup.
- Custom deployment setup: Unlike some frameworks that offer pre-configured deployment solutions, I had to set up my own deployment process for the site.
Conclusion
In this blog we've explored how to create a simple statically generated website using Bun, complete with dynamic routing for blog posts. We've covered the benefits and drawbacks to this approach, like having a simpler setup at the cost of maintenance and out-of-the-box feature support.
I hope I've inspired you to learn and try something like this for yourself! Thanks for giving it a read.
And don’t forget to check out Mitch's Pretty Awesome Computer Things LLC's catalog of awesome software — you might find something useful!