From f88476604d6c25885ae9ecc91d9b294629effe8a Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 16:22:11 +0100 Subject: [PATCH 01/12] custom html templates, inline css, drop /style.css route --- module.nix | 8 +++++ src/config.rs | 5 +++ src/render.rs | 85 ++++++++++++++++++++++++++++----------------------- src/web.rs | 25 +++++++++------ 4 files changed, 76 insertions(+), 47 deletions(-) diff --git a/module.nix b/module.nix index 7326c7e..f6409fd 100644 --- a/module.nix +++ b/module.nix @@ -75,6 +75,12 @@ in description = "Allow new guestbook submissions. When false, the form is hidden and submissions are rejected."; }; + templateFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Custom HTML template file with {{title}}, {{form}}, and {{entries}} placeholders. Uses built-in default if null."; + }; + user = mkOption { type = types.str; default = "guestbook"; @@ -115,6 +121,8 @@ in BOOK_MAX_MESSAGE_LENGTH = toString cfg.maxMessageLength; BOOK_MAX_WEBSITE_LENGTH = toString cfg.maxWebsiteLength; BOOK_OPEN_REGISTRATION = if cfg.openRegistration then "true" else "false"; + } // lib.optionalAttrs (cfg.templateFile != null) { + BOOK_TEMPLATE = cfg.templateFile; }; serviceConfig = { Type = "simple"; diff --git a/src/config.rs b/src/config.rs index 84a1128..769d263 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,6 +14,7 @@ pub struct Config { pub max_message_length: usize, pub max_website_length: usize, pub open_registration: bool, + pub template: Option, } impl Config { @@ -56,6 +57,10 @@ impl Config { open_registration: env::var("BOOK_OPEN_REGISTRATION") .map(|v| v != "false") .unwrap_or(true), + template: env::var("BOOK_TEMPLATE").ok().map(|path| { + std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("failed to read template {path}: {e}")) + }), }) } } diff --git a/src/render.rs b/src/render.rs index 68f6fab..cbcf3fa 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,39 +1,49 @@ use crate::entries::Entry; -pub fn render_page(site_title: &str, site_url: &str, entries: &[Entry], form_html: &str) -> String { - let nav_url = site_url.trim_end_matches('/'); - let mut html = format!( - r#" +pub const DEFAULT_TEMPLATE: &str = r#" - {site_title} - + {{title}} + -

guestbook

-

If you visited my site, please sign my guestbook!

-{form_html} -"# - ); + {{form}} + {{entries}} + + +"#; +pub fn render_page(template: &str, title: &str, entries: &[Entry], form_html: &str) -> String { + let entries_html = render_entries(entries); + template + .replace("{{title}}", title) + .replace("{{form}}", form_html) + .replace("{{entries}}", &entries_html) +} + +fn render_entries(entries: &[Entry]) -> String { + let mut html = String::new(); for entry in entries { html.push_str(&render_entry(entry)); } - - html.push_str("\n\n"); html } fn render_entry(entry: &Entry) -> String { - let mut header = format!("
\n

{} - {}", entry.meta.date, entry.meta.name); + let mut header = format!( + "

\n

{} - {}", + entry.meta.date, entry.meta.name + ); if !entry.meta.website.is_empty() { header.push_str(&format!( " ({})", @@ -52,14 +62,6 @@ pub const FORM_HTML: &str = r#"

"#; -pub const STYLE_CSS: &str = "body { - max-width: 70ch; - line-height: 1.5; - margin: 0 auto; - padding: 1rem; -} -"; - #[cfg(test)] mod tests { use super::*; @@ -79,23 +81,24 @@ mod tests { } #[test] - fn test_render_page_contains_nav() { - let html = render_page("ily.rs", "https://ily.rs", &[], FORM_HTML); - assert!(html.contains(r#"ily.rs"#)); - assert!(html.contains(r#"links"#)); + fn test_render_default_template() { + let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML); + assert!(html.contains("ily.rs")); + assert!(html.contains("action=\"/submit\"")); } #[test] - fn test_render_page_contains_form() { - let html = render_page("ily.rs", "https://ily.rs", &[], FORM_HTML); - assert!(html.contains(r#"action="/submit""#)); - assert!(html.contains(r#"style="display:none""#)); // honeypot + fn test_render_custom_template() { + let custom = "{{title}} {{form}} {{entries}}"; + let html = render_page(custom, "my site", &[], FORM_HTML); + assert!(html.contains("my site")); + assert!(html.contains("action=\"/submit\"")); } #[test] fn test_render_entry_no_website() { let entry = make_entry("alice", "2026-04-09", "Hello!"); - let html = render_page("ily.rs", "https://ily.rs", &[entry], FORM_HTML); + let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML); assert!(html.contains("alice")); assert!(html.contains("Hello!")); assert!(!html.contains(""#)); } #[test] fn test_render_preserves_html_in_body() { let entry = make_entry("carol", "2026-04-09", "Bold "); - let html = render_page("ily.rs", "https://ily.rs", &[entry], FORM_HTML); + let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML); assert!(html.contains("Bold")); assert!(html.contains("")); } + + #[test] + fn test_render_empty_form_when_closed() { + let html = render_page(DEFAULT_TEMPLATE, "test", &[], ""); + assert!(!html.contains("action=\"/submit\"")); + } } diff --git a/src/web.rs b/src/web.rs index 4e7b21c..5d2e92b 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,7 +1,6 @@ use axum::{ extract::State, - http::header, - response::{Html, IntoResponse}, + response::Html, routing::{get, post}, Form, Router, }; @@ -11,7 +10,7 @@ use uuid::Uuid; use crate::config::Config; use crate::entries::{self, Entry, EntryMeta, Status}; -use crate::render::{self, FORM_HTML, STYLE_CSS}; +use crate::render::{self, DEFAULT_TEMPLATE, FORM_HTML}; pub struct AppState { pub config: Config, @@ -32,7 +31,6 @@ pub fn router(state: Arc) -> Router { Router::new() .route("/", get(index)) .route("/submit", post(submit)) - .route("/style.css", get(style)) .with_state(state) } @@ -40,9 +38,10 @@ async fn index(State(state): State>) -> Html { let entries_dir = state.config.data_dir.join("entries"); let entries = entries::read_approved(&entries_dir); let form = if state.config.open_registration { FORM_HTML } else { "" }; + let template = state.config.template.as_deref().unwrap_or(DEFAULT_TEMPLATE); let html = render::render_page( + template, &state.config.site_title, - &state.config.site_url, &entries, form, ); @@ -113,10 +112,6 @@ async fn submit( Html("Thanks! Your message is pending approval.".to_string()) } -async fn style() -> impl IntoResponse { - ([(header::CONTENT_TYPE, "text/css")], STYLE_CSS) -} - #[cfg(test)] mod tests { use super::*; @@ -138,6 +133,7 @@ mod tests { max_message_length: 1000, max_website_length: 100, open_registration: true, + template: None, } } @@ -270,6 +266,17 @@ mod tests { assert!(body.contains("too long")); } + #[tokio::test] + async fn test_custom_template() { + let dir = tempfile::tempdir().unwrap(); + let mut config = test_config(dir.path()); + config.template = Some("{{form}}{{entries}}".into()); + let (app, _rx) = test_app(config); + let html = get_index(&app).await; + assert!(html.contains("custom nav")); + assert!(html.contains("action=\"/submit\"")); + } + #[tokio::test] async fn test_valid_submission_creates_entry() { let dir = tempfile::tempdir().unwrap(); From d751eb62a96697f9ed91811d2a9a2a8ad918128b Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 16:55:01 +0100 Subject: [PATCH 02/12] pre-wrap default template, t0sig-style form and entries --- src/render.rs | 58 +++++++++++++++++++++++++++--------------- templates/default.html | 33 ++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 templates/default.html diff --git a/src/render.rs b/src/render.rs index cbcf3fa..a029320 100644 --- a/src/render.rs +++ b/src/render.rs @@ -7,18 +7,30 @@ pub const DEFAULT_TEMPLATE: &str = r#" {{title}} -

guestbook

- {{form}} - {{entries}} +
+{{title}}
+
+guestbook
+=========
+
+{{form}}
+
+entries
+=======
+{{entries}}
+
"#; @@ -40,27 +52,32 @@ fn render_entries(entries: &[Entry]) -> String { } fn render_entry(entry: &Entry) -> String { - let mut header = format!( - "
\n

{} - {}", - entry.meta.date, entry.meta.name - ); + let mut header = format!("{} - {}", entry.meta.date, entry.meta.name); if !entry.meta.website.is_empty() { header.push_str(&format!( " ({})", entry.meta.website, entry.meta.website )); } - header.push_str("

\n"); - format!("{header} {}\n
\n", entry.body) + format!( + "\n{header}\n\n{}\n\n------------------------------------------------------------\n", + entry.body + ) } -pub const FORM_HTML: &str = r#"
- - - - - -
"#; +pub const FORM_HTML: &str = r#"If you visited my site, please sign my guestbook! +
+Your name: + + +Your website (optional): + + +Your message: + + + +
"#; #[cfg(test)] mod tests { @@ -85,6 +102,7 @@ mod tests { let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML); assert!(html.contains("ily.rs")); assert!(html.contains("action=\"/submit\"")); + assert!(html.contains("
"));
     }
 
     #[test]
@@ -99,9 +117,9 @@ mod tests {
     fn test_render_entry_no_website() {
         let entry = make_entry("alice", "2026-04-09", "Hello!");
         let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
-        assert!(html.contains("alice"));
+        assert!(html.contains("2026-04-09 - alice"));
         assert!(html.contains("Hello!"));
-        assert!(!html.contains("
+
+
+  
+  
+  {{title}}
+  
+
+
+
+{{title}}
+
+guestbook
+=========
+
+{{form}}
+
+entries
+=======
+{{entries}}
+
+ + From ef6a19054946f51d10d0ccc1575cdbd618749220 Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 16:58:42 +0100 Subject: [PATCH 03/12] configurable separator, .env.example --- .env.example | 13 +++++++++++++ module.nix | 7 +++++++ src/config.rs | 3 +++ src/render.rs | 28 ++++++++++++++-------------- src/web.rs | 2 ++ 5 files changed, 39 insertions(+), 14 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..28e5594 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +BOOK_PORT=8123 +BOOK_DATA_DIR=./data +BOOK_SITE_TITLE=guestbook +BOOK_SITE_URL=https://example.com +BOOK_TELEGRAM_BOT_TOKEN=your-bot-token-here +BOOK_TELEGRAM_CHAT_ID=0 +BOOK_HONEYPOT=true +BOOK_MAX_NAME_LENGTH=50 +BOOK_MAX_MESSAGE_LENGTH=1000 +BOOK_MAX_WEBSITE_LENGTH=100 +BOOK_OPEN_REGISTRATION=true +BOOK_SEPARATOR=------------------------------------------------------------ +# BOOK_TEMPLATE=./templates/default.html diff --git a/module.nix b/module.nix index f6409fd..577bbf6 100644 --- a/module.nix +++ b/module.nix @@ -75,6 +75,12 @@ in description = "Allow new guestbook submissions. When false, the form is hidden and submissions are rejected."; }; + separator = mkOption { + type = types.str; + default = "------------------------------------------------------------"; + description = "Separator between guestbook entries."; + }; + templateFile = mkOption { type = types.nullOr types.path; default = null; @@ -121,6 +127,7 @@ in BOOK_MAX_MESSAGE_LENGTH = toString cfg.maxMessageLength; BOOK_MAX_WEBSITE_LENGTH = toString cfg.maxWebsiteLength; BOOK_OPEN_REGISTRATION = if cfg.openRegistration then "true" else "false"; + BOOK_SEPARATOR = cfg.separator; } // lib.optionalAttrs (cfg.templateFile != null) { BOOK_TEMPLATE = cfg.templateFile; }; diff --git a/src/config.rs b/src/config.rs index 769d263..af4798f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ pub struct Config { pub max_website_length: usize, pub open_registration: bool, pub template: Option, + pub separator: String, } impl Config { @@ -57,6 +58,8 @@ impl Config { open_registration: env::var("BOOK_OPEN_REGISTRATION") .map(|v| v != "false") .unwrap_or(true), + separator: env::var("BOOK_SEPARATOR") + .unwrap_or_else(|_| "------------------------------------------------------------".into()), template: env::var("BOOK_TEMPLATE").ok().map(|path| { std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("failed to read template {path}: {e}")) diff --git a/src/render.rs b/src/render.rs index a029320..a55bd10 100644 --- a/src/render.rs +++ b/src/render.rs @@ -35,23 +35,23 @@ entries "#; -pub fn render_page(template: &str, title: &str, entries: &[Entry], form_html: &str) -> String { - let entries_html = render_entries(entries); +pub fn render_page(template: &str, title: &str, entries: &[Entry], form_html: &str, separator: &str) -> String { + let entries_html = render_entries(entries, separator); template .replace("{{title}}", title) .replace("{{form}}", form_html) .replace("{{entries}}", &entries_html) } -fn render_entries(entries: &[Entry]) -> String { +fn render_entries(entries: &[Entry], separator: &str) -> String { let mut html = String::new(); for entry in entries { - html.push_str(&render_entry(entry)); + html.push_str(&render_entry(entry, separator)); } html } -fn render_entry(entry: &Entry) -> String { +fn render_entry(entry: &Entry, separator: &str) -> String { let mut header = format!("{} - {}", entry.meta.date, entry.meta.name); if !entry.meta.website.is_empty() { header.push_str(&format!( @@ -60,7 +60,7 @@ fn render_entry(entry: &Entry) -> String { )); } format!( - "\n{header}\n\n{}\n\n------------------------------------------------------------\n", + "\n{header}\n\n{}\n\n{separator}\n", entry.body ) } @@ -99,16 +99,16 @@ mod tests { #[test] fn test_render_default_template() { - let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML); + let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML, "---"); assert!(html.contains("ily.rs")); assert!(html.contains("action=\"/submit\"")); - assert!(html.contains("
"));
+        assert!(html.contains("
"));
     }
 
     #[test]
     fn test_render_custom_template() {
         let custom = "{{title}} {{form}} {{entries}}";
-        let html = render_page(custom, "my site", &[], FORM_HTML);
+        let html = render_page(custom, "my site", &[], FORM_HTML, "---");
         assert!(html.contains("my site"));
         assert!(html.contains("action=\"/submit\""));
     }
@@ -116,31 +116,31 @@ mod tests {
     #[test]
     fn test_render_entry_no_website() {
         let entry = make_entry("alice", "2026-04-09", "Hello!");
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
+        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
         assert!(html.contains("2026-04-09 - alice"));
         assert!(html.contains("Hello!"));
-        assert!(html.contains("----"));
+        assert!(html.contains("---"));
     }
 
     #[test]
     fn test_render_entry_with_website() {
         let mut entry = make_entry("bob", "2026-04-09", "Hi!");
         entry.meta.website = "https://bob.com".into();
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
+        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
         assert!(html.contains(r#""#));
     }
 
     #[test]
     fn test_render_preserves_html_in_body() {
         let entry = make_entry("carol", "2026-04-09", "Bold ");
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML);
+        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
         assert!(html.contains("Bold"));
         assert!(html.contains(""));
     }
 
     #[test]
     fn test_render_empty_form_when_closed() {
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[], "");
+        let html = render_page(DEFAULT_TEMPLATE, "test", &[], "", "---");
         assert!(!html.contains("action=\"/submit\""));
     }
 }
diff --git a/src/web.rs b/src/web.rs
index 5d2e92b..75a1049 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -44,6 +44,7 @@ async fn index(State(state): State>) -> Html {
         &state.config.site_title,
         &entries,
         form,
+        &state.config.separator,
     );
     Html(html)
 }
@@ -134,6 +135,7 @@ mod tests {
             max_website_length: 100,
             open_registration: true,
             template: None,
+            separator: "---".into(),
         }
     }
 

From b041941a4a47cf96195d6c7dd0506d112df5bbac Mon Sep 17 00:00:00 2001
From: lew 
Date: Thu, 9 Apr 2026 17:11:59 +0100
Subject: [PATCH 04/12] classes on all elements, configurable form
 labels/style/textarea

---
 .env.example           |   8 ++
 module.nix             |  56 +++++++++++++
 src/config.rs          |  27 +++++++
 src/render.rs          | 173 +++++++++++++++++++++++++++++++----------
 src/web.rs             |  21 +++--
 templates/default.html |   1 +
 6 files changed, 240 insertions(+), 46 deletions(-)

diff --git a/.env.example b/.env.example
index 28e5594..2013aa5 100644
--- a/.env.example
+++ b/.env.example
@@ -10,4 +10,12 @@ BOOK_MAX_MESSAGE_LENGTH=1000
 BOOK_MAX_WEBSITE_LENGTH=100
 BOOK_OPEN_REGISTRATION=true
 BOOK_SEPARATOR=------------------------------------------------------------
+BOOK_FORM_PROMPT=If you visited my site, please sign my guestbook!
+BOOK_BUTTON_TEXT=sign
+BOOK_LABEL_NAME=Your name:
+BOOK_LABEL_WEBSITE=Your website (optional):
+BOOK_LABEL_MESSAGE=Your message:
+BOOK_TEXTAREA_ROWS=8
+BOOK_TEXTAREA_COLS=60
+# BOOK_STYLE=.entry-name { font-weight: bold; }
 # BOOK_TEMPLATE=./templates/default.html
diff --git a/module.nix b/module.nix
index 577bbf6..6464d15 100644
--- a/module.nix
+++ b/module.nix
@@ -81,6 +81,54 @@ in
       description = "Separator between guestbook entries.";
     };
 
+    style = mkOption {
+      type = types.str;
+      default = "";
+      description = "Custom CSS injected into a style tag. Use class names: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, .guestbook-textarea, .guestbook-button, .entry-header, .entry-name, .entry-website, .entry-body, .entry-separator";
+    };
+
+    formPrompt = mkOption {
+      type = types.str;
+      default = "If you visited my site, please sign my guestbook!";
+      description = "Text shown above the form.";
+    };
+
+    buttonText = mkOption {
+      type = types.str;
+      default = "sign";
+      description = "Submit button text.";
+    };
+
+    labelName = mkOption {
+      type = types.str;
+      default = "Your name:";
+      description = "Label for the name field.";
+    };
+
+    labelWebsite = mkOption {
+      type = types.str;
+      default = "Your website (optional):";
+      description = "Label for the website field.";
+    };
+
+    labelMessage = mkOption {
+      type = types.str;
+      default = "Your message:";
+      description = "Label for the message field.";
+    };
+
+    textareaRows = mkOption {
+      type = types.int;
+      default = 8;
+      description = "Number of rows for the message textarea.";
+    };
+
+    textareaCols = mkOption {
+      type = types.int;
+      default = 60;
+      description = "Number of columns for the message textarea.";
+    };
+
     templateFile = mkOption {
       type = types.nullOr types.path;
       default = null;
@@ -128,6 +176,14 @@ in
           BOOK_MAX_WEBSITE_LENGTH = toString cfg.maxWebsiteLength;
           BOOK_OPEN_REGISTRATION = if cfg.openRegistration then "true" else "false";
           BOOK_SEPARATOR = cfg.separator;
+          BOOK_STYLE = cfg.style;
+          BOOK_FORM_PROMPT = cfg.formPrompt;
+          BOOK_BUTTON_TEXT = cfg.buttonText;
+          BOOK_LABEL_NAME = cfg.labelName;
+          BOOK_LABEL_WEBSITE = cfg.labelWebsite;
+          BOOK_LABEL_MESSAGE = cfg.labelMessage;
+          BOOK_TEXTAREA_ROWS = toString cfg.textareaRows;
+          BOOK_TEXTAREA_COLS = toString cfg.textareaCols;
         } // lib.optionalAttrs (cfg.templateFile != null) {
           BOOK_TEMPLATE = cfg.templateFile;
         };
diff --git a/src/config.rs b/src/config.rs
index af4798f..b552b1e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -16,6 +16,14 @@ pub struct Config {
     pub open_registration: bool,
     pub template: Option,
     pub separator: String,
+    pub style: String,
+    pub form_prompt: String,
+    pub button_text: String,
+    pub label_name: String,
+    pub label_website: String,
+    pub label_message: String,
+    pub textarea_rows: u32,
+    pub textarea_cols: u32,
 }
 
 impl Config {
@@ -64,6 +72,25 @@ impl Config {
                 std::fs::read_to_string(&path)
                     .unwrap_or_else(|e| panic!("failed to read template {path}: {e}"))
             }),
+            style: env::var("BOOK_STYLE").unwrap_or_default(),
+            form_prompt: env::var("BOOK_FORM_PROMPT")
+                .unwrap_or_else(|_| "If you visited my site, please sign my guestbook!".into()),
+            button_text: env::var("BOOK_BUTTON_TEXT")
+                .unwrap_or_else(|_| "sign".into()),
+            label_name: env::var("BOOK_LABEL_NAME")
+                .unwrap_or_else(|_| "Your name:".into()),
+            label_website: env::var("BOOK_LABEL_WEBSITE")
+                .unwrap_or_else(|_| "Your website (optional):".into()),
+            label_message: env::var("BOOK_LABEL_MESSAGE")
+                .unwrap_or_else(|_| "Your message:".into()),
+            textarea_rows: env::var("BOOK_TEXTAREA_ROWS")
+                .unwrap_or_else(|_| "8".into())
+                .parse()
+                .map_err(|_| "BOOK_TEXTAREA_ROWS must be a number")?,
+            textarea_cols: env::var("BOOK_TEXTAREA_COLS")
+                .unwrap_or_else(|_| "60".into())
+                .parse()
+                .map_err(|_| "BOOK_TEXTAREA_COLS must be a number")?,
         })
     }
 }
diff --git a/src/render.rs b/src/render.rs
index a55bd10..2b3ec1b 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -1,3 +1,4 @@
+use crate::config::Config;
 use crate::entries::Entry;
 
 pub const DEFAULT_TEMPLATE: &str = r#"
@@ -10,13 +11,13 @@ pub const DEFAULT_TEMPLATE: &str = r#"
     pre {
       font: unset;
       max-width: 70ch;
-
       margin: 0 auto;
       padding: 1rem;
       white-space: pre-wrap;
       word-wrap: break-word;
     }
   
+  {{style}}
 
 
 
@@ -35,12 +36,43 @@ entries
 
 "#;
 
-pub fn render_page(template: &str, title: &str, entries: &[Entry], form_html: &str, separator: &str) -> String {
-    let entries_html = render_entries(entries, separator);
+pub fn render_page(template: &str, config: &Config, entries: &[Entry], form_html: &str) -> String {
+    let entries_html = render_entries(entries, &config.separator);
+    let style = if config.style.is_empty() {
+        String::new()
+    } else {
+        format!("", config.style)
+    };
     template
-        .replace("{{title}}", title)
+        .replace("{{title}}", &config.site_title)
         .replace("{{form}}", form_html)
         .replace("{{entries}}", &entries_html)
+        .replace("{{style}}", &style)
+}
+
+pub fn render_form(config: &Config) -> String {
+    format!(
+        r#"{prompt}
+
+ + + + + + + + + + +
"#, + prompt = config.form_prompt, + label_name = config.label_name, + label_website = config.label_website, + label_message = config.label_message, + rows = config.textarea_rows, + cols = config.textarea_cols, + button = config.button_text, + ) } fn render_entries(entries: &[Entry], separator: &str) -> String { @@ -52,37 +84,54 @@ fn render_entries(entries: &[Entry], separator: &str) -> String { } fn render_entry(entry: &Entry, separator: &str) -> String { - let mut header = format!("{} - {}", entry.meta.date, entry.meta.name); + let mut header = format!( + "{} - {}", + entry.meta.date, entry.meta.name + ); if !entry.meta.website.is_empty() { header.push_str(&format!( - " (
{})", + " ({})", entry.meta.website, entry.meta.website )); } + header.push_str(""); format!( - "\n{header}\n\n{}\n\n{separator}\n", + "\n{header}\n\n{}\n\n{separator}\n", entry.body ) } -pub const FORM_HTML: &str = r#"If you visited my site, please sign my guestbook! -
-Your name: - - -Your website (optional): - - -Your message: - - - -
"#; - #[cfg(test)] mod tests { use super::*; use crate::entries::{Entry, EntryMeta, Status}; + use std::path::PathBuf; + + fn test_config() -> Config { + Config { + port: 0, + data_dir: PathBuf::from("./data"), + site_title: "test".into(), + site_url: "https://test.rs".into(), + telegram_bot_token: "fake".into(), + telegram_chat_id: 0, + honeypot: true, + max_name_length: 50, + max_message_length: 1000, + max_website_length: 100, + open_registration: true, + template: None, + separator: "---".into(), + style: String::new(), + form_prompt: "Sign my guestbook!".into(), + button_text: "sign".into(), + label_name: "Your name:".into(), + label_website: "Your website (optional):".into(), + label_message: "Your message:".into(), + textarea_rows: 8, + textarea_cols: 60, + } + } fn make_entry(name: &str, date: &str, body: &str) -> Entry { Entry { @@ -99,48 +148,90 @@ mod tests { #[test] fn test_render_default_template() { - let html = render_page(DEFAULT_TEMPLATE, "ily.rs", &[], FORM_HTML, "---"); - assert!(html.contains("ily.rs")); - assert!(html.contains("action=\"/submit\"")); - assert!(html.contains("
"));
+        let config = test_config();
+        let form = render_form(&config);
+        let html = render_page(DEFAULT_TEMPLATE, &config, &[], &form);
+        assert!(html.contains("test"));
+        assert!(html.contains("guestbook-form"));
     }
 
     #[test]
     fn test_render_custom_template() {
-        let custom = "{{title}} {{form}} {{entries}}";
-        let html = render_page(custom, "my site", &[], FORM_HTML, "---");
-        assert!(html.contains("my site"));
-        assert!(html.contains("action=\"/submit\""));
+        let config = test_config();
+        let custom = "{{title}} {{form}} {{entries}} {{style}}";
+        let form = render_form(&config);
+        let html = render_page(custom, &config, &[], &form);
+        assert!(html.contains("test"));
+        assert!(html.contains("guestbook-form"));
     }
 
     #[test]
-    fn test_render_entry_no_website() {
+    fn test_render_entry_classes() {
+        let config = test_config();
         let entry = make_entry("alice", "2026-04-09", "Hello!");
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
-        assert!(html.contains("2026-04-09 - alice"));
-        assert!(html.contains("Hello!"));
-        assert!(html.contains("---"));
+        let form = render_form(&config);
+        let html = render_page(DEFAULT_TEMPLATE, &config, &[entry], &form);
+        assert!(html.contains("entry-header"));
+        assert!(html.contains("entry-name"));
+        assert!(html.contains("entry-body"));
+        assert!(html.contains("entry-separator"));
     }
 
     #[test]
     fn test_render_entry_with_website() {
+        let config = test_config();
         let mut entry = make_entry("bob", "2026-04-09", "Hi!");
         entry.meta.website = "https://bob.com".into();
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
-        assert!(html.contains(r#""#));
+        let form = render_form(&config);
+        let html = render_page(DEFAULT_TEMPLATE, &config, &[entry], &form);
+        assert!(html.contains("entry-website"));
+        assert!(html.contains(r#"href="https://bob.com">"#));
     }
 
     #[test]
     fn test_render_preserves_html_in_body() {
-        let entry = make_entry("carol", "2026-04-09", "Bold ");
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[entry], FORM_HTML, "---");
+        let config = test_config();
+        let entry = make_entry("carol", "2026-04-09", "Bold");
+        let form = render_form(&config);
+        let html = render_page(DEFAULT_TEMPLATE, &config, &[entry], &form);
         assert!(html.contains("Bold"));
-        assert!(html.contains(""));
     }
 
     #[test]
     fn test_render_empty_form_when_closed() {
-        let html = render_page(DEFAULT_TEMPLATE, "test", &[], "", "---");
-        assert!(!html.contains("action=\"/submit\""));
+        let config = test_config();
+        let html = render_page(DEFAULT_TEMPLATE, &config, &[], "");
+        assert!(!html.contains("guestbook-form"));
+    }
+
+    #[test]
+    fn test_render_custom_style() {
+        let mut config = test_config();
+        config.style = ".entry-name { color: red; }".into();
+        let html = render_page(DEFAULT_TEMPLATE, &config, &[], "");
+        assert!(html.contains(".entry-name { color: red; }"));
+        assert!(html.contains("
+  {{style}}
 
 
 

From 23fe1478610b58cd2478bb7287f38c23387517e4 Mon Sep 17 00:00:00 2001
From: lew 
Date: Thu, 9 Apr 2026 17:14:35 +0100
Subject: [PATCH 05/12] BOOK_STYLE_FILE for multiline css from .env

---
 .env.example  | 1 +
 module.nix    | 8 ++++++++
 src/config.rs | 9 ++++++++-
 3 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/.env.example b/.env.example
index 2013aa5..cf935ec 100644
--- a/.env.example
+++ b/.env.example
@@ -17,5 +17,6 @@ BOOK_LABEL_WEBSITE=Your website (optional):
 BOOK_LABEL_MESSAGE=Your message:
 BOOK_TEXTAREA_ROWS=8
 BOOK_TEXTAREA_COLS=60
+# BOOK_STYLE_FILE=./my-styles.css
 # BOOK_STYLE=.entry-name { font-weight: bold; }
 # BOOK_TEMPLATE=./templates/default.html
diff --git a/module.nix b/module.nix
index 6464d15..1bbe6ef 100644
--- a/module.nix
+++ b/module.nix
@@ -87,6 +87,12 @@ in
       description = "Custom CSS injected into a style tag. Use class names: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, .guestbook-textarea, .guestbook-button, .entry-header, .entry-name, .entry-website, .entry-body, .entry-separator";
     };
 
+    styleFile = mkOption {
+      type = types.nullOr types.path;
+      default = null;
+      description = "Path to a CSS file. Takes precedence over style.";
+    };
+
     formPrompt = mkOption {
       type = types.str;
       default = "If you visited my site, please sign my guestbook!";
@@ -177,6 +183,8 @@ in
           BOOK_OPEN_REGISTRATION = if cfg.openRegistration then "true" else "false";
           BOOK_SEPARATOR = cfg.separator;
           BOOK_STYLE = cfg.style;
+        } // lib.optionalAttrs (cfg.styleFile != null) {
+          BOOK_STYLE_FILE = cfg.styleFile;
           BOOK_FORM_PROMPT = cfg.formPrompt;
           BOOK_BUTTON_TEXT = cfg.buttonText;
           BOOK_LABEL_NAME = cfg.labelName;
diff --git a/src/config.rs b/src/config.rs
index b552b1e..75e93bd 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -72,7 +72,14 @@ impl Config {
                 std::fs::read_to_string(&path)
                     .unwrap_or_else(|e| panic!("failed to read template {path}: {e}"))
             }),
-            style: env::var("BOOK_STYLE").unwrap_or_default(),
+            style: env::var("BOOK_STYLE_FILE")
+                .ok()
+                .map(|path| {
+                    std::fs::read_to_string(&path)
+                        .unwrap_or_else(|e| panic!("failed to read style file {path}: {e}"))
+                })
+                .or_else(|| env::var("BOOK_STYLE").ok())
+                .unwrap_or_default(),
             form_prompt: env::var("BOOK_FORM_PROMPT")
                 .unwrap_or_else(|_| "If you visited my site, please sign my guestbook!".into()),
             button_text: env::var("BOOK_BUTTON_TEXT")

From 47d6f2e6f068cb26802dad9cbc52d2bdc2e09ea0 Mon Sep 17 00:00:00 2001
From: lew 
Date: Thu, 9 Apr 2026 17:15:22 +0100
Subject: [PATCH 06/12] document .env.example with descriptions

---
 .env.example | 63 +++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 55 insertions(+), 8 deletions(-)

diff --git a/.env.example b/.env.example
index cf935ec..abff93a 100644
--- a/.env.example
+++ b/.env.example
@@ -1,22 +1,69 @@
+# Port to listen on (binds to 127.0.0.1).
 BOOK_PORT=8123
+
+# Directory for guestbook entry files.
 BOOK_DATA_DIR=./data
+
+# Site title shown in nav and page title.
 BOOK_SITE_TITLE=guestbook
+
+# Base URL of the main site (for absolute nav links).
 BOOK_SITE_URL=https://example.com
+
+# Telegram bot token.
 BOOK_TELEGRAM_BOT_TOKEN=your-bot-token-here
+
+# Telegram chat ID for moderation messages.
 BOOK_TELEGRAM_CHAT_ID=0
+
+# Enable honeypot field for spam prevention.
 BOOK_HONEYPOT=true
+
+# Maximum length for names. 0 for unlimited.
 BOOK_MAX_NAME_LENGTH=50
+
+# Maximum length for messages. 0 for unlimited.
 BOOK_MAX_MESSAGE_LENGTH=1000
+
+# Maximum length for website URLs. 0 for unlimited.
 BOOK_MAX_WEBSITE_LENGTH=100
+
+# Allow new guestbook submissions. When false, the form is hidden and submissions are rejected.
 BOOK_OPEN_REGISTRATION=true
+
+# Separator between guestbook entries.
 BOOK_SEPARATOR=------------------------------------------------------------
-BOOK_FORM_PROMPT=If you visited my site, please sign my guestbook!
-BOOK_BUTTON_TEXT=sign
-BOOK_LABEL_NAME=Your name:
-BOOK_LABEL_WEBSITE=Your website (optional):
-BOOK_LABEL_MESSAGE=Your message:
-BOOK_TEXTAREA_ROWS=8
-BOOK_TEXTAREA_COLS=60
+
+# Path to a CSS file. Takes precedence over BOOK_STYLE.
 # BOOK_STYLE_FILE=./my-styles.css
-# BOOK_STYLE=.entry-name { font-weight: bold; }
+
+# Custom CSS injected into a style tag.
+# Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input,
+#          .guestbook-textarea, .guestbook-button, .entry-header, .entry-name,
+#          .entry-website, .entry-body, .entry-separator
+# BOOK_STYLE=
+
+# Text shown above the form.
+BOOK_FORM_PROMPT=If you visited my site, please sign my guestbook!
+
+# Submit button text.
+BOOK_BUTTON_TEXT=sign
+
+# Label for the name field.
+BOOK_LABEL_NAME=Your name:
+
+# Label for the website field.
+BOOK_LABEL_WEBSITE=Your website (optional):
+
+# Label for the message field.
+BOOK_LABEL_MESSAGE=Your message:
+
+# Number of rows for the message textarea.
+BOOK_TEXTAREA_ROWS=8
+
+# Number of columns for the message textarea.
+BOOK_TEXTAREA_COLS=60
+
+# Custom HTML template file with {{title}}, {{form}}, {{entries}}, and {{style}} placeholders.
+# Uses built-in default if unset.
 # BOOK_TEMPLATE=./templates/default.html

From e7aa4cae96623066e3ce4841fa24b8009a555340 Mon Sep 17 00:00:00 2001
From: lew 
Date: Thu, 9 Apr 2026 17:16:20 +0100
Subject: [PATCH 07/12] default css as a standalone file for reference

---
 templates/default.css | 8 ++++++++
 1 file changed, 8 insertions(+)
 create mode 100644 templates/default.css

diff --git a/templates/default.css b/templates/default.css
new file mode 100644
index 0000000..b744070
--- /dev/null
+++ b/templates/default.css
@@ -0,0 +1,8 @@
+pre {
+  font: unset;
+  max-width: 70ch;
+  margin: 0 auto;
+  padding: 1rem;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}

From 232a1d452676a087bc9bbc6a296910ad6baecd7f Mon Sep 17 00:00:00 2001
From: lew 
Date: Thu, 9 Apr 2026 17:24:57 +0100
Subject: [PATCH 08/12] point .env.example at default css

---
 .env.example | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.env.example b/.env.example
index abff93a..50a6c05 100644
--- a/.env.example
+++ b/.env.example
@@ -35,7 +35,7 @@ BOOK_OPEN_REGISTRATION=true
 BOOK_SEPARATOR=------------------------------------------------------------
 
 # Path to a CSS file. Takes precedence over BOOK_STYLE.
-# BOOK_STYLE_FILE=./my-styles.css
+# BOOK_STYLE_FILE=./templates/default.css
 
 # Custom CSS injected into a style tag.
 # Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input,

From 250dc0240794a6f13541d22dc83ec34a9fac9361 Mon Sep 17 00:00:00 2001
From: lew 
Date: Thu, 9 Apr 2026 17:34:40 +0100
Subject: [PATCH 09/12] replace pre with div.page-container, document template

---
 src/render.rs          |  7 +++----
 templates/default.css  | 19 +++++++++++++++++--
 templates/default.html | 39 ++++++++++++++++++++++++++++++++++-----
 3 files changed, 54 insertions(+), 11 deletions(-)

diff --git a/src/render.rs b/src/render.rs
index 2b3ec1b..1759cf4 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -8,8 +8,7 @@ pub const DEFAULT_TEMPLATE: &str = r#"
   
   {{title}}
   
-  {{style}}
-
-
-
-{{title}} - -guestbook -========= - -{{form}} - -entries -======= -{{entries}} -
- - -"#; +pub const DEFAULT_TEMPLATE: &str = include_str!("../templates/default.html"); pub fn render_page(template: &str, config: &Config, entries: &[Entry], form_html: &str) -> String { let entries_html = render_entries(entries, &config.separator); @@ -200,7 +168,7 @@ mod tests { fn test_render_empty_form_when_closed() { let config = test_config(); let html = render_page(DEFAULT_TEMPLATE, &config, &[], ""); - assert!(!html.contains("guestbook-form")); + assert!(!html.contains("action=\"/submit\"")); } #[test] From f794fa3ce0ed70e6cab67b2e7f51d05558304290 Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 17:42:32 +0100 Subject: [PATCH 11/12] include_str default css, fix placeholder replacement in comments --- src/render.rs | 8 +++++--- templates/default.html | 44 ++++++++++++------------------------------ 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/src/render.rs b/src/render.rs index 9dd3e2e..514317b 100644 --- a/src/render.rs +++ b/src/render.rs @@ -2,14 +2,16 @@ use crate::config::Config; use crate::entries::Entry; pub const DEFAULT_TEMPLATE: &str = include_str!("../templates/default.html"); +pub const DEFAULT_STYLE: &str = include_str!("../templates/default.css"); pub fn render_page(template: &str, config: &Config, entries: &[Entry], form_html: &str) -> String { let entries_html = render_entries(entries, &config.separator); - let style = if config.style.is_empty() { - String::new() + let css = if config.style.is_empty() { + DEFAULT_STYLE } else { - format!("", config.style) + &config.style }; + let style = format!(""); template .replace("{{title}}", &config.site_title) .replace("{{form}}", form_html) diff --git a/templates/default.html b/templates/default.html index 93004e5..e1f6074 100644 --- a/templates/default.html +++ b/templates/default.html @@ -2,32 +2,21 @@ Default guestbook template. Copy this file and point BOOK_TEMPLATE at your copy to customize. + Placeholders are inserted with double curly braces, e.g. curly-title-curly. + Available placeholders: - {{title}} - Site title (BOOK_SITE_TITLE). Useful in and headings. - {{form}} - The submission form (labels, inputs, button). Controlled by - BOOK_FORM_PROMPT, BOOK_LABEL_NAME, BOOK_LABEL_WEBSITE, - BOOK_LABEL_MESSAGE, BOOK_BUTTON_TEXT, BOOK_TEXTAREA_ROWS, - BOOK_TEXTAREA_COLS. Empty when BOOK_OPEN_REGISTRATION=false. - {{entries}} - Approved guestbook entries, newest first. Entry separator - controlled by BOOK_SEPARATOR. - {{style}} - Custom CSS from BOOK_STYLE or BOOK_STYLE_FILE, wrapped in - a <style> tag. Empty when neither is set. + title - Site title (BOOK_SITE_TITLE). Useful in <title> and headings. + form - The submission form (labels, inputs, button). Controlled by + BOOK_FORM_PROMPT, BOOK_LABEL_NAME, BOOK_LABEL_WEBSITE, + BOOK_LABEL_MESSAGE, BOOK_BUTTON_TEXT, BOOK_TEXTAREA_ROWS, + BOOK_TEXTAREA_COLS. Empty when BOOK_OPEN_REGISTRATION=false. + entries - Approved guestbook entries, newest first. Entry separator + controlled by BOOK_SEPARATOR. + style - Custom CSS from BOOK_STYLE or BOOK_STYLE_FILE, wrapped in + a <style> tag. Uses built-in default.css when neither is set. - Available CSS classes on rendered elements: - - .page-container - Main page wrapper - .guestbook-prompt - Text above the form - .guestbook-form - The <form> element - .guestbook-label - Field labels - .guestbook-input - Text inputs (name, website) - .guestbook-textarea - Message textarea - .guestbook-button - Submit button - .entry-header - Entry date/name/website line - .entry-name - Author name within the header - .entry-website - Website link within the header - .entry-body - Entry message content (raw HTML, not escaped) - .entry-separator - Divider between entries + See default.css for available CSS classes on rendered elements. --> <!DOCTYPE html> <html lang="en"> @@ -35,15 +24,6 @@ <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{title}} - {{style}} From 7f86743f4af97a4b3bed3bca0cdb8cba5dcfbc04 Mon Sep 17 00:00:00 2001 From: lew Date: Thu, 9 Apr 2026 18:08:37 +0100 Subject: [PATCH 12/12] refactor: clean-up of some unused options after the template rework --- .env.example | 3 --- LICENSE | 2 +- module.nix | 7 +------ src/config.rs | 10 ++-------- src/render.rs | 2 +- src/web.rs | 2 +- 6 files changed, 6 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index 50a6c05..35fddc1 100644 --- a/.env.example +++ b/.env.example @@ -7,9 +7,6 @@ BOOK_DATA_DIR=./data # Site title shown in nav and page title. BOOK_SITE_TITLE=guestbook -# Base URL of the main site (for absolute nav links). -BOOK_SITE_URL=https://example.com - # Telegram bot token. BOOK_TELEGRAM_BOT_TOKEN=your-bot-token-here diff --git a/LICENSE b/LICENSE index 5e8a6c2..e9c6128 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Llywelwyn +Copyright (c) 2026 Lewis Wynne Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/module.nix b/module.nix index 1bbe6ef..e1defbe 100644 --- a/module.nix +++ b/module.nix @@ -30,11 +30,6 @@ in description = "Site title shown in nav and page title."; }; - siteUrl = mkOption { - type = types.str; - description = "Base URL of the main site (for absolute nav links)."; - }; - telegramChatId = mkOption { type = types.int; description = "Telegram chat ID for moderation messages."; @@ -174,7 +169,7 @@ in BOOK_PORT = toString cfg.port; BOOK_DATA_DIR = cfg.dataDir; BOOK_SITE_TITLE = cfg.siteTitle; - BOOK_SITE_URL = cfg.siteUrl; + BOOK_TELEGRAM_CHAT_ID = toString cfg.telegramChatId; BOOK_HONEYPOT = if cfg.honeypot then "true" else "false"; BOOK_MAX_NAME_LENGTH = toString cfg.maxNameLength; diff --git a/src/config.rs b/src/config.rs index 75e93bd..b86eced 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ pub struct Config { pub port: u16, pub data_dir: PathBuf, pub site_title: String, - pub site_url: String, + pub telegram_bot_token: String, pub telegram_chat_id: i64, pub honeypot: bool, @@ -41,7 +41,7 @@ impl Config { .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from("./data")), site_title: env::var("BOOK_SITE_TITLE").unwrap_or_else(|_| "guestbook".into()), - site_url: env::var("BOOK_SITE_URL").map_err(|_| "BOOK_SITE_URL is required")?, + telegram_bot_token: env::var("BOOK_TELEGRAM_BOT_TOKEN") .map_err(|_| "BOOK_TELEGRAM_BOT_TOKEN is required")?, telegram_chat_id: env::var("BOOK_TELEGRAM_CHAT_ID") @@ -115,7 +115,6 @@ mod tests { env::set_var("BOOK_PORT", "9999"); env::set_var("BOOK_DATA_DIR", "/tmp/gb"); env::set_var("BOOK_SITE_TITLE", "test.rs"); - env::set_var("BOOK_SITE_URL", "https://test.rs"); env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC"); env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345"); @@ -124,14 +123,12 @@ mod tests { assert_eq!(config.listen_addr(), "127.0.0.1:9999"); assert_eq!(config.data_dir, PathBuf::from("/tmp/gb")); assert_eq!(config.site_title, "test.rs"); - assert_eq!(config.site_url, "https://test.rs"); assert_eq!(config.telegram_chat_id, 12345); // Clean up env::remove_var("BOOK_PORT"); env::remove_var("BOOK_DATA_DIR"); env::remove_var("BOOK_SITE_TITLE"); - env::remove_var("BOOK_SITE_URL"); env::remove_var("BOOK_TELEGRAM_BOT_TOKEN"); env::remove_var("BOOK_TELEGRAM_CHAT_ID"); } @@ -139,7 +136,6 @@ mod tests { #[test] fn test_defaults() { let _lock = ENV_LOCK.lock().unwrap(); - env::set_var("BOOK_SITE_URL", "https://test.rs"); env::set_var("BOOK_TELEGRAM_BOT_TOKEN", "123:ABC"); env::set_var("BOOK_TELEGRAM_CHAT_ID", "12345"); @@ -148,7 +144,6 @@ mod tests { assert_eq!(config.data_dir, PathBuf::from("./data")); assert_eq!(config.site_title, "guestbook"); - env::remove_var("BOOK_SITE_URL"); env::remove_var("BOOK_TELEGRAM_BOT_TOKEN"); env::remove_var("BOOK_TELEGRAM_CHAT_ID"); } @@ -156,7 +151,6 @@ mod tests { #[test] fn test_missing_required() { let _lock = ENV_LOCK.lock().unwrap(); - env::remove_var("BOOK_SITE_URL"); env::remove_var("BOOK_TELEGRAM_BOT_TOKEN"); env::remove_var("BOOK_TELEGRAM_CHAT_ID"); diff --git a/src/render.rs b/src/render.rs index 514317b..9fcf179 100644 --- a/src/render.rs +++ b/src/render.rs @@ -81,7 +81,7 @@ mod tests { port: 0, data_dir: PathBuf::from("./data"), site_title: "test".into(), - site_url: "https://test.rs".into(), + telegram_bot_token: "fake".into(), telegram_chat_id: 0, honeypot: true, diff --git a/src/web.rs b/src/web.rs index 2c38b17..5fd7bab 100644 --- a/src/web.rs +++ b/src/web.rs @@ -129,7 +129,7 @@ mod tests { port: 0, data_dir: dir.to_path_buf(), site_title: "test".into(), - site_url: "https://test.rs".into(), + telegram_bot_token: "fake".into(), telegram_chat_id: 0, honeypot: true,