first commit

This commit is contained in:
yuzu 2025-05-03 20:42:44 -05:00
commit 929f0b23a4
26 changed files with 964 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
zig-out/
zig-cache/
*.core
.jetzig
.zig-cache/
.env

60
build.zig Normal file
View File

@ -0,0 +1,60 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "yuzucchii.xyz",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const options = b.addOptions();
options.addOption([]const u8, "BLOGS_PASSWORD", "admin");
exe.root_module.addOptions("dev", options);
// Example Dependency
// -------------------
// const iguanas_dep = b.dependency("iguanas", .{ .optimize = optimize, .target = target });
// exe.root_module.addImport("iguanas", iguanas_dep.module("iguanas"));
//
const uuid = b.dependency("uuid", .{ .optimize = optimize, .target = target });
exe.root_module.addImport("uuid", uuid.module("uuid"));
// ^ Add all dependencies before `jetzig.jetzigInit()` ^
try jetzig.jetzigInit(b, exe, .{});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const lib_unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const exe_unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);
}

44
build.zig.zon Normal file
View File

@ -0,0 +1,44 @@
.{
// This is the default name used by packages depending on this one. For
// example, when a user runs `zig fetch --save <url>`, this field is used
// as the key in the `dependencies` table. Although the user can choose a
// different name, most users will stick with this provided value.
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = .yuzucchiidotxyz,
.fingerprint = 0x4831d874912025d4,
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
.jetzig = .{
.url = "https://github.com/jetzig-framework/jetzig/archive/1feb18fb74e626fe068ec67532318640a9cb83be.tar.gz",
.hash = "jetzig-0.0.0-IpAgLfMzDwDyAqZ05btcLDd9dfE_bxUbfOI_Wx7a19ed",
},
.uuid = .{
.url = "git+https://github.com/r4gus/uuid-zig#9d66e23e32cd7208d1becb4fd9352f8a27f89551",
.hash = "uuid-0.3.0-oOieIYF1AAA_BtE7FvVqqTn5uEYTvvz7ycuVnalCOf8C",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

8
compose.yml Normal file
View File

@ -0,0 +1,8 @@
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: ${JETQUERY_USERNAME}
POSTGRES_PASSWORD: ${JETQUERY_PASSWORD}
ports:
- 5432:5432

11
config/database.zig Normal file
View File

@ -0,0 +1,11 @@
pub const database = .{
.development = .{
.adapter = .postgresql,
},
.testing = .{
.adapter = .postgresql,
},
.production = .{
.adapter = .postgresql,
},
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/jetzig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

10
public/styles.css Normal file
View File

@ -0,0 +1,10 @@
/* Root stylesheet. Load into your Zmpl template with:
*
* <link rel="stylesheet" href="/styles.css" />
*
*/
.message {
font-weight: bold;
font-size: 3rem;
}

BIN
public/zmpl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -0,0 +1,26 @@
const jetquery = @import("jetzig").jetquery;
pub const Blog = jetquery.Model(
@This(),
"blogs",
struct {
id: i32,
title: []const u8,
content: ?[]const u8,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{},
);
pub const Session = jetquery.Model(
@This(),
"sessions",
struct {
id: i32,
session_id: []const u8,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,
},
.{},
);

View File

@ -0,0 +1,19 @@
const std = @import("std");
const jetquery = @import("jetquery");
const t = jetquery.schema.table;
pub fn up(repo: anytype) !void {
try repo.createTable(
"sessions",
&.{
t.primaryKey("id", .{}),
t.column("session_id", .string, .{}),
t.timestamps(.{}),
},
.{},
);
}
pub fn down(repo: anytype) !void {
try repo.dropTable("sessions", .{});
}

View File

View File

@ -0,0 +1,82 @@
/// Demo middleware. Assign middleware by declaring `pub const middleware` in the
/// `jetzig_options` defined in your application's `src/main.zig`.
///
/// Middleware is called before and after the request, providing full access to the active
/// request, allowing you to execute any custom code for logging, tracking, inserting response
/// headers, etc.
///
/// This middleware is configured in the demo app's `src/main.zig`:
///
/// ```
/// pub const jetzig_options = struct {
/// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")};
/// };
/// ```
const std = @import("std");
const jetzig = @import("jetzig");
const Zmd = @import("zmd").Zmd;
const fragments = @import("zmd").html.DefaultFragments;
/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
/// function allows you to access them in various middleware callbacks defined below, where they
/// can also be modified.
my_custom_value: []const u8,
const DemoMiddleware = @This();
/// Initialize middleware.
pub fn init(request: *jetzig.http.Request) !*DemoMiddleware {
var middleware = try request.allocator.create(DemoMiddleware);
middleware.my_custom_value = "initial value";
return middleware;
}
/// Invoked immediately after the request is received but before it has started processing.
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
/// request, including any other middleware in the chain.
pub fn afterRequest(self: *DemoMiddleware, request: *jetzig.http.Request) !void {
// Middleware can invoke `request.redirect` or `request.render`. All request processing stops
// and the response is immediately returned if either of these two functions are called
// during middleware processing.
// _ = request.redirect("/foobar", .moved_permanently);
// _ = request.render(.unauthorized);
try request.server.logger.DEBUG(
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
.{self.my_custom_value},
);
self.my_custom_value = @tagName(request.method);
}
/// Invoked immediately before the response renders to the client.
/// The response can be modified here if needed.
pub fn beforeResponse(
self: *DemoMiddleware,
request: *jetzig.http.Request,
response: *jetzig.http.Response,
) !void {
try request.server.logger.DEBUG(
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
.{ self.my_custom_value, @tagName(response.status_code) },
);
}
/// Invoked immediately after the response has been finalized and sent to the client.
/// Response data can be accessed for logging, but any modifications will have no impact.
pub fn afterResponse(
self: *DemoMiddleware,
request: *jetzig.http.Request,
response: *jetzig.http.Response,
) !void {
_ = self;
_ = response;
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
}
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
/// freed before the next request starts processing.
pub fn deinit(self: *DemoMiddleware, request: *jetzig.http.Request) void {
request.allocator.destroy(self);
}

169
src/app/views/blogs.zig Normal file
View File

@ -0,0 +1,169 @@
const std = @import("std");
const jetzig = @import("jetzig");
const Zmd = @import("zmd").Zmd;
const tokens = @import("zmd").tokens;
const Node = @import("zmd").Node;
/// Default fragments. Pass this to `Zmd.toHtml` or provide your own.
/// Formatters can be functions receiving an allocator, the current node, and the rendered
/// content, or a 2-element tuple containing the open and close for each node.
pub const Fragments = struct {
pub fn root(allocator: std.mem.Allocator, node: Node) ![]const u8 {
return try std.fmt.allocPrint(allocator, "{s}", .{node.content});
}
pub fn block(allocator: std.mem.Allocator, node: Node) ![]const u8 {
const style = "font-family: Monospace;";
return if (node.meta) |meta|
std.fmt.allocPrint(allocator,
\\<pre class="language-{s}" style="{s}"><code>{s}</code></pre>
, .{ meta, style, node.content })
else
std.fmt.allocPrint(allocator,
\\<pre style="{s}"><code>{s}</code></pre>
, .{ style, node.content });
}
pub fn link(allocator: std.mem.Allocator, node: Node) ![]const u8 {
return std.fmt.allocPrint(allocator,
\\<a href="{s}">{s}</a>
, .{ node.href.?, node.title.? });
}
pub fn image(allocator: std.mem.Allocator, node: Node) ![]const u8 {
return std.fmt.allocPrint(allocator,
\\<img src="{s}" title="{s}" />
, .{ node.href.?, node.title.? });
}
pub const h1 = .{ "<h1>", "</h1>\n" };
pub const h2 = .{ "<h2>", "</h2>\n" };
pub const h3 = .{ "<h3>", "</h3>\n" };
pub const h4 = .{ "<h4>", "</h4>\n" };
pub const h5 = .{ "<h5>", "</h5>\n" };
pub const h6 = .{ "<h6>", "</h6>\n" };
pub const bold = .{ "<b>", "</b>" };
pub const italic = .{ "<i>", "</i>" };
pub const unordered_list = .{ "<ul>", "</ul>" };
pub const ordered_list = .{ "<ol>", "</ol>" };
pub const list_item = .{ "<li>", "</li>" };
pub const code = .{ "<span style=\"font-family: Monospace\">", "</span>" };
pub const paragraph = .{ "\n<p>", "</p>\n" };
pub const default = .{ "", "" };
};
/// Escape HTML entities.
pub fn escape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
const replacements = .{
.{ "&", "&amp;" },
.{ "<", "&lt;" },
.{ ">", "&gt;" },
};
var output = input;
inline for (replacements) |replacement| {
output = try std.mem.replaceOwned(u8, allocator, output, replacement[0], replacement[1]);
}
return output;
}
pub fn index(request: *jetzig.Request) !jetzig.View {
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
const blogs = try request.repo.all(query);
var root = try request.data(.object);
try root.put("blogs", blogs);
return request.render(.ok);
}
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
const query = jetzig.database.Query(.Blog).find(id);
const blog = try request.repo.execute(query) orelse return request.fail(.not_found);
const Blog = struct {
id: i32,
title: []const u8,
content: ?[]const u8,
created_at: jetzig.jetquery.DateTime,
updated_at: jetzig.jetquery.DateTime,
};
var zmd = Zmd.init(request.allocator);
defer zmd.deinit();
try zmd.parse(blog.content orelse {
return request.fail(.unprocessable_entity);
});
const blob = try zmd.toHtml(Fragments);
const eb = Blog{
.id = blog.id,
.title = blog.title,
.content = blob,
.created_at = blog.created_at,
.updated_at = blog.updated_at,
};
var root = try request.data(.object);
try root.put("blog", eb);
try request.process();
return request.render(.ok);
}
pub fn new(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub fn post(request: *jetzig.Request) !jetzig.View {
const params = try request.params();
const title = params.getT(.string, "title") orelse {
return request.fail(.unprocessable_entity);
};
const content = params.getT(.string, "content") orelse {
return request.fail(.unprocessable_entity);
};
try request.repo.insert(.Blog, .{ .title = title, .content = content });
return request.redirect("/blogs", .moved_permanently);
}
test "index" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/blogs", .{});
try response.expectStatus(.ok);
}
test "get" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/blogs/example-id", .{});
try response.expectStatus(.ok);
}
test "new" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.GET, "/blogs/new", .{});
try response.expectStatus(.ok);
}
test "post" {
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
defer app.deinit();
const response = try app.request(.POST, "/blogs", .{});
try response.expectStatus(.created);
}

View File

@ -0,0 +1,6 @@
<div>
<h1>{{.blog.title}}</h1>
<div class="mt-6 prose prose-lg dark:prose-dark">
{{zmpl.fmt.raw(zmpl.ref("blog.content"))}}
</div>
</div>

View File

@ -0,0 +1,9 @@
<div>
@for (.blogs) |blog| {
<a href="/blogs/{{blog.id}}">{{blog.title}}</a>
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d %H:%M")}}
<br/>
}
<hr/>
<a href="/blogs/new">New Blog</a>
</div>

View File

@ -0,0 +1,11 @@
<div>
<form action="/blogs" method="POST">
<label>Title</label>
<input name="title" />
<label>Content</label>
<textarea name="content"></textarea>
<input type="submit" />
</form>
</div>

View File

@ -0,0 +1,3 @@
<div>
<span>Content goes here</span>
</div>

58
src/app/views/login.zig Normal file
View File

@ -0,0 +1,58 @@
const std = @import("std");
const jetzig = @import("jetzig");
const uuid4 = @import("uuid").v4;
pub fn index(request: *jetzig.Request) !jetzig.View {
// check if logged in
const cookies = try request.cookies();
if (cookies.get("session")) |session| if (session.value.len != 0)
return request.redirect("/blogs", .moved_permanently);
return request.render(.ok);
}
pub fn post(request: *jetzig.Request) !jetzig.View {
// ask for password
const cookies = try request.cookies();
const env_map = try request.allocator.create(std.process.EnvMap);
env_map.* = try std.process.getEnvMap(request.allocator);
defer env_map.deinit();
const secrets = @import("dev").BLOGS_PASSWORD;
std.debug.print("body data: {s}\n", .{request.body});
const login_data = std.json.parseFromSliceLeaky(struct {
password: []const u8,
}, request.allocator, request.body, .{}) catch {
return request.fail(.bad_request);
};
var buf: [0x100]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
const allocator = fba.allocator();
if (std.mem.eql(u8, login_data.password, secrets)) {
// logged in, creating cookie
const uuid = try std.fmt.allocPrint(allocator, "{d}", .{uuid4.new()});
try cookies.put(.{
.name = "session",
.value = uuid,
.path = "/",
.http_only = true,
.secure = false,
.max_age = 60 * 60 * 24 * 7, // 1 week
});
// post to Session table
try request.repo.insert(.Session, .{ .session_id = uuid });
return request.render(.created);
} else {
return request.fail(.unauthorized);
}
}

View File

@ -0,0 +1,30 @@
<div>
<form id="login-form">
<label>Discrete password</label>
<input name="password" type="password"/>
<button type="submit">Login</button>
</form>
<script>
document.querySelector('#login-form').addEventListener('submit', function(event) {
event.preventDefault();
const password = document.querySelector('input[name="password"]').value;
fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ password })
})
.then(res => res.json())
.then(res => {
window.location.href = '/blogs';
})
.catch(err => {
console.error('Login error:', err);
});
});
</script>
</div>

49
src/app/views/logout.zig Normal file
View File

@ -0,0 +1,49 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn index(request: *jetzig.Request) !jetzig.View {
return request.render(.ok);
}
pub fn post(request: *jetzig.Request) !jetzig.View {
// delete entry
const cookies = try request.cookies();
const session = cookies.get("session") orelse {
return request.redirect("/login", .moved_permanently);
};
const query = jetzig.database.Query(.Session)
.findBy(.{ .session_id = session.value });
_ = request.repo.execute(query) catch {
return request.redirect("/login", .moved_permanently);
};
// delete it
const delete_query = jetzig.database.Query(.Session)
.delete()
.where(.{ .{ .session_id, .eql, session.value } });
_ = request.repo.execute(delete_query) catch {
return request.fail(.internal_server_error);
};
// delete cookie
try cookies.put(.{
.name = "session",
.value = "",
.path = "/",
.max_age = 0,
.http_only = true,
.secure = false,
.expires = 0,
});
try request.headers.append("Set-Cookie",
"session=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax");
std.debug.print("Deleting session cookie at path: {?s}\n", .{session.path});
return request.render(.ok);
}

View File

@ -0,0 +1,28 @@
<button id="logout-button">Logout</button>
<script>
document.getElementById('logout-button').addEventListener('click', function () {
fetch('/logout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action: 'logout' })
})
.then(response => {
if (!response.ok) {
throw new Error(`Logout failed: ${response.status}`);
}
return response.json(); // if you expect JSON back
})
.then(data => {
console.log('Logout successful:', data);
// window.location.href = '/login'; // redirect if needed
})
.catch(error => {
console.error('Error during logout:', error);
});
});
</script>

64
src/app/views/root.zig Normal file
View File

@ -0,0 +1,64 @@
const std = @import("std");
const jetzig = @import("jetzig");
const Article = struct {
title: [255]u8,
blob: [255]u8
};
/// `src/app/views/root.zig` represents the root URL `/`
/// The `index` view function is invoked when when the HTTP verb is `GET`.
/// Other view types are invoked either by passing a resource ID value (e.g. `/1234`) or by using
/// a different HTTP verb:
///
/// GET / => index(request, data)
/// GET /1234 => get(id, request, data)
/// POST / => post(request, data)
/// PUT /1234 => put(id, request, data)
/// PATCH /1234 => patch(id, request, data)
/// DELETE /1234 => delete(id, request, data)
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
// The first call to `data.object()` or `data.array()` sets the root response data value.
// JSON requests return a JSON string representation of the root data value.
// Zmpl templates can access all values in the root data value.
var root = try data.object();
// Add a string to the root object.
try root.put("title", data.string("yuzucchii.xyz"));
// Request params have the same type as a `data.object()` so they can be inserted them
// directly into the response data. Fetch `http://localhost:8080/?message=hello` to set the
// param. JSON data is also accepted when the `content-type: application/json` header is
// present.
const params = try request.params();
var obj = try data.object();
try obj.put("title",data.string("The simplest article ever"));
try obj.put("description",data.string("Hello in this article I being to do things"));
var objj = try data.object();
try objj.put("title",data.string("The most complicated thing ever"));
try objj.put("description",data.string("Dude, that shit is like harder than a rock"));
var array = try root.put("articles", .array);
try array.append(obj);
try array.append(objj);
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
const blogs = try request.repo.all(query);
try root.put("articles", blogs);
try root.put("message_param", params.get("message"));
// Set arbitrary response headers as required. `content-type` is automatically assigned for
// HTML, JSON responses.
//
// Static files located in `public/` in the root of your project directory are accessible
// from the root path (e.g. `public/jetzig.png`) is available at `/jetzig.png` and the
// content type is inferred from the extension using MIME types.
try request.response.headers.append("x-example-header", "example header value");
// Render the response and set the response code.
return request.render(.ok);
}

View File

@ -0,0 +1,5 @@
@args title: []const u8, blob: []const u8
<div class="mb-4">
<h2 class="text-2xl font-semibold mb-2">{{title}}</h2>
<p class="text-lg">{{blob}}</p>
</div>

View File

@ -0,0 +1,32 @@
@args articles: Zmpl.Array
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/styles.css" />
<title> {{.title}} </title
</head>
<body>
<div class="mx-auto max-w-3xl mt-sm">
<h1 class="text-3xl font-bold mb-4">Hello, I like doing things.</h1>
<p class="text-lg mb-6">I created this website using Jetzig in order to store things I do.</p>
<div class="mb-8">
<a href="/blogs" class="text-blue-500 hover:underline">Blog</a>
<a href="#" class="text-blue-500 hover:underline ml-4">TTRPG</a>
<a href="#" class="text-blue-500 hover:underline ml-4">Design</a>
<a href="#" class="text-blue-500 hover:underline ml-4">Writing</a>
</div>
@partial root/article_blob(title: "hi test", blob: "owo")
@for (.articles) |article| {
@partial root/article_blob(title: article.title, blob: article.content)
}
</div>
</body>
</html>

234
src/main.zig Normal file
View File

@ -0,0 +1,234 @@
const std = @import("std");
const builtin = @import("builtin");
const jetzig = @import("jetzig");
const zmd = @import("zmd");
pub const routes = @import("routes");
pub const static = @import("static");
// Override default settings in `jetzig.config` here:
pub const jetzig_options = struct {
/// Middleware chain. Add any custom middleware here, or use middleware provided in
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
pub const middleware: []const type = &.{
// jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware,
// jetzig.middleware.HtmxMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
};
// Maximum bytes to allow in request body.
pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
// Maximum filesize for `public/` content.
pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
// Maximum length of a header name. There is no limit imposed by the HTTP specification but
// AWS load balancers reference 40 as a limit so we use that as a baseline:
// https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_HttpHeaderConditionConfig.html
// This can be increased if needed.
pub const max_bytes_header_name: u16 = 40;
/// Maximum number of `multipart/form-data`-encoded fields to accept per request.
pub const max_multipart_form_fields: usize = 20;
// Log message buffer size. Log messages exceeding this size spill to heap with degraded
// performance. Log messages should aim to fit in the message buffer.
pub const log_message_buffer_len: usize = 4096;
// Maximum log pool size. When a log buffer is no longer required it is returned to a pool
// for recycling. When logging i/o is slow, a high volume of requests will result in this
// pool growing. When the pool size reaches the maximum value defined here, log events are
// freed instead of recycled.
pub const max_log_pool_len: usize = 256;
// Number of request threads. Defaults to number of detected CPUs.
pub const thread_count: ?u16 = null;
// Number of response worker threads.
pub const worker_count: u16 = 4;
// Total number of connections managed by worker threads.
pub const max_connections: u16 = 512;
// Per-thread stack memory to use before spilling into request arena (possibly with allocations).
pub const buffer_size: usize = 64 * 1024;
// The size of each item in the available memory pool used by requests for rendering.
// Total retained allocation: `worker_count * max_connections`.
pub const arena_size: usize = 1024 * 1024;
// Path relative to cwd() to serve public content from. Symlinks are not followed.
pub const public_content_path = "public";
// HTTP buffer. Must be large enough to store all headers. This should typically not be modified.
pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
// The number of worker threads to spawn on startup for processing Jobs (NOT the number of
// HTTP server worker threads).
pub const job_worker_threads: usize = 4;
// Duration before looking for more Jobs when the queue is found to be empty, in
// milliseconds.
pub const job_worker_sleep_interval_ms: usize = 10;
/// Database Schema. Set to `@import("Schema")` to load `src/app/database/Schema.zig`.
pub const Schema = @import("Schema");
/// HTTP cookie configuration
pub const cookies: jetzig.http.Cookies.CookieOptions = switch (jetzig.environment) {
.development, .testing => .{
.domain = "localhost",
.path = "/",
},
.production => .{
.same_site = .strict,
.secure = true,
.http_only = true,
.path = "/",
},
};
/// Key-value store options.
/// Available backends:
/// * memory: Simple, in-memory hashmap-backed store.
/// * file: Rudimentary file-backed store.
/// * valkey: Valkey-backed store with connection pool.
///
/// When using `.file` or `.valkey` backend, you must also set `.file_options` or
/// `.valkey_options` respectively.
///
/// ## File backend:
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
//
// ## Valkey backend
// .backend = .valkey,
// .valkey_options = .{
// .host = "localhost",
// .port = 6379,
// .timeout = 1000, // in milliseconds, i.e. 1 second.
// .connect = .lazy, // Connect on first use, or `auto` to connect on server startup.
// .buffer_size = 8192,
// .pool_size = 8,
// },
/// Available configuration options for `store`, `job_queue`, and `cache` are identical.
///
/// For production deployment, the `valkey` backend is recommended for all use cases.
///
/// The general-purpose key-value store is exposed as `request.store` in views and is also
/// available in as `env.store` in all jobs/mailers.
pub const store: jetzig.kv.Store.Options = .{
.backend = .memory,
};
/// Job queue options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for jobs queue.
/// The job queue is managed internally by Jetzig.
pub const job_queue: jetzig.kv.Store.Options = .{
.backend = .memory,
};
/// Cache options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache.
pub const cache: jetzig.kv.Store.Options = .{
.backend = .memory,
};
/// SMTP configuration for Jetzig Mail. It is recommended to use a local SMTP relay,
/// e.g.: https://github.com/juanluisbaptiste/docker-postfix
///
/// Each configuration option can be overridden with environment variables:
/// `JETZIG_SMTP_PORT`
/// `JETZIG_SMTP_ENCRYPTION`
/// `JETZIG_SMTP_HOST`
/// `JETZIG_SMTP_USERNAME`
/// `JETZIG_SMTP_PASSWORD`
// pub const smtp: jetzig.mail.SMTPConfig = .{
// .port = 25,
// .encryption = .none, // .insecure, .none, .tls, .start_tls
// .host = "localhost",
// .username = null,
// .password = null,
// };
/// Force email delivery in development mode (instead of printing email body to logger).
pub const force_development_email_delivery = false;
// Set custom fragments for rendering markdown templates. Any values will fall back to
// defaults provided by Zmd (https://github.com/jetzig-framework/zmd/blob/main/src/zmd/html.zig).
pub const markdown_fragments = struct {
pub const root = .{
"<div class='p-5'>",
"</div>",
};
pub const h1 = .{
"<h1 class='text-2xl mb-3 text-green font-bold'>",
"</h1>",
};
pub const h2 = .{
"<h2 class='text-xl mb-3 font-bold'>",
"</h2>",
};
pub const h3 = .{
"<h3 class='text-lg mb-3 font-bold'>",
"</h3>",
};
pub const paragraph = .{
"<p class='p-3'>",
"</p>",
};
pub const code = .{
"<span class='font-mono bg-gray-900 p-2 text-white'>",
"</span>",
};
pub const unordered_list = .{
"<ul class='list-disc ms-8 leading-8'>",
"</ul>",
};
pub const ordered_list = .{
"<ul class='list-decimal ms-8 leading-8'>",
"</ul>",
};
pub fn block(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 {
return try std.fmt.allocPrint(allocator,
\\<pre class="w-1/2 font-mono mt-4 ms-3 bg-gray-900 p-2 text-white"><code class="language-{?s}">{s}</code></pre>
, .{ node.meta, node.content });
}
pub fn link(allocator: std.mem.Allocator, node: zmd.Node) ![]const u8 {
return try std.fmt.allocPrint(allocator,
\\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a>
, .{ node.href.?, node.title.? });
}
};
};
pub fn init(app: *jetzig.App) !void {
_ = app;
// Example custom route:
// app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = if (builtin.mode == .Debug) gpa.allocator() else std.heap.c_allocator;
defer if (builtin.mode == .Debug) std.debug.assert(gpa.deinit() == .ok);
var app = try jetzig.init(allocator);
defer app.deinit();
try app.start(routes, .{});
}