|
| 1 | +const std = @import("std"); |
1 | 2 | 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"); |
3 | 8 |
|
4 | 9 | 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.", |
9 | 14 | .og_url = "https://merlionjs.com", |
10 | 15 | .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.", |
13 | 18 | .extra_head = "<style>" ++ page_css ++ "</style>", |
14 | 19 | }; |
15 | 20 |
|
16 | | -const page_node = page(); |
17 | | -comptime { |
18 | | - mer.lint.check(page_node); |
19 | | -} |
20 | | - |
21 | 21 | 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); |
23 | 26 | } |
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 — <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(" · Validation by "), |
127 | | - h.a(.{ .href = "https://github.com/justrach/dhi" }, "dhi"), |
128 | | - h.raw(" · 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