Skip to main content

max / makenotwork

server: landing "Last shipped" velocity line + show_on_landing Lane D launch work. Add an operator-controlled "Last shipped" line on the landing page, drawn from the most recent published changelog post flagged show_on_landing; suppressed entirely when none (no placeholder). - migration 136: show_on_landing on blog_posts - blog editor: owner-only "Show on landing" checkbox, changelog project only - landing route reads the most-recent eligible changelog post - mobile-aware sub-line under the browse CTA - tests: omit with zero eligible, surface most recent, ignore non-changelog
Co-Authored-By
Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-06-07 21:12 UTC
Signed with PGP, not checked
Commit: 2828007db53eb6474a3a5ab0e5e5a0a3d5616a9f
Parent: 132431d
14 files changed, +263 insertions, -4 deletions
@@ -23,6 +23,9 @@
23 23 };
24 24 }
25 25
26 + // Present only on the changelog project editor; null elsewhere.
27 + var landingToggle = document.getElementById('post-show-on-landing');
28 +
26 29 function goBack() {
27 30 window.location.href = '/dashboard/project/' + projectSlug;
28 31 }
@@ -35,6 +38,7 @@
35 38 }
36 39 var payload = { title: f.title, body_markdown: f.body, is_published: publish };
37 40 if (f.slug) payload.slug = f.slug;
41 + if (landingToggle) payload.show_on_landing = landingToggle.checked;
38 42
39 43 fetch('/api/projects/' + projectId + '/blog', {
40 44 method: 'POST',
@@ -62,10 +66,12 @@
62 66 postStatus.innerHTML = '<span style="color: var(--danger);">Slug is required</span>';
63 67 return;
64 68 }
69 + var updatePayload = { title: f.title, slug: f.slug, body_markdown: f.body, is_published: publish };
70 + if (landingToggle) updatePayload.show_on_landing = landingToggle.checked;
65 71 fetch('/api/blog/' + postId, {
66 72 method: 'PUT',
67 73 headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
68 - body: JSON.stringify({ title: f.title, slug: f.slug, body_markdown: f.body, is_published: publish })
74 + body: JSON.stringify(updatePayload)
69 75 })
70 76 .then(function(res) {
71 77 if (!res.ok) return apiErrorMessage(res, 'Failed to update post').then(function(m) { throw new Error(m); });
@@ -5832,6 +5832,13 @@
5832 5832 margin: 1.25rem 0 0;
5833 5833 }
5834 5834
5835 + .landing-mobile-note {
5836 + text-align: center;
5837 + margin: 0.4rem 0 0;
5838 + font-size: 0.8rem;
5839 + color: var(--text-muted);
5840 + }
5841 +
5835 5842 .founder-tagline {
5836 5843 font-family: var(--font-body);
5837 5844 font-size: 0.95rem;
@@ -5845,6 +5852,24 @@
5845 5852 gap: 0.5rem;
5846 5853 }
5847 5854
5855 + .landing-velocity {
5856 + font-family: var(--font-mono);
5857 + font-size: 0.8rem;
5858 + text-align: center;
5859 + margin: 1rem auto 0;
5860 + color: var(--text-muted);
5861 + }
5862 +
5863 + .landing-velocity a {
5864 + color: var(--text);
5865 + text-decoration: none;
5866 + border-bottom: 1px solid var(--border);
5867 + }
5868 +
5869 + .landing-velocity a:hover {
5870 + border-bottom-color: var(--text);
5871 + }
5872 +
5848 5873 .founder-tagline-detail {
5849 5874 display: block;
5850 5875 }
@@ -21,13 +21,14 @@
21 21 body_html: &str,
22 22 publish: bool,
23 23 web_only: bool,
24 + show_on_landing: bool,
24 25 ) -> Result<DbBlogPost> {
25 26 let published_at = if publish { Some(Utc::now()) } else { None };
26 27
27 28 let post = sqlx::query_as::<_, DbBlogPost>(
28 29 r#"
29 - INSERT INTO blog_posts (project_id, author_id, title, slug, body_markdown, body_html, published_at, web_only)
30 - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
30 + INSERT INTO blog_posts (project_id, author_id, title, slug, body_markdown, body_html, published_at, web_only, show_on_landing)
31 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
31 32 RETURNING *
32 33 "#,
33 34 )
@@ -39,6 +40,7 @@
39 40 .bind(body_html)
40 41 .bind(published_at)
41 42 .bind(web_only)
43 + .bind(show_on_landing)
42 44 .fetch_one(pool)
43 45 .await?;
44 46
@@ -126,6 +128,39 @@
126 128 Ok(posts)
127 129 }
128 130
131 + /// Fetch the single landing-page "Last shipped" post: the most recent
132 + /// published, landing-flagged post belonging to a public project with the
133 + /// given slug (CHANGELOG_PROJECT_SLUG). Returns `None` when nothing qualifies,
134 + /// which the landing route uses to suppress the velocity line entirely.
135 + ///
136 + /// `show_on_landing` is set on rows across every project, but the slug join
137 + /// confines the landing reader to the changelog project, so the flag is inert
138 + /// elsewhere.
139 + #[tracing::instrument(skip_all)]
140 + pub async fn get_landing_changelog_post(
141 + pool: &PgPool,
142 + changelog_slug: &str,
143 + ) -> Result<Option<DbBlogPost>> {
144 + let post = sqlx::query_as::<_, DbBlogPost>(
145 + r#"
146 + SELECT bp.*
147 + FROM blog_posts bp
148 + JOIN projects p ON p.id = bp.project_id
149 + WHERE p.slug = $1
150 + AND p.is_public = true
151 + AND bp.show_on_landing = true
152 + AND bp.published_at IS NOT NULL
153 + ORDER BY bp.published_at DESC
154 + LIMIT 1
155 + "#,
156 + )
157 + .bind(changelog_slug)
158 + .fetch_optional(pool)
159 + .await?;
160 +
161 + Ok(post)
162 + }
163 +
129 164 /// Update a blog post's fields.
130 165 ///
131 166 /// `publish_at` uses a double-Option: `None` = no change, `Some(None)` = clear schedule,
@@ -145,6 +180,7 @@
145 180 publish: bool,
146 181 publish_at: Option<Option<chrono::DateTime<chrono::Utc>>>,
147 182 web_only: Option<bool>,
183 + show_on_landing: Option<bool>,
148 184 ) -> Result<DbBlogPost> {
149 185 let update_publish_at = publish_at.is_some();
150 186 let publish_at_value = publish_at.flatten();
@@ -169,6 +205,7 @@
169 205 END,
170 206 publish_at = CASE WHEN $7 THEN $8 ELSE publish_at END,
171 207 web_only = COALESCE($9, web_only),
208 + show_on_landing = COALESCE($10, show_on_landing),
172 209 updated_at = NOW()
173 210 WHERE id = $1
174 211 RETURNING *
@@ -183,6 +220,7 @@
183 220 .bind(update_publish_at)
184 221 .bind(publish_at_value)
185 222 .bind(web_only)
223 + .bind(show_on_landing)
186 224 .fetch_one(pool)
187 225 .await?;
188 226
@@ -277,6 +277,12 @@
277 277 pub post_slug: String,
278 278 pub post_body: String,
279 279 pub post_is_published: bool,
280 + /// Whether this editor is for the platform changelog project. Gates the
281 + /// owner-only "Show on landing" control so it never appears on a regular
282 + /// creator's blog, where the flag would be inert.
283 + pub is_changelog_project: bool,
284 + /// Current value of the post's landing flag (false for new posts).
285 + pub post_show_on_landing: bool,
280 286 }
281 287
282 288 /// HTMX partial: onboarding checklist (returned by the restore handler).