feat: added custom head and footer to posts ea6bb03e
Steve · 2026-04-08 17:36 4 file(s) · +67 −5
apps/posts/src/db.rs +10 −0
166 166
        [],
167 167
    )
168 168
    .ok();
169 +
    conn.execute(
170 +
        "INSERT OR IGNORE INTO settings (key, value) VALUES ('custom_header', '')",
171 +
        [],
172 +
    )
173 +
    .ok();
174 +
    conn.execute(
175 +
        "INSERT OR IGNORE INTO settings (key, value) VALUES ('custom_footer', '<a href=\"/feed.xml\" class=\"rss-link\" title=\"RSS Feed\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"32\" height=\"32\" fill=\"currentColor\" viewBox=\"0 0 256 256\"><path fill=\"currentColor\" d=\"M104.08 151.92A67.52 67.52 0 0 1 124 200a4 4 0 0 1-8 0a60 60 0 0 0-60-60a4 4 0 0 1 0-8a67.52 67.52 0 0 1 48.08 19.92M56 84a4 4 0 0 0 0 8a108 108 0 0 1 108 108a4 4 0 0 0 8 0A116 116 0 0 0 56 84m116 0A162.92 162.92 0 0 0 56 36a4 4 0 0 0 0 8a155 155 0 0 1 110.31 45.69A155 155 0 0 1 212 200a4 4 0 0 0 8 0a162.92 162.92 0 0 0-48-116M60 188a8 8 0 1 0 8 8a8 8 0 0 0-8-8\"/></svg></a>')",
176 +
        [],
177 +
    )
178 +
    .ok();
169 179
170 180
    Arc::new(Mutex::new(conn))
171 181
}
apps/posts/src/server.rs +47 −0
42 42
    nav_links: Vec<NavLink>,
43 43
    favicon_url: String,
44 44
    og_image_url: String,
45 +
    header_html: String,
46 +
    footer_html: String,
45 47
}
46 48
47 49
#[derive(Template)]
65 67
    favicon_url: String,
66 68
    og_image_url: String,
67 69
    site_url: String,
70 +
    header_html: String,
71 +
    footer_html: String,
68 72
}
69 73
70 74
#[derive(Template)]
77 81
    favicon_url: String,
78 82
    og_image_url: String,
79 83
    site_url: String,
84 +
    header_html: String,
85 +
    footer_html: String,
80 86
}
81 87
82 88
#[derive(Template)]
89 95
    favicon_url: String,
90 96
    og_image_url: String,
91 97
    site_url: String,
98 +
    header_html: String,
99 +
    footer_html: String,
92 100
}
93 101
94 102
#[derive(Template)]
128 136
    default_css: String,
129 137
    favicon_url: String,
130 138
    og_image_url: String,
139 +
    custom_header: String,
140 +
    custom_footer: String,
131 141
    success: bool,
132 142
}
133 143
140 150
    favicon_url: String,
141 151
    og_image_url: String,
142 152
    site_url: String,
153 +
    header_html: String,
154 +
    footer_html: String,
143 155
}
144 156
145 157
#[derive(Template)]
257 269
    custom_css: String,
258 270
    favicon_url: String,
259 271
    og_image_url: String,
272 +
    custom_header: String,
273 +
    custom_footer: String,
260 274
}
261 275
262 276
// --- Helpers ---
283 297
    }
284 298
}
285 299
300 +
fn get_header_footer_html(db: &db::Db) -> (String, String) {
301 +
    let custom_header = db::get_setting(db, "custom_header")
302 +
        .ok()
303 +
        .flatten()
304 +
        .unwrap_or_default();
305 +
    let custom_footer = db::get_setting(db, "custom_footer")
306 +
        .ok()
307 +
        .flatten()
308 +
        .unwrap_or_default();
309 +
    let header_html = render_markdown(&custom_header);
310 +
    let footer_html = render_markdown(&custom_footer);
311 +
    (header_html, footer_html)
312 +
}
313 +
286 314
fn render_markdown(content: &str) -> String {
287 315
    let mut options = Options::empty();
288 316
    options.insert(Options::ENABLE_STRIKETHROUGH);
514 542
515 543
            let favicon_url = get_favicon_url(&state.db);
516 544
            let og_image_url = get_og_image_url(&state.db);
545 +
            let (header_html, footer_html) = get_header_footer_html(&state.db);
517 546
            WebTemplate(IndexTemplate {
518 547
                blog_title,
519 548
                blog_description,
523 552
                favicon_url,
524 553
                og_image_url,
525 554
                site_url: state.site_url.clone(),
555 +
                header_html,
556 +
                footer_html,
526 557
            })
527 558
            .into_response()
528 559
        }
544 575
            let nav_links = get_nav_links(&state.db);
545 576
            let favicon_url = get_favicon_url(&state.db);
546 577
            let og_image_url = get_og_image_url(&state.db);
578 +
            let (header_html, footer_html) = get_header_footer_html(&state.db);
547 579
            WebTemplate(PostTemplate {
548 580
                blog_title,
549 581
                nav_links,
552 584
                favicon_url,
553 585
                og_image_url,
554 586
                site_url: state.site_url.clone(),
587 +
                header_html,
588 +
                footer_html,
555 589
            })
556 590
            .into_response()
557 591
        }
574 608
            let nav_links = get_nav_links(&state.db);
575 609
            let favicon_url = get_favicon_url(&state.db);
576 610
            let og_image_url = get_og_image_url(&state.db);
611 +
            let (header_html, footer_html) = get_header_footer_html(&state.db);
577 612
            WebTemplate(PageTemplate {
578 613
                blog_title,
579 614
                nav_links,
582 617
                favicon_url,
583 618
                og_image_url,
584 619
                site_url: state.site_url.clone(),
620 +
                header_html,
621 +
                footer_html,
585 622
            })
586 623
            .into_response()
587 624
        }
599 636
    let favicon_url = get_favicon_url(&state.db);
600 637
    let og_image_url = get_og_image_url(&state.db);
601 638
639 +
    let (header_html, footer_html) = get_header_footer_html(&state.db);
640 +
602 641
    match db::get_published_posts(&state.db) {
603 642
        Ok(posts) => WebTemplate(PostsListTemplate {
604 643
            blog_title,
607 646
            favicon_url,
608 647
            og_image_url,
609 648
            site_url: state.site_url.clone(),
649 +
            header_html,
650 +
            footer_html,
610 651
        })
611 652
        .into_response(),
612 653
        Err(e) => {
946 987
    let custom_css = db::get_setting(&state.db, "custom_css").ok().flatten().unwrap_or_default();
947 988
    let favicon_url = db::get_setting(&state.db, "favicon_url").ok().flatten().unwrap_or_default();
948 989
    let og_image_url = db::get_setting(&state.db, "og_image_url").ok().flatten().unwrap_or_default();
990 +
    let custom_header = db::get_setting(&state.db, "custom_header").ok().flatten().unwrap_or_default();
991 +
    let custom_footer = db::get_setting(&state.db, "custom_footer").ok().flatten().unwrap_or_default();
949 992
    let default_css = Static::get("styles.css")
950 993
        .map(|f| String::from_utf8_lossy(&f.data).into_owned())
951 994
        .unwrap_or_default();
959 1002
        default_css,
960 1003
        favicon_url,
961 1004
        og_image_url,
1005 +
        custom_header,
1006 +
        custom_footer,
962 1007
        success: q.success,
963 1008
    })
964 1009
    .into_response()
976 1021
    let _ = db::set_setting(&state.db, "custom_css", &form.custom_css);
977 1022
    let _ = db::set_setting(&state.db, "favicon_url", form.favicon_url.trim());
978 1023
    let _ = db::set_setting(&state.db, "og_image_url", form.og_image_url.trim());
1024 +
    let _ = db::set_setting(&state.db, "custom_header", &form.custom_header);
1025 +
    let _ = db::set_setting(&state.db, "custom_footer", &form.custom_footer);
979 1026
    Redirect::to("/admin/settings?success=true").into_response()
980 1027
}
981 1028
apps/posts/templates/admin_settings.html +4 −0
29 29
      <label for="custom_css">custom CSS (overrides default styles)</label>
30 30
      <textarea id="custom_css" name="custom_css" class="post-content">{% if custom_css.is_empty() %}{{ default_css }}{% else %}{{ custom_css }}{% endif %}</textarea>
31 31
    </div>
32 +
    <label for="custom_header">custom header (markdown or HTML, shown above nav on all pages)</label>
33 +
    <textarea id="custom_header" name="custom_header" class="nav-links-input">{{ custom_header }}</textarea>
34 +
    <label for="custom_footer">custom footer (markdown or HTML, shown at bottom of all pages)</label>
35 +
    <textarea id="custom_footer" name="custom_footer" class="post-content">{{ custom_footer }}</textarea>
32 36
    <button type="submit">save</button>
33 37
  </form>
34 38
  <h3>Data Export</h3>
apps/posts/templates/base.html +6 −5
20 20
  <link rel="stylesheet" href="/custom-styles.css">
21 21
</head>
22 22
<body>
23 +
  {% if !header_html.is_empty() %}
24 +
  <div class="custom-header">
25 +
    {{ header_html|safe }}
26 +
  </div>
27 +
  {% endif %}
23 28
  <header class="header">
24 29
    <a href="/" class="logo">{{ blog_title }}</a>
25 30
    <nav class="links">
32 37
    {% block content %}{% endblock %}
33 38
  </main>
34 39
  <footer class="footer">
35 -
    <a href="/feed.xml" class="rss-link" title="RSS Feed">
36 -
      <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256">
37 -
        <path fill="currentColor" d="M104.08 151.92A67.52 67.52 0 0 1 124 200a4 4 0 0 1-8 0a60 60 0 0 0-60-60a4 4 0 0 1 0-8a67.52 67.52 0 0 1 48.08 19.92M56 84a4 4 0 0 0 0 8a108 108 0 0 1 108 108a4 4 0 0 0 8 0A116 116 0 0 0 56 84m116 0A162.92 162.92 0 0 0 56 36a4 4 0 0 0 0 8a155 155 0 0 1 110.31 45.69A155 155 0 0 1 212 200a4 4 0 0 0 8 0a162.92 162.92 0 0 0-48-116M60 188a8 8 0 1 0 8 8a8 8 0 0 0-8-8"/>
38 -
      </svg>
39 -
    </a>
40 +
    {{ footer_html|safe }}
40 41
  </footer>
41 42
</body>
42 43
</html>