From c584d3960722ce5c80610c67dc2c83303e596427 Mon Sep 17 00:00:00 2001
From: Guus Waals <_@guusw.nl>
Date: Wed, 29 Oct 2025 19:02:55 +0800
Subject: [PATCH] Update server post and fix post titles
---
Cargo.lock | 127 ++++++++++++++++++++++++++++++++++
Cargo.toml | 1 +
lua/lemon.lua | 152 +++++++++++++++++++++++++++++++++++++++++
posts/blog-server.md | 73 +++++++++++++++++++-
src/main.rs | 31 ++++++---
src/post_manager.rs | 83 +++++++++++++++-------
src/template_engine.rs | 71 +++++++++++++------
templates/header.html | 16 +++--
8 files changed, 493 insertions(+), 61 deletions(-)
create mode 100644 lua/lemon.lua
diff --git a/Cargo.lock b/Cargo.lock
index 0f855f2..34beb46 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11,6 +11,56 @@ dependencies = [
"libc",
]
+[[package]]
+name = "anstream"
+version = "0.6.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
+dependencies = [
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.60.2",
+]
+
[[package]]
name = "async-trait"
version = "0.1.89"
@@ -101,6 +151,7 @@ version = "0.1.0"
dependencies = [
"axum",
"chrono",
+ "clap",
"once_cell",
"pulldown-cmark",
"serde",
@@ -150,6 +201,52 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "clap"
+version = "4.5.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -225,6 +322,12 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
[[package]]
name = "http"
version = "1.3.1"
@@ -338,6 +441,12 @@ dependencies = [
"cc",
]
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
[[package]]
name = "itoa"
version = "1.0.15"
@@ -429,6 +538,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -631,6 +746,12 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
[[package]]
name = "syn"
version = "2.0.108"
@@ -791,6 +912,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
diff --git a/Cargo.toml b/Cargo.toml
index 12461c5..0cffcfb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,3 +12,4 @@ pulldown-cmark = "0.12"
serde = { version = "1.0", features = ["derive"] }
chrono = "0.4"
once_cell = "1.19"
+clap = { version = "4.5", features = ["derive"] }
diff --git a/lua/lemon.lua b/lua/lemon.lua
new file mode 100644
index 0000000..15bef70
--- /dev/null
+++ b/lua/lemon.lua
@@ -0,0 +1,152 @@
+Lemon = {
+ ws = {},
+ ws_file = '.nvim.workspace.lua',
+ term_buf = nil,
+ term_win_cmd = 'belowright 12split',
+}
+
+function LoadWorkspace()
+ -- Load persistent configuration from .workspace.lua
+ local loaded, workspace = pcall(dofile, Lemon.ws_file)
+ if not loaded then return nil end
+ return workspace
+end
+
+function WriteWorkspace()
+ -- A very minimal serializer for workspace configuration
+ local s = { l = "", ls = {}, i = "" }
+ local function w(v) s.l = s.l .. v end
+ local function nl()
+ s.ls[#s.ls + 1] = s.l; s.l = s.i;
+ end
+ local function wv(v)
+ local t = type(v)
+ if t == 'table' then
+ w('{'); local pi = s.i; s.i = s.i .. " "
+ for k1, v1 in pairs(v) do
+ nl(); w('['); wv(k1); w('] = '); wv(v1); w(',')
+ end
+ s.i = pi; nl(); w('}');
+ elseif t == 'number' then
+ w(tostring(v))
+ elseif t == 'string' then
+ w('"' .. v .. '"')
+ else
+ w(tostring(v))
+ end
+ end
+
+ -- Write the workspace file
+ w("return "); wv(Lemon.workspace); nl()
+ vim.fn.writefile(s.ls, Lemon.ws_file)
+end
+
+-- Loads the workspace from the file, or return the default
+---@param default table
+---@return table
+function InitWorkspace(default)
+ Lemon.ws = LoadWorkspace()
+ if Lemon.ws == nil then
+ Lemon.ws = default
+ end
+ return Lemon.ws
+end
+
+function TermShow()
+ local info = GetTermInfo()
+
+ if info == nil then
+ -- Create new terminal buffer
+ vim.cmd(Lemon.term_win_cmd)
+ vim.cmd('terminal')
+ Lemon.term_buf = vim.api.nvim_get_current_buf()
+ -- Mark buffer so we can identify it later
+ vim.api.nvim_buf_set_var(Lemon.term_buf, 'lemon_terminal', true)
+ info = GetTermInfo()
+ elseif info.win == nil then
+ -- Buffer exists but not visible, open it
+ vim.cmd(Lemon.term_win_cmd)
+ vim.api.nvim_win_set_buf(0, Lemon.term_buf)
+ else
+ -- Window is visible, switch to it
+ vim.api.nvim_set_current_win(info.win)
+ end
+return info
+end
+
+-- Find or create persistent terminal buffer, open window, and run command
+function TermRun(cmd)
+ local info = TermShow()
+
+ -- Send command to terminal
+ vim.fn.chansend(info.job_id, '\021' .. cmd .. '\n')
+ vim.fn.feedkeys("G", "n")
+end
+
+-- Get terminal buffer and job_id if valid, returns {buf, job_id, win}
+-- win is nil if terminal is not currently visible
+function GetTermInfo()
+ if Lemon.term_buf == nil or not vim.api.nvim_buf_is_valid(Lemon.term_buf) then
+ return nil
+ end
+
+ local job_id = vim.api.nvim_buf_get_var(Lemon.term_buf, 'terminal_job_id')
+
+ -- Find window showing the terminal buffer
+ local win = nil
+ for _, w in ipairs(vim.api.nvim_list_wins()) do
+ if vim.api.nvim_win_get_buf(w) == Lemon.term_buf then
+ win = w
+ break
+ end
+ end
+
+ return { buf = Lemon.term_buf, job_id = job_id, win = win }
+end
+
+-- Compatibility wrapper - returns window ID if terminal is visible
+function SwitchToExistingTerm()
+ local info = GetTermInfo()
+ return info and info.win or nil
+end
+
+-- Runs the make command and runs the callback when it completes
+function MakeAnd(run_callback)
+ -- Create a one-time autocmd that fires when make completes
+ local group = vim.api.nvim_create_augroup('MakeAnd', { clear = false })
+
+ vim.api.nvim_create_autocmd('QuickFixCmdPost', {
+ group = group,
+ pattern = 'make',
+ once = true,
+ callback = function()
+ local qf_list = vim.fn.getqflist()
+ local has_errors = false
+
+ for _, item in ipairs(qf_list) do
+ if item.valid == 1 then
+ has_errors = true
+ break
+ end
+ end
+
+ vim.schedule(function()
+ if not has_errors then
+ run_callback()
+ else
+ vim.api.nvim_echo({ { "Build failed", "ErrorMsg" } }, false, {})
+ end
+ end)
+ end
+ })
+
+ vim.cmd('silent make')
+end
+
+function TabCurrent()
+ return vim.fn.tabpagenr()
+end
+
+function TabSwitch(tab)
+ vim.cmd('tabnext ' .. tab)
+end
diff --git a/posts/blog-server.md b/posts/blog-server.md
index d5e614a..678eb09 100644
--- a/posts/blog-server.md
+++ b/posts/blog-server.md
@@ -1,10 +1,79 @@
# Blog setup with markdown, rust & git
-I've recently went trough a lot of changes in my career such as starting freelancing, setting up my own projects and wanting to take a serious attempt at solo game-dev. With this I figured it'd be nice to have a dev blog of sorts to keep track of progress and maybe share some interesting research or developments.
+Hey everyone. This is the first post I write for the blog I'm starting, which is Coincidentally about how I've structured the software surrounding my blog.
+
+First off, I've been wanting to do a lot recently:
+
+- Moving away from Windows to using Linux and MacOS for development and gaming
+- Switching from VSCode/Cursor to neovim
+- Switching from working for an employer to becoming a solo dev/freelancer
+- Dedicate time to solo game development
+
+Because of this, I figured it would be a good time to set up a blog, both for documenting whatever I'm working on and to get the word out about my work.
## The blog design
I wanted to have a setup that generates static html from something like markdown. Then recently I stumbled across this [https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html](https://gaultier.github.io/blog/making_my_static_blog_generator_11_times_faster.html)
+I decided to do something similar, however I used rust as it has some popular existing libraries for web servers and parsing markdown. The reason for writing an application to host the blog is that I wanted to have it automatically respond to a git webhook which would automatically pull the latest git repo and then rebuild the articles from their markdown files.
+
+## HTML
+
+Of course there is also some styling and markup required for the shell, like the navigation bar and footers.\
+I decided to go with a simple list of html templates that get included by the application and scan it for tags to inject certain magic values, like the list of posts or timestamps.
+
+For example, here is what the page you're looking at looks like:
+
+```html
+$