website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
1f7058f
— pyrossh
2026-07-14T14:31:01+05:30
Task 3: static pages - layout, home, CV, robots, 404
- app/404.zig +83 -0
- app/_mercss.css +1 -0
- app/about.zig +40 -0
- app/cv.zig +22 -0
- app/index.zig +91 -0
- app/layout.zig +124 -0
- 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
|
+
}
|