Enzo is a web application framework that focuses heavily on server side rendering through JSX templating. We use Preact as a server side JSX engine and also give you the ability to use Preact components on the frontend through Hydration.
To avoid page refreshes and add interactivity we lean on HTMX to handle updating the DOM as users interact with your application.
Moving most of your logic from the frontend to the backend reduces a lot of complexity introduced by libraries like React. The problem is most server side templates engines can't match the developer friendliness of JSX and its clean way of seperating UI into components.
So we're taking the best parts of React and using them exclusively on the server. But what about all the great interactivity that React gives us? This is where HTMX comes in.
React at a high level updates the dom based on state changes. The dom updating can be a bit of a blackbox that works great but isn't easy to understand or optimize for. HTMX works on a much simpler idea. When user interactions happen that require DOM updates, we fetch HTML from the server and replace DOM on the frontend. No diffing, no shadow dom. A predicatable system for creating interactive UI's.
The last thing we need is an ultra fast runtime to make all this server side logic run as fast as possible. There is where Bun comes in. Bun will keep request times down and make working with and compiling typescript code a breeze.
- File Router for components in
/src/pages
- Form Alert Message system driven by the session
- Automatic Wrapping/Unwrapping of page components from the index.html
- Hono for routing
- Preact as a JSX templating engine
- HTMX for frontend interactivity
- Tailwind for styling
- Zod for form validation
- Running on Bun
Clone repo
git clone https://github.com/travierm/enzo.git enzo
Install deps:
cd enzo/ && bun install
Clone .env.example
cp ./.env.example .env
To development server on localhost:3000:
bun dev
app.post("/login", async (c) => {
const body = await validateForm(
c,
z.object({
email: z.string(),
password: z.string(),
})
);
if (!body.success) {
return c.redirect("/login");
}
try {
await handleAuth(c, body.data.email, body.data.password);
} catch (e) {
await createAlert(c, {
type: "error",
message: "Invalid email or password",
});
return c.redirect("/login");
}
return c.redirect("/");
});
Create a component and let Enzo know you will need it hydrated
import { applyHydration } from "@/core/applyHydration";
import { useState } from "preact/hooks";
type Props = {
name: string;
};
const CounterComponent = (props: Props) => {
const [count, setCount] = useState(0);
return (
<div>
<h1>{props.name}</h1>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
export const Counter = applyHydration(CounterComponent);
Add you client side component to your server side page
Note: You will be limited to passing props that can be parsed via JSON.parse on the client side. Functions for instance can not be passed in as a prop.
export default function Blog() {
return (
<Layout>
<div class="flex items-center justify-center">
<h1 class="text-2xl mt-4">Blog Page</h1>
<Counter name="My Counter" />
<Counter name="My Counter2" />
</div>
</Layout>
);
}
Tell your client to hydrate the component
import { Counter } from "./Counter";
import { hydrateComponent } from "@/core/applyHydration";
hydrateComponent(Counter);
document.addEventListener("htmx:afterSwap", () => {
// hydrate components after htmx swaps
hydrateComponent(Counter);
});
You must fully write out tailwind classes in order for Tailwind to pick them up on scan
// this works
export function Example() {
const colorMap = {
info: "bg-blue-600",
};
const color = colorMap["info"];
return <div class={`${color}`}></div>
}
// this will fail to have classes generated for it
const color = 'blue';
return <div class={`bg-${color}-500`}>