diff --git a/package.json b/package.json index c8a5097..7d5529c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@profullstack/cli-tools", - "version": "0.68.1", + "version": "0.68.2", "private": true, "description": "Local command-line tools, in TypeScript, exposed on PATH.", "type": "module", diff --git a/src/blog.ts b/src/blog.ts index d74cf4e..be3b4f5 100644 --- a/src/blog.ts +++ b/src/blog.ts @@ -124,7 +124,10 @@ export function tracker(siteId: string | null): string { } /** - * The sponsored bar that runs at the foot of every page. + * The sponsored bar at the top of every post, just under the heading. + * + * Not at the foot of the page: no ads in footers. A post has no other ad, so + * the unit moves up into the content rather than being dropped. * * `text_link` by default, not a 728x90 or 300x250: it is a 40px full-width * strip that carries its own "Sponsored" mark inside the frame, so `ad.js` @@ -133,21 +136,26 @@ export function tracker(siteId: string | null): string { * * The slot id is the author's own, so it is configuration rather than a * constant: a shared one would bill every installation's impressions to - * whoever happened to be in the file. `ad.js` loads only when there is a slot - * for it to fill, and the tracker is emitted here so no page carries it twice. + * whoever happened to be in the file. */ -export function adUnit( - config: Pick, -): string { - const tag = tracker(config.trackerSiteId); - if (!config.adSlotId) return tag; - - const slot = +export function adSlot(config: Pick): string { + if (!config.adSlotId) return ''; + return ( ``; + ` data-format="${esc(config.adFormat)}">` + ); +} - return [slot, '', tag, ''] - .filter((line, index, all) => line !== '' || all[index + 1] !== '') +/** + * The CrawlProof scripts, last thing before `` so nothing about the post + * waits on a third-party host. `ad.js` loads only when there is a slot for it + * to fill, and the tracker is emitted here so no page carries it twice. + */ +export function adScripts(config: Pick): string { + const tag = tracker(config.trackerSiteId); + if (!config.adSlotId) return tag; + return [tag, ''] + .filter(Boolean) .join('\n'); } @@ -194,7 +202,8 @@ export function renderPost( const disclosure = config.disclosure ? `\n\n

${typogrify(config.disclosure)}

` : ''; - const footer = adUnit(config); + const slot = adSlot(config); + const scripts = adScripts(config); // Omitted entirely rather than emitted empty: a canonical pointing nowhere is // worse than none. const canonicalTag = canonical ? `\n` : ''; @@ -216,7 +225,7 @@ export function renderPost(

${heading}

${byline}${disclosure} - +${slot ? `\n${slot}\n` : ''} @@ -228,7 +237,7 @@ ${content} ${identity(config.links)} -${footer ? `\n${footer}\n` : ''} +${scripts ? `\n${scripts}\n` : ''} `; diff --git a/test/blog.test.ts b/test/blog.test.ts index 54cd904..ef36a9a 100644 --- a/test/blog.test.ts +++ b/test/blog.test.ts @@ -4,7 +4,8 @@ import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { - adUnit, + adScripts, + adSlot, blogDir, createPost, DEFAULT_DIR, @@ -125,14 +126,23 @@ describe('renderPost', () => { expect(html).toContain("A Post — Someone's Blog"); }); - it('carries the CrawlProof tracker and ad unit, last thing before ', () => { - expect(html).toContain(tracker(CONFIGURED.trackerSiteId)); + it('puts the ad unit at the top of the post, never at the foot of the page', () => { 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. - const unit = adUnit(CONFIGURED); - expect(html.indexOf(unit)).toBeGreaterThan(html.indexOf('')); - expect(html.indexOf(unit)).toBeLessThan(html.indexOf('')); + // No ads in footers: the slot sits under the heading, inside the article. + const slot = html.indexOf(adSlot(CONFIGURED)); + expect(slot).toBeGreaterThan(html.indexOf('

')); + expect(slot).toBeLessThan(html.indexOf('