I’ve always loved how simple HTML is. Just open a file, write some tags, and you’ve got a page. No configs, no dependencies, no ceremony.
But the moment you try to do something slightly more advanced — like reusing layouts or building components — HTML shows its limits. You either end up:
- Copy-pasting code all over the place (not fun 😅)
- Or dragging in a full-blown framework like React or Vue (also not fun for small projects) So I started wondering: what if plain HTML could be just a little more dynamic?
That question turned into a little side project I’ve been hacking on: Zin Engine.
What Is Zin Engine?
It’s not a product. It’s not trying to compete with frameworks. It’s just me experimenting with how far I can stretch HTML with a minimal templating engine and server written in Go that gives plain old HTML some new tricks.
The idea is simple:
- Write regular HTML,
- Add a sprinkle of templating magic,
- And suddenly you can reuse layouts and build small dynamic sites… without leaving HTML.
Key Features
- 🗂️ Use plain .html files as your only source of truth — no framework or build step required
- 🔧 Dynamic rendering with custom tags
- 📊 Render content from MySQL, JSON, CSV, Google Sheets (), APIs, and even .txt files
- 🧩 Built-in support for layouts using template.html
- 🔗 Clean URLs like /about → about.html
- 🛠 Smart handling of 404/500 pages, .env files, and .zinignore
- ⚡ Zero client-side JS — everything renders server-side in Go
- 🌐 Runs on port 9001 by default (easy to use behind NGINX)
Here’s a quick taste 👇
1. Using Layouts, Including Partials, Markdown, and Text
One of Zin’s best tricks is letting you define a single base layout (template.html) that all your pages automatically inherit. That way, your header, sidebar, and footer stay consistent, and only the middle content changes.
The <zin-include />
tag isn’t just for HTML snippets like a navbar or footer — it can also pull in Markdown (.md) and text (.txt) files. Markdown gets parsed into HTML automatically, so you can write docs in .md and drop them right into your layout.
Example base template (template.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zin Engine Docs</title>
...
</head>
<body>
<header>
<zin-include file="common/navbar.html" />
</header>
<main>
<!-- Sidebar from HTML -->
<zin-include file="common/sidebar.html" />
<div class="content">
{{ .children }}
</div>
</main>
<footer>
<!-- Inject .md file into content -->
<zin-include file="common/footer.html" />
</footer>
</body>
</html>
Then, let’s say you create a page at /index.html:
<h1>Getting Started</h1>
<p>Welcome to the Zin Engine docs! 🚀</p>
<!-- Inject .md file (as parsed HTML) -->
<zin-include file="data/examples.md" />
<!-- Inject .txt file (injected as-is) -->
<zin-include file="data/quote.txt" />
When you visit http://localhost:9001
, Zin will automatically wrap that page inside template.html, replacing {{ .children }}
with the content of index.html all stitched together seamlessly.
So the final rendered page looks like your base template, but with the page (index.html in this example) content injected into the content area.
2. Pull in Data Anywhere 📊
Zin makes it super easy to load data from multiple sources — files, APIs, databases, even Google Sheets:
<!-- Load from file -->
<zin-data src="file://data/users.csv" as="users" />
<!-- Load from external API -->
<zin-data src="https://jsonplaceholder.typicode.com/users" as="apiData" />
<!-- Load from MySQL -->
<zin-data src="mysql://SELECT * FROM users" as="dbUsers" />
<!-- Load from Google Sheets using ENV var -->
<zin-data src="sheets://SELECT * FROM {{process.env.SHEET_ID}}.Sheet1" as="sheetUsers" />
<!-- Show any single value -->
<p>App Title: {{process.env.APP_NAME}}</p>
<p>User Name: {{users[0].name || "Guest"}}</p>
<!-- Loop through users -->
<zin-repeat for="users">
<div class="user-card">
<h3>{{name}}</h3>
<p>Email: {{email}}</p>
<p>Phone: {{phone}}</p>
</div>
</zin-repeat>
You can feed Zin data in multiple ways — then loop through it or drop values directly into your HTML.
3. Handle Time Like a Pro ⏰
Working with dates and times usually means importing a whole library. In Zin, it’s just a tag:
<!-- Default (now, datetime, unix ms) -->
<zin-time />
<!-- Tomorrow in 12-hour format -->
<zin-time when="tomorrow" view="datetime:12" />
<!-- 3 weeks ago from today -->
<zin-time when="today -3w" view="dayname" />
<!-- First day of next month -->
<zin-time when="now +1m @startOf:month" view="date" />
<!-- Now in Tokyo timezone -->
<zin-time view="time:12" tz="Asia/Tokyo" />
Need a timestamp, or the first day of next month? Done in one line.
4. Build Forms Without Pain 📝
Zin even helps with forms, including validation and Google reCAPTCHA (Optional):
<zin-form name="contact-form1" action="{{process.env.CONTACT_FORM}}">
<div class="data-field">
<label for="name">Your Name</label>
<input name="name">
</div>
<div class="data-field">
<label for="email">Email</label>
<input required name="email" data-validator="required|email">
</div>
<div class="data-field">
<label for="phone">Phone</label>
<input name="phone" data-validator="mobile">
</div>
<button type="submit">Submit</button>
</zin-form>
You get clean HTML, built-in validation, and secure handling without writing extra JavaScript.
And that’s just scratching the surface — Zin Engine also has tags for crypto, random values, environment variables, and more. You can check out the full list on the GitHub repo.
Why I Built This
Honestly? Curiosity.
I love HTML for its simplicity, but I often found myself wishing I could do just a little more without setting up a whole toolchain. Zin Engine was my way of experimenting with that idea.
It’s definitely not production-ready, and it’s not meant to compete with frameworks. It’s just a fun exploration into how far plain HTML can go if we give it a few extra powers.
Check It Out
If you want to play with it, the code’s here:
👉 Zin Engine on GitHub
I’d love feedback from fellow devs — whether you think this is neat, weird, or a bad idea altogether 😅.
Top comments (0)