From 9bab148a82e920c272306f5e531f790738926dc1 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 19 Aug 2026 08:15:24 +0000 Subject: [PATCH 1/2] blog-post: emit the CrawlProof tracker in new posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every page under /blog/** now carries CrawlProof's cookieless pageview tag, so the template has to as well or each new post silently drops out of the numbers. The tag is the one deliberate break from smolweb validity, which forbids scripts from another host. It goes last before , and nothing on the page depends on it — the post reads identically with JavaScript off. Co-Authored-By: Claude Opus 5 (1M context) --- src/blog.ts | 16 +++++++++++++++- test/blog.test.ts | 9 +++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/blog.ts b/src/blog.ts index f882396..abc7a8c 100644 --- a/src/blog.ts +++ b/src/blog.ts @@ -96,13 +96,25 @@ export function nextNumber(posts: readonly Pick[]): string { return String(highest + 1).padStart(3, '0'); } +/** + * CrawlProof's cookieless pageview tracker, on every page of the blog. + * + * This is the one deliberate departure from smolweb validity: the guidelines + * forbid scripts served from another host. Nothing on the page depends on it — + * the post reads identically with JavaScript off — so the "usable without + * JavaScript" half of the rule still holds. + */ +export const TRACKER = + ''; + /** * Render a post file. * * Deliberately smolweb-valid, which is stricter than "valid HTML": an explicit * ``, `` and ``; `` * rather than a bare ``, because every `` needs a `content` - * attribute; and every `

` closed. + * attribute; and every `

` closed. The one exception is {@link TRACKER}, the + * external analytics tag, which smolweb's no-third-party-script rule forbids. */ export function renderPost({ title, description, date, body = '' }: NewPost): string { const day = date.slice(0, 10); @@ -142,6 +154,8 @@ ${content} +${TRACKER} + `; diff --git a/test/blog.test.ts b/test/blog.test.ts index 9eb366c..6def164 100644 --- a/test/blog.test.ts +++ b/test/blog.test.ts @@ -14,6 +14,7 @@ import { nextNumber, readPosts, renderPost, + TRACKER, typogrify, type Post, } from '../src/blog.ts'; @@ -105,6 +106,14 @@ describe('renderPost', () => { expect(html).toContain('How this was written:'); }); + it('carries the CrawlProof tracker, last thing before ', () => { + expect(html).toContain(TRACKER); + // Placement matters: the tag is async, but keeping it after the article + // means nothing about the post waits on a third-party host. + expect(html.indexOf(TRACKER)).toBeGreaterThan(html.indexOf('')); + expect(html.indexOf(TRACKER)).toBeLessThan(html.indexOf('')); + }); + it('escapes a description that tries to break out of its attribute', () => { const hostile = renderPost({ title: 'x', From e140b86fb3c3f1425cf584fc4d5d128f9c8192c6 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Wed, 19 Aug 2026 08:18:40 +0000 Subject: [PATCH 2/2] blog-post: emit the thin sponsored bar too New posts get the CrawlProof text_link unit (slot 50ba73a3) alongside the tracker, matching the block now on all 20 live pages byte for byte. text_link rather than a banner: it is a 40px full-width strip carrying its own Sponsored mark, so ad.js adds no caption, and an unsold slot collapses to nothing rather than leaving a banner-shaped hole. Co-Authored-By: Claude Opus 5 (1M context) --- src/blog.ts | 17 ++++++++++++++++- test/blog.test.ts | 17 ++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/blog.ts b/src/blog.ts index abc7a8c..bf49389 100644 --- a/src/blog.ts +++ b/src/blog.ts @@ -107,6 +107,21 @@ export function nextNumber(posts: readonly Pick[]): string { export const TRACKER = ''; +/** + * The sponsored bar that runs at the foot of every page. + * + * `text_link` on purpose, not a 728x90 or 300x250: it is a 40px full-width + * strip that carries its own "Sponsored" mark inside the frame, so `ad.js` + * prepends no extra caption, and an unsold or blocked slot collapses to + * nothing instead of leaving a banner-shaped hole. + */ +export const AD_UNIT = [ + '

', + '', + TRACKER, + '', +].join('\n'); + /** * Render a post file. * @@ -154,7 +169,7 @@ ${content} -${TRACKER} +${AD_UNIT} diff --git a/test/blog.test.ts b/test/blog.test.ts index 6def164..8307e30 100644 --- a/test/blog.test.ts +++ b/test/blog.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { + AD_UNIT, blogDir, createPost, DEFAULT_DIR, @@ -106,12 +107,18 @@ describe('renderPost', () => { expect(html).toContain('How this was written:'); }); - it('carries the CrawlProof tracker, last thing before ', () => { + it('carries the CrawlProof tracker and ad unit, last thing before ', () => { expect(html).toContain(TRACKER); - // Placement matters: the tag is async, but keeping it after the article - // means nothing about the post waits on a third-party host. - expect(html.indexOf(TRACKER)).toBeGreaterThan(html.indexOf('')); - expect(html.indexOf(TRACKER)).toBeLessThan(html.indexOf('')); + expect(html).toContain('data-cp-ad'); + // Placement matters: the tags are async, but keeping them after the + // article means nothing about the post waits on a third-party host. + expect(html.indexOf(AD_UNIT)).toBeGreaterThan(html.indexOf('')); + expect(html.indexOf(AD_UNIT)).toBeLessThan(html.indexOf('')); + }); + + it('runs the thin text_link bar, not a banner', () => { + expect(html).toContain('data-format="text_link"'); + expect(html).not.toMatch(/banner_\d+x\d+/); }); it('escapes a description that tries to break out of its attribute', () => {