Skip to content

Commit 39c92e1

Browse files
authored
Merge pull request #77 from stack127labs/chore/check-handoff-file
Show ad networks on the homepage and refresh approved SEO counts
2 parents a6c44f6 + 46fc49e commit 39c92e1

19 files changed

Lines changed: 579 additions & 18 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { PageCardCount } from "@/app/ui/cards/page-card";
2+
import { getAdNetworkDirectoryData } from "@/lib/ad-networks";
3+
4+
export async function cardCounts(): Promise<PageCardCount[]> {
5+
const { networks_tracked, listed } = await getAdNetworkDirectoryData();
6+
return [
7+
{ label: "networks", value: networks_tracked },
8+
{ label: "sites", value: listed },
9+
];
10+
}

‎app/(site)/(directories)/card-counts.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { PageCardCount } from "@/app/ui/cards/page-card";
2+
import { cardCounts as adNetworks } from "./ad-networks/card-counts";
23
import { cardCounts as cliApps } from "./cli-apps/card-counts";
34
import { cardCounts as eppStatusCodes } from "./epp-status-codes/card-counts";
45
import { cardCounts as keyboardShortcuts } from "./keyboard-shortcuts/card-counts";
@@ -10,6 +11,7 @@ import { cardCounts as tldDirectory } from "./tld-directory/card-counts";
1011

1112
/** Live homepage card counts. Each directory computes its own from its records. */
1213
const counts: Record<string, () => Promise<PageCardCount[]>> = {
14+
"ad-networks": adNetworks,
1315
"cli-apps": cliApps,
1416
"epp-status-codes": eppStatusCodes,
1517
"keyboard-shortcuts": keyboardShortcuts,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"id": "site--spot-ad-networks",
3+
"type": "toolSpot",
4+
"takeaway": "Website after website carries an ad slot, and an ad network fills each one.",
5+
"source": "conceptual figure, no external data",
6+
"dataAsOf": "2026-10-21",
7+
"width": 140,
8+
"height": 110,
9+
"seed": 127,
10+
"data": { "motif": "adSlotSites" }
11+
}
Lines changed: 11 additions & 0 deletions
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// generated by scripts/stack127/marker-art/generate.mjs. do not edit.
2+
// edit the sibling .spec.json and rerun: npm run stack127:marker-art
3+
export const siteSpotAdNetworksArt = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 140 110" role="img" aria-labelledby="ma-title-site--spot-ad-networks" data-marker-art="site--spot-ad-networks">
4+
<title id="ma-title-site--spot-ad-networks">Website after website carries an ad slot, and an ad network fills each one.</title>
5+
<path d="M35.05 8.66 C69.51 10.27, 106.49 9.79, 132.55 9.20 M34.66 10.23 C65.27 10.25, 95.01 9.46, 131.91 10.29 M131.18 10.16 C134.05 23.48, 132.25 38.48, 132.76 73.24 M131.49 9.52 C131.82 31.24, 131.44 52.73, 131.81 73.44" stroke-width="1.4" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
6+
<path d="M24.14 19.03 C60.85 16.66, 97.64 17.06, 123.51 19.11 M23.34 17.64 C56.91 18.55, 90.06 18.90, 122.24 18.10 M120.81 18.36 C122.57 32.44, 121.32 46.72, 122.28 84.15 M122.53 17.68 C121.90 44.53, 122.33 70.29, 121.34 84.53" stroke-width="1.6" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
7+
<path d="M11.13 26.23 C47.57 24.44, 84.94 25.89, 111.19 25.81 M11.57 26.74 C47.24 26.97, 82.94 26.85, 112.65 25.89 M110.24 26.64 C112.14 42.79, 111.36 61.11, 111.76 101.18 M111.97 25.82 C112.58 46.13, 112.19 66.08, 112.52 100.01 M112.87 100.41 C86.39 101.50, 60.42 100.36, 11.55 99.38 M112.00 99.64 C89.26 100.39, 67.38 101.47, 11.49 100.32 M13.00 100.65 C12.72 81.10, 12.51 63.96, 12.38 26.11 M11.26 99.39 C11.67 84.51, 12.06 68.46, 11.44 26.71" stroke-width="1.9" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-ink,#f2f2ee);fill:none"/>
8+
<path d="M13.13 38.53 C49.09 40.34, 86.22 39.57, 112.18 37.91 M11.93 39.05 C49.11 38.67, 86.64 38.25, 111.49 38.78" stroke-width="1.4" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-ink,#f2f2ee);fill:none"/>
9+
<path d="M19.87 55.34 C21.00 55.08, 26.44 53.32, 31.64 47.08 M19.50 55.80 C24.17 53.56, 27.96 49.68, 30.48 47.99 M23.87 60.74 C27.76 58.05, 29.49 51.91, 31.35 49.19 M24.93 61.22 C26.15 58.24, 27.60 54.92, 31.07 48.82 M19.32 64.37 C23.85 60.85, 31.59 56.65, 38.58 47.16 M19.62 66.52 C26.53 60.20, 33.27 54.67, 41.19 48.48 M27.16 72.09 C31.98 61.81, 36.62 56.92, 39.56 48.56 M24.84 70.04 C29.52 67.19, 31.89 61.49, 40.74 48.48 M29.56 67.95 C38.34 59.39, 43.71 54.00, 48.53 50.04 M29.00 66.74 C37.10 60.67, 43.87 53.97, 50.14 47.49 M36.32 71.18 C38.00 65.56, 44.21 56.07, 47.66 47.57 M34.46 71.39 C38.98 66.09, 42.08 60.38, 49.89 48.57 M39.23 66.02 C46.29 57.27, 54.61 50.39, 57.44 47.68 M37.89 66.38 C43.74 60.91, 47.77 57.42, 59.49 47.13 M42.77 69.70 C47.39 64.26, 50.33 58.80, 58.30 46.95 M43.57 70.69 C47.50 65.28, 52.74 58.20, 58.63 48.41 M47.31 64.58 C55.72 59.86, 60.93 54.97, 68.18 47.15 M48.18 65.87 C53.21 60.76, 60.34 55.02, 69.36 47.42 M51.80 70.12 C54.06 67.21, 61.15 61.63, 68.13 46.73 M52.04 71.00 C57.00 64.51, 61.26 57.20, 69.31 48.42 M56.91 67.02 C59.95 63.88, 65.83 58.89, 76.66 47.68 M55.95 66.67 C63.74 60.57, 70.17 54.15, 78.55 48.66 M63.80 69.26 C69.12 63.38, 73.93 54.54, 77.89 48.07 M62.44 71.93 C66.18 65.35, 69.12 60.41, 77.44 46.80 M65.88 67.39 C73.20 60.39, 79.56 54.05, 87.26 49.64 M67.18 65.54 C72.38 61.06, 75.98 57.50, 86.88 47.45 M72.93 71.63 C75.41 65.21, 79.64 62.34, 87.72 46.31 M71.70 69.91 C77.67 62.61, 82.85 55.11, 86.09 47.56 M74.19 67.41 C80.72 61.47, 87.84 55.50, 95.42 47.81 M76.05 66.57 C81.14 60.97, 87.86 56.18, 96.90 46.95 M82.45 70.40 C85.11 64.21, 91.12 54.97, 95.89 48.19 M81.49 70.15 C85.24 64.15, 89.28 57.77, 95.53 48.75 M85.87 65.92 C88.59 60.41, 94.44 59.50, 102.43 53.18 M85.92 65.60 C89.42 62.50, 93.21 57.99, 101.58 51.63 M90.69 68.89 C96.62 62.90, 98.03 55.21, 101.45 53.63 M91.15 71.15 C94.80 64.22, 100.02 55.42, 102.94 51.22 M93.50 66.93 C97.85 64.62, 99.57 63.77, 101.83 62.36 M94.07 66.20 C97.66 64.86, 100.82 62.98, 102.68 62.11 M99.11 70.22 C99.57 68.10, 100.96 65.85, 102.24 61.87 M99.83 70.72 C100.46 68.14, 101.06 65.44, 101.87 62.72" stroke-width="1.3" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-primary,#bafc3c);fill:none"/>
10+
<path d="M22.74 46.18 C53.34 48.26, 82.85 48.09, 103.51 48.57 M22.23 47.35 C54.43 47.85, 85.78 48.58, 101.56 47.38 M103.12 49.21 C100.20 54.54, 100.72 58.59, 100.52 67.58 M102.56 48.20 C101.38 52.83, 103.00 56.95, 101.39 68.71 M101.07 68.03 C80.92 69.08, 55.96 66.73, 21.17 66.50 M102.11 67.32 C71.43 69.01, 41.77 69.68, 21.31 68.44 M21.67 68.16 C21.69 63.64, 21.99 59.71, 23.60 49.71 M21.62 67.32 C22.73 61.83, 22.17 53.86, 22.26 48.76" stroke-width="2" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-primary,#bafc3c);fill:none"/>
11+
<path d="M21.76 79.69 C47.58 79.01, 73.29 79.58, 91.23 78.38 M22.35 79.37 C37.35 79.04, 52.18 79.14, 91.99 78.48" stroke-width="1.5" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
12+
<path d="M21.08 89.07 C40.42 88.11, 57.84 89.34, 69.95 89.81 M22.56 88.93 C33.24 88.45, 45.27 88.20, 70.24 89.53" stroke-width="1.5" stroke-linecap="round" fill-rule="evenodd" style="stroke:var(--ma-sub,#9d9c95);fill:none"/>
13+
</svg>`;

‎content_docs/ad-networks/production-runbook.md‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Ad-network production runbook
22

3-
Ad-network collectors, syncs, snapshots, and backups run as cron jobs on OVH. Weekly niche sorting runs on homelab. All ad-network pages stay unpublished and return 404. Resuming automation does not authorize publication.
3+
Ad-network collectors, syncs, snapshots, and backups run as cron jobs on OVH. Weekly niche sorting runs on homelab. The directory, stats, 21 niche pages, 44 network pages, and checker were published on October 21, 2026. Snigel stays retired and returns 404. Automation updates existing pages; it does not publish new ones.
44

55
Cron is running again since October 20, 2026, 15:00 UTC, on self-hosted Postgres 17. It was paused from October 19, 12:15 UTC, until the sync sent only changed rows. History: [OVH cron plan](../../dev-space/ad-networks-ovh-cron-plan.md).
66

@@ -20,6 +20,18 @@ The env file also sets `GITHUB_ALERT_TOKEN`, `AD_NETWORK_DB_PATH`, and `AD_NETWO
2020

2121
The wrapper is [`run-job.sh`](../../scripts/ad-networks/ovh/run-job.sh). The full schedule is [`crontab.txt`](../../scripts/ad-networks/ovh/crontab.txt). Homelab's `stack127-ad-network-niche.timer` runs Sunday at 06:00 UTC.
2222

23+
## Counts in titles and headings
24+
25+
The approved titles and headings use rounded site counts. The existing revalidation step refreshes only those count tokens from the target database, including decreases. The cache worker repeats this before processing ad-network events, so a stopped sync or direct data import can recover through the durable queue. No separate schedule is needed.
26+
27+
Editorial wording stays in the page tables. The count refresher recognizes only the approved page and phrase combinations. Descriptions use no site-count snapshots. Publir, EthicalAds, Concert, Sovrn, and Media.net keep their approved count-free titles and headings. The 20-niche taxonomy stays fixed. A refresh within the same rounding bucket makes no page-field update.
28+
29+
Before the new copy is published, older wording is skipped. If a page matches only part of the approved count phrases, the refresh fails and rolls back rather than rewriting its words. Counts below ten lose their numeric prefix and can regain it when the count recovers.
30+
31+
Before preparing a publication plan, render its desired count tokens from that target's current data. Keep the publisher's exact conflict checks: if a scheduled refresh changes a frozen row, prepare a new plan. Check the live page after publishing. A static rebuild still takes several minutes, so data and heading changes reach the public site through the normal verified refresh process.
32+
33+
Restoring an older plan also refuses to overwrite newer count changes. To recover old wording after that conflict, prepare a new plan with the previous wording and current counts. Do not force an old numeric snapshot back into the page tables.
34+
2335
## How a job runs
2436

2537
Collectors use HTTP and SQLite without holding a production connection. Sync then opens one connection, takes the existing runtime and reviewed-sync locks, and pins a read-only SQLite view. It sends only rows whose compact contents differ from the last successful sync. Before each target batch, its old hashes are durably invalidated. New hashes are saved after that batch commits. An interrupted sync cannot reuse an obsolete hash, even if the source later returns to its old value.
@@ -90,7 +102,7 @@ The coordinator performs these steps after review and release authorization. Kee
90102
ssh ovh '/srv/stack127/scripts/ad-networks/ovh/run-job.sh backup'
91103
```
92104

93-
Check the job logs and Node cache-worker service. The queue must drain without retries or permission errors. Check `/ad-networks`, `/ad-network-checker`, and an existing network child URL: all must return 404.
105+
Check the job logs and Node cache-worker service. The queue must drain without retries or permission errors. Check `/ad-networks`, `/ad-network-checker`, and an existing network child URL: all must return 200. `/ad-networks/snigel` must return 404.
94106
6. Install the new schedule and re-enable the niche timer:
95107

96108
```bash
@@ -99,7 +111,7 @@ The coordinator performs these steps after review and release authorization. Kee
99111
```
100112

101113
Confirm verification starts at minute 40. Do not install the saved five-minute schedule.
102-
7. Check connections, table sizes, queue age, logs, and disk space after the first hour and first day. Collectors should hold no production session; each sync uses one. Compare the live revision and unpublished HTTP responses again.
114+
7. Check connections, table sizes, queue age, logs, and disk space after the first hour and first day. Collectors should hold no production session; each sync uses one. Compare the live revision and publication responses again.
103115

104116
A local synthetic benchmark scanned 107,146 publishers and 114,284 relationships in 12.4 seconds with no changed rows, zero row payloads, and 12 SQL calls. PostgreSQL responses were simulated; this measures the SQLite scan and hash work. Measure actual OVH sync time during step 4. The old no-change sync took about 12 minutes.
105117

@@ -125,4 +137,4 @@ ssh homelab 'cd /srv/data/projects/stack127 && node --experimental-strip-types s
125137
ssh ovh '/srv/stack127/scripts/ad-networks/ovh/run-job.sh rdap-import --source-sqlite /srv/stack127-data/state/rdap.sqlite && rm /srv/stack127-data/state/rdap.sqlite'
126138
```
127139

128-
The importer copies only newer RDAP checkpoints and dates, then syncs and refreshes. After its successful backup, disable homelab's old `stack127-ad-network-backup.timer`. This does not affect the niche timer. Pages stay unpublished.
140+
The importer copies only newer RDAP checkpoints and dates, then syncs and refreshes. After its successful backup, disable homelab's old `stack127-ad-network-backup.timer`. This does not affect the niche timer or change which pages are published.

‎lib/card-art.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { siteSpotAdNetworksArt } from "@/app/(site)/art/site--spot-ad-networks";
12
import { siteSpotPlatformFeesArt } from "@/app/(site)/art/site--spot-platform-fees";
23
import { siteSpotLaunchDirectoriesArt } from "@/app/(site)/art/site--spot-launch-directories";
34
import { siteSpotLicensesArt } from "@/app/(site)/art/site--spot-licenses";
@@ -95,6 +96,7 @@ import { siteSpotStorageToolsArt } from "@/app/(site)/art/site--spot-storage-too
9596

9697
/** Approved generated SVGs keyed by references stored in the content tables. */
9798
const art: Record<string, string> = {
99+
"ad-networks": siteSpotAdNetworksArt,
98100
"cli-apps": siteSpotCliAppsArt,
99101
"epp-status-codes": eppStatusArt,
100102
"keyboard-shortcuts": siteSpotKeyboardShortcutsArt,

‎lib/card-icons.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "lucide-react";
99

1010
const icons: Record<string, LucideIcon> = {
11+
"ad-networks": Network,
1112
"cli-apps": SquareTerminal,
1213
"epp-status-codes": ListChecks,
1314
"keyboard-shortcuts": Keyboard,

‎lib/page-registry-helpers.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function resolvePagePublicationState<Page extends RegistryPublicati
4040
/** Curated collection, course, reference, and utility pages shown on the homepage. */
4141
export const homePageSlugs = [
4242
"about",
43+
"ad-networks",
4344
"cli-apps",
4445
"epp-status-codes",
4546
"keyboard-shortcuts",

0 commit comments

Comments
 (0)