Skip to main content

max / makenotwork

Stop base.html and the page templates emitting the same head tags twice base.html emitted og:site_name, og:type and twitter:card before {% block head %}, and nine page templates emitted their own on top. Every item page carried og:type as both website and product, every post as both website and article, and the project pages carried twitter:card as both summary_large_image and summary. Base's copy came first, so a consumer taking the first occurrence read the generic value and the page's real one never won. og:type and twitter:card become blocks with base's generic value as the default; the nine pages override the block instead of restating the tag. Where the card was entangled with og:image inside a conditional, the whole conditional moves into the social_card block rather than duplicating the condition. og:site_name was seven identical copies and is now base's alone. Sealed by tests/head_meta_seal.rs, which reads the templates and fails on any child of base.html restating an owned tag outside its block. There was no test anywhere in the tree mentioning these tags, which is why this stood. Also drops db::items::media::update_item_cover_image_url. It set cover_image_url without touching cover_s3_key, the same url/key drift removed from the project side in 3e3b1d15, and it had no callers anywhere in the tree. update_item_cover is the live path and writes url, key and size together under a CAS. Every remaining writer of either column now moves both.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-16 00:35 UTC
Signed with PGP, not checked
Commit: f95a0c655e5f4db6fd9e21e7459fed809b4a6bd4
Parent: 09d411f
12 files changed, +208 insertions, -68 deletions
@@ -10,13 +10,21 @@
10 10 <meta name="description" content="{% block meta_description %}Sell your work directly. 0% platform fee, only Stripe's ~3% processing. Music, software, writing, and more.{% endblock %}">
11 11
12 12 {# Global OG / Twitter card metadata. Per-page templates emit their own
13 - og:title / og:description / og:image / og:url / twitter:* tags from
13 + og:title / og:description / og:url / og:image / twitter:* tags from
14 14 within {% block head %}; only the page-invariant tags live here.
15 15 Pages without overrides (cart, library, policy) get a generic but
16 - valid card from site_name + type + card. #}
16 + valid card from site_name + type + card.
17 +
18 + og:type and twitter:card are blocks rather than plain tags because a
19 + page needs to CONTRADICT them, not add to them: an item is a product,
20 + a post is an article, a page with no image wants a summary card. Emitted
21 + unconditionally they shipped twice on every page that had its own, and
22 + a consumer that takes the first occurrence read this generic value and
23 + never the page's. Override the block; do not emit the tag again inside
24 + {% block head %}. #}
17 25 <meta property="og:site_name" content="Makenotwork">
18 - <meta property="og:type" content="website">
19 - <meta name="twitter:card" content="summary_large_image">
26 + {% block og_type %}<meta property="og:type" content="website">{% endblock %}
27 + {% block social_card %}<meta name="twitter:card" content="summary_large_image">{% endblock %}
20 28
21 29 {% block head %}{% endblock %}
22 30 </head>
@@ -3,12 +3,8 @@
3 3
4 4 {% block title %}{{ item.title }} - Makenotwork{% endblock %}
5 5
6 - {% block head %}
7 - <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
8 - <meta property="og:description" content="{{ item.description }}">
9 - <meta property="og:type" content="music.song">
10 - <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
11 - <meta property="og:site_name" content="Makenotwork">
6 + {% block og_type %}<meta property="og:type" content="music.song">{% endblock %}
7 + {% block social_card %}
12 8 {% match item.content %}
13 9 {% when crate::types::ItemContent::Audio with { cover_url, .. } %}
14 10 {% if let Some(img) = cover_url %}
@@ -21,6 +17,12 @@
21 17 {% when _ %}
22 18 <meta name="twitter:card" content="summary">
23 19 {% endmatch %}
20 + {% endblock %}
21 +
22 + {% block head %}
23 + <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
24 + <meta property="og:description" content="{{ item.description }}">
25 + <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
24 26 <meta name="twitter:title" content="{{ item.title }} - {{ creator_username }}">
25 27 <meta name="twitter:description" content="{{ item.description }}">
26 28 <script type="application/ld+json">
@@ -3,15 +3,8 @@
3 3 {% block title %}{{ title }} - {{ project_title }}{% endblock %}
4 4 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Reading) }}"{% endblock %}
5 5
6 - {% block head %}
7 - <meta property="og:title" content="{{ title }} - {{ project_title }}">
8 - <meta property="og:description" content="{{ creator_display_name.as_deref().unwrap_or(&creator_username) }} on Makenotwork">
9 - <meta property="og:type" content="article">
10 - <meta property="og:url" content="{{ host_url }}/p/{{ project_slug }}/blog/{{ post_slug }}">
11 - <link rel="canonical" href="{{ host_url }}/p/{{ project_slug }}/blog/{{ post_slug }}">
12 - <meta property="og:site_name" content="Makenotwork">
13 - <meta name="twitter:title" content="{{ title }} - {{ project_title }}">
14 - <meta name="twitter:description" content="{{ creator_display_name.as_deref().unwrap_or(&creator_username) }} on Makenotwork">
6 + {% block og_type %}<meta property="og:type" content="article">{% endblock %}
7 + {% block social_card %}
15 8 {% if let Some(img) = project_cover_image_url %}
16 9 <meta name="twitter:card" content="summary_large_image">
17 10 <meta property="og:image" content="{{ img }}">
@@ -21,6 +14,15 @@
21 14 <meta property="og:image" content="{{ host_url }}/static/images/og-card.png">
22 15 <meta name="twitter:image" content="{{ host_url }}/static/images/og-card.png">
23 16 {% endif %}
17 + {% endblock %}
18 +
19 + {% block head %}
20 + <meta property="og:title" content="{{ title }} - {{ project_title }}">
21 + <meta property="og:description" content="{{ creator_display_name.as_deref().unwrap_or(&creator_username) }} on Makenotwork">
22 + <meta property="og:url" content="{{ host_url }}/p/{{ project_slug }}/blog/{{ post_slug }}">
23 + <link rel="canonical" href="{{ host_url }}/p/{{ project_slug }}/blog/{{ post_slug }}">
24 + <meta name="twitter:title" content="{{ title }} - {{ project_title }}">
25 + <meta name="twitter:description" content="{{ creator_display_name.as_deref().unwrap_or(&creator_username) }} on Makenotwork">
24 26 <script type="application/ld+json">
25 27 {
26 28 "@context": "https://schema.org",
@@ -8,10 +8,8 @@
8 8 {% block head %}
9 9 <meta property="og:title" content="Makenotwork: Sell your work directly. Keep what you earn.">
10 10 <meta property="og:description" content="A platform for selling music, software, writing, and more. 0% platform fee, only Stripe's ~3% processing.">
11 - <meta property="og:type" content="website">
12 11 <meta property="og:url" content="{{ host_url }}/">
13 12 <link rel="canonical" href="{{ host_url }}/">
14 - <meta name="twitter:card" content="summary_large_image">
15 13 <meta property="og:image" content="{{ host_url }}/static/images/og-card.png">
16 14 <meta name="twitter:image" content="{{ host_url }}/static/images/og-card.png">
17 15 {% endblock %}
@@ -4,17 +4,17 @@
4 4 {% block title %}{{ item.title }} - Makenotwork{% endblock %}
5 5 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} item-page"{% endblock %}
6 6
7 + {% block og_type %}<meta property="og:type" content="product">{% endblock %}
8 + {% block social_card %}{% if item.cover_image_url.is_some() || project_cover_image_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}{% endblock %}
9 +
7 10 {% block head %}
8 11 {# Tier 0 creator theme (inherited from parent project): primitive-layer override. #}
9 12 <style id="creator-theme">{{ theme_css|safe }}</style>
10 13 <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
11 14 <meta property="og:description" content="{{ item.description }}">
12 - <meta property="og:type" content="product">
13 15 <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
14 16 <link rel="canonical" href="{{ host_url }}/i/{{ item.id }}">
15 - <meta property="og:site_name" content="Makenotwork">
16 17 <meta property="product:price:amount" content="{{ item.price }}">
17 - {% if item.cover_image_url.is_some() || project_cover_image_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}
18 18 <meta name="twitter:title" content="{{ item.title }} - {{ creator_username }}">
19 19 <meta name="twitter:description" content="{{ item.description }}">
20 20 {% if let Some(img) = item.cover_image_url %}
@@ -4,16 +4,15 @@
4 4 {% block title %}{{ project.title }} - {{ creator_username }}{% endblock %}
5 5 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} project-page"{% endblock %}
6 6
7 + {% block social_card %}{% if project.cover_image_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}{% endblock %}
8 +
7 9 {% block head %}
8 10 {# Tier 0 creator theme: primitive-layer override; semantic layer derives from it. #}
9 11 <style id="creator-theme">{{ theme_css|safe }}</style>
10 12 <meta property="og:title" content="{{ project.title }} by {{ creator_username }}">
11 13 <meta property="og:description" content="{{ project.description }}">
12 - <meta property="og:type" content="website">
13 14 <meta property="og:url" content="{{ host_url }}/p/{{ project.slug }}">
14 15 <link rel="canonical" href="{{ host_url }}/p/{{ project.slug }}">
15 - <meta property="og:site_name" content="Makenotwork">
16 - {% if project.cover_image_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}
17 16 <meta name="twitter:title" content="{{ project.title }} by {{ creator_username }}">
18 17 <meta name="twitter:description" content="{{ project.description }}">
19 18 {% if let Some(img) = project.cover_image_url %}
@@ -3,12 +3,7 @@
3 3 {% block title %}{{ project.title }} - {{ creator_username }}{% endblock %}
4 4 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} project-paywall-page"{% endblock %}
5 5
6 - {% block head %}
7 - <meta property="og:title" content="{{ project.title }} by {{ creator_username }}">
8 - <meta property="og:description" content="{{ project.description }}">
9 - <meta property="og:type" content="website">
10 - <meta property="og:url" content="{{ host_url }}/p/{{ project.slug }}">
11 - <link rel="canonical" href="{{ host_url }}/p/{{ project.slug }}">
6 + {% block social_card %}
12 7 {% if let Some(img) = project.cover_image_url %}
13 8 <meta property="og:image" content="{{ img }}">
14 9 <meta name="twitter:card" content="summary_large_image">
@@ -18,6 +13,13 @@
18 13 {% endif %}
19 14 {% endblock %}
20 15
16 + {% block head %}
17 + <meta property="og:title" content="{{ project.title }} by {{ creator_username }}">
18 + <meta property="og:description" content="{{ project.description }}">
19 + <meta property="og:url" content="{{ host_url }}/p/{{ project.slug }}">
20 + <link rel="canonical" href="{{ host_url }}/p/{{ project.slug }}">
21 + {% endblock %}
22 +
21 23 {% block content %}
22 24 {% include "partials/site_header.html" %}
23 25
@@ -3,12 +3,8 @@
3 3 {% block title %}{{ item.title }} - Makenotwork{% endblock %}
4 4 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Reading) }}"{% endblock %}
5 5
6 - {% block head %}
7 - <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
8 - <meta property="og:description" content="{{ item.description }}">
9 - <meta property="og:type" content="article">
10 - <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
11 - <meta property="og:site_name" content="Makenotwork">
6 + {% block og_type %}<meta property="og:type" content="article">{% endblock %}
7 + {% block social_card %}
12 8 {% if let Some(img) = item.cover_image_url %}
13 9 <meta property="og:image" content="{{ img }}">
14 10 <meta name="twitter:card" content="summary_large_image">
@@ -16,6 +12,12 @@
16 12 {% else %}
17 13 <meta name="twitter:card" content="summary">
18 14 {% endif %}
15 + {% endblock %}
16 +
17 + {% block head %}
18 + <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
19 + <meta property="og:description" content="{{ item.description }}">
20 + <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
19 21 <meta name="twitter:title" content="{{ item.title }} - {{ creator_username }}">
20 22 <meta name="twitter:description" content="{{ item.description }}">
21 23 <script type="application/ld+json">
@@ -3,16 +3,16 @@
3 3 {% block title %}{{ user.display_name_or_username() }} - Makenotwork{% endblock %}
4 4 {% block body_attrs %} class="{{ crate::shell::measure(crate::shell::Measure::Wide) }} user-page"{% endblock %}
5 5
6 + {% block og_type %}<meta property="og:type" content="profile">{% endblock %}
7 + {% block social_card %}{% if user.avatar_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}{% endblock %}
8 +
6 9 {% block head %}
7 10 {# Tier 0 creator theme: primitive-layer override; semantic layer derives from it. #}
8 11 <style id="creator-theme">{{ theme_css|safe }}</style>
9 12 <meta property="og:title" content="{{ user.display_name_or_username() }} - Makenotwork">
10 13 <meta property="og:description" content="{% if let Some(bio) = user.bio %}{{ bio }}{% else %}Creator on Makenotwork{% endif %}">
11 - <meta property="og:type" content="profile">
12 14 <meta property="og:url" content="{{ host_url }}/u/{{ user.username }}">
13 15 <link rel="canonical" href="{{ host_url }}/u/{{ user.username }}">
14 - <meta property="og:site_name" content="Makenotwork">
15 - {% if user.avatar_url.is_some() %}<meta name="twitter:card" content="summary_large_image">{% else %}<meta name="twitter:card" content="summary">{% endif %}
16 16 <meta name="twitter:title" content="{{ user.display_name_or_username() }} - Makenotwork">
17 17 <meta name="twitter:description" content="{% if let Some(bio) = user.bio %}{{ bio }}{% else %}Creator on Makenotwork{% endif %}">
18 18 {% if let Some(img) = user.avatar_url %}
@@ -3,12 +3,8 @@
3 3
4 4 {% block title %}{{ item.title }} - Makenotwork{% endblock %}
5 5
6 - {% block head %}
7 - <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
8 - <meta property="og:description" content="{{ item.description }}">
9 - <meta property="og:type" content="video.other">
10 - <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
11 - <meta property="og:site_name" content="Makenotwork">
6 + {% block og_type %}<meta property="og:type" content="video.other">{% endblock %}
7 + {% block social_card %}
12 8 {% match item.content %}
13 9 {% when crate::types::ItemContent::Video with { cover_url, .. } %}
14 10 {% if let Some(img) = cover_url %}
@@ -21,6 +17,12 @@
21 17 {% when _ %}
22 18 <meta name="twitter:card" content="summary">
23 19 {% endmatch %}
20 + {% endblock %}
21 +
22 + {% block head %}
23 + <meta property="og:title" content="{{ item.title }} - {{ creator_username }}">
24 + <meta property="og:description" content="{{ item.description }}">
25 + <meta property="og:url" content="{{ host_url }}/i/{{ item.id }}">
24 26 <meta name="twitter:title" content="{{ item.title }} - {{ creator_username }}">
25 27 <meta name="twitter:description" content="{{ item.description }}">
26 28 <script type="application/ld+json">
@@ -128,26 +128,6 @@
128 128 Ok(())
129 129 }
130 130
131 - /// Update the cover image URL for an item (defense-in-depth: verifies ownership).
132 - #[tracing::instrument(skip_all)]
133 - pub async fn update_item_cover_image_url(
134 - pool: &PgPool,
135 - item_id: ItemId,
136 - user_id: UserId,
137 - url: &str,
138 - ) -> Result<()> {
139 - sqlx::query(
140 - "UPDATE items SET cover_image_url = $2, updated_at = NOW() WHERE id = $1 AND project_id IN (SELECT id FROM projects WHERE user_id = $3)",
141 - )
142 - .bind(item_id)
143 - .bind(url)
144 - .bind(user_id)
145 - .execute(pool)
146 - .await?;
147 -
148 - Ok(())
149 - }
150 -
151 131 /// Atomically update cover image URL, S3 key, and file size in a single UPDATE
152 132 /// (defense-in-depth: verifies ownership), guarded by a compare-and-swap on the
153 133 /// existing `cover_s3_key`.
@@ -1,0 +1,145 @@
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 + }