Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion src/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,40 @@ export function nextNumber(posts: readonly Pick<Post, 'n'>[]): 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 =
'<script data-site="099436d8-e1b1-4b4e-bc04-b3fbff5c4ead" src="https://crawlproof.com/stats.js" async></script>';

/**
* 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 = [
'<aside data-cp-ad data-slot="50ba73a3-22b6-4264-9b2d-7f866759e287" data-format="text_link"></aside>',
'',
TRACKER,
'<script src="https://crawlproof.com/ad.js" async></script>',
].join('\n');

/**
* Render a post file.
*
* Deliberately smolweb-valid, which is stricter than "valid HTML": an explicit
* `<html lang>`, `<head>` and `<body>`; `<meta http-equiv="Content-Type">`
* rather than a bare `<meta charset>`, because every `<meta>` needs a `content`
* attribute; and every `<p>` closed.
* attribute; and every `<p>` 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);
Expand Down Expand Up @@ -142,6 +169,8 @@ ${content}

</article>

${AD_UNIT}

</body>
</html>
`;
Expand Down
16 changes: 16 additions & 0 deletions test/blog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

import {
AD_UNIT,
blogDir,
createPost,
DEFAULT_DIR,
Expand All @@ -14,6 +15,7 @@ import {
nextNumber,
readPosts,
renderPost,
TRACKER,
typogrify,
type Post,
} from '../src/blog.ts';
Expand Down Expand Up @@ -105,6 +107,20 @@ describe('renderPost', () => {
expect(html).toContain('How this was written:');
});

it('carries the CrawlProof tracker and ad unit, last thing before </body>', () => {
expect(html).toContain(TRACKER);
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('</article>'));
expect(html.indexOf(AD_UNIT)).toBeLessThan(html.indexOf('</body>'));
});

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', () => {
const hostile = renderPost({
title: 'x',
Expand Down
Loading