Skip to content

Commit 9d8391a

Browse files
committed
feat(site): ship release dashboard and mercss upgrades
1 parent e341808 commit 9d8391a

11 files changed

Lines changed: 2157 additions & 407 deletions

File tree

examples/site/app/index.zig

Lines changed: 16 additions & 300 deletions
Original file line numberDiff line numberDiff line change
@@ -1,310 +1,26 @@
1+
const std = @import("std");
12
const mer = @import("mer");
2-
const h = mer.h;
3+
4+
const version = mer.version;
5+
const release_path = "/v" ++ version;
6+
const page_css = @embedFile("page.css");
7+
const page_template = @embedFile("page.html");
38

49
pub const meta: mer.Meta = .{
5-
.title = "merjs \u{2014} A Zig-native web framework",
6-
.description = "A Next.js competitor written in Zig. Zero Node.js, zero node_modules. SSR, file-based routing, type-safe APIs, and WASM for client interactivity.",
7-
.og_title = "merjs \u{2014} A Zig-native web framework. No Node. No npm. Just WASM.",
8-
.og_description = "A Next.js competitor written in Zig. Zero Node.js, zero node_modules. SSR, file-based routing, type-safe APIs, and WASM for client interactivity.",
10+
.title = "merjs - A Zig-native web framework",
11+
.description = "File-based routing, SSR, typed APIs, and WASM client logic. A web framework built in Zig with zero Node.js runtime.",
12+
.og_title = "merjs - Zig-native web framework",
13+
.og_description = "File-based routing, SSR, typed APIs, and WASM client logic. No Node. No npm. Just Zig.",
914
.og_url = "https://merlionjs.com",
1015
.twitter_card = "summary_large_image",
11-
.twitter_title = "merjs \u{2014} A Zig-native web framework",
12-
.twitter_description = "Zero Node.js. Zero node_modules. Pure Zig all the way down.",
16+
.twitter_title = "merjs - Zig-native web framework",
17+
.twitter_description = "A web framework built in Zig with zero Node.js runtime.",
1318
.extra_head = "<style>" ++ page_css ++ "</style>",
1419
};
1520

16-
const page_node = page();
17-
comptime {
18-
mer.lint.check(page_node);
19-
}
20-
2121
pub fn render(req: mer.Request) mer.Response {
22-
return mer.render(req.allocator, page_node);
22+
const html = std.fmt.allocPrint(req.allocator, page_template, .{ version, release_path, version, release_path, version, version, release_path, version }) catch {
23+
return mer.internalError("render failed");
24+
};
25+
return mer.html(html);
2326
}
24-
25-
fn page() h.Node {
26-
return h.div(.{ .class = "page" }, .{
27-
// Hero
28-
h.h1(.{ .class = "lede" }, .{
29-
h.text("The web doesn't need"),
30-
h.br(),
31-
h.em(.{}, "another"),
32-
h.text(" JavaScript"),
33-
h.br(),
34-
h.text("framework. It needs"),
35-
h.br(),
36-
h.span(.{ .class = "red" }, "no runtime at all."),
37-
}),
38-
39-
// Release banner
40-
h.div(.{ .class = "release-banner" }, .{
41-
h.span(.{ .class = "release-badge" }, "NEW"),
42-
h.div(.{ .class = "release-content" }, .{
43-
h.span(.{ .class = "release-text" }, "v0.2.5 — Now on Zig 0.16 with one-line install:"),
44-
h.raw("<code class=\"release-code\" onclick=\"navigator.clipboard.writeText(this.textContent); this.classList.add('copied'); setTimeout(() => this.classList.remove('copied'), 1500);\">curl -fsSL merjs.trilok.ai/install.sh | bash</code>"),
45-
}),
46-
}),
47-
48-
// Benchmark comparison
49-
// Benchmark comparison
50-
h.div(.{ .class = "bench" }, .{
51-
h.div(.{ .class = "bench-title" }, .{h.raw("vs Next.js &mdash; <span class=\"red\">at a glance</span>")}),
52-
h.p(.{ .class = "bench-sub" }, "Head-to-head on the metrics that matter."),
53-
h.div(.{ .class = "bench-legend" }, .{
54-
h.div(.{ .class = "bench-legend-item" }, .{ h.div(.{ .class = "bench-legend-dot mer" }, ""), h.text(" merjs") }),
55-
h.div(.{ .class = "bench-legend-item" }, .{ h.div(.{ .class = "bench-legend-dot next" }, ""), h.text(" Next.js") }),
56-
}),
57-
benchRow("Cold Start", "8%", "< 5 ms", "80%", "~1-3 s"),
58-
benchRow("Throughput", "95%", "115,093 req/s", "8%", "2,060 req/s"),
59-
benchRow("Avg Latency", "51%", "40.85 ms", "90%", "72.04 ms"),
60-
benchRow("Binary Size", "8%", "260 KB", "85%", "~300 MB node_modules"),
61-
benchRow("Build Time", "67%", "~22.5 s", "90%", "~30 s"),
62-
h.p(.{ .class = "bench-note" }, .{
63-
h.text("Throughput and latency measured locally on Apple M-series with "),
64-
h.code(.{}, "wrk -t4 -c50"),
65-
h.text(". Next.js numbers from CI (GitHub Actions). merjs is an early experiment \u{2014} Next.js is mature and production-grade. Binary size is the release-stripped native binary ("),
66-
h.code(.{}, "-Doptimize=ReleaseSmall"),
67-
h.text(")."),
68-
}),
69-
}),
70-
71-
h.hr(.{ .class = "rule" }),
72-
73-
// Items
74-
h.div(.{ .class = "items" }, .{
75-
item("01", "Node.js solved the wrong problem.", .{
76-
h.text("It unified the language, not the stack. You still ship a 400MB runtime to run a "),
77-
h.code(.{}, "hello world"),
78-
h.text(". You still wait 30 seconds for "),
79-
h.strong(.{}, "npm install"),
80-
h.text(". You still debug dependency conflicts that have nothing to do with your product. The problem was never \"which language\" \u{2014} it was \"why do we need a runtime at all.\""),
81-
}),
82-
item("02",
83-
\\<span class="red">WASM</span> closes the last gap.
84-
, .{
85-
h.text("The real reason JS won the server: it already ran in the browser. That moat is gone. WebAssembly is a compile target for "),
86-
h.em(.{}, "any"),
87-
h.text(" language. Zig compiles to "),
88-
h.code(.{}, "wasm32-freestanding"),
89-
h.text(" in a single step. Write client logic in Zig, compile to "),
90-
h.strong(.{}, ".wasm"),
91-
h.text(", ship it directly. No transpiler. No bundler. The browser runs it natively."),
92-
}),
93-
item("03",
94-
\\One language. <em>Two targets.</em>
95-
, .{
96-
h.text("The server compiles to a "),
97-
h.strong(.{}, "native binary"),
98-
h.text(". The client compiles to "),
99-
h.strong(.{}, ".wasm"),
100-
h.text(". File-based routing, SSR, type-safe APIs, hot reload \u{2014} everything Next.js does, in Zig. Zero node_modules. A single "),
101-
h.code(.{}, "zig build serve"),
102-
h.text("."),
103-
}),
104-
item("04",
105-
\\Type safety without a <span class="red">build step.</span>
106-
, .{
107-
h.text("Validation constraints are comptime. API schemas are Zig structs. JSON serialization is "),
108-
h.code(.{}, "std.json"),
109-
h.text(". No codegen. No schema files. No runtime overhead. The compiler catches it, or it doesn't compile."),
110-
}),
111-
item("05",
112-
\\This is <em>early proof.</em>
113-
, .{
114-
h.text("merjs is a bet \u{2014} that systems languages, WASM, and file-based routing can meet in one place and produce something better than what we have today. The node_modules folder had a good run. "),
115-
h.strong(.{}, "It's time to move on."),
116-
}),
117-
}),
118-
119-
// Footer
120-
h.div(.{ .class = "footer" }, .{
121-
h.a(.{ .href = "/dashboard", .class = "btn-primary" }, "See it in action"),
122-
h.a(.{ .href = "/about", .class = "btn-ghost" }, "Read the philosophy"),
123-
h.p(.{ .class = "footer-note" }, .{
124-
h.text("Built in "),
125-
h.a(.{ .href = "https://ziglang.org" }, "Zig 0.16"),
126-
h.raw(" &middot; Validation by "),
127-
h.a(.{ .href = "https://github.com/justrach/dhi" }, "dhi"),
128-
h.raw(" &middot; Zero node_modules"),
129-
}),
130-
}),
131-
});
132-
}
133-
134-
fn benchRow(label: []const u8, mer_width: []const u8, mer_val: []const u8, next_width: []const u8, next_val: []const u8) h.Node {
135-
return h.div(.{ .class = "bench-row" }, .{
136-
h.div(.{ .class = "bench-label" }, label),
137-
h.div(.{ .class = "bench-bars" }, .{
138-
h.div(.{ .class = "bench-bar-wrap" }, .{
139-
h.div(.{ .class = "bench-bar mer", .style = "width: " ++ mer_width ++ ";" }, mer_val),
140-
h.div(.{ .class = "bench-bar-tag" }, "merjs"),
141-
}),
142-
h.div(.{ .class = "bench-bar-wrap" }, .{
143-
h.div(.{ .class = "bench-bar next", .style = "width: " ++ next_width ++ ";" }, next_val),
144-
h.div(.{ .class = "bench-bar-tag" }, "Next.js"),
145-
}),
146-
}),
147-
});
148-
}
149-
150-
fn item(num: []const u8, heading: []const u8, body_children: anytype) h.Node {
151-
return h.div(.{ .class = "item" }, .{
152-
h.div(.{ .class = "item-num" }, num),
153-
h.div(.{ .class = "item-body" }, .{
154-
h.div(.{ .class = "item-heading" }, .{h.raw(heading)}),
155-
h.p(.{ .class = "item-text" }, body_children),
156-
}),
157-
});
158-
}
159-
160-
const page_css =
161-
\\.page { max-width: 800px; margin: 0 auto; padding: 56px 40px 120px; }
162-
\\.release-banner {
163-
\\ display: flex; align-items: flex-start; gap: 12px;
164-
\\ background: var(--bg2); border: 1px solid var(--border);
165-
\\ border-radius: 8px; padding: 14px 18px; margin-bottom: 48px;
166-
\\}
167-
\\.release-badge {
168-
\\ background: var(--red); color: var(--bg);
169-
\\ font-size: 11px; font-weight: 700; letter-spacing: 0.05em;
170-
\\ padding: 4px 8px; border-radius: 4px; flex-shrink: 0; margin-top: 2px;
171-
\\}
172-
\\.release-content {
173-
\\ display: flex; flex-direction: column; gap: 6px;
174-
\\ flex: 1;
175-
\\}
176-
\\.release-text {
177-
\\ font-size: 14px; color: var(--text); line-height: 1.4;
178-
\\}
179-
\\.release-code {
180-
\\ font-family: 'SF Mono', 'Fira Code', monospace;
181-
\\ font-size: 13px;
182-
\\ background: var(--bg3);
183-
\\ border: 1px solid var(--border);
184-
\\ border-radius: 6px;
185-
\\ padding: 8px 12px;
186-
\\ color: var(--text);
187-
\\ cursor: pointer;
188-
\\ user-select: all;
189-
\\ transition: background 0.15s, border-color 0.15s;
190-
\\ display: inline-block;
191-
\\ position: relative;
192-
\\}
193-
\\.release-code:hover {
194-
\\ background: var(--bg);
195-
\\ border-color: var(--text);
196-
\\}
197-
\\.release-code.copied {
198-
\\ background: var(--red);
199-
\\ color: var(--bg);
200-
\\}
201-
\\.release-code.copied::after {
202-
\\ content: "copied!";
203-
\\ position: absolute;
204-
\\ right: -60px;
205-
\\ top: 50%;
206-
\\ transform: translateY(-50%);
207-
\\ font-size: 11px;
208-
\\ color: var(--red);
209-
\\ font-weight: 600;
210-
\\}
211-
\\.lede {
212-
\\ font-family: 'DM Serif Display', Georgia, serif;
213-
\\ font-size: clamp(36px, 5vw, 58px);
214-
\\ line-height: 1.08;
215-
\\ letter-spacing: -0.03em;
216-
\\ color: var(--text);
217-
\\ margin-bottom: 56px;
218-
\\}
219-
\\.lede .red { color: var(--red); }
220-
\\.lede em { font-style: italic; }
221-
\\.rule { border: none; border-top: 1px solid var(--border); margin: 48px 0; }
222-
\\.items { display: flex; flex-direction: column; }
223-
\\.item {
224-
\\ display: grid;
225-
\\ grid-template-columns: 40px 1fr;
226-
\\ gap: 16px;
227-
\\ padding: 36px 0;
228-
\\ border-bottom: 1px solid var(--border);
229-
\\}
230-
\\.item:first-child { border-top: 1px solid var(--border); }
231-
\\.item-num { font-size: 11px; color: var(--red); font-weight: 600; letter-spacing: 0.08em; padding-top: 6px; }
232-
\\.item-heading {
233-
\\ font-family: 'DM Serif Display', Georgia, serif;
234-
\\ font-size: clamp(20px, 2.6vw, 28px);
235-
\\ line-height: 1.15; letter-spacing: -0.02em;
236-
\\ color: var(--text); margin-bottom: 12px;
237-
\\}
238-
\\.item-heading .red { color: var(--red); }
239-
\\.item-heading em { font-style: italic; }
240-
\\.item-text { font-size: 15px; color: var(--muted); line-height: 1.75; max-width: 580px; }
241-
\\.item-text strong { color: var(--text); font-weight: 500; }
242-
\\.item-text code {
243-
\\ font-family: 'SF Mono', 'Fira Code', monospace;
244-
\\ font-size: 13px; background: var(--bg3);
245-
\\ border-radius: 4px; padding: 1px 6px; color: var(--text);
246-
\\}
247-
\\.bench { margin-top: 0; }
248-
\\.bench-title {
249-
\\ font-family: 'DM Serif Display', Georgia, serif;
250-
\\ font-size: clamp(22px, 3vw, 32px);
251-
\\ letter-spacing: -0.02em; color: var(--text); margin-bottom: 8px;
252-
\\}
253-
\\.bench-title .red { color: var(--red); }
254-
\\.bench-sub { font-size: 13px; color: var(--muted); margin-bottom: 32px; line-height: 1.5; }
255-
\\.bench-legend { display: flex; gap: 20px; margin-bottom: 24px; }
256-
\\.bench-legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
257-
\\.bench-legend-dot { width: 10px; height: 10px; border-radius: 2px; }
258-
\\.bench-legend-dot.mer { background: var(--red); }
259-
\\.bench-legend-dot.next { background: var(--border); }
260-
\\.bench-row { margin-bottom: 28px; }
261-
\\.bench-label { font-size: 12px; font-weight: 600; color: var(--text); letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 10px; }
262-
\\.bench-bars { display: flex; flex-direction: column; gap: 6px; }
263-
\\.bench-bar-wrap { display: flex; align-items: center; gap: 10px; }
264-
\\.bench-bar {
265-
\\ height: 32px; border-radius: 4px;
266-
\\ display: flex; align-items: center; padding: 0 12px;
267-
\\ font-size: 12px; font-weight: 600; color: #fff;
268-
\\ white-space: nowrap; min-width: max-content;
269-
\\}
270-
\\.bench-bar.mer { background: var(--red); }
271-
\\.bench-bar.next { background: var(--border); color: var(--muted); }
272-
\\.bench-bar-tag { font-size: 11px; color: var(--muted); white-space: nowrap; flex-shrink: 0; min-width: 40px; }
273-
\\.bench-note { font-size: 11px; color: var(--muted); margin-top: 32px; line-height: 1.6; font-style: italic; }
274-
\\.footer { margin-top: 72px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
275-
\\.btn-primary {
276-
\\ display: inline-flex; align-items: center;
277-
\\ background: var(--red); color: var(--bg);
278-
\\ font-size: 14px; font-weight: 600;
279-
\\ padding: 12px 26px; border-radius: 6px;
280-
\\ transition: opacity 0.15s;
281-
\\}
282-
\\.btn-primary:hover { opacity: 0.88; }
283-
\\.btn-ghost {
284-
\\ display: inline-flex; align-items: center;
285-
\\ color: var(--muted); font-size: 14px;
286-
\\ border: 1px solid var(--border);
287-
\\ padding: 12px 26px; border-radius: 6px;
288-
\\ transition: color 0.15s, border-color 0.15s;
289-
\\}
290-
\\.btn-ghost:hover { color: var(--text); border-color: var(--text); }
291-
\\.footer-note { width: 100%; margin-top: 28px; font-size: 12px; color: var(--muted); }
292-
\\.footer-note a { border-bottom: 1px solid var(--border); padding-bottom: 1px; }
293-
\\.footer-note a:hover { color: var(--text); }
294-
\\@media (max-width: 600px) {
295-
\\ .page { padding: 24px 16px 64px; }
296-
\\ .lede { font-size: 28px; margin-bottom: 32px; }
297-
\\ .lede br { display: none; }
298-
\\ .rule { margin: 28px 0; }
299-
\\ .item { grid-template-columns: 1fr; gap: 8px; padding: 20px 0; }
300-
\\ .item-num { padding-top: 0; }
301-
\\ .item-heading { font-size: 18px; }
302-
\\ .item-text { font-size: 14px; max-width: 100%; }
303-
\\ .footer { flex-direction: column; align-items: stretch; gap: 10px; margin-top: 40px; }
304-
\\ .btn-primary, .btn-ghost { justify-content: center; text-align: center; padding: 14px 20px; }
305-
\\ .footer-note { text-align: center; }
306-
\\ .bench-legend { gap: 14px; }
307-
\\ .bench-bar { height: 24px; font-size: 11px; }
308-
\\ .bench-row { margin-bottom: 22px; }
309-
\\}
310-
;

0 commit comments

Comments
 (0)