Skip to main content

max / makenotwork

5.6 KB · 146 lines History Blame Raw
1 //! Seal: no template emits a `<head>` tag that `base.html` already owns.
2 //!
3 //! `base.html` emits three page-invariant social tags before `{% block head %}`:
4 //! `og:site_name` as a plain tag, and `og:type` / `twitter:card` as blocks with
5 //! generic defaults. A page that needs a different type or card overrides the
6 //! block. A page that emits the tag again inside `{% block head %}` instead
7 //! ships it twice, and the two disagree.
8 //!
9 //! That is not hypothetical. Until 2026-08-15 base emitted all three
10 //! unconditionally and nine page templates emitted their own on top, so every
11 //! item page carried `og:type` as both `website` and `product`, every post as
12 //! both `website` and `article`, and the project pages carried `twitter:card`
13 //! as both `summary_large_image` and `summary`. Base's copy came first in the
14 //! document, so a consumer taking the first occurrence read the generic value
15 //! and the page's real one never won. Nothing caught it because nothing looked:
16 //! there was no test anywhere in the tree mentioning these tags.
17 //!
18 //! Rendering every page to check this would need a database and a fixture per
19 //! template. Reading the templates needs neither, and the defect is a property
20 //! of the source rather than of any particular row, so this is a source seal in
21 //! the same shape as the `frontend_globals` ratchet.
22
23 use std::fs;
24 use std::path::{Path, PathBuf};
25
26 /// Tags `base.html` owns, with the block a page must override to change one.
27 /// `None` means the tag is invariant and a page may not restate it at all.
28 const OWNED: &[(&str, Option<&str>)] = &[
29 ("og:site_name", None),
30 ("og:type", Some("og_type")),
31 ("twitter:card", Some("social_card")),
32 ];
33
34 fn templates_dir() -> PathBuf {
35 Path::new(env!("CARGO_MANIFEST_DIR")).join("templates")
36 }
37
38 /// Every `.html` under `templates/`, walked rather than listed so a new page
39 /// is covered the day it lands.
40 fn template_files(dir: &Path, out: &mut Vec<PathBuf>) {
41 for entry in fs::read_dir(dir).expect("templates/ is readable") {
42 let path = entry.expect("readable dir entry").path();
43 if path.is_dir() {
44 template_files(&path, out);
45 } else if path.extension().is_some_and(|e| e == "html") {
46 out.push(path);
47 }
48 }
49 }
50
51 /// Remove `{% block <name> %}...{% endblock %}` from `src`.
52 ///
53 /// Askama's only nestable construct that closes with `{% endblock %}` is a
54 /// block, and no sanctioned override nests one, so pairing each opener with the
55 /// next `{% endblock %}` is exact here. A nested block inside an override would
56 /// under-strip and fail the seal, which is the safe direction.
57 fn strip_block(src: &str, name: &str) -> String {
58 let opener = format!("{{% block {name} %}}");
59 let mut out = String::with_capacity(src.len());
60 let mut rest = src;
61 while let Some(start) = rest.find(&opener) {
62 out.push_str(&rest[..start]);
63 let after = &rest[start + opener.len()..];
64 match after.find("{% endblock %}") {
65 Some(end) => rest = &after[end + "{% endblock %}".len()..],
66 // Unterminated: leave the remainder in place so the assertion sees
67 // it rather than silently swallowing the rest of the file.
68 None => {
69 rest = after;
70 break;
71 }
72 }
73 }
74 out.push_str(rest);
75 out
76 }
77
78 #[test]
79 fn no_template_restates_a_tag_base_html_owns() {
80 let dir = templates_dir();
81 let base = dir.join("base.html");
82 let base_src = fs::read_to_string(&base).expect("base.html is readable");
83
84 // The seal is only meaningful if base still emits what it claims to own.
85 for (tag, block) in OWNED {
86 assert!(
87 base_src.contains(tag),
88 "base.html no longer emits {tag}, but this seal still forbids pages \
89 from emitting it. Update OWNED or restore the tag."
90 );
91 if let Some(block) = block {
92 assert!(
93 base_src.contains(&format!("{{% block {block} %}}")),
94 "base.html emits {tag} outside a `{block}` block, so a page has \
95 no sanctioned way to override it. Wrap it in the block."
96 );
97 }
98 }
99
100 let mut files = Vec::new();
101 template_files(&dir, &mut files);
102 files.sort();
103
104 let mut offenders = Vec::new();
105 for path in files {
106 if path == base {
107 continue;
108 }
109 let src = fs::read_to_string(&path).expect("template is readable");
110 // Only children of base.html inherit its head; standalone templates
111 // (partials, embeds) own their own markup.
112 if !src.contains(r#"{% extends "base.html" %}"#) {
113 continue;
114 }
115
116 let rel = path
117 .strip_prefix(&dir)
118 .unwrap_or(&path)
119 .display()
120 .to_string();
121 for (tag, block) in OWNED {
122 let searchable = match block {
123 Some(block) => strip_block(&src, block),
124 None => src.clone(),
125 };
126 if searchable.contains(tag) {
127 offenders.push(match block {
128 Some(block) => format!(
129 "{rel} emits {tag} outside `{{% block {block} %}}`; base.html \
130 already emits it, so the page ships two and the first wins"
131 ),
132 None => {
133 format!("{rel} emits {tag}, which base.html already emits for every page")
134 }
135 });
136 }
137 }
138 }
139
140 assert!(
141 offenders.is_empty(),
142 "templates restate head tags base.html owns:\n {}",
143 offenders.join("\n ")
144 );
145 }
146