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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
41 changes: 25 additions & 16 deletions src/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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<BlogConfig, 'adSlotId' | 'adFormat' | 'trackerSiteId'>,
): string {
const tag = tracker(config.trackerSiteId);
if (!config.adSlotId) return tag;

const slot =
export function adSlot(config: Pick<BlogConfig, 'adSlotId' | 'adFormat'>): string {
if (!config.adSlotId) return '';
return (
`<aside data-cp-ad data-slot="${esc(config.adSlotId)}"` +
` data-format="${esc(config.adFormat)}"></aside>`;
` data-format="${esc(config.adFormat)}"></aside>`
);
}

return [slot, '', tag, '<script src="https://crawlproof.com/ad.js" async></script>']
.filter((line, index, all) => line !== '' || all[index + 1] !== '')
/**
* The CrawlProof scripts, last thing before `</body>` 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<BlogConfig, 'adSlotId' | 'trackerSiteId'>): string {
const tag = tracker(config.trackerSiteId);
if (!config.adSlotId) return tag;
return [tag, '<script src="https://crawlproof.com/ad.js" async></script>']
.filter(Boolean)
.join('\n');
}

Expand Down Expand Up @@ -194,7 +202,8 @@ export function renderPost(
const disclosure = config.disclosure
? `\n\n<p><small>${typogrify(config.disclosure)}</small></p>`
: '';
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<link rel="canonical" href="${esc(canonical)}">` : '';
Expand All @@ -216,7 +225,7 @@ export function renderPost(
<h1>${heading}</h1>

${byline}${disclosure}

${slot ? `\n${slot}\n` : ''}
<nav>
<a href="../blog">back to my blog postings</a>
</nav>
Expand All @@ -228,7 +237,7 @@ ${content}
</nav>
${identity(config.links)}
</article>
${footer ? `\n${footer}\n` : ''}
${scripts ? `\n${scripts}\n` : ''}
</body>
</html>
`;
Expand Down
33 changes: 23 additions & 10 deletions test/blog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';

import {
adUnit,
adScripts,
adSlot,
blogDir,
createPost,
DEFAULT_DIR,
Expand Down Expand Up @@ -125,14 +126,23 @@ describe('renderPost', () => {
expect(html).toContain("<title>A Post &mdash; Someone's Blog</title>");
});

it('carries the CrawlProof tracker and ad unit, last thing before </body>', () => {
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('</article>'));
expect(html.indexOf(unit)).toBeLessThan(html.indexOf('</body>'));
// No ads in footers: the slot sits under the heading, inside the article.
const slot = html.indexOf(adSlot(CONFIGURED));
expect(slot).toBeGreaterThan(html.indexOf('<h1>'));
expect(slot).toBeLessThan(html.indexOf('<nav>'));
expect(slot).toBeLessThan(html.indexOf('</article>'));
});

it('keeps the CrawlProof scripts last thing before </body>', () => {
expect(html).toContain(tracker(CONFIGURED.trackerSiteId));
// The tags are async, but keeping them after the article means nothing
// about the post waits on a third-party host.
const scripts = adScripts(CONFIGURED);
expect(scripts).toContain('ad.js');
expect(html.indexOf(scripts)).toBeGreaterThan(html.indexOf('</article>'));
expect(html.indexOf(scripts)).toBeLessThan(html.indexOf('</body>'));
});

it('runs the thin text_link bar, not a banner', () => {
Expand Down Expand Up @@ -171,7 +181,8 @@ describe('renderPost', () => {
expect(bare).not.toContain('crawlproof.com');
expect(bare).not.toContain('data-cp-ad');
expect(bare).not.toContain('<script');
expect(adUnit(EMPTY_CONFIG)).toBe('');
expect(adSlot(EMPTY_CONFIG)).toBe('');
expect(adScripts(EMPTY_CONFIG)).toBe('');
expect(tracker(null)).toBe('');
});

Expand All @@ -183,7 +194,9 @@ describe('renderPost', () => {
});

it('emits the tracker without an ad unit when only a site id is set', () => {
const only = adUnit({ ...EMPTY_CONFIG, trackerSiteId: 'site-id-1234' });
const trackerOnly = { ...EMPTY_CONFIG, trackerSiteId: 'site-id-1234' };
expect(adSlot(trackerOnly)).toBe('');
const only = adScripts({ ...EMPTY_CONFIG, trackerSiteId: 'site-id-1234' });
expect(only).toBe(tracker('site-id-1234'));
expect(only).not.toContain('ad.js');
});
Expand Down
Loading