website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


1f7058fpyrossh 2026-07-14T14:31:01+05:30
Task 3: static pages - layout, home, CV, robots, 404
Files changed (7) hide show
  1. app/404.zig +83 -0
  2. app/_mercss.css +1 -0
  3. app/about.zig +40 -0
  4. app/cv.zig +22 -0
  5. app/index.zig +91 -0
  6. app/layout.zig +124 -0
  7. app/robots.zig +8 -0
app/404.zig ADDED
@@ -0,0 +1,83 @@
1
+ const std = @import("std");
2
+ const mer = @import("mer");
3
+ const config = @import("config");
4
+
5
+ pub fn render(req: mer.Request) mer.Response {
6
+ var buf: std.Io.Writer.Allocating = .init(req.allocator);
7
+ const w = &buf.writer;
8
+
9
+ w.writeAll(
10
+ \\<!DOCTYPE html>
11
+ \\<html lang="en">
12
+ \\<head>
13
+ \\ <meta charset="UTF-8">
14
+ \\ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
+ \\ <title>404 —
16
+ ) catch return mer.notFound();
17
+ w.writeAll(config.site_title) catch return mer.notFound();
18
+ w.writeAll(
19
+ \\</title>
20
+ \\ <link rel="preconnect" href="https://fonts.googleapis.com">
21
+ \\ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
22
+ \\ <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
23
+ \\ <noscript><link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet"></noscript>
24
+ \\ <style>
25
+ \\ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
26
+ \\ :root { --bg:#f0ebe3; --bg2:#e8e2d9; --bg3:#ddd5cc; --text:#252530; --muted:#8a7f78; --border:#d5cdc4; --red:#e8251f; }
27
+ \\ body { background:var(--bg); color:var(--text); font-family:'DM Sans',system-ui,sans-serif; min-height:100vh; display:flex; align-items:center; justify-content:center; overflow:hidden; }
28
+ \\ a { color:inherit; text-decoration:none; }
29
+ \\ .scene { position:relative; display:flex; flex-direction:column; align-items:center; gap:32px; text-align:center; padding:40px 24px; z-index:1; }
30
+ \\ .bg-num { position:fixed; top:50%; left:50%; transform:translate(-50%,-50%); font-family:'DM Serif Display',Georgia,serif; font-size:min(60vw, 500px); font-weight:700; color:var(--red); opacity:0.06; line-height:0.85; letter-spacing:-0.04em; pointer-events:none; z-index:0; user-select:none; }
31
+ \\ .wordmark { font-family:'DM Serif Display',Georgia,serif; font-size:18px; letter-spacing:-0.02em; }
32
+ \\ .wordmark span { color:var(--red); }
33
+ \\ .code-wrap { position:relative; }
34
+ \\ .code { font-family:'DM Serif Display',Georgia,serif; font-size:clamp(100px, 20vw, 180px); font-weight:700; line-height:1; color:var(--red); letter-spacing:-0.04em; position:relative; }
35
+ \\ .code::after { content:'404'; position:absolute; top:0; left:0; color:var(--bg3); clip-path:inset(0 0 50% 0); animation:glitch 3s ease-in-out infinite; }
36
+ \\ @keyframes glitch {
37
+ \\ 0%,90%,100% { transform:translate(0); }
38
+ \\ 92% { transform:translate(-3px, 2px); }
39
+ \\ 94% { transform:translate(3px, -1px); }
40
+ \\ 96% { transform:translate(-2px, 1px); }
41
+ \\ }
42
+ \\ .divider { width:40px; height:3px; background:var(--red); border-radius:2px; }
43
+ \\ h1 { font-family:'DM Serif Display',Georgia,serif; font-size:clamp(24px, 4vw, 36px); letter-spacing:-0.02em; line-height:1.2; }
44
+ \\ .sub { font-size:15px; color:var(--muted); max-width:380px; line-height:1.7; }
45
+ \\ .path { font-family:'SF Mono','Fira Code',monospace; font-size:13px; color:var(--red); background:rgba(232,37,31,0.06); border:1px solid rgba(232,37,31,0.15); border-radius:8px; padding:10px 20px; letter-spacing:0.02em; }
46
+ \\ .actions { display:flex; gap:12px; flex-wrap:wrap; justify-content:center; }
47
+ \\ .btn { display:inline-flex; align-items:center; gap:6px; font-size:14px; font-weight:500; padding:12px 24px; border-radius:8px; transition:all 0.2s; }
48
+ \\ .btn-red { background:var(--red); color:#fff; }
49
+ \\ .btn-red:hover { opacity:0.9; transform:translateY(-1px); box-shadow:0 4px 12px rgba(232,37,31,0.25); }
50
+ \\ .btn-ghost { border:1px solid var(--border); color:var(--muted); }
51
+ \\ .btn-ghost:hover { color:var(--text); border-color:var(--text); }
52
+ \\ .hint { font-size:12px; color:var(--muted); opacity:0.7; }
53
+ \\ .hint code { font-family:'SF Mono',monospace; font-size:11px; background:var(--bg2); padding:2px 6px; border-radius:3px; }
54
+ \\ </style>
55
+ \\</head>
56
+ \\<body>
57
+ \\<div class="bg-num">404</div>
58
+ \\<div class="scene">
59
+ \\ <a href="/" class="wordmark">
60
+ ) catch return mer.notFound();
61
+ w.writeAll(config.site_title) catch return mer.notFound();
62
+ w.writeAll(
63
+ \\</a>
64
+ \\ <div class="code-wrap">
65
+ \\ <div class="code">404</div>
66
+ \\ </div>
67
+ \\ <div class="divider"></div>
68
+ \\ <h1>This route doesn't exist yet.</h1>
69
+ \\ <p class="sub">You're looking for a page that hasn't been built. Drop a <code>.zig</code> file in <code>app/</code> and it becomes a route automatically.</p>
70
+ \\ <div class="path" id="path"></div>
71
+ \\ <div class="actions">
72
+ \\ <a href="/" class="btn btn-red">Back to home</a>
73
+ \\ <a href="/about" class="btn btn-ghost">About</a>
74
+ \\ </div>
75
+ \\ <p class="hint">Create <code>app/your-page.zig</code> → route appears at <code>/your-page</code></p>
76
+ \\</div>
77
+ \\<script>document.getElementById('path').textContent = location.pathname;</script>
78
+ \\</body>
79
+ \\</html>
80
+ ) catch return mer.notFound();
81
+
82
+ return .{ .status = .not_found, .content_type = .html, .body = buf.written() };
83
+ }
app/_mercss.css ADDED
@@ -0,0 +1 @@
1
+ /* mercss stub */
app/about.zig ADDED
@@ -0,0 +1,40 @@
1
+ const mer = @import("mer");
2
+ const h = mer.h;
3
+
4
+ pub const prerender = true;
5
+
6
+ pub const meta: mer.Meta = .{
7
+ .title = "About",
8
+ .description = "About this merjs project.",
9
+ };
10
+
11
+ const page_node = page();
12
+
13
+ pub fn render(req: mer.Request) mer.Response {
14
+ return mer.render(req.allocator, page_node);
15
+ }
16
+
17
+ fn page() h.Node {
18
+ return h.div(.{ .class = "page" }, .{
19
+ h.h1(.{ .class = "title" }, "About"),
20
+ h.p(.{}, .{
21
+ h.text("This project is built with "),
22
+ h.a(.{ .href = "https://github.com/justrach/merjs" }, "merjs"),
23
+ h.text(" — a Zig-native web framework with file-based routing, SSR, and WASM support."),
24
+ }),
25
+ h.div(.{ .class = "features" }, .{
26
+ feature("File-based routing", "Drop a .zig file in app/ or api/ and it becomes a route."),
27
+ feature("Server-side rendering", "Pages render at request time via render(req)."),
28
+ feature("Type-safe APIs", "Return Zig structs as JSON — no hand-rolled serialization."),
29
+ feature("Zero node_modules", "One zig build serve. That's it."),
30
+ }),
31
+ h.a(.{ .href = "/", .class = "btn" }, "Back to home"),
32
+ });
33
+ }
34
+
35
+ fn feature(title: []const u8, desc: []const u8) h.Node {
36
+ return h.div(.{ .class = "feature" }, .{
37
+ h.div(.{ .class = "feature-title" }, title),
38
+ h.p(.{ .class = "feature-desc" }, desc),
39
+ });
40
+ }
app/cv.zig ADDED
@@ -0,0 +1,22 @@
1
+ const mer = @import("mer");
2
+
3
+ pub const meta: mer.Meta = .{
4
+ .title = "Curriculum Vitae",
5
+ .description = "Peter John CV",
6
+ .extra_head = "<link rel=\"stylesheet\" href=\"/assets/css/workers/cv.css\" />",
7
+ };
8
+
9
+ pub fn render(req: mer.Request) mer.Response {
10
+ _ = req;
11
+ return mer.html(
12
+ \\<div>
13
+ \\ <h1 style="display:flex;align-items:flex-start;font-weight:800;font-size:2rem;margin-bottom:1rem">
14
+ \\ Curriculum Vitae
15
+ \\ <a href="/assets/pdfs/resume.pdf" target="_blank" rel="noopener noreferrer" style="margin-left:0.4rem;margin-top:0.8rem;color:var(--color-link)">↗</a>
16
+ \\ </h1>
17
+ \\ <div style="border:1px solid black">
18
+ \\ <img src="/assets/pdfs/resume.svg" alt="Resume" style="width:100%" />
19
+ \\ </div>
20
+ \\</div>
21
+ );
22
+ }
app/index.zig ADDED
@@ -0,0 +1,91 @@
1
+ const std = @import("std");
2
+ const mer = @import("mer");
3
+ const config = @import("config");
4
+
5
+ pub const meta: mer.Meta = .{
6
+ .title = config.site_title,
7
+ .description = "Tech Lead from Bangalore who likes to create frameworks and programming languages.",
8
+ .extra_head = "<link rel=\"stylesheet\" href=\"/assets/css/workers/home.css\" />",
9
+ };
10
+
11
+ pub fn render(req: mer.Request) mer.Response {
12
+ var buf: std.Io.Writer.Allocating = .init(req.allocator);
13
+ const w = &buf.writer;
14
+
15
+ w.writeAll(
16
+ \\<div class="pageContainer">
17
+ \\ <h1 class="title">Hello!</h1>
18
+ \\ <p class="description">
19
+ \\ <a href="https://www.linkedin.com/in/pyrossh" target="_blank" rel="noopener noreferrer" class="linkUnderline"><strong>I'm</strong></a>
20
+ \\ a tech lead based out of Bangalore, India and have worked for startups and enterprises
21
+ \\ that focus on products in the healthcare and finance domains. I like to build
22
+ \\ frameworks and automate common tasks to make it easier to develop applications.
23
+ \\ </p>
24
+ \\ <p class="description">
25
+ \\ I enjoy working on open-source projects, and during the last decade, I've created a
26
+ \\ number of projects that are widely used.
27
+ \\ </p>
28
+ \\ <div class="gridContainer">
29
+ \\ <section class="interests">
30
+ \\ <h2>Interests</h2>
31
+ \\ <ul style="list-style:none;padding:0">
32
+ ) catch return mer.internalError("write failed");
33
+
34
+ for (&[_][]const u8{ "HTML", "CSS", "Javascript", "S3", "SQL", "Bun", "Astro", "Tauri", "Remix", "Zig", "K8s", "Iceberg" }) |interest| {
35
+ w.print("<li>{s}</li>\n", .{interest}) catch return mer.internalError("write failed");
36
+ }
37
+
38
+ w.writeAll(
39
+ \\ </ul>
40
+ \\ </section>
41
+ \\ <section class="contact">
42
+ \\ <h2>Contact</h2>
43
+ \\ <ul style="list-style:none;padding:0">
44
+ \\ <li><strong>email:</strong><span>[email protected]</span></li>
45
+ \\ <li><strong>crates:</strong><a href="https://crates.io/users/pyrossh">https://crates.io/users/pyrossh</a></li>
46
+ \\ <li><strong>npm:</strong><a href="https://www.npmjs.com/~pyrossh">https://www.npmjs.com/~pyrossh</a></li>
47
+ \\ <li><strong>linkedin:</strong><a href="https://www.linkedin.com/in/pyrossh">https://www.linkedin.com/in/pyrossh</a></li>
48
+ \\ <li><strong>google group:</strong><a href="https://groups.google.com/g/rust-embed-devs">rust-embed-devs</a></li>
49
+ \\ </ul>
50
+ \\ </section>
51
+ \\ </div>
52
+ \\ <section class="tools">
53
+ \\ <h2>Tools</h2>
54
+ \\ <ul style="list-style:none;padding:0">
55
+ ) catch return mer.internalError("write failed");
56
+
57
+ for (config.tools) |tool| {
58
+ w.print("<li><a href=\"{s}\" target=\"_blank\" rel=\"noopener noreferrer\"><div>{s}</div>", .{ tool.link, tool.name }) catch return mer.internalError("write failed");
59
+ if (tool.image) |img| {
60
+ w.print("<img src=\"{s}\" alt=\"{s}\" />", .{ img, tool.name }) catch return mer.internalError("write failed");
61
+ }
62
+ w.writeAll("</a></li>\n") catch return mer.internalError("write failed");
63
+ }
64
+
65
+ w.writeAll(
66
+ \\ </ul>
67
+ \\ </section>
68
+ \\ <section>
69
+ \\ <h2>Git</h2>
70
+ \\ <div class="repos">
71
+ ) catch return mer.internalError("write failed");
72
+
73
+ for (config.repos) |repo| {
74
+ w.writeAll("<div class=\"repo\">") catch return mer.internalError("write failed");
75
+ w.print("<h4><a href=\"/repos/{s}\">{s}</a></h4>", .{ repo.title, repo.title }) catch return mer.internalError("write failed");
76
+ w.print("<p>{s}</p>", .{ repo.description }) catch return mer.internalError("write failed");
77
+ w.writeAll("<div class=\"tags\">") catch return mer.internalError("write failed");
78
+ for (repo.tags) |tag| {
79
+ w.print("<span class=\"tag\">#{s}</span>", .{ tag }) catch return mer.internalError("write failed");
80
+ }
81
+ w.writeAll("</div></div>\n") catch return mer.internalError("write failed");
82
+ }
83
+
84
+ w.writeAll(
85
+ \\ </div>
86
+ \\ </section>
87
+ \\</div>
88
+ ) catch return mer.internalError("write failed");
89
+
90
+ return mer.html(buf.written());
91
+ }
app/layout.zig ADDED
@@ -0,0 +1,124 @@
1
+ const std = @import("std");
2
+ const mer = @import("mer");
3
+ const config = @import("config");
4
+
5
+ pub fn wrap(allocator: std.mem.Allocator, path: []const u8, body: []const u8, meta: mer.Meta) []const u8 {
6
+ var buf: std.Io.Writer.Allocating = .init(allocator);
7
+ const w = &buf.writer;
8
+
9
+ const title = if (meta.title.len > 0) meta.title else config.site_title;
10
+ const description = if (meta.description.len > 0) meta.description else config.site_description;
11
+
12
+ w.writeAll(
13
+ \\<!DOCTYPE html>
14
+ \\<html lang="en">
15
+ \\<head>
16
+ \\ <meta charset="utf-8" />
17
+ \\ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
18
+ \\ <meta name="theme-color" content="#131618" />
19
+ \\ <link rel="icon" type="image/svg+xml" href="/assets/icons/icon.svg" />
20
+ \\ <link rel="alternate" type="application/rss+xml" title="pyrossh" href="/rss.xml" />
21
+ \\ <link rel="canonical" href="https://pyrossh.dev/" />
22
+ \\ <link rel="stylesheet" href="/assets/css/shared.css" />
23
+ \\ <title>
24
+ ) catch return body;
25
+ w.writeAll(title) catch return body;
26
+ w.writeAll(
27
+ \\</title>
28
+ \\ <meta name="title" content="
29
+ ) catch return body;
30
+ w.writeAll(title) catch return body;
31
+ w.writeAll(
32
+ \\" />
33
+ \\ <meta name="description" content="
34
+ ) catch return body;
35
+ w.writeAll(description) catch return body;
36
+ w.writeAll(
37
+ \\" />
38
+ \\ <meta property="og:title" content="
39
+ ) catch return body;
40
+ w.writeAll(title) catch return body;
41
+ w.writeAll(
42
+ \\" />
43
+ \\ <meta property="og:description" content="
44
+ ) catch return body;
45
+ w.writeAll(description) catch return body;
46
+ w.writeAll(
47
+ \\" />
48
+ \\ <meta property="og:image" content="/assets/icons/icon.svg" />
49
+ \\ <meta name="twitter:card" content="summary_large_image" />
50
+ \\ <script src="https://unpkg.com/[email protected]"></script>
51
+ ) catch return body;
52
+ if (meta.extra_head) |extra| {
53
+ w.writeAll("\n ") catch return body;
54
+ w.writeAll(extra) catch return body;
55
+ }
56
+ w.writeAll(
57
+ \\</head>
58
+ \\<body>
59
+ \\ <div class="safe-area"></div>
60
+ \\ <header>
61
+ \\ <div class="wrapper">
62
+ \\ <nav>
63
+ \\ <a href="/" class="logo
64
+ ) catch return body;
65
+ if (std.mem.eql(u8, path, "/")) {
66
+ w.writeAll(" active") catch return body;
67
+ }
68
+ w.writeAll(
69
+ \\">木 pyrossh</a>
70
+ \\ <div class="links">
71
+ \\ <span id="astro-color-scheme-switch">
72
+ \\ <button id="theme-change" type="button" onclick="document.documentElement.toggleAttribute('data-theme')">
73
+ \\ <svg style="display:var(--btn-light)" viewBox="0 0 24 24" width="1.2em" height="1.2em"><path fill="currentColor" d="M11.288 4.713Q11 4.425 11 4V2q0-.425.288-.712T12 1t.713.288T13 2v2q0 .425-.288.713T12 5t-.712-.288M16.95 7.05q-.275-.275-.275-.687t.275-.713l1.4-1.425q.3-.3.712-.3t.713.3q.275.275.275.7t-.275.7L18.35 7.05q-.275.275-.7.275t-.7-.275M20 13q-.425 0-.713-.288T19 12t.288-.712T20 11h2q.425 0 .713.288T23 12t-.288.713T22 13zm-8.712 9.713Q11 22.425 11 22v-2q0-.425.288-.712T12 19t.713.288T13 20v2q0 .425-.288.713T12 23t-.712-.288M5.65 7.05l-1.425-1.4q-.3-.3-.3-.725t.3-.7q.275-.275.7-.275t.7.275L7.05 5.65q.275.275.275.7t-.275.7q-.3.275-.7.275t-.7-.275m12.7 12.725l-1.4-1.425q-.275-.3-.275-.712t.275-.688t.688-.275t.712.275l1.425 1.4q.3.275.288.7t-.288.725q-.3.3-.725.3t-.7-.3M2 13q-.425 0-.712-.288T1 12t.288-.712T2 11h2q.425 0 .713.288T5 12t-.288.713T4 13zm2.225 6.775q-.275-.275-.275-.7t.275-.7L5.65 16.95q.275-.275.687-.275t.713.275q.3.3.3.713t-.3.712l-1.4 1.4q-.3.3-.725.3t-.7-.3M7.75 16.25Q6 14.5 6 12t1.75-4.25T12 6t4.25 1.75T18 12t-1.75 4.25T12 18t-4.25-1.75"/></svg>
74
+ \\ <svg style="display:var(--btn-dark)" viewBox="0 0 24 24" width="1.2em" height="1.2em"><path fill="currentColor" d="m15 8l-3-3l3-3l3 3zm5 3l-2-2l2-2l2 2zm-7.925 11q-2.1 0-3.937-.8t-3.2-2.162t-2.163-3.2t-.8-3.938q0-3.65 2.325-6.437T10.225 2q-.45 2.475.275 4.838t2.5 4.137t4.138 2.5t4.837.275q-.65 3.6-3.45 5.925T12.075 22"/></svg>
75
+ \\ </button>
76
+ \\ </span>
77
+ \\ <div>|</div>
78
+ ) catch return body;
79
+
80
+ for (config.nav_items) |item| {
81
+ w.writeAll(" <a href=\"") catch return body;
82
+ w.writeAll(item.href) catch return body;
83
+ w.writeAll("\"") catch return body;
84
+ if (std.mem.startsWith(u8, path, item.href)) {
85
+ w.writeAll(" class=\"active\"") catch return body;
86
+ }
87
+ w.writeAll(">") catch return body;
88
+ w.writeAll(item.label) catch return body;
89
+ w.writeAll("</a>\n <div>|</div>\n") catch return body;
90
+ }
91
+
92
+ w.writeAll(
93
+ \\ </div>
94
+ \\ </nav>
95
+ \\ </div>
96
+ \\ </header>
97
+ \\ <div class="safe-area"></div>
98
+ \\ <div class="wrapper">
99
+ \\ <main>
100
+ ) catch return body;
101
+
102
+ w.writeAll(body) catch return body;
103
+
104
+ w.writeAll(
105
+ \\ </main>
106
+ \\ </div>
107
+ \\ <footer>
108
+ \\ <div class="wrapper">
109
+ \\ <div class="container">
110
+ \\ <div class="spacer"></div>
111
+ \\ <p class="copyright-container">
112
+ \\ Copyright © <current-year>2026</current-year>
113
+ \\ <a class="link" href="https://pyrossh.dev">pyrossh</a>
114
+ \\ </p>
115
+ \\ </div>
116
+ \\ </div>
117
+ \\ </footer>
118
+ \\ <div class="safe-area"></div>
119
+ \\</body>
120
+ \\</html>
121
+ ) catch return body;
122
+
123
+ return buf.written();
124
+ }
app/robots.zig ADDED
@@ -0,0 +1,8 @@
1
+ const mer = @import("mer");
2
+
3
+ pub const meta: mer.Meta = .{};
4
+
5
+ pub fn render(req: mer.Request) mer.Response {
6
+ _ = req;
7
+ return mer.text(.ok, "User-agent: *\nAllow: /\n");
8
+ }