From 327e2ceb0d6291a111f7fc876ddea5be260a05de Mon Sep 17 00:00:00 2001 From: RaviTejaKNTS Date: Fri, 16 Oct 2026 00:02:28 +0530 Subject: [PATCH 01/10] Add ad networks mockups and data tracker Static mockups for the directory, Mediavine network page, niches, food niche, site, and stats pages, plus a tracker for the data and code work still to do. Co-Authored-By: Claude Opus 5.5 --- .../ad-networks-mockups/ad-networks.html | 137 +++ .../assets/logos/aditude.com.png | Bin 0 -> 1591 bytes .../assets/logos/adnimation.com.png | Bin 0 -> 656 bytes .../assets/logos/adsense.google.com.png | Bin 0 -> 3654 bytes .../assets/logos/anymindgroup.com.png | Bin 0 -> 766 bytes .../assets/logos/aps.amazon.com.png | Bin 0 -> 658 bytes .../assets/logos/buysellads.com.png | Bin 0 -> 603 bytes .../assets/logos/clickio.com.png | Bin 0 -> 1915 bytes .../assets/logos/ethicalads.io.png | Bin 0 -> 1336 bytes .../assets/logos/ezoic.com.png | Bin 0 -> 676 bytes .../assets/logos/freestar.com.png | Bin 0 -> 615 bytes .../assets/logos/geniee.co.jp.png | Bin 0 -> 2090 bytes .../assets/logos/mediavine.com.png | Bin 0 -> 540 bytes .../assets/logos/mgid.com.png | Bin 0 -> 584 bytes .../assets/logos/monetizemore.com.png | Bin 0 -> 1007 bytes .../assets/logos/monumetric.com.png | Bin 0 -> 939 bytes .../assets/logos/newormedia.com.png | Bin 0 -> 2287 bytes .../assets/logos/nitropay.com.png | Bin 0 -> 622 bytes .../assets/logos/optidigital.com.png | Bin 0 -> 634 bytes .../assets/logos/playwire.com.png | Bin 0 -> 962 bytes .../assets/logos/pubfuture.com.png | Bin 0 -> 891 bytes .../assets/logos/pubgalaxy.com.png | Bin 0 -> 605 bytes .../assets/logos/publift.com.png | Bin 0 -> 1008 bytes .../assets/logos/publir.com.png | Bin 0 -> 423 bytes .../assets/logos/pubnation.com.png | Bin 0 -> 610 bytes .../assets/logos/pubpower.io.png | Bin 0 -> 1455 bytes .../assets/logos/raptive.com.png | Bin 0 -> 670 bytes .../assets/logos/refinery89.com.png | Bin 0 -> 533 bytes .../assets/logos/revcontent.com.png | Bin 0 -> 544 bytes .../assets/logos/setupad.com.png | Bin 0 -> 620 bytes .../assets/logos/snack-media.com.png | Bin 0 -> 631 bytes .../assets/logos/snigel.com.png | Bin 0 -> 1111 bytes .../assets/logos/sparteo.com.png | Bin 0 -> 573 bytes .../assets/logos/taboola.com.png | Bin 0 -> 590 bytes .../assets/logos/teads.com.png | Bin 0 -> 1587 bytes .../assets/logos/themoneytizer.com.png | Bin 0 -> 924 bytes .../assets/logos/venatus.com.png | Bin 0 -> 426 bytes dev-space/ad-networks-mockups/assets/mock.css | 826 ++++++++++++------ dev-space/ad-networks-mockups/assets/mock.js | 56 ++ .../assets/stack127-wordmark-on-dark.png | Bin 0 -> 26894 bytes .../assets/stack127-wordmark-on-light.png | Bin 0 -> 26834 bytes dev-space/ad-networks-mockups/food.html | 133 +++ dev-space/ad-networks-mockups/mediavine.html | 369 ++++++++ dev-space/ad-networks-mockups/niches.html | 296 +++++++ dev-space/ad-networks-mockups/site.html | 136 +++ dev-space/ad-networks-mockups/stats.html | 178 ++++ dev-space/ad-networks-tracker.md | 129 +++ 47 files changed, 2015 insertions(+), 245 deletions(-) create mode 100644 dev-space/ad-networks-mockups/ad-networks.html create mode 100644 dev-space/ad-networks-mockups/assets/logos/aditude.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/adnimation.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/adsense.google.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/anymindgroup.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/aps.amazon.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/buysellads.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/clickio.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/ethicalads.io.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/ezoic.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/freestar.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/geniee.co.jp.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/mediavine.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/mgid.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/monetizemore.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/monumetric.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/newormedia.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/nitropay.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/optidigital.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/playwire.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/pubfuture.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/pubgalaxy.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/publift.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/publir.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/pubnation.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/pubpower.io.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/raptive.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/refinery89.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/revcontent.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/setupad.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/snack-media.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/snigel.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/sparteo.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/taboola.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/teads.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/themoneytizer.com.png create mode 100644 dev-space/ad-networks-mockups/assets/logos/venatus.com.png create mode 100644 dev-space/ad-networks-mockups/assets/mock.js create mode 100644 dev-space/ad-networks-mockups/assets/stack127-wordmark-on-dark.png create mode 100644 dev-space/ad-networks-mockups/assets/stack127-wordmark-on-light.png create mode 100644 dev-space/ad-networks-mockups/food.html create mode 100644 dev-space/ad-networks-mockups/mediavine.html create mode 100644 dev-space/ad-networks-mockups/niches.html create mode 100644 dev-space/ad-networks-mockups/site.html create mode 100644 dev-space/ad-networks-mockups/stats.html create mode 100644 dev-space/ad-networks-tracker.md diff --git a/dev-space/ad-networks-mockups/ad-networks.html b/dev-space/ad-networks-mockups/ad-networks.html new file mode 100644 index 00000000..c013fba6 --- /dev/null +++ b/dev-space/ad-networks-mockups/ad-networks.html @@ -0,0 +1,137 @@ + + + + + +Ad networks for publishers: list of 45 with join rules | stack127 + + + + +
+
+
+ +
+
+
+

All ad networks

+
+ + +
+
+ +
+
+
+
+
+
+
+

What do ad networks require to join?

+

Most ad networks set a monthly traffic minimum, shown on each card under "To join". Self-serve networks such as Google AdSense and Ezoic take sites of any size. Managed networks such as Mediavine and Raptive only take sites with steady traffic.

+

Networks count traffic in different ways. A session is one visit, and a pageview is one page loaded, so one session often includes several pageviews. A minimum of 50,000 sessions is harder to reach than 50,000 pageviews.

+

Traffic isn't the only rule. Many networks also check where your visitors come from, whether your content is original, and whether your site follows Google's ad policies. Each network's page lists its full rules.

+
+
+

How do we confirm a site uses a network?

+

We read each site's ads.txt file, the public list of companies allowed to sell its ad space. When it names a network, we check that network's own seller list for the same site. A site counts as confirmed live only when both agree.

+

We re-check every site regularly, and a site that drops a network loses its confirmed status. That's why a network's confirmed count can be lower than its total.

+
+
+
+
+
+ + + diff --git a/dev-space/ad-networks-mockups/assets/logos/aditude.com.png b/dev-space/ad-networks-mockups/assets/logos/aditude.com.png new file mode 100644 index 0000000000000000000000000000000000000000..f6e454aa56191222e6ef902864ee9c0def367b64 GIT binary patch literal 1591 zcmV-72FUq|P)% zy=xpt7{-4(sSl_PMBv6L6G2>D2ni|D$O|bXkmFv+YgA~3v9L&KpB`(Fx=-!*#-I@K2j2<|o8|}xu&-={m`^=7J?myb(Hlx?| z4bvo^EBf9wW^TIzI%hvk;y$nee4?91+J`5AJ}WVFIi}v03q!S#=*~8pDSQBP2y$fc4BOm0FnJ=l3o74(5M=uQQ#5g@W3q5?D0cVVkb6|_b`5Js1pge}ig zj0*11p$b|dKy-W!6_9WC%`sbP5-)V2f>sF7*k#j-6p}tL_5dtj3<0^S)QGx#eywYthvRYF?BLs*s`a=J71#}vO z(R<(oMMe=>AXL!wgjkyZ%ka2LUX%T8fFO*9_^aTBKBv(Mu{HssP za)6%-`fb?dS_D`{k5Bf`Q2}2Qr-y>C3L3Y|wFt;4dGlZb*q7NCfecXO%#uw)Yj(LZ z0VNENX&)l8FR!Cju6hfS)WG65Az-UBgTOk79*IzW-ssS4^&f@`>oo>^cRgwb2b zp2bB*4?OSNm|dVA|j4YR?No&QJ7DFBSM+KMURj zlw9MVD0zJ$R4@dNb+bzawcaRs6HsD}f1>2A+A-A7aaEQ8D|PI$7XcLw&!B3j;93Q5 z^jFtowz6IJB0v-{KRFIH9sSWVJXNtx%XZmGK!uX`9Od(CS=S0iX`{+Lodi@^o z#fE7=HYKmi@R(z)pzooA{s#CI_(tu%>Gdwz%J_Dfh>^|8n(+*f3yq_xU+w18y+b=k?95C3*d7U zv3D}Ac>$aP+jjOWyIiCK`B)sdR4}1{48!BS#&2iZwCPO027W<3&Ym%Tq{=KF1B*cz zJ;>^GhFx~KQ7;utZI#srpHW=%oA%uxjCN$kb*7hquTW##&a~;Yx$Y2n;$xe|T+j!8 zcO8#P&|;Aat_ctq4^g`}eM;WDy6qnD87k-%>pVM4GVlZQ=qwAZsv?z6Bq{GY*F&Jh z8o$9YN@8Xbd#O7lcY;coO_AroW8iBOe8s}a*DgOTvdcZI153C!HJJdPqJr)-9kkp5 zY6t#p5Jrit7^{@cP1)t1=|D-tBLlR9MzouWa+h)7S#O4_;*gqlxmR;jlMEUr1Bda< zzSD5v=WzOvXCqzb% zJWsiPyF4=*jh>?nip`po`W33^m~v@12^lKL!*z)~Ga8K+P);qc0-SoJHN8)#2?#H< z%(y}>wrDRM&=6m1LeKn-LznfpjEJjT3U1AWWo-MMT@_0dd8bV;lIv)sDb-V p%t07kW-f?)h(TAyt^eog{{VqiqmD1 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 YFBh*5C}1%k$^ZZW07*qoM6N<$f(r}mU;qFB literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/anymindgroup.com.png b/dev-space/ad-networks-mockups/assets/logos/anymindgroup.com.png new file mode 100644 index 0000000000000000000000000000000000000000..93c8930f1132fe1b603a981c5eebf526b229ca81 GIT binary patch literal 766 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!!UKFlT-^(NY&RwXQP9Q&2;-r@ z7X&>DcYo;b31ojgr)M>Bv1P+yAOf<%q^?!pDj@soIb9$Zs1e8pl0d}}H4MTRgMn5^ zmIV0)Gwetb?tk#S|EQ3C=3&$QQ>Q0oD?E37=%O^?q-WePGH)`-72{Cyxyu0x3t|quJZC1Gs(FhYQ4Mv^JkTqms3*}8dvjfs24Q2 zKVO2wV~58&+YiFa_PyR!m(y=@n8UdJU-_%c+Y347Uid3gnk>@fbbfj&hf9L?REb;j zmr5MDd*nFV2e)rJTJMWaPBwM=Cu_(ma3s7zp(QPq3FFK~sb5owKi*7Co>{dBzh{CgHeN_s!ihjkC8HSl*a9vCp`8c-0l_ z)nUsd(#3bNJFQ>*=fJuU#l9~m-f#LiF;?N#r2Sjv_ig#_t?8t3R_^w*g_&)CnbVwp zNIv>{ugtV&>dY(5M>3z!n%ugvz2nuk{M+8l8)WP&e=%mvTxL_7be}Qu+#|u_&62Yw zvls^dWV#)Gdf(p4td*<@JdEMDtRlYU-WE?}VmROs*HGK9&{$<5u5`xK@HnkW~zxC4s9qF2-57xn4+0l zcfe9W_Tx3PR89fEBUUlLFRbZQlTkV$ylLN$&}%%Kx4uebT7RheAxo2uK!v4*g61MN z#)s1_n!GzzKKv~yeHg#OdtSG3H-{3tp7d6Z`#U*o{^=Z=&C*rboxV=1NanA4!a=d- o^$u%(Uzn0q5>(U@oL*G_itWyOseiTUz|_Xz>FVdQ&MBb@0E>N9SpWb4 literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/aps.amazon.com.png b/dev-space/ad-networks-mockups/assets/logos/aps.amazon.com.png new file mode 100644 index 0000000000000000000000000000000000000000..1480a646ab01391af0f50ef1c6d4ec8d75cc3647 GIT binary patch literal 658 zcmV;D0&V??P)qLvE0@dqYJPyMa5naU`H{3f}=IQiK~SNNP-O5T`()3rE8Ru>2WT+OHY$G#Y-t_vXzYc7+A#I%R|!nseY3C@q@1BLZNIFhtXnd!EuJ zNiv`tyhC#feB8CdHR&?rMjn{PgD(u|1{Y|)vHxtZN`U54zVO2a{u>Cz58UWl6U~J+ zAprV{V~+UE;buNbz>KmyFjW8?@tORH2PpBDTL95@%D^wjg0k{nA=F%}$c#`^;W?jA zH0KI)N#&JOMZTIhNlCd@uqlNB*Axa^Qy6ef?hU+gEnp#@4<|l&A7Jv+#QLqdY4ZlE z;iNw!n2dHKZW+}URGKeio^Qq!O`_}QxkWl*XwXh|y;I@|gs2LhpQwOj%(NiA~1sdi{2^PDd~8#nlWz zl!-+~wn0*;-A=YAQ&`1uxE1rW3DjZn!UAgDUoI=74`D*5Xe|&=Q7?loFge$k2)0jPQI3m;l($tZd3oLD89tB8K9AQAApigX07*qoM6N<$g8Z{Cg8%>k literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/buysellads.com.png b/dev-space/ad-networks-mockups/assets/logos/buysellads.com.png new file mode 100644 index 0000000000000000000000000000000000000000..1dbc113e70887b239bee0fcbf70a5b838c71e488 GIT binary patch literal 603 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 Bo9_Ss literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/ethicalads.io.png b/dev-space/ad-networks-mockups/assets/logos/ethicalads.io.png new file mode 100644 index 0000000000000000000000000000000000000000..97072246d67c55b6597fc2c3db1ccd5ac1fb5537 GIT binary patch literal 1336 zcmV-81;_e{P)>+GwxaV5{1b#N_n({P6eteYxNJ{rFV!Ujkt1vp_HY*+t%FP;_1D>&a$q(gQLS(f2qpT;KTI6yZ`_M zT1iAfR9JP;p(v(a0;sPlw zo7{+Cw)~kvo4Aq*{hM|Eq-tGcS)psS@naLzZ-qa}%$EikVQLY+Sukp{xbB zs*1&Ot+5XIVsKZBia1>$03HbeCNT=w7XH`ExkF1W$AF9_L~+IH zSNE%K%tt|4_Ok>N>qB?Q$Jyr=8$cfXtj6PI$(Zk-Bf1FOKL=g02*@#XW)&F7aR z9-k{=aF*nY2-C^nZ%rDoAQn4W{&+l&_V~j>Q!SA6Ptd;Q#}s_P(S#XxrI>g3A7J-# z7YPD125srj^o}NU`IW9}BmRNA!FWMuOea4=kB)-32 zAE=lnY}c<1R zOo#G@x#6-2Va~z}MmE-k5_jHTa9gkn*C`I-g?};JM9FJ_GZsNd`kSbCN$Ld&06}^&*{0@U-GpS z+S9|s?aTE)FW@%fA8{VMM7gSC-c`QZ52%<-#HW0000*fwvI_18z+GLN?PE%DL3tjs@BmgY4-*Fl3%sSYpjdU7 z2m)vmneR;m7c6^zA{udaenDQ|ADptQ{EQ!eT2vem8Hxwu-vDO=6}hNS1Y1x!PkGy( zd0<$}AMBY23Su*Ul)(cknpQA@n8Q@M8ZiDg@^{14-w((e5r9#j_*Zg*xH}9Mhf%(E z(%)sx9akT~fI9t&d50He#Q;DKWPYlso8+djP~{5*tnVZWXKpk6^JNJVpY_!TOdSF6 zF2n#7*_{A0r4uFETb%Xj>!%@5N%LSSS13T>2CP7KdAfwvwjze>8*-nfqwIPQ7NsJg z1ESgcwv`^x!6hAxB&pUo=`nlC0kI@I^Q^ZWjO3~74gGMyNTh2dd!~MnNNIdbUNf|L z6ht?YS4#sgsiTmvl+%2TC}%z1j8X=EN+d5h`kU9mYUlR6cDU!cTVYvsYKi(90+5W6 zBx4d-q^fpkUNG#jkwykp_cTJ_z8Bv%gfXbGf;yiM;BG_S!-6pw5y*$decCcV#6>-A zMB{WYhGvbGQ_-P=|32^M1IF=)b3SD4k9lr=o(`IF)O9w3z8ZDqux+`hyuX-%o{sy* z!qM)9BW^An=PS?o(zOF(z4jSQY(_8SaP|Gj;qv_}WBpI-_5TNY=|SDs`SA__0000< KMNUMnLSTYLy)>Nw literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/freestar.com.png b/dev-space/ad-networks-mockups/assets/logos/freestar.com.png new file mode 100644 index 0000000000000000000000000000000000000000..8b6d2237de38a4774a2237b1ce56099292fc721b GIT binary patch literal 615 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 WF&sb(4kxkz0000f0;pNt zKsrDQ9U;XdZJI;*zjw3?e}Dm*q#xhM@A2_bGdF)s{^Uo%PjBtOxo{=YHe89c4Ob#< z!w*9wcxwkvMII#~Flq&nu^M!Np{Hv;ix-9OH=4jFU?kkzSeB zKtGR?DH2{Titp_ldDqZhAFaq(4Pu|crgV8EKAVR;N?y>Z774YAb>>?-TGaix+hsB^^ZNuI{T5CA^{giikGQ^^7gdN?ZUYl;+xpF7P*N<+qXmzalR* z)IGrMDc&|=7B5zI^C+1hDYm1VDkPk?CJ`Tzj;C!~`&pnP!Ck}xQDSE#tcd89IA`i? zLJd6F4SAFd5fb)dJ@v};VI($Gra(JtjqDUBY@SN_&1$lj5@neA(>|h zC||PB1k8DGQEU{A=AvSi@^(pI=?}iJ0~o782VfR2%vloi4M;%4#Y!ur^w+I5k4~Lk z;4j4SKBt{YN5OZon(9a8bk!z&^qu{VE$^AWHGGP^M;vrdSjD>T326}taQ;5>`!AF5 zjr=$n8;bn<-&4iU$>)Z#OY1pfU8C8kXYoQ=-_5H|7jA~zXU?s-x&G%v%Wx&qHe89c d4Ob#p4#BP=js3& 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 JPDHLkV1j3|N6r8M literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/nitropay.com.png b/dev-space/ad-networks-mockups/assets/logos/nitropay.com.png new file mode 100644 index 0000000000000000000000000000000000000000..8881e5b56d0d6aa927a494e0ca27ca5d981f2824 GIT binary patch literal 622 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!bOU@sT-^(NHs3ec0!CZ!8$b|* z3nbwp5Qf!>{xv|2d?i7C!3_M$HYv?-=bQ=Ja_>ix%0Bh~3=E8qJY5_^B3hSDzc{T~ zfybqLg~Qc9|DV5$5)krfN|;xbwd0Y6>vX-I#1(#}tH_s{*4cRM$(3s>RzV_PP&z~jM>$I`4CC%0`( zUto7lWrx-3%#}5JPD-DcJ0;*}gk;Ml&$NA?HVU0xA*>qyGi+$E8B`C)oiiJKFx0JO~v2WO_mM?NvkD%9PX-B@2 z;8`D1ueGQPUD&gLMfL2b!>hMxDQaYv{rs^`sBFrlw3W@XO*Q zmTQ1Yhn-lj-FB53QR*9bIr%&toC2iI1?KH(z8&ki`SZ!ke}ra+S)N^^6`*z^I>znZ zyuwP|S8`J%e>NNtZL@zGv{QWk;zIVr+W#5a*4uRM`7g5%m<|{`UHx3vIVCg!0C+YV AVgLXD literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/optidigital.com.png b/dev-space/ad-networks-mockups/assets/logos/optidigital.com.png new file mode 100644 index 0000000000000000000000000000000000000000..d56e104ed2d683dd76a4e902a6441cc3b7c268f7 GIT binary patch literal 634 zcmV-=0)_pFP)q5#yO0MwuW)Sv*2QHm+RZd5o6;00HhvL_t(og}qnpj>I4c6hEt0-~Vw>TeV`n%Zl0K z$0loJL>Q*X*jEiM+i*8RDIuv4pjEu=G$aHtHdd1q1UTYjdL02o=N}aTpbvl$0vO~U z{W5^W0Pf!bn8yIiRKP`u$8rF0Zst()JsN=P{lx~2I6oAjmmp-{Gf2%yf&hj`D>PY5 zAYDLdx)No7wi%*;Ks@0h`ot<@a*N?WH*0(t?xI^cL zvKnUXh^6vAYZR{%7*^$WFZJF#XipLFQI*{cw&WXw@|x>7*~$Q=>%Q3#m4&|m*bHDU zpVKRVp1=-CY&4oBah`Bo#>(Sq8S!GJ3{v3m@N+^Wd{%3kZj^ZZ`%6?RW3lfDUfPDA zXcDQ-#9K4<`L+VLG)ON;8HAyoL0`ul!lib8>BI-pkr`t}4xA>}xddWkY)ss=q-m|V zr?2HfnAMtl8age}Kj%^Bf)e7tYM^B}d8CbsOdPgykB{p9f&cE1f&AAqFo@#%m^Mfy zA%$4|)>ANOVpvYW_OQJ>?*H;>TLIX8@}Lk10VLC#&%W)4=QT}*ou_YxXg}YxA39kz UO8QSwcK`qY07*qoM6N<$f{f}SRR910 literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/playwire.com.png b/dev-space/ad-networks-mockups/assets/logos/playwire.com.png new file mode 100644 index 0000000000000000000000000000000000000000..bf172316478c7e429455c5b9dee137f11240f1b8 GIT binary patch literal 962 zcmV;z13mnSP)_R&`ohT2oU~Sy@>OJ8=n$)LB_s5LluNe#~86 zU0PaNI97-Qq2vgY+60^6U0q#KQBhM%(`vCj{cs~AU1krvLGuVwPx()$noRqfX}eG}Vb5oV#~||2 z@V%ZTO&K99rKR`5$TXPU$E%uGS2pJ_17nbKnqb8rkAz69C_cL6<}(_$3sKX{&tyH$ z4#!!$AL#nmGfz#TM{?GK2G@sae-s_+P^h|5jsJJ^sUyY^m_+J9clV&a6E|G*WO@1D zg#BC)gsz1jK#bwve}lU71A4?>2VH1LqE>a~Oi5+% zIVHF3)Sdyx6YXwOa%Uaeb)`Pw+thjI160(au<9|cOy{dS{}@1YQ;m_EH)g@~XxE!O zt~TI$6)pT!4OZBVT>}XKIT4`U0I&{?$p`fTegq)ZKus3?g}n@rnu%NKKtK^Yebnp6}_w)eJ#{X0Uu>q|(9e|Vv09gQO699Q(rjRWHFtqW@7G#px_nkPq0HD)= z6m9%6=KoG@tIL>m+$>HT(1H3+HBf14MHx##l@$7J7q%i)#%V0VKAf>UNgBy=@0#H$F z^btS*Y6$_Dc-=p{n%=b>a+F-Uuxu{2iqQ>6NXf;MU;nj$VrJ7d2UxaqbO5bUvEFV0 kiWOU59}t(TH{UDt5AmPEB|_hsS^xk507*qoM6N<$g69&h>i_@% literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/pubfuture.com.png b/dev-space/ad-networks-mockups/assets/logos/pubfuture.com.png new file mode 100644 index 0000000000000000000000000000000000000000..3e04f84d505c89fa66e910fce8077dd81d150de7 GIT binary patch literal 891 zcmV->1BCpEP)5x*pwDyQLoh{;z;7% z0007-Nkl0qwb%Z!Y>=O41uGRjIn%}C|3c9F}Q_aU+u0KDniC(ub< z5lR6k1J2|?5#|8ETW9U+Ba9ueuP+_eU2x_B?DvlXr7Xg-M;Hu5fK@Ub#{2p5s8h=b<_qb{+0O4eKkK^l>To0@7%$~#Hz>Py z-EDQ8$h)$!i^;&pG5gV+P|FCW?VJJKc3IrDZKgSaMF0oW4JB(qS= zdCOLAlPl+A?HO(@91Ia|oXml54IzniFii3))<3q#^$ew`bkG-(ALIDQW#ri_j%cLz z>5b4a0bbj{$pJFxpDj0zCpH|(Dys6JJCjs|o?rb-{Q;|kYGL13 RKpFr5002ovPDHLkV1meut7!lL literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/pubgalaxy.com.png b/dev-space/ad-networks-mockups/assets/logos/pubgalaxy.com.png new file mode 100644 index 0000000000000000000000000000000000000000..750bc73411a9acdd761de23364fbd2af7f54e727 GIT binary patch literal 605 zcmV-j0;2tiP)P)t-sM{rDG zW9Lg`;w^0BYE;ZgMW<(G?uKFScwF9zT<9Kj;RR^f3UluTa_Ns>?*MrA0(A9NYVu)e z@v&5X7N34^Lu0P9&+?G`QNYr000hjQchCZuyCxgF#rGo zvq?ljR9J=W*4vVUAPffJ%1IPkJk;v@Kk07mDhU!A-I<-eXl`r1`JoZKTt4;BODkoq zQs5~E*D7rofrZlTyb*Nmw?YbZs@Fx=~mm@w5e(6m@U3Bpv9fSy|{pakK)Ac4AV zBcTW->{g=MO6`GY73)+_;87b!!X|*UFdhKX0t@gNftMg7K%gW5-TJ*S{0Io3&g#a~ z0tQwgI^C{5CA?M<0qL2P3*eIi1jw1JJ%|tq2v`Lh0m~*38Q>^y_r>mjU)DfnSoRX# zIQzFsdrxP8H_l5)+*ZQpGDJe^wO%8zG9<4{>Dd`Xs(<|>7#R$4$8L*wmw~+Rm1kwx z%}^-E$sp-|u#t3zY(FEzoBr>eY+?cmhV#87X+dPakwS=TS}6D*j9SdG@DMe-eJLlJ raL_iw@xn(rYUiYMu0GTJslTi**vDj+DGxR600000NkvXXu0mjfY*7sE literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/publift.com.png b/dev-space/ad-networks-mockups/assets/logos/publift.com.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4cc456e503b2b6c89d542ca18ca3b0f02c41fb GIT binary patch literal 1008 zcmVvZ?2DdxdyoB z011@{01j}`ok&m#E8a;!5j)_j8$d;6C&4CR1h~BD_at~0EMbKRV*S$t zY!*tWj$rF((?9~$D`;>cOhYyei8Sb#05@&K{vc#Wm=6--v~bmDUlAdBZk!1hBmN!{ zky*k|LUkI}x?O+w{iulG!1SfTJ8;EM0%+=-1SPyYYzCruBsc?M@`@o29+vcTyb{0> z7%^|}*|&Y9@k9^=@Faww{@K!>pZh)#XwE316y6Im?HULneiF8YCqhXDPMdk6w)js% zfjxN_?p>Y#7=e&J08Rs{u=t$+Lx8YwOtAR*VBHAG0LcJ0iyY$7K#rgWN+Kavr0AcK z(~F>p#1f!x1hDujyFX|U10~*;b+h2(;`5nMWH;eQjs|k^xcz(I2}MQlWX#0X=bwsL zzwb_uIYKE8p_?BWe*UY5_4WNsCG=5J=*HT2~G0dV`leHi%lv+`ktCJrJN0l0z5!9P^?9xCTa2~Ay0 ze{?Uh1dGQr@WBEQ2`vy}3uJWz%$^U;L!HtTL4Zi;sz0l}1EEv)JU|gy33L*xy&p1R z$iE=K4t|ob2$VZ_JU&!EIRA|Z4Yw0W3!N*sKzUH=;_*(Y>zuwOV`V`brPu@Q$2(`A zjw6kqM&k7Q9va^9M%W}YkmJL5Q7(SG3t=R1kK{yP4uVUU9c`=>ZW+W^^hz*z`G4v#VOk3k0numfK`^jt_jI#y0X_STke z3cwn6>cY8+Vn$qhLP7p?vAb|K9$0f}5KyrlbtKSTu z4zeyBqzA$n6>$Ve>hwjBduZ;VDCUO*XCQf%h)on_?8g*#|0GSm^rA%)cSQK?XIuKD eW2;~<3I71cXE}^-gkt~z0000KL z^#8yA3`yER1_ZIC8Z6zp{=wtN)7C8vXh}VL_0pHGU*B*%r~_)}DhcunX7~>V6_)j{ zUccJSz`!W!>Eaj?(fW4!#iAn$9IoPm+qf6~`+r?*_g%mAYie%tx86N@rfAbLS-WXw zj9$I-_SZdm@jvqm{nQ_@%xS1+6iE9hUCf=%Akf6xc`!uRL4G46gU{A5X5AY~XJQXc zW4`JV60q4IVv)!bg$7Fj)$CFMzNta)1h#d*XkhxT6Q*R#mBA+mL~w2;AgykXf*rZyKYw_Xnj*UV^IndCA!-Z)`SmYkZe{6r&dPGg-`Jv$pcjgmV@S zuAh?+-p`-Q#dB35!E`@!Tl-tn?{27dcGfZYJQ9d_6UDo!_4SB z|3TCib0%fJ1N$?4%{V`>{n?{7{lkGb8T=bo3l}iO@oxC@`uFN8_R}k^=D$#=y8sM2 N22WQ%mvv4FO#md5uXz9f literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/pubnation.com.png b/dev-space/ad-networks-mockups/assets/logos/pubnation.com.png new file mode 100644 index 0000000000000000000000000000000000000000..096e14c5c66509b5137cc5cf5205fdb8cedb1ce7 GIT binary patch literal 610 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!yaRkfT-^(NSlHR&NLF4R&W136 z3jhEA&&bMp{``4!3ybG3UVQ)lebuT}GiJ;wnp~`jCH%{ySMn{$F&QqvNZ% zR?7r8W|yB)Y{7i`40d`4Sa&e|X9$S9&FW%u`|_Qtvr-O=%^J?|UgkADxAN;O(S}X4 zma<3XI!xViIjd+{`fRJ$MX(Y!VDh z+VYPVOYqIPXs57ZDer=3FHSx(YvWjD%W&n|Exy)!3{K)&H!B#J>t8U53o+bc$Y9Y> zxm?m7`(fEr)fXCv%;z*6T9?bP;A~#z7r6yr50)%U`lGNVhKsF0N2uY#3)==K4!7%X zmQ3g~-`Ts+Xc}Y8>wJdX>BX0KMV0RpxKb@#9sQPZNhz!KZ>|TyX7d?0%J`VhKF#d+ zx%19G{v-NZKihR&0)~?KgMNmG$=3gh<6F=F{UOgckcobh+jaXt`%iZL q{`uYZxBIi>?#QQyEIYaTg|N)>zK1LtLFa+7%i!ti=d#Wzp$PzFz5M|I literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/pubpower.io.png b/dev-space/ad-networks-mockups/assets/logos/pubpower.io.png new file mode 100644 index 0000000000000000000000000000000000000000..9e84881dd9b52cd734aeb3017d0b1d8bcac6fcb5 GIT binary patch literal 1455 zcmV;g1yK5lP)4b z#r}am<7phke}|B%6(+G0Pv969N7RntN$g5IRur>%L~MmoDbU~E#iO`%B@~##?P71Y z5$YxUI<=e~y967P{y__Ep4ng-REw{(mZRI?1>U6Merg#g8%FK;idrCJ8*=V0;d5;2C^?tMR28deit3 zp2MqE115zds#}qd@f&;%U&Ngv%oya091@nSi%s~6aEf(uJR%bT>}o(@Uig$%eRIEo zqb(IVgIg=cj0s2hPK_*cB2-$2NfGDNt-_O7Q^9<~ax2$Fkz=^LVoVp`6WgsM&l5@Z zrAT!vb{B}k-|$oXr=sr@yk9fFAFFY3q5m=Lt)a17$i9T1DQkWT-zpHi$Z|*5^o%@{ z{>x!=qE(XZ-d^I*)(rlwd}zHU^$&8`=S3`4vXXZQ1Cx{OFI`?=RX$kn;HnIhar`_9 zG^ii>SeX1=Lbgfys~pEQ9c;@m?c#t)ORMm8++7f&2`2GFT$kxTF1BC}>#?naD+_`w z;!na<_G4ODmUT^}t-*SHALnKIpT&onzHwa95ru*r=EP^M!dBrFbHuAKR_N<`^(y>J ze3=|B>4<7a4sYS#nZ6zJ7Uxr#srY|J?D;CJ#`a9#3E_8gnCU2Qjid7Zp_Eg926rXl zx>zmFQ)%IHyW@;<$!Oh#vvsE`yeFJpKPE(yRKlJ3tg{F8dievrBv7K=&vRb>jKDel z(eEfRiJMYuip2r@bVLO(hgGT%=O<#{jv6QAcRLZK;IBqdv+coLqFIF*HP*Ewif%cakIOQB2Zh%k6%HhtRk&Qa=y0SX z!jFZ_fC-UsRTaBG#DmIPvnfvEA=Q>?Oe9b_!!$32P}e92jsL`!43TmCLM%jGQkH8A zxKG~WZ%Hzgg2t-}?|h(!(Vh$wSSu2sL6lh#3DnvG{XI3bm%?}=IVmwbB$B6+GWTg# zH0ZW>)0~nLThv!3@9CXzrTV*$`<2Ty<0AF^gYvb*TpDv-19}hC$aGZPkS$?SB=V(6 zr*LzDaTn7f`1^s_a{E%-oBrFbS>5d0iDyZs$5mvmJJny{ypC%d2+@lPd5e?7wTV|r zmN}U;Vp0^n>h|_w?UiY(#Nh^VJgl%lvzpyXy)HieFs0IdjEk*X_azpSjI(tFZ_})Z zeM%UBoR4XPEs0j$nOGD>yYDn1I;GxP^tP8p@!<2?6(Pow0(-Q({JCF?d&LdfUGyx7 zym~Kg5%JkZ5jjt#c5eFbvNYylapBp61;UEh1Vxu@Sb(QgzXs5-QDAk8sVM{wINJvovI2pAWQCSpK6 znGOvn6A>^S97s(`w?sgqR8Ge=GJXpR9t{j4IW~wG7dtE|Y8x6uARbQv0t>pYlM?^{ z0rN>jK~z|U-Iwcff-n$;LsFKTa#2KWQS|*^vOyZ-kUkD|kV4$021ALQ2D%X~t&?IiXoy$nTe@01y091Agb^xW* zfhUL~;A{rCgNOmuPXU`(0z$I#?>cyAz_i`JpJ)36*hOOVf1dzaO2G$?S)?h!=MPN+ z^*rj}Q_i&PsE}G(Uwjs2WIPKfRJNr34aBC9t!rPWZ|?0Cm7C!4mjD0&07*qoM6N<$ Ef<5LAkpKVy literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/refinery89.com.png b/dev-space/ad-networks-mockups/assets/logos/refinery89.com.png new file mode 100644 index 0000000000000000000000000000000000000000..7b75539a169e3c8112d042cc0458e34ef072854d GIT binary patch literal 533 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!JOX?|T-^(N{%>dgzn$sd9wi{- z?|u^q7b>{l;K!xbfBOx8TxA3iOXhvJ_s2w=@3D*>%NQ6KGd*1#Ln2zYPQO^zWFX+8%ea`aw~YJG|M}at z-I^uv?9#K6`_Je3mZfp-Hea#w=F3-|2Eq@Wbbs)1q$~8?yDFIW*^&7WC&M+SgL}Q2 zY6Tt$gfrb>3EmB%bBF&u}~2X?s$;JcHn= zvKenyD$F{XsK(HoF^!{`si9!{n+Z%khxW=mo58e3)L zq8&|wtwp|gy!gtQ$9i$k3*kR)8|EBbtS~D*gMssV=>B^RfhKo3r5v2xc_mt`V^%V5 zUa|P*`iciL7c71$m=O1}f6erUwC*_Bg{ifxzew{blwYoEj>}!vsJ@{5(88ee;tRer z#y4Es%=|^KprSF#enE4>?1u~wCVsg8i^HGCh~bUGmCO5?U#8Yi@@D>;ALxxr6ZPFO1*WkMMsNw^7x4{Jfynp{DlBE`@#XlY&CMG6pYippO zpxD^h(b3To5)yE5aDRV)N=iyNI5;04ADNk%85tQcFfcZz03`NBjvtt5*wlvNE|7FV1V%e5#pwpQ=a38tS0iy)~PMvyj#mS{?I}jlu z;mz(!8O%x`AmvF<7ft8j|H| zGu<`lHfXI(p7OrhUo-%XYfbrR*iyJa*b1m@u$wJ>>EMWS}8_${AhtuM=4U;t;c>5QTY->5@gg z8EuO!@eYC~-}t`SVkcFp4*B$gS|qkA`)j`FvMGI5Dv5M<+mq@`z4im!eZ1oQ`&U

&LwAFIG0r@R%@TWkOT01I?dPE!EM z=6I_@lUyhj3#C&SJpcd!=1D|BR9Jk$uKw! zW*lcK<6P~HUTi8qO?~-;29V~y{1p{E`wkd2fUz(Ci3;|<17@9n_T>*c|JwTUN#UdK zfJFnyB53^;t_YU46MPEKHaSiDfaSBWw+Z^;_R)AXwUnk%D)^VzwS=kf&;UdPbDN@e z060D?)9(-Q0~F>yy1BWkG=f6uqwTu!)I|B-W{Tj=NR37S$O*YYDm0S940*}WVrzgE z326c?G$**Z{I)N&kVaCN`|k@abQb`~cz<0opb-?N+&o2#`!s?=xvbEqOCumHy5_lif2qdg>9pf15p|egTfrvbaMg9;w)o=6{UdQ#QgA< z0G$Ng{6{YAQW}%nM|b#t$OT!p4BJf82ny8|0I2*OZm!*zR#O@iDOASAc~Ra_Ls+Y^ z&QXPB9#64HAX%W%ZV^qS*l2ia{cB8EGA+m71qyA`15TllLN})uJ8EC$_~WB3EJ@KHe{s2e*07?D;ME(Fm{(6-D06_i#JO1eJ{{T?_)ZhR5{r?eH{=v}yAYlF~X8x$3_22=hSTK<5b{>at;vc&(7sQy@c{*i=el{#J$mNMf|<00055Nkl*VzMg0;&FJt3k-i%gf8l>t_^vcZiX~e+=ZWSLNxKBfk;1 z$`vAcey&ei(cN#@RF~q4gAeh`xTC)5=78rdP3es=2a&?lJ=-hah8Z zgvv~e)F8uGwod`J*W(3XA|s|Rm~|0(KtpMDnnZ}&VZNA&leSkVeD$Mo&gDc2ZA-I3 z(KtM%Z99K2Bq-ijb&L^*`^&(gDRu9f*pK#$-R%oxnhyv;US3{aUS9v7egmsq8DD$9 RCbj?o002ovPDHLkV1lGMFlPV& literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/snigel.com.png b/dev-space/ad-networks-mockups/assets/logos/snigel.com.png new file mode 100644 index 0000000000000000000000000000000000000000..71e7ee1e60c76bffb0945819b0338eb7d4b239cf GIT binary patch literal 1111 zcmV-d1gQIoP)4Zb!kVoZNIMjDR z-ef)3Z#>(%Q}V{M@JlMu(7W?)RN0MM~#z${6~_Wxgby-2b! zcF4J+?VG0Kkvx`mJ03r!jn;;?(f564<7}g`QP=hFE*l4*ZxOUb=(PtpTZHBe0QE5o zurCGVdjh)C-I76V5R}Ri5X3-R2tz=3p2CwesQ=RWC!fzGfOJoQ7#IqtB*2dl!VzE) zolY@ONga=ZF>o3h3B3k3ABx~KP%5S4UX%<%w_HVb2FD;p;UFNrWo>}@UCcnOb=(Wi zK(|W<&xnDlQVLRfIZabLIVL@XKStw=U;!cuCztOb+VVQi^El4t8{IF)d=3n%)0zM> znC?X;UJc|MfQ61n$ry-tF2A^+YdS;4OSzYv!6OS$yc=RrF&z(5N@6h0`TXJ$RIHee z(i89ut{?J?Vr&9~ffSU4Aj;|T5ui|k0a7^Ge18PTz;)15KUtU#x&gqY2@Hl45)cvz zuzL4$HZ0Zp+-1Z1+LSL70yWS`oXwZjapgh3e&gTQf=MrLE>cC*|0Kh0LriPi(*z015028^qB|vQWTeY%|K!a-;)b( z&XFgQuf}CGC%_WkrxI_1hSbEnpb`uK;JP^ZbZ8;pGr!$<_XIOTk7g1YR zeyvqApZ$X|u3FiAwv6+%;i?d0st@E`JT_ew8)>b90g#WE*mPB2>dbAtIszN6TKLPP zqCOx?6JK1(A?F8t#@jIS=bgr+1M7n?2Mo)A4QhKL@P8u?3|a6xPu~n#)pE$fF%J;F z81vL}%=3dL?i=Op5`5^w0tQW$7&QH|0F&y%6Uh~6)OA;3J#B|MG3;Ef5qsZ=jqAkV z>T$ju_i0xCVCz_g#M-nMEMK?z%V+^oJI|5&ARu%MqT!b*xjO!=rnH`~jT;*elrn5> z-VOlbX;v1_Aieg{;<>SNsodgeSKnxB>)7ha807NJ@-1j<0}cA;T7&%Z4eNjC1FS;W dJezp^{|Ch%os|CGv0lEMH literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/sparteo.com.png b/dev-space/ad-networks-mockups/assets/logos/sparteo.com.png new file mode 100644 index 0000000000000000000000000000000000000000..fd4898a5c2907778fa57d2911dd0114794b54d23 GIT binary patch literal 573 zcmV-D0>b@?P))uXUHM>!mYgl-_O6IqI>Od)?eV~`McEw@B!E_)<@ff1n~I)=NEl8 zAs#XFwnzZrJi+-zZ?C|6GanDMAFqIWPfsBP#+bPP3SiH~&}pr0p69kK%eg7oGo^!4 z%EobQ%d)&`sE?Ggul?UB04b%7qNwJ02;c&1j2RlgyHN;XjWO47j{y)u*f0#Q?Xv)k zF`bU{Jhz_b4Z;oqd}E>&ML`e*1VKPi6aXlt2*VI5<<#Mr1dt>NtJR9b;c#`lUawI~ zb@tiqc7x*%rXWpItCXsR+xPv>^ps_3<2bgCOeFN3ED6=f9!OjAxN-1Tt zEbCpAP1v>xOa_or-V~pveBZZ1h#G8~rt`PqVzC&^v+KIOu(P(I>$aa$$K#RZa!HaT z!0BMpTBEf_h-tUCN!zfk26&#wX0w^>xN4&+VHk$BJYUqeD2*|#eH!Wqh~v0-TeUfl zqG)Uluy8sOTN}p=(p;1iYk-A2E+kEWwi+-}dzg70ADQPa{RY_10hp;RoStCM0&qCn zGorAiVo!gXT_7G2iEY*SOOt|AUfwx9SR5*Oy2FZ!65to`r}~Hg49RNzA)5=W=yy9=@w^*ktT>$~;W@zbw;_IS)3=ywPS1mJ1Dt+x{qK%d17(k|y1GUb{x` zrq{u#EGun&Coi7~iTuy5g#|4CJ&DC;&p*-j_q!RN2TMH6T>amvX|sxzqk7;! z^|R9&{yXF}iTnON!;rLY#ZGR&Ehnv;w*RPRxmWki%0cAR;kzGZ3NHQ0xa;RD1B<_! zW#(({pG}>c`&!(ckIl2Np!mDbCA+&l%Ur_$-}t`%NJ7?+1Md#*Kg_=}#`{_U_t6+& Pgfn=$`njxgN@xNATg(VO literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/teads.com.png b/dev-space/ad-networks-mockups/assets/logos/teads.com.png new file mode 100644 index 0000000000000000000000000000000000000000..7dedcb39d5104faa22583cce31d60ea486ace510 GIT binary patch literal 1587 zcmV-32F&@1P)gmSRN{{I;5W@Y7as88j5v23_oQo>1 z^)m**)57a5xA+!wF3m?**ZzZqR6xr8l6wD$c>RiJ{hD_`N6`Pv-2H-$ zJtM{T&*A-+t?Y55!ezMqxXmzA*AKb-9*gGxh@5H?k{D6;|MX^3R z&X9!S7Ki4pPq6rOrxm&R4!!z&Hn?+U*)<))|HRhPU9kI*Z2p9c{DX-ebK@Xs-%Ux< zCPdPomF15_v;AY+@q5X1WYb)XZU?iY{hBW2!vG_g1n$tQ2xB4FJ9 zd~W`XUjK+{{(OV~g>L_UdJLBD|A}QNCd!96vkIs62d(!8v-SkH`Xo8f>SU<@gL52M z-4l7{7;4`MqVpd}*C#U0oj|eGTdod|>=A+K8Dih4NU#2Nmn<2_B0|y;hwKEt{<%)A zAW_!GQ>%F~wElsEM{rF0!3`Av005D6QchC<3jY4eQ2qq|{?iN-{?$sw6hi(%9R4FE z{wKd`FH__`LsjgAF&$$fe~QD9+LVU5wc=VVZ5OpYHi;N&#_*nR<=W(lbzTw&Dajo8Fn_69z#N@QS*s`f?)4r z@4ZB0Nz~ZVlgFKO)Duwz5mD5CIkShrad#@1_wvd4 zPp@b@JiI!MwbzSYlwLJ@hVxHj$Hy04*{Am}Im=SO$;T|y38NG8&knCP+S`vYkfl&H z70++e!a~+{adA@Y8_7If);zdA>MSdp>Pw+*qSS4meLaS?)G$ZvzF6UC=?0`_Fx{ryoCIm zoSdWXTI^YB)VrIVy!U3J(IEdRLaUNX24IAvzIX{G0xuRsgTc4_$K?`o&X@`0j7J3x=;NxmFibN>RARM90*Kuw?YU| z*lf1ER0ArNN)DWP8A=G`x;}FNvER=H-T_d~2ILq(MBhy~oa=J^Ogqa(V!u28NdUqZm%?D_jW&uWdBoaxKP0Rvt$7-U;?e_41 zeVR^fA`(Hqb1b>ylu+dMc)5ULe}A6^AY;VobdDx&p+4A0)-Duzo(Iqvz6cN7`k6&7 zl;BDO4xR->V@`Y#U)3|8W;oz-5iG#V2S^Tph_?77XzyZxTo%wE2#{nR=ULT8e3Ja> zMYXem+*EPigM(}CjihjpWFg{4#c3>d41?ge4DF?k^MLo@dV%|Dr67Pr^)Wn&ii=s- zxMm0n_>v)jkoE@#e7>2HWIhydt;raGt-zxw6p9w)!(r6Sd%OV5^WLg-I2?iiVUeOP z7)5Xq!!6*3LktZx@ZYPnTCGArO?6EPfrv(!1aOb}-EaoILitDsfS#XT_n}%{V}TQ( z`EHK`Uhl$yP^C%$9Ln@(rk&PdER--usI&2#*zFl=7?29(BcumlC9U}hLlXdX)ep0Y zAdpO>dHe@b3zR^zO6ZVK0JIKQ3(ls;deA2XT@xM?hNd0UlS#Z1tmF#ruyoZ%F@vfj z^K^(jD8XSR3k&r&n#RCHrq!nj4)|WvwC$cMFMU3P8!c8i&?FXY21WMi5Xu%kBjjb* le7d32>9XzpHD2SN<1c@E505Lj_YeR8002ovPDHLkV1oOKDgpoi literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/logos/themoneytizer.com.png b/dev-space/ad-networks-mockups/assets/logos/themoneytizer.com.png new file mode 100644 index 0000000000000000000000000000000000000000..95bd1e6dc5839ec87477c42a2965fae3eae887ee GIT binary patch literal 924 zcmV;N17rM&P)4c!>S(^X!I{>2`(pzs%%4 zTip^c-x)jTX?^5IVBHch|NsBo03+Q3D%=1W-48F_3M}MHV(pNd_RG-XKvwatwdGc7 z-x@gKGfUtpMf179@0+Ff&(!_x@c7f&=i*b%0000NbW%=J0F}#&N{wO}Q|l!IX$5ZQuX>4q$8}pb_(@ z-%Mt9ob0zZV0Qs9BMCe=Z+cc$tZ#DH3zC^X8WH+Yi#Q0bI4}`%^Un`gs_)#MAW|tH zg!Jxije1letZLtYs^}3#SafJcd!Xow#Xx)t=d2IF7*c;$Jx+-hx(-6pct$^ZV1D!h zMn;C07OqhjfbGju`aBGsA-2&MJ}6Pd(PH&`l}%H z=S;?~bpQg;9SL^*ssp3~_v6j7t-y~9(V$;$vM*e~rOd#7qXWDUdzk^P1f&*#;>Wp> z6rib2go-RUssp&YaGbCFSG0BA7U#?5ns>D!QskIYh??UszQ0H5Qt3`0p2nn6zfTD6 z@P#!WNRxm-OW_>B@m%XJ1i;fAnVgFUW+e!<+hjq2GTxoE`)orXp5>hy!+4!1 z1jg%(nRt~$^(5f#%pufJCL|FfJPApCP)H;DtD~L-*%#zTBqV88G>2L}bI=)@^Ooi= z4bj__;%)40{6!r+&|AYg2b!N1z~43>+|4-oh7kqkcSBZadTW+}n#tJi+fF&SSB7?v zSTSJi0R`#0QT?Vr9W#ewrn8%lu8#~WLmWG7<#8a)F|QmBT~!N29iuvsJHx{;uyNZM z5xV_yCNS#V<^3~=*g_nLMZu!Fz%IhbbSU@+j34fuH!t2PL(}D#ug{FWKej%i4BU*8 y-{qUo%ni!b>Jmv3_}<~|_4V!G`9U(f{r?9vLNEWCf*=S00000@7C*l$Z9auInEv^p26~3LEY~ zNb4pq?^u`^?VoR9WmaJgz>6 zF#rGn{7FPXR9J=WlwDK8Fc3wX+}hoKM2)S*0&4O9|44!iLn01v;YlawZFlCL+3X}K zicBVx`PbA<`IE<%5j3zVcq*w0N^2t2$TJmB1s*S={mlZ8p(>UNY6C}s$7>bJ5!&4% z5G?9_-6=*|Ab6V&bqigR0>Nq>HG1{hb@lZEyniH(Pj6IFF@mHXvD*nFu!s*ZedjrW ztm4}76Br~*n4zpdTrtnb&eRJjFuDO;)Ehtwj1NN(QPY9A2ZzTbCJMFW1pB`L&DE)2 z1>qQrI|asmtuH6e3Bs>1-zNx*aF;XZ1Yr>yPH^(ATjmx8s)th?zsgJ|llgOO0f2A_ U&L+I>IRF3v07*qoM6N<$g3Im1fB*mh literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/mock.css b/dev-space/ad-networks-mockups/assets/mock.css index ca413f8d..4226132e 100644 --- a/dev-space/ad-networks-mockups/assets/mock.css +++ b/dev-space/ad-networks-mockups/assets/mock.css @@ -13,32 +13,29 @@ --color-accent-text: #3a7000; --color-accent-contrast: #141414; --color-link: var(--color-accent-text); - --color-success-text: #2f7a1d; - --color-warning-text: #a05a00; - --color-danger-text: #b3261e; - --color-data-chartreuse: #8fc51f; - --color-data-cyan: #1596a3; - --color-data-blue: #4f6fd1; - --color-data-yellow: #c78a14; - --color-data-red: #c8554f; - --color-data-violet: #865fc2; - --color-data-green: #3f8f52; --focus-ring: 0 0 0 2px var(--color-accent-text); --text-body: 1.0625rem; --text-small: 0.875rem; --text-h1: clamp(2rem, 5vw, 2.6rem); - --text-h2: 1.7rem; - --text-h3: 1.15rem; --line-body: 1.6; - --line-h1: 1.08; - --line-heading: 1.2; + --line-h1: 1.02; + --line-ui: 1.25; --tracking-display: -0.02em; + --weight-semibold: 600; + --weight-bold: 700; --measure-prose: 760px; --container-gutter: clamp(24px, 2.5vw, 36px); --page-padding-top: 30px; --page-padding-bottom: 44px; + --space-1: 4px; + --space-2: 6px; + --space-3: 8px; + --space-4: 10px; + --space-5: 12px; + --space-6: 14px; + --space-7: 16px; + --space-8: 18px; --radius: 2px; - --sidebar-width: clamp(260px, 20vw, 300px); --top-bar-height: 58px; --font-display: "Space Grotesk", ui-sans-serif, system-ui, sans-serif; --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; @@ -54,16 +51,6 @@ --color-border: rgba(242, 242, 238, 0.14); --color-border-strong: rgba(242, 242, 238, 0.3); --color-accent-text: #bafc3c; - --color-success-text: #7ddc64; - --color-warning-text: #ffb757; - --color-danger-text: #ff8a80; - --color-data-chartreuse: #bafc3c; - --color-data-cyan: #45cbd3; - --color-data-blue: #7691f2; - --color-data-yellow: #efb84f; - --color-data-red: #ed746c; - --color-data-violet: #aa82df; - --color-data-green: #65bd76; } @media (max-width: 900px) { :root { --container-gutter: 22px; --page-padding-top: 24px; --page-padding-bottom: 36px; } @@ -73,241 +60,590 @@ html, body { margin: 0; background: var(--color-bg); color: var(--color-text); } body { font: var(--text-body) / var(--line-body) var(--font-sans); -webkit-font-smoothing: antialiased; } a { color: var(--color-link); text-decoration: none; } -a:hover, a:focus-visible { text-decoration: underline; } button { font: inherit; color: inherit; } :focus-visible { outline: none; box-shadow: var(--focus-ring); } -.mono { font-family: var(--font-mono); } -.muted { color: var(--color-text-muted); } -.small { font-size: var(--text-small); } -.ok { color: var(--color-success-text); } -.warn { color: var(--color-warning-text); } -.bad { color: var(--color-danger-text); } -.accent-ink { color: var(--color-accent-text); } -.num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } -.right { text-align: right; } -.icon { width: 1em; height: 1em; flex: none; vertical-align: -0.125em; } +.icon { width: 1em; height: 1em; flex: none; } -/* Shell */ -.topbar { position: sticky; top: 0; z-index: 20; height: var(--top-bar-height); display: flex; align-items: center; justify-content: space-between; padding: 0 28px; background: var(--color-bg); border-bottom: 1px solid var(--color-border); } -.logo { font: 700 1.9rem/1 var(--font-display); letter-spacing: -0.04em; color: var(--color-text); } -.logo span { color: var(--color-accent-text); } -.logo:hover { text-decoration: none; } -.topnav { display: flex; gap: 14px; align-items: center; font: var(--text-small) var(--font-mono); } -.topnav a { color: var(--color-text-muted); } -.topnav a:hover { color: var(--color-text); text-decoration: none; } -.topnav .dot { color: var(--color-text-muted); } -.theme-btn { background: none; border: 0; cursor: pointer; padding: 6px; color: var(--color-text); display: inline-flex; } -.shell { display: grid; grid-template-columns: var(--sidebar-width) minmax(0, 1fr); min-height: calc(100vh - var(--top-bar-height)); } -.sidebar { border-right: 1px solid var(--color-border); padding: 28px; position: sticky; top: var(--top-bar-height); align-self: start; max-height: calc(100vh - var(--top-bar-height)); overflow: auto; } -.sidebar h2 { font: 700 var(--text-h3) / 1.2 var(--font-display); letter-spacing: var(--tracking-display); margin: 0 0 18px; padding-bottom: 18px; border-bottom: 1px solid var(--color-border); } -.sidebar h3 { font: 600 var(--text-small) var(--font-mono); color: var(--color-text-muted); margin: 22px 0 8px 10px; } -.side-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; } -.side-list a { display: grid; grid-template-columns: 22px 1fr auto; gap: 8px; align-items: center; padding: 7px 10px; border-radius: var(--radius); color: var(--color-text); font-size: 0.95rem; line-height: 1.3; } -.side-list a:hover { background: var(--color-surface-2); text-decoration: none; } -.side-list a.active { background: var(--color-surface-2); font-weight: 600; } -.side-list a.active .lead { color: var(--color-accent-text); } -.side-list .lead { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); display: inline-flex; } -.side-list .count { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); } -.main { min-width: 0; } -.canvas { padding: var(--page-padding-top) var(--container-gutter) var(--page-padding-bottom); } -.canvas.flush-bottom { padding-bottom: 0; } -.drawer-btn { display: none; } +/* Top bar (site-nav.module.css) */ +.topbar { position: sticky; top: 0; z-index: 100; background: var(--color-bg); border-bottom: 1px solid var(--color-border); } +.topbar nav { display: grid; grid-template-columns: auto 1fr auto; gap: var(--space-8); align-items: center; min-height: var(--top-bar-height); padding: 0 clamp(20px, 2vw, 28px); } +.brand { display: inline-flex; align-items: center; height: 28px; } +.brand img { height: 100%; width: auto; display: none; } +[data-theme="dark"] .brand .on-dark, :root:not([data-theme="dark"]) .brand .on-light { display: block; } +.topbar .links { display: flex; align-items: center; justify-content: flex-end; gap: clamp(10px, 1.6vw, 18px); } +.topbar .links a { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.topbar .links a:hover { color: var(--color-text); } +.topbar .dot { color: var(--color-text-muted); user-select: none; } +.theme-btn { background: none; border: 0; cursor: pointer; padding: 6px; display: inline-flex; color: var(--color-text); } +@media (max-width: 960px) { .topbar nav { padding: 0 22px; } } +@media (max-width: 680px) { .topbar .secondary { display: none; } } +@media (max-width: 420px) { .topbar nav { grid-template-columns: minmax(0, 1fr) auto; } .topbar .links { display: none; } } -/* Header */ -.crumbs { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); margin-bottom: 14px; } +/* Page with no sidebar: one full-width column (app-shell.module.css .pageNone) */ +.page { display: grid; grid-template-rows: 1fr auto; min-height: calc(100vh - var(--top-bar-height)); } +.canvas { min-width: 0; padding: var(--page-padding-top) var(--container-gutter) var(--page-padding-bottom); } + +/* Footer */ +.footer { border-top: 1px solid var(--color-border); color: var(--color-text-muted); padding: var(--space-4) var(--container-gutter); font: var(--text-small) var(--font-mono); } +.footer-in { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-7); min-height: 42px; } +.footer-in img { height: 18px; width: auto; display: none; } +[data-theme="dark"] .footer-in .on-dark, :root:not([data-theme="dark"]) .footer-in .on-light { display: block; } +.footer-links { display: flex; flex-wrap: wrap; gap: var(--space-6); } +.footer a { color: var(--color-text-muted); } + +/* PageHeader (page-header.module.css) */ +.page-header { display: grid; gap: var(--space-5); width: 100%; min-width: 0; } +.page-header h1 { font: var(--weight-bold) var(--text-h1) / var(--line-h1) var(--font-display); letter-spacing: var(--tracking-display); color: var(--color-text); margin: 0; max-width: 820px; overflow-wrap: anywhere; isolation: isolate; } +.page-header h1 mark { position: relative; z-index: -1; background: var(--color-accent); color: var(--color-accent-contrast); border-radius: var(--radius); padding-inline: 0.16em; box-decoration-break: clone; -webkit-box-decoration-break: clone; } +.page-header > p { margin: 0; max-width: var(--measure-prose); overflow-wrap: anywhere; } + +/* PageTitleIcon */ +.title-icon { color: var(--color-accent-text); display: inline-flex; margin-right: 0.24em; vertical-align: -0.06em; } +.title-icon svg { width: 0.82em; height: 0.82em; } + +/* Breadcrumbs */ +.crumbs { font: var(--text-small) / var(--line-ui) var(--font-mono); color: var(--color-text-muted); } +.crumbs ol { display: flex; flex-wrap: wrap; gap: var(--space-1) 0; list-style: none; margin: 0; padding: 0; } +.crumbs li { display: inline-flex; align-items: baseline; } +.crumbs li:not(:last-child)::after { content: "/"; opacity: 0.6; padding-inline: var(--space-3); } .crumbs a { color: var(--color-text-muted); } -.crumbs b { color: var(--color-text); font-weight: 600; } -.crumbs .sep { margin: 0 6px; opacity: 0.6; } -h1 { font: 700 var(--text-h1) / var(--line-h1) var(--font-display); letter-spacing: var(--tracking-display); margin: 0; max-width: 900px; } -h1 mark { background: var(--color-accent); color: var(--color-accent-contrast); padding: 0 0.12em; border-radius: var(--radius); } -h1 .title-icon { color: var(--color-accent-text); width: 0.85em; height: 0.85em; vertical-align: -0.06em; margin-right: 0.3em; } -h2 { font: 700 var(--text-h2) / var(--line-heading) var(--font-display); letter-spacing: var(--tracking-display); margin: 0 0 12px; } -h3 { font: 700 var(--text-h3) / var(--line-heading) var(--font-display); letter-spacing: var(--tracking-display); margin: 0 0 8px; } -.meta { display: flex; flex-wrap: wrap; gap: 6px 14px; font: var(--text-small) var(--font-mono); color: var(--color-text-muted); margin: 14px 0 0; } -.lede { max-width: var(--measure-prose); margin: 16px 0 0; } -.prose { max-width: var(--measure-prose); } -.prose p { margin: 0 0 16px; } -.hero { position: relative; border-bottom: 1px solid var(--color-border); background-image: radial-gradient(var(--color-border-strong) 1px, transparent 1px); background-size: 22px 22px; background-position: 11px 11px; } -.hero .canvas { padding-bottom: 36px; } -.doodle { position: absolute; right: 40px; top: 36px; width: 120px; color: var(--color-text-muted); } -@media (max-width: 1100px) { .doodle { display: none; } } +.crumbs a:hover { color: var(--color-text); text-decoration: underline; text-underline-offset: 3px; } +.crumbs [aria-current="page"] { color: var(--color-text); font-weight: var(--weight-semibold); } -/* Sections */ -.section { padding: 40px var(--container-gutter) 0; } -.section:last-child { padding-bottom: var(--page-padding-bottom); } -.section-head { display: flex; justify-content: space-between; align-items: end; gap: 16px; margin-bottom: 18px; flex-wrap: wrap; } -.section-head p { margin: 4px 0 0; max-width: var(--measure-prose); } -.rule { border: 0; border-top: 1px solid var(--color-border); margin: 0; } +/* PageMeta */ +.meta { font: var(--text-small) / 1.4 var(--font-mono); color: var(--color-text-muted); display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-7); } +.meta span { display: inline-flex; align-items: center; gap: var(--space-2); white-space: nowrap; } +.meta .icon { width: 15px; height: 15px; } -/* Workbench */ -.workbench { background: var(--color-surface); border-top: 1px solid var(--color-border-strong); border-bottom: 1px solid var(--color-border-strong); padding: 32px var(--container-gutter); margin-top: 28px; } -.hero .workbench { margin-top: 24px; } -.field-label { display: flex; gap: 6px; align-items: center; font-weight: 600; margin-bottom: 8px; } -.field-label .req { color: var(--color-danger-text); } -.hint { display: inline-grid; place-items: center; width: 16px; height: 16px; border: 1px solid var(--color-border-strong); border-radius: 50%; font: 600 11px var(--font-mono); color: var(--color-text-muted); cursor: help; } -.input-row { display: grid; grid-template-columns: 1fr auto; gap: 10px; } -.input { height: 48px; padding: 0 14px; background: var(--color-bg); border: 1px solid var(--color-border-strong); border-radius: var(--radius); color: var(--color-text); font: var(--text-body) var(--font-mono); width: 100%; } -.input::placeholder { color: var(--color-text-muted); } -.btn { height: 48px; padding: 0 22px; border-radius: var(--radius); border: 1px solid var(--color-border-strong); background: var(--color-surface); font-weight: 600; cursor: pointer; white-space: nowrap; display: inline-flex; align-items: center; gap: 8px; } -.btn.primary { background: var(--color-accent); color: var(--color-accent-contrast); border-color: var(--color-accent); min-width: 200px; justify-content: center; } -.btn.ghost { border-color: transparent; background: none; } -.btn.ghost:hover { background: var(--color-surface-2); } -.btn.sm { height: 34px; padding: 0 12px; font-size: var(--text-small); } -.try { margin-top: 12px; font-size: var(--text-small); color: var(--color-text-muted); display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } -.try a { font-family: var(--font-mono); } -.result { border-top: 1px solid var(--color-border); margin-top: 28px; padding-top: 28px; } -.result-answer { font-size: var(--text-body); margin: 0 0 18px; max-width: var(--measure-prose); } -.result-answer strong { font-family: var(--font-display); font-size: 1.5rem; letter-spacing: var(--tracking-display); display: block; line-height: 1.25; margin-bottom: 6px; } -.result-foot { margin-top: 16px; font: var(--text-small) var(--font-mono); color: var(--color-text-muted); display: flex; gap: 6px 16px; flex-wrap: wrap; align-items: center; } -.tag-example { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); border: 1px dashed var(--color-border-strong); padding: 1px 8px; border-radius: var(--radius); } +/* EntryHero: full-width dot band (dot-band.module.css) */ +.band { position: relative; border-bottom: 1px solid var(--color-border); background-size: 26px 26px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ccircle cx='1.5' cy='1.5' r='1.2' fill='%2314141430'/%3E%3C/svg%3E"); } +[data-theme="dark"] .band { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Ccircle cx='1.5' cy='1.5' r='1.2' fill='%23f2f2ee2e'/%3E%3C/svg%3E"); } +.band :where(h1, p) { background: var(--color-bg); width: fit-content; } -/* Tables */ -.table-frame { overflow-x: auto; border: 1px solid var(--color-border); border-radius: var(--radius); } -table.data { width: 100%; border-collapse: collapse; font-size: var(--text-body); } -table.data th { background: var(--color-surface-2); text-align: left; font: 700 var(--text-body) var(--font-display); padding: 12px 14px; white-space: nowrap; } -table.data td { padding: 11px 14px; border-top: 1px solid var(--color-border); vertical-align: top; } -table.data td.mono, table.data td .mono { font-family: var(--font-mono); } -table.data tbody tr:hover td { background: var(--color-surface); } -table.data th.right, table.data td.right { text-align: right; } -table.data .sub { display: block; font: var(--text-small) var(--font-mono); color: var(--color-text-muted); margin-top: 2px; } -table.data .sortable::after { content: " \2195"; color: var(--color-text-muted); font-weight: 400; } -table.data .sorted::after { content: " \2193"; color: var(--color-accent-text); } +/* Hero search: one box for networks, sites, and niches */ +.hero-search { display: grid; gap: var(--space-4); max-width: var(--measure-prose); margin-top: var(--space-5); } +.hero-search form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--space-3); } +.search-box { display: flex; align-items: center; height: 52px; min-width: 0; background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius); } +.search-box:focus-within { border-color: var(--color-accent-text); box-shadow: var(--focus-ring); } +.search-box .icon { width: 20px; height: 20px; margin-left: 14px; color: var(--color-text-muted); } +.search-box input { flex: 1; min-width: 0; height: 100%; padding: 0 14px; background: transparent; border: 0; outline: 0; color: var(--color-text); font: var(--text-body) var(--font-mono); } +.search-box input::placeholder { color: var(--color-text-muted); } +.search-box input:focus-visible { box-shadow: none; } +.search-box kbd { margin-right: 12px; padding: 1px 7px; border: 1px solid var(--color-border-strong); border-radius: var(--radius); color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-3); height: 52px; padding: 0 22px; border-radius: var(--radius); font-weight: var(--weight-semibold); white-space: nowrap; cursor: pointer; } +.btn.primary { background: var(--color-accent); border: 1px solid var(--color-accent); color: var(--color-accent-contrast); } +.btn.primary:hover { filter: brightness(0.96); } +.search-try { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-5); font: var(--text-small) / var(--line-ui) var(--font-mono); color: var(--color-text-muted); background: var(--color-bg); width: fit-content; } +.search-try a { color: var(--color-text); text-decoration: underline; text-decoration-color: var(--color-border-strong); text-underline-offset: 3px; } +.search-try a:hover { text-decoration-color: currentColor; } +.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +@media (max-width: 560px) { + .hero-search form { grid-template-columns: 1fr; } + .search-box kbd { display: none; } +} -/* Labels and chips */ -.label { font: var(--text-small) var(--font-mono); white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; } -.label::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; } -.label.confirmed { color: var(--color-success-text); } -.label.claimed { color: var(--color-warning-text); } -.label.ended { color: var(--color-text-muted); } -.label.ended::before { background: none; border: 1px solid currentColor; } -.chip { display: inline-flex; align-items: center; gap: 6px; font: var(--text-small) var(--font-mono); border: 1px solid var(--color-border); padding: 2px 8px; border-radius: var(--radius); color: var(--color-text); white-space: nowrap; } -a.chip:hover { border-color: var(--color-border-strong); text-decoration: none; } -.chips { display: flex; flex-wrap: wrap; gap: 6px; } -.evidence { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 6px 10px; overflow-x: auto; white-space: nowrap; } -.evidence b { color: var(--color-text); font-weight: 600; } -.type { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); } +/* Section below the hero */ +.sec-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: var(--space-3) var(--space-7); margin-bottom: var(--space-8); } +.sec-head h2 { font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); letter-spacing: var(--tracking-display); margin: 0; } +.sec-head span { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); } +.more { display: flex; justify-content: center; margin-top: var(--space-8); } +.btn.secondary { height: 42px; background: var(--color-surface); border: 1px solid var(--color-border-strong); color: var(--color-text); } +.btn.secondary:hover { background: var(--color-surface-2); } -/* Cards */ -.grid { display: grid; gap: 14px; } -.grid.c3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } -.grid.c2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.grid.c4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } -@media (max-width: 1200px) { .grid.c4 { grid-template-columns: repeat(3, minmax(0, 1fr)); } } -@media (max-width: 1100px) { .grid.c3, .grid.c4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } } -@media (max-width: 680px) { .grid.c2, .grid.c3, .grid.c4 { grid-template-columns: 1fr; } } -.card { display: block; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); padding: 18px; color: var(--color-text); transition: border-color 0.15s, background 0.15s; } -a.card:hover { text-decoration: none; border-color: var(--color-border-strong); } -a.card.feature:hover { border-color: var(--color-accent); } -a.card.tool:hover { background: var(--color-surface-2); } -.card-title { font: 700 1.15rem/1.25 var(--font-display); letter-spacing: var(--tracking-display); margin: 0; } -a.card .card-title { color: var(--color-link); } -.card-desc { font-size: var(--text-small); margin: 6px 0 0; } -.card-foot { margin-top: 14px; padding-top: 12px; border-top: 1px dashed var(--color-border); font: var(--text-small) var(--font-mono); color: var(--color-text-muted); display: flex; justify-content: space-between; gap: 8px; } -.card-big { font: 700 2rem/1 var(--font-display); letter-spacing: -0.03em; margin: 14px 0 4px; } -.facts { list-style: none; margin: 14px 0 0; padding: 0; } -.facts li { display: grid; grid-template-columns: 18px 1fr auto; gap: 8px; align-items: center; padding: 7px 0; border-top: 1px solid var(--color-border); font-size: var(--text-small); } -.facts li .v { font-family: var(--font-mono); text-align: right; } -.facts li svg { color: var(--color-text-muted); } -.monogram { width: 40px; height: 40px; border: 1px solid var(--color-border-strong); border-radius: var(--radius); display: grid; place-items: center; font: 700 1rem var(--font-display); flex: none; background: var(--color-bg); } -.card-head { display: flex; gap: 12px; align-items: center; } +/* Network cards */ +.net-grid { display: grid; gap: var(--space-5); grid-template-columns: repeat(4, minmax(0, 1fr)); } +@media (max-width: 1300px) { .net-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@media (max-width: 1000px) { .net-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +@media (max-width: 560px) { .net-grid { grid-template-columns: 1fr; } } +.net-card { position: relative; display: grid; gap: var(--space-7); align-content: start; min-width: 0; padding: var(--space-8); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); } +.net-card:hover, .net-card:focus-within { border-color: var(--color-accent-text); } +.net-top { display: flex; align-items: center; gap: var(--space-4); min-width: 0; } +.net-top h3 { font: var(--weight-bold) 1.06rem / 1.2 var(--font-display); letter-spacing: -0.01em; margin: 0; min-width: 0; overflow-wrap: anywhere; } +.net-top h3 a { color: var(--color-text); } +.net-top h3 a::after { content: ""; position: absolute; inset: 0; } +.net-top h3 a:focus-visible { box-shadow: none; } +.logo-tile { width: 32px; height: 32px; flex: none; display: grid; place-items: center; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius); color: #141414; font: var(--weight-bold) 15px var(--font-display); } +.logo-tile img { width: 20px; height: 20px; } +.net-num { font: var(--weight-bold) 2.1rem / 1 var(--font-display); letter-spacing: var(--tracking-display); } +.net-unit { margin-top: var(--space-1); font-size: var(--text-small); color: var(--color-text-muted); } +.net-unit b { font: var(--weight-semibold) var(--text-small) var(--font-mono); color: var(--color-accent-text); } +.net-join { display: grid; gap: 2px; padding-top: var(--space-5); border-top: 1px solid var(--color-border); } +.net-join .lab { font: 0.78rem / 1.3 var(--font-mono); color: var(--color-text-muted); } +.net-join .val { font-weight: var(--weight-semibold); font-size: 0.94rem; line-height: 1.35; } -/* Metrics */ -.metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); border: 1px solid var(--color-border); border-radius: var(--radius); } -.metric { padding: 16px 18px; border-right: 1px solid var(--color-border); } -.metric:last-child { border-right: 0; } -.metric .k { font-size: var(--text-small); display: flex; gap: 6px; align-items: center; } -.metric .v { font: 700 1.7rem/1.15 var(--font-display); letter-spacing: -0.02em; margin-top: 6px; } -.metric .s { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); margin-top: 2px; } -.metric-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--color-border); } -.metric-list li { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--color-border); } -.metric-list li .v { font-family: var(--font-mono); text-align: right; } -.metric-list li .v small { display: block; color: var(--color-text-muted); font-size: var(--text-small); } +/* Confirmed ring with its info bubble */ +.ring { position: relative; z-index: 1; display: grid; flex: none; margin-left: auto; padding: 2px; background: none; border: 0; border-radius: 50%; cursor: help; } +.ring .track { stroke: var(--color-surface-2); } +.ring .done { stroke: var(--color-accent-text); } +[data-theme="dark"] .ring .done { stroke: var(--color-accent); } +.bubble { position: absolute; right: -6px; bottom: calc(100% + 8px); width: 230px; padding: 8px 10px; background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.36); color: var(--color-text); font: var(--text-small) / 1.45 var(--font-sans); text-align: left; pointer-events: none; opacity: 0; transform: translateY(4px); transition: opacity 0.12s, transform 0.12s; } +.ring:hover .bubble, .ring:focus-visible .bubble, .ring.open .bubble { opacity: 1; transform: none; } +.bubble b { font-family: var(--font-mono); font-weight: var(--weight-semibold); color: var(--color-accent-text); } -/* Bars */ -.bars { display: grid; gap: 8px; } -.bar-row { display: grid; grid-template-columns: minmax(120px, 190px) 1fr 70px; gap: 12px; align-items: center; font-size: var(--text-small); } -.bar-row .name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.bar-row .track { height: 14px; background: var(--color-surface-2); border-radius: var(--radius); overflow: hidden; } -.bar-row .fill { height: 100%; background: var(--color-data-chartreuse); } -.bar-row .val { font-family: var(--font-mono); text-align: right; } -.stack { display: flex; height: 16px; border-radius: var(--radius); overflow: hidden; gap: 2px; } -.stack span { display: block; height: 100%; } -.legend { display: flex; flex-wrap: wrap; gap: 6px 16px; font: var(--text-small) var(--font-mono); margin-top: 10px; } -.legend i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: -1px; } -.d1 { background: var(--color-data-chartreuse); } .d2 { background: var(--color-data-cyan); } .d3 { background: var(--color-data-blue); } .d4 { background: var(--color-data-yellow); } .d5 { background: var(--color-data-violet); } .d6 { background: var(--color-data-green); } .d7 { background: var(--color-data-red); } .d0 { background: var(--color-border-strong); } -.chart-box { border: 1px solid var(--color-border); border-radius: var(--radius); padding: 18px; background: var(--color-surface); } -.chart-box h3 { margin-bottom: 4px; } -.chart-box .cap { font-size: var(--text-small); color: var(--color-text-muted); margin: 10px 0 0; font-family: var(--font-mono); } +/* Numbers inside the intro */ +.page-header .fig { font: var(--weight-semibold) 0.95em var(--font-mono); } +.page-header .fig.live { color: var(--color-accent-text); } -/* Controls */ -.filters { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 14px; } -.select { height: 38px; display: inline-flex; align-items: center; gap: 8px; padding: 0 10px; border: 1px solid var(--color-border-strong); border-radius: var(--radius); background: var(--color-surface); font-size: var(--text-small); } -.select b { font-weight: 600; font-family: var(--font-mono); } -.filter-input { height: 38px; flex: 1 1 220px; padding: 0 12px; border: 1px solid var(--color-border-strong); border-radius: var(--radius); background: var(--color-bg); color: var(--color-text); font: var(--text-small) var(--font-mono); } -.seg { display: inline-flex; border: 1px solid var(--color-border-strong); border-radius: var(--radius); overflow: hidden; } -.seg button { background: none; border: 0; padding: 7px 12px; font-size: var(--text-small); cursor: pointer; border-right: 1px solid var(--color-border); } -.seg button:last-child { border-right: 0; } -.seg button.on { color: var(--color-accent-text); box-shadow: inset 0 -2px 0 var(--color-accent); font-weight: 600; } -.ftag { font: var(--text-small) var(--font-mono); border: 1px solid var(--color-accent-text); color: var(--color-accent-text); padding: 3px 8px; border-radius: var(--radius); } -.count-line { font-size: var(--text-small); color: var(--color-text-muted); margin: 0 0 12px; } -.pagination { display: flex; gap: 6px; align-items: center; margin-top: 16px; font: var(--text-small) var(--font-mono); } -.pagination a, .pagination span.cur { min-width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid var(--color-border); border-radius: var(--radius); color: var(--color-text); padding: 0 8px; } -.pagination span.cur { background: var(--color-accent); color: var(--color-accent-contrast); border-color: var(--color-accent); font-weight: 600; } -.pagination .gap { color: var(--color-text-muted); padding: 0 4px; } -.tabs { display: flex; gap: 22px; border-bottom: 1px solid var(--color-border); margin-bottom: 18px; overflow-x: auto; } -.tabs button { background: none; border: 0; padding: 10px 0; cursor: pointer; font-weight: 600; color: var(--color-text-muted); white-space: nowrap; } -.tabs button.on { color: var(--color-text); box-shadow: inset 0 -2px 0 var(--color-accent); } +/* Sort controls beside the section heading */ +.sec-head { align-items: center; } +.sort-controls { display: flex; flex-wrap: wrap; gap: var(--space-4); } +.sel { position: relative; display: inline-flex; align-items: center; gap: var(--space-4); height: 42px; padding-left: 12px; background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius); cursor: pointer; } +.sel[hidden] { display: none; } +.sel:focus-within { border-color: var(--color-accent-text); box-shadow: var(--focus-ring); } +.sel .k { font: 0.78rem var(--font-mono); color: var(--color-text-muted); } +.sel select { appearance: none; -webkit-appearance: none; height: 100%; padding: 0 38px 0 0; background: transparent; border: 0; outline: 0; color: var(--color-text); font: 0.94rem var(--font-sans); cursor: pointer; } +.sel select:focus-visible { box-shadow: none; } +.sel::after { content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; border-right: 1.6px solid var(--color-text-muted); border-bottom: 1.6px solid var(--color-text-muted); transform: translateY(-70%) rotate(45deg); pointer-events: none; } +.sort-note { margin: calc(var(--space-8) * -1 + var(--space-3)) 0 var(--space-8); font: var(--text-small) var(--font-mono); color: var(--color-text-muted); } +.sort-note b { color: var(--color-text); font-weight: var(--weight-semibold); } -/* Callout and state */ -.callout { border: 1px solid var(--color-border); background: var(--color-surface); border-radius: var(--radius); padding: 14px 16px; display: grid; grid-template-columns: 20px 1fr; gap: 10px; max-width: var(--measure-prose); } -.callout .t { font: 700 1rem var(--font-display); margin: 0 0 2px; } -.callout p { margin: 0; } -.callout.warning .t, .callout.warning svg { color: var(--color-warning-text); } -.callout.tip svg { color: var(--color-accent-text); } -.state { border: 1px dashed var(--color-border-strong); border-radius: var(--radius); padding: 28px; text-align: left; } -.state h3 { margin-bottom: 6px; } -.state p { margin: 0 0 14px; max-width: 620px; } -.notice-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--color-border); } -.notice-list li { display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 12px 0; border-bottom: 1px solid var(--color-border); } -.notice-list li p { margin: 2px 0 0; font-size: var(--text-small); } -.notice-list li b { font-weight: 600; } -.notice-list svg { margin-top: 3px; } -details.disc { border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); margin-top: 18px; } -details.disc summary { cursor: pointer; padding: 12px 0; font-weight: 600; display: flex; justify-content: space-between; list-style: none; } -details.disc summary::-webkit-details-marker { display: none; } -details.disc summary span { font: var(--text-small) var(--font-mono); color: var(--color-text-muted); font-weight: 400; } -pre.code { background: #141414; color: #e8e8e2; font: 0.85rem/1.6 var(--font-mono); padding: 14px 16px; border-radius: var(--radius); overflow-x: auto; margin: 0 0 14px; border: 1px solid var(--color-border); } -pre.code .hl { background: rgba(186, 252, 60, 0.16); display: inline-block; width: 100%; } -pre.code .cm { color: #8a8a84; } -.faq { max-width: var(--measure-prose); border-top: 1px solid var(--color-border); } -.faq div { border-bottom: 1px solid var(--color-border); padding: 14px 0; } -.faq h3 { margin: 0 0 4px; } -.faq p { margin: 0; } +/* Reading content below the cards (prose.module.css). Starts at the cards' left edge. */ +.prose { display: grid; gap: var(--space-13, 40px); max-width: var(--measure-prose); } +.prose section { display: grid; gap: var(--space-7); } +.prose h2 { font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); letter-spacing: var(--tracking-display); margin: 0; } +.prose p { margin: 0; } -/* Two column layouts */ -.split { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: 28px; align-items: start; } -.split.even { grid-template-columns: repeat(2, minmax(0, 1fr)); } -@media (max-width: 1100px) { .split, .split.even { grid-template-columns: 1fr; } } +/* Section bar (app/ui/site/section-bar.module.css). Scrolls away; only the top bar is sticky. */ +.section-bar { display: flex; align-items: center; gap: 32px; min-width: 0; padding-inline: clamp(20px, 2vw, 28px); background: var(--color-bg); border-bottom: 1px solid var(--color-border); } +.section-bar .identity { flex: 0 0 auto; color: var(--color-text-muted); font: var(--text-small) var(--font-mono); text-transform: lowercase; white-space: nowrap; } +.section-bar .scroll { min-width: 0; overflow-x: auto; overscroll-behavior-x: contain; } +.section-bar ul { display: flex; gap: 24px; width: max-content; margin: 0; padding: 0; list-style: none; } +.section-bar .link { display: block; padding-block: var(--space-5); border-bottom: 2px solid transparent; color: var(--color-text-muted); font-size: var(--text-body); line-height: var(--line-ui); white-space: nowrap; } +.section-bar a:hover { color: var(--color-text); } +.section-bar .link[aria-current="page"] { color: var(--color-accent-text); border-bottom-color: var(--color-accent-text); } +@media (max-width: 960px) { .section-bar { gap: 20px; padding-inline: 22px; } } +@media (max-width: 680px) { .section-bar .identity { display: none; } } -/* Mockup chrome: clearly not part of the design */ -.mk-bar { margin: 0 var(--container-gutter); padding: 8px 10px; border: 1px dashed #c34cff; border-radius: 4px; display: flex; flex-wrap: wrap; gap: 6px 10px; align-items: center; font: 12px/1.4 var(--font-mono); color: #c34cff; background: rgba(195, 76, 255, 0.05); } -.mk-bar.inset { margin: 0 0 16px; } -.mk-bar .mk-name { font-weight: 600; margin-right: 4px; } -.mk-bar button { font: 12px var(--font-mono); background: none; border: 1px solid rgba(195, 76, 255, 0.45); color: #c34cff; border-radius: 3px; padding: 3px 8px; cursor: pointer; } -.mk-bar button.on { background: #c34cff; color: #fff; border-color: #c34cff; } -.mk-bar .mk-note { color: #a78bb5; margin-left: auto; } -.mk-banner { font: 12px/1.5 var(--font-mono); color: #c34cff; border-bottom: 1px dashed #c34cff; padding: 8px 28px; background: rgba(195, 76, 255, 0.06); } -.mk-banner a { color: #c34cff; text-decoration: underline; } -[data-variant]:not(.on) { display: none !important; } -.sample { border-bottom: 1px dotted #c34cff; } +/* Network logo in the h1, in place of the title icon. White tile so any logo reads on both themes. */ +.title-logo { display: inline-grid; place-items: center; width: 0.9em; height: 0.9em; margin-right: 0.28em; vertical-align: -0.1em; background: #fff; border: 1px solid var(--color-border); border-radius: var(--radius); } +.title-logo img { width: 66%; height: 66%; } +.meta a { color: inherit; display: inline-flex; align-items: center; gap: var(--space-2); text-decoration: underline; text-decoration-color: var(--color-border); text-underline-offset: 3px; } +.meta a:hover { color: var(--color-text); text-decoration-color: currentColor; } -@media (max-width: 900px) { - .shell { grid-template-columns: 1fr; } - .sidebar { display: none; } - .drawer-btn { display: inline-flex; margin: 0 0 14px; } - .topnav a:not(.keep), .topnav .dot { display: none; } - .input-row { grid-template-columns: 1fr; } - .metric { border-right: 0; border-bottom: 1px solid var(--color-border); } - .bar-row { grid-template-columns: 110px 1fr 56px; } - .mk-bar .mk-note { margin-left: 0; } +/* Sites table on a network page */ +.table-tools { display: flex; flex-wrap: wrap; gap: var(--space-3); } +.mini-search { display: flex; align-items: center; gap: var(--space-3); height: 42px; width: 240px; padding: 0 12px; background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius); } +.mini-search:focus-within { border-color: var(--color-accent-text); box-shadow: var(--focus-ring); } +.mini-search .icon { width: 16px; height: 16px; color: var(--color-text-muted); flex: none; } +.mini-search input { flex: 1; min-width: 0; height: 100%; background: transparent; border: 0; outline: 0; color: var(--color-text); font: 0.94rem var(--font-mono); } +.mini-search input::placeholder { color: var(--color-text-muted); } +.mini-search input:focus-visible { box-shadow: none; } +.page > * { min-width: 0; } +.table-scroll { overflow-x: auto; } +.sites { width: 100%; border-collapse: collapse; font-size: 0.97rem; } +.sites th { padding: 0 var(--space-5) var(--space-4); border-bottom: 1px solid var(--color-border-strong); color: var(--color-text-muted); font: 500 0.8rem var(--font-mono); text-align: left; text-transform: lowercase; white-space: nowrap; } +.sites td { padding: var(--space-6) var(--space-5); border-top: 1px solid var(--color-border); vertical-align: middle; white-space: nowrap; } +.sites tbody tr:first-child td { border-top: 0; } +.sites tbody tr:hover td { background: var(--color-surface); } +.sites .r { text-align: right; } +.sites .mono { font: 0.86rem var(--font-mono); } +.sites .mu { color: var(--color-text-muted); } +.sites .dom { color: var(--color-text); font-weight: var(--weight-semibold); text-decoration: none; } +.sites .dom:hover { text-decoration: underline; text-underline-offset: 3px; } +.sites .pub { display: block; margin-top: 2px; color: var(--color-text-muted); font-size: var(--text-small); } +.meter { display: inline-flex; gap: 2px; margin-right: var(--space-3); vertical-align: -1px; } +.meter i { width: 5px; height: 12px; background: var(--color-surface-2); border-radius: 1px; } +.meter i.f { background: var(--color-accent-text); } +[data-theme="dark"] .meter i.f { background: var(--color-accent); } +.table-foot { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: var(--space-5); margin-top: var(--space-8); color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.pager { display: flex; gap: var(--space-2); } +.pager a, .pager span { display: grid; place-items: center; min-width: 36px; height: 36px; padding: 0 10px; border: 1px solid var(--color-border-strong); border-radius: var(--radius); color: var(--color-text); text-decoration: none; } +.pager [aria-current] { background: var(--color-accent); border-color: var(--color-accent); color: var(--color-accent-contrast); font-weight: var(--weight-semibold); } +.pager span { border-color: transparent; color: var(--color-text-muted); } +/* Phones: each row becomes a two-line block, no sideways scroll */ +@media (max-width: 700px) { + .mini-search { width: 100%; } + .table-tools { width: 100%; } + .sites thead { display: none; } + .sites, .sites tbody, .sites tr { display: block; } + .sites tr { padding: var(--space-6) 0; border-top: 1px solid var(--color-border); } + .sites tbody tr:first-child { border-top: 0; } + .sites td { display: inline; padding: 0; border: 0; font: var(--text-small) var(--font-mono); color: var(--color-text-muted); } + .sites td.site { display: block; margin-bottom: var(--space-3); font: inherit; } + .table-tools { display: grid; grid-template-columns: 1fr 1fr; } + .table-tools .mini-search { grid-column: 1 / -1; } + .table-tools .sel { min-width: 0; } + .table-tools .sel select { width: 100%; } + .sites td:not(.site):not(.mgr):not(.checked)::after { content: " · "; } + .sites td.checked { display: none; } + .sites td.mgr::before { content: "managed by "; } + .sites td.mgr[data-none]::before { content: "manager "; } + .sites tbody tr:hover td { background: none; } +} + +/* Network details: key numbers, then tabs. + Every panel is in the HTML. Without the script all panels show, stacked under their own h3. + With it, inactive panels get hidden="until-found", so find-in-page still opens them. */ +.details { display: grid; gap: var(--space-8); } +.details-main { display: grid; gap: var(--space-8); align-content: start; min-width: 0; max-width: 980px; } +.details-main > h2 { margin: 0; font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); letter-spacing: var(--tracking-display); } +.details > h2 { margin: 0; font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); letter-spacing: var(--tracking-display); } +.key-nums { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border: 1px solid var(--color-border); border-radius: var(--radius); } +.key-nums > div { display: flex; flex-direction: column-reverse; justify-content: flex-end; gap: var(--space-2); padding: var(--space-7) var(--space-8); border-left: 1px solid var(--color-border); } +.key-nums > div:first-child { border-left: 0; } +.key-nums dt { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.key-nums dd { margin: 0; font: var(--weight-semibold) 1.6rem / 1.1 var(--font-mono); letter-spacing: -0.02em; } +.tablist { display: none; gap: 24px; border-bottom: 1px solid var(--color-border); } +.is-tabbed .tablist { display: flex; overflow-x: auto; } +.tablist button { padding: var(--space-5) 0; margin-bottom: -1px; background: none; border: 0; border-bottom: 2px solid transparent; color: var(--color-text-muted); font: var(--text-body) / var(--line-ui) var(--font-sans); white-space: nowrap; cursor: pointer; } +.tablist button:hover { color: var(--color-text); } +.tablist button[aria-selected="true"] { color: var(--color-text); border-bottom-color: var(--color-accent-text); } +.tablist button:focus-visible { outline: 0; box-shadow: var(--focus-ring); } +.tab-panel { display: grid; gap: var(--space-3); } +.tab-panel + .tab-panel { margin-top: var(--space-8); } +.is-tabbed .tab-panel + .tab-panel { margin-top: 0; } +.tab-panel h3 { margin: 0; color: var(--color-text-muted); font: var(--weight-semibold) var(--text-small) var(--font-mono); text-transform: lowercase; } +.is-tabbed .tab-panel h3 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } +.facts { margin: 0; } +.facts > div { display: grid; grid-template-columns: minmax(150px, 30%) 1fr; gap: var(--space-2) var(--space-8); padding: var(--space-5) 0; border-top: 1px solid var(--color-border); } +.facts > div:first-child { border-top: 0; } +.facts dt { color: var(--color-text-muted); } +.facts dd { margin: 0; } +.facts dd small { display: block; margin-top: 2px; color: var(--color-text-muted); font-size: var(--text-small); } +.facts .np { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.details .src { margin: 0; color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.details .src a { color: inherit; text-decoration: underline; text-decoration-color: var(--color-border-strong); text-underline-offset: 3px; } +@media (max-width: 700px) { + .key-nums > div { padding: var(--space-6) var(--space-5); } + .key-nums dd { font-size: 1.25rem; } + .facts > div { grid-template-columns: 1fr; } +} + +:root { --viz-ac: var(--color-accent-text); --viz-ac2: rgba(58, 112, 0, 0.55); --viz-ac3: rgba(58, 112, 0, 0.26); --viz-n1: rgba(20, 20, 20, 0.42); --viz-n2: rgba(20, 20, 20, 0.24); --viz-n3: rgba(20, 20, 20, 0.12); --viz-on-ac: #fff; } +[data-theme="dark"] { --viz-ac: #bafc3c; --viz-ac2: rgba(186, 252, 60, 0.62); --viz-ac3: rgba(186, 252, 60, 0.3); --viz-n1: rgba(242, 242, 238, 0.42); --viz-n2: rgba(242, 242, 238, 0.24); --viz-n3: rgba(242, 242, 238, 0.12); --viz-on-ac: #141414; } +.viz { min-width: 0; } +.viz svg { display: block; overflow: visible; } +.viz svg text { font: 12px var(--font-mono); } +.f-ac { fill: var(--viz-ac); } .f-ac2 { fill: var(--viz-ac2); } .f-ac3 { fill: var(--viz-ac3); } +.f-n1 { fill: var(--viz-n1); } .f-n2 { fill: var(--viz-n2); } .f-n3 { fill: var(--viz-n3); } .f-track { fill: var(--color-surface-2); } +.s-ac { stroke: var(--viz-ac); fill: none; } .s-bd { stroke: var(--color-border); } .s-bds { stroke: var(--color-border-strong); } +.t-mu { fill: var(--color-text-muted); } .t-tx { fill: var(--color-text); } .t-ac { fill: var(--color-accent-text); } +/* legend */ +.legend { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-7); margin: 0; padding: 0; list-style: none; color: var(--color-text-muted); font: 0.8rem var(--font-mono); } +.legend li { display: inline-flex; align-items: center; gap: var(--space-2); } +.legend i { width: 9px; height: 9px; border-radius: 1px; flex: none; } +.legend b { color: var(--color-text); font-weight: var(--weight-semibold); } +.legend.col { flex-direction: column; } +.bg-ac { background: var(--viz-ac); } .bg-ac2 { background: var(--viz-ac2); } .bg-ac3 { background: var(--viz-ac3); } +.bg-n1 { background: var(--viz-n1); } .bg-n2 { background: var(--viz-n2); } .bg-n3 { background: var(--viz-n3); } .bg-track { background: var(--color-surface-2); } +/* ranked bars */ +.bars { display: grid; grid-template-columns: auto minmax(40px, 1fr) auto; gap: var(--space-4) var(--space-5); align-items: center; margin: 0; font-size: 0.92rem; } +.bars dt { color: var(--color-text-muted); white-space: nowrap; } +.bars dt.hi { color: var(--color-text); font-weight: var(--weight-semibold); } +.bars .track { height: 8px; background: var(--color-surface-2); border-radius: 1px; overflow: hidden; } +.bars .track i { display: block; height: 100%; background: var(--viz-n1); } +.bars .track i.hi { background: var(--viz-ac); } +.bars dd { margin: 0; color: var(--color-text-muted); font: 0.82rem var(--font-mono); text-align: right; white-space: nowrap; } +/* waffle */ +/* stacked bar */ +.stackbar { display: flex; gap: 2px; height: 14px; border-radius: 1px; overflow: hidden; } +/* donut */ +.donut-wrap { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-7) var(--space-8); } +/* publisher blocks */ +.blocks { display: grid; grid-template-columns: 14fr 11fr 22fr; grid-template-rows: 9fr 7fr 6fr; gap: 2px; height: 210px; } +.blocks > div { display: flex; flex-direction: column; justify-content: space-between; min-width: 0; padding: 8px 9px; border-radius: 1px; overflow: hidden; font-size: 0.8rem; line-height: 1.25; } +.blocks b { font: var(--weight-semibold) 0.95rem var(--font-mono); } +.blocks .tall { grid-row: 1 / 4; } +.blocks .on-ac { color: var(--viz-on-ac); } +/* type scale */ +.typescale { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-3) var(--space-7); } +.typescale span { font-family: var(--font-mono); font-weight: var(--weight-semibold); letter-spacing: -0.03em; line-height: 1; } +.typescale small { margin-left: 4px; color: var(--color-text-muted); font: 0.75rem var(--font-mono); letter-spacing: 0; } +.typescale .lead { color: var(--color-accent-text); } +/* period switch */ +.seg { display: inline-flex; width: max-content; border: 1px solid var(--color-border-strong); border-radius: var(--radius); } +.seg button { padding: 4px 10px; background: none; border: 0; color: var(--color-text-muted); font: 0.8rem var(--font-mono); cursor: pointer; } +.seg button[aria-pressed="true"] { background: var(--color-surface-2); color: var(--color-text); } +.seg button:focus-visible { outline: 0; box-shadow: var(--focus-ring); } + +/* Guide text beside the facts: stays true when the data changes, so it holds no figures. */ +.details-body { display: grid; gap: var(--space-13, 40px); } +.details-prose { display: grid; gap: var(--space-8); align-content: start; max-width: var(--measure-prose); } +.details-prose section { display: grid; gap: var(--space-3); } +.details-prose h3 { margin: 0; font: var(--weight-semibold) 1.12rem / 1.3 var(--font-display); letter-spacing: -0.01em; } +.details-prose p { margin: 0; color: var(--color-text); line-height: var(--line-body); } +.details-prose a { color: var(--color-link); text-underline-offset: 3px; } +@media (min-width: 1200px) { + .details-body { grid-template-columns: minmax(300px, 1fr) minmax(0, 1.45fr); gap: 56px; } + .details-prose { order: -1; padding-right: 40px; border-right: 1px solid var(--color-border); } +} + + +/* Chart hover: guide line, dot, and a small label with the exact numbers */ +.viz.ix { position: relative; touch-action: pan-y; } +.viz.ix:focus-visible { outline: 0; box-shadow: var(--focus-ring); border-radius: var(--radius); } +.viz-tip { position: absolute; z-index: 2; transform: translate(-50%, -100%); padding: 6px 9px; background: var(--color-surface); border: 1px solid var(--color-border-strong); border-radius: var(--radius); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22); color: var(--color-text-muted); font: 0.8rem / 1.35 var(--font-mono); white-space: nowrap; pointer-events: none; } +.viz-tip b { color: var(--color-text); font-weight: var(--weight-semibold); } +.viz-tip .up { color: var(--color-accent-text); } +.viz-guide { position: absolute; width: 1px; background: var(--color-border-strong); pointer-events: none; } +.viz-dot { position: absolute; width: 11px; height: 11px; border-radius: 50%; background: var(--viz-ac); border: 2px solid var(--color-bg); transform: translate(-50%, -50%); pointer-events: none; } +.viz svg.hovering rect[data-i] { opacity: 0.3; } +.viz svg.hovering rect[data-i].on { opacity: 1; } + +/* Stats report: question-led groups, main charts full width, related charts in pairs. No boxes. */ +.stats { display: grid; } +.stats > header h2 { margin: 0; font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); letter-spacing: var(--tracking-display); } +.stats > header p { margin: var(--space-3) 0 0; color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.grp { display: grid; gap: 34px; margin-top: 40px; padding-top: 34px; border-top: 1px solid var(--color-border); } +.grp > h3 { margin: 0; font: var(--weight-bold) 1.35rem / 1.25 var(--font-display); letter-spacing: -0.015em; } +.blk { display: flex; flex-direction: column; gap: var(--space-5); min-width: 0; } +.blk > h4 { margin: 0; color: var(--color-text-muted); font: var(--weight-semibold) var(--text-small) var(--font-mono); text-transform: lowercase; } +.blk > .desc { margin: -6px 0 0; color: var(--color-text-muted); font-size: var(--text-small); line-height: 1.45; } +.blk .head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--space-2) var(--space-5); } +.blk .num { font: var(--weight-semibold) 2rem / 1 var(--font-mono); letter-spacing: -0.03em; } +.blk .num-note { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); } +.summary > div { display: flex; flex-direction: column-reverse; justify-content: flex-end; gap: var(--space-2); padding: 18px 18px 16px 0; } +.summary > div + div { padding-left: 18px; border-left: 1px solid var(--color-border); } +.summary dd { margin: 0; font: var(--weight-semibold) 2rem / 1 var(--font-mono); letter-spacing: -0.03em; } +.summary dd.ac { color: var(--color-accent-text); } +.summary dt { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.pair, .trio { display: grid; gap: 34px; } +@media (min-width: 640px) { + .pair { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0; } + .pair > .blk:first-child { padding-right: 32px; } + .pair > .blk + .blk { padding-left: 32px; border-left: 1px solid var(--color-border); } + .trio { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 34px 0; } + .trio > .age { grid-column: 1 / -1; } + .trio > .blk:nth-child(2) { padding-right: 32px; } + .trio > .blk:nth-child(3) { padding-left: 32px; border-left: 1px solid var(--color-border); } +} +@media (min-width: 1100px) { + .trio { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr); gap: 0; } + .trio > .age { grid-column: auto; padding-right: 32px; } + .trio > .blk:nth-child(2) { padding-left: 32px; border-left: 1px solid var(--color-border); } +} +@media (max-width: 639px) { + .summary { grid-template-columns: 1fr 1fr; } + .summary > div:nth-child(3) { padding-left: 0; border-left: 0; } + .summary > div:nth-child(n + 3) { border-top: 1px solid var(--color-border); } + .summary dd { font-size: 1.45rem; } +} + +/* Desktop rows: related charts side by side so nothing stretches across the full width. */ +.row { display: grid; gap: 34px; } +@media (min-width: 1100px) { + .row.r-21 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 0; } + .row.r-11 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 0; } + .row.r-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; } + .row > .blk:not(:first-child) { padding-left: 32px; border-left: 1px solid var(--color-border); } + .row > .blk:not(:last-child) { padding-right: 32px; } +} +@media (min-width: 640px) and (max-width: 1099px) { + .row.r-3 { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 34px 0; } + .row.r-3 > .blk:nth-child(1) { padding-right: 32px; } + .row.r-3 > .blk:nth-child(2) { padding-left: 32px; border-left: 1px solid var(--color-border); } + .row.r-3 > .blk:nth-child(3) { grid-column: 1 / -1; } +} + +/* Niches page */ +.nsec { display: grid; gap: var(--space-6); } +.nsec + .nsec { margin-top: 48px; padding-top: 40px; border-top: 1px solid var(--color-border); } +.nsec h2 { margin: 0; font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); letter-spacing: var(--tracking-display); } +.nsec .sec-head { margin-bottom: 0; } +.nsec-desc { margin: -4px 0 var(--space-3); color: var(--color-text-muted); font-size: var(--text-small); } +.treemap { position: relative; } +.treemap .cell { position: absolute; display: flex; flex-direction: column; justify-content: space-between; padding: 7px 9px; overflow: hidden; border-radius: 1px; color: var(--color-text); font-size: 0.82rem; line-height: 1.2; text-decoration: none; } +.treemap .cell.on-ac { color: var(--viz-on-ac); } +.treemap .cell b { font: var(--weight-semibold) 0.92rem var(--font-mono); } +.treemap .cell:hover, .treemap .cell:focus-visible { outline: 2px solid var(--color-text); outline-offset: -2px; } +.treemap .viz-tip { transform: translate(-50%, -100%); } +.ntable { width: 100%; border-collapse: collapse; font-size: 0.97rem; } +.ntable th { padding: 0 var(--space-5) var(--space-4); border-bottom: 1px solid var(--color-border-strong); color: var(--color-text-muted); font: 500 0.8rem var(--font-mono); text-align: left; text-transform: lowercase; white-space: nowrap; } +.ntable td { padding: var(--space-6) var(--space-5); border-top: 1px solid var(--color-border); vertical-align: middle; white-space: nowrap; } +.ntable tbody tr:first-child td { border-top: 0; } +.ntable tbody tr:hover td { background: var(--color-surface); } +.ntable .r { text-align: right; } +.ntable .mono { font: 0.86rem var(--font-mono); } +.ntable .mu { color: var(--color-text-muted); } +.ntable .niche a { color: var(--color-text); font-weight: var(--weight-semibold); text-decoration: none; } +.ntable .niche a:hover { text-decoration: underline; text-underline-offset: 3px; } +.ntable .sharecell { display: flex; align-items: center; gap: var(--space-4); min-width: 150px; } +.minibar { flex: 1; max-width: 120px; height: 8px; background: var(--color-surface-2); border-radius: 1px; overflow: hidden; } +.minibar i { display: block; height: 100%; background: var(--viz-ac); } +.ntable .pct { color: var(--color-text-muted); } +.ntable .tops ul { display: flex; gap: var(--space-2) var(--space-7); margin: 0; padding: 0; list-style: none; } +.ntable .tops a, .ntable .easy a { color: var(--color-text); text-decoration: none; } +.ntable .tops a:hover, .ntable .easy a:hover { text-decoration: underline; text-underline-offset: 3px; } +.ntable .tops span { color: var(--color-text-muted); font: 0.82rem var(--font-mono); } +.ntable .easy span { display: block; margin-top: 2px; font-size: var(--text-small); } +.sel select:disabled { opacity: 0.45; cursor: not-allowed; } +/* network grid */ +:root { --heat-rgb: 58, 112, 0; --heat-hot: #fff; } +[data-theme="dark"] { --heat-rgb: 186, 252, 60; --heat-hot: #141414; } +.heat-scroll { overflow-x: auto; border: 1px solid var(--color-border); border-radius: var(--radius); } +.heat { width: 100%; border-collapse: separate; border-spacing: 2px; font: 0.86rem var(--font-mono); } +.heat th { padding: 8px 10px; color: var(--color-text-muted); font-weight: 500; white-space: nowrap; } +.heat thead th { text-align: center; } +.heat thead th:first-child, .heat tbody th { position: sticky; left: 0; z-index: 1; background: var(--color-bg); text-align: left; } +.heat tbody th a { color: var(--color-text); font: var(--weight-semibold) 0.92rem var(--font-sans); text-decoration: none; } +.heat td { min-width: 64px; padding: 9px 6px; background: rgba(var(--heat-rgb), var(--a)); border-radius: 1px; color: var(--color-text); text-align: center; } +.heat td.hot { color: var(--heat-hot); font-weight: var(--weight-semibold); } +.heat tr.x th a { text-decoration: underline; text-underline-offset: 3px; } +.heat td.x { outline: 1px solid var(--color-border-strong); } +.heat-unit { margin: 0; color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.nprose { margin-top: 64px; } +/* phones: table rows become two-line blocks */ +@media (max-width: 760px) { + .ntable thead { display: none; } + .ntable, .ntable tbody, .ntable tr { display: block; } + .ntable tr { display: grid; grid-template-columns: 1fr auto; gap: 6px 12px; padding: var(--space-6) 0; border-top: 1px solid var(--color-border); } + .ntable tbody tr:first-child { border-top: 0; } + .ntable td { padding: 0; border: 0; white-space: normal; } + .ntable .niche { grid-column: 1; } + .ntable td.num { grid-column: 2; grid-row: 1; } + .ntable .sharecell { grid-column: 1 / -1; min-width: 0; } + .ntable .tops, .ntable .easy { grid-column: 1 / -1; } + .ntable .tops ul { flex-wrap: wrap; } + .ntable .easy { color: var(--color-text-muted); font-size: var(--text-small); } + .ntable .easy::before { content: "Easiest to join: "; } + .ntable .easy span { display: inline; margin-left: 4px; } + .ntable .easy span::before { content: "· "; } + .ntable tbody tr:hover td { background: none; } +} + +/* Site page */ +.title-logo.letter { color: #141414; font: var(--weight-bold) 0.6em / 1 var(--font-display); letter-spacing: 0; } +.page-header > p a { color: var(--color-link); text-underline-offset: 3px; } +.site-facts { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); margin: 0 0 8px; border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); } +.site-facts > div { display: flex; flex-direction: column-reverse; justify-content: flex-end; gap: var(--space-2); min-width: 0; padding: 16px 16px 15px; border-left: 1px solid var(--color-border); } +.site-facts > div:first-child { padding-left: 0; border-left: 0; } +.site-facts dt { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.site-facts dd { margin: 0; font: var(--weight-semibold) 1.05rem / 1.3 var(--font-sans); overflow-wrap: anywhere; } +.site-facts dd a { color: var(--color-text); text-underline-offset: 3px; text-decoration-color: var(--color-border-strong); } +.site-facts .mu, .rel .mu { color: var(--color-text-muted); font-weight: 400; } +.site-sec { margin-top: 0; } +.nsec.site-sec { margin-top: 44px; padding-top: 38px; border-top: 1px solid var(--color-border); } +.note { margin: 0; color: var(--color-text-muted); font-size: var(--text-small); } +.note a { color: var(--color-link); text-underline-offset: 3px; } +.rel { width: 100%; border-collapse: collapse; font-size: 0.97rem; } +.rel th { padding: 0 var(--space-5) var(--space-4); border-bottom: 1px solid var(--color-border-strong); color: var(--color-text-muted); font: 500 0.8rem var(--font-mono); text-align: left; white-space: nowrap; } +.rel td { padding: var(--space-6) var(--space-5); border-top: 1px solid var(--color-border); white-space: nowrap; } +.rel tbody tr:first-child td { border-top: 0; } +.rel .r { text-align: right; } .rel .mono { font: 0.86rem var(--font-mono); color: var(--color-text-muted); } +.rel .net { display: flex; align-items: center; gap: var(--space-5); } +.rel .net a { color: var(--color-text); font-weight: var(--weight-semibold); text-decoration: none; } +.rel .net a:hover { text-decoration: underline; text-underline-offset: 3px; } +.state { display: inline-flex; align-items: center; gap: var(--space-3); color: var(--color-text-muted); font-size: var(--text-small); } +.state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; border: 1.5px solid var(--color-text-muted); } +.state.ok { color: var(--color-text); } +.state.ok::before { background: var(--viz-ac); border-color: var(--viz-ac); } +/* history */ +.gantt { display: grid; gap: var(--space-4); } +.g-axis { display: grid; grid-template-columns: repeat(10, 1fr); margin-left: 150px; color: var(--color-text-muted); font: 0.78rem var(--font-mono); } +.g-axis span { padding-left: 4px; border-left: 1px solid var(--color-border); } +.g-row { display: grid; grid-template-columns: 150px 1fr; align-items: center; } +.g-name { color: var(--color-text-muted); font-size: 0.92rem; } +.g-track { position: relative; height: 30px; background: repeating-linear-gradient(90deg, var(--color-border) 0 1px, transparent 1px 10%); } +.g-bar { position: absolute; top: 3px; bottom: 3px; display: flex; align-items: center; padding: 0 8px; overflow: hidden; border-radius: 1px; color: var(--color-text); font: 0.78rem var(--font-mono); font-style: normal; white-space: nowrap; text-overflow: ellipsis; } +.g-bar.on-ac { color: var(--viz-on-ac); font-weight: var(--weight-semibold); } +.events { display: grid; gap: var(--space-3); margin: var(--space-5) 0 0; padding: 0; list-style: none; } +.events li { display: flex; gap: var(--space-7); font-size: 0.95rem; } +.events .mono { width: 80px; flex: none; color: var(--color-text-muted); font: 0.86rem var(--font-mono); } +/* ads.txt */ +.adstxt-nums { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); } +.adstxt-nums div { display: grid; gap: 2px; } +.adstxt-nums b { font: var(--weight-semibold) 1.6rem / 1 var(--font-mono); letter-spacing: -0.02em; } +.adstxt-nums span { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.blk .sub { margin: var(--space-5) 0 0; color: var(--color-text-muted); font: var(--weight-semibold) var(--text-small) var(--font-mono); } +.adstxt { margin: 0; padding: 14px 16px; overflow-x: auto; background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius); color: var(--color-text-muted); font: 0.82rem / 1.7 var(--font-mono); } +.adstxt .c { color: var(--color-text-muted); opacity: 0.7; } +.adstxt mark { background: var(--viz-ac3); color: var(--color-text); padding: 1px 3px; border-radius: 1px; } +.adstxt .hl { color: var(--color-text); } +.filelink { display: inline-flex; align-items: center; gap: var(--space-2); color: var(--color-text-muted); font: var(--text-small) var(--font-mono); text-underline-offset: 3px; } +.filelink .icon { width: 14px; height: 14px; } +/* similar */ +.similar { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--color-border); } +.similar li { display: flex; justify-content: space-between; align-items: center; gap: var(--space-5); padding: 14px 16px 14px 0; border-bottom: 1px solid var(--color-border); } +.similar li:not(:nth-child(3n + 1)) { padding-left: 16px; border-left: 1px solid var(--color-border); } +.similar a { color: var(--color-text); font-weight: var(--weight-semibold); text-decoration: none; overflow-wrap: anywhere; } +.similar a:hover { text-decoration: underline; text-underline-offset: 3px; } +.similar li > span { flex: none; color: var(--color-text-muted); font: 0.82rem var(--font-mono); } +/* check another */ +.check-form { display: grid; grid-template-columns: minmax(0, 520px) max-content; justify-content: start; gap: var(--space-3); } +@media (max-width: 1099px) { + .site-facts { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .site-facts > div:nth-child(4) { padding-left: 0; border-left: 0; } + .site-facts > div:nth-child(n + 4) { border-top: 1px solid var(--color-border); } + .similar { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .similar li:not(:nth-child(3n + 1)) { padding-left: 0; border-left: 0; } + .similar li:nth-child(2n) { padding-left: 16px; border-left: 1px solid var(--color-border); } +} +@media (max-width: 640px) { + .site-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .site-facts > div { padding: 14px 12px; } + .site-facts > div:nth-child(4) { padding-left: 12px; border-left: 1px solid var(--color-border); } + .site-facts > div:nth-child(2n + 1) { padding-left: 0; border-left: 0; } + .site-facts > div:nth-child(n + 3) { border-top: 1px solid var(--color-border); } + .g-axis { margin-left: 0; grid-template-columns: repeat(5, 1fr); } + .g-axis span:nth-child(2n) { display: none; } + .g-row { grid-template-columns: 1fr; gap: 4px; } + .rel thead { display: none; } + .rel, .rel tbody, .rel tr { display: block; } + .rel tr { display: grid; grid-template-columns: 1fr auto; gap: 6px 10px; padding: 12px 0; border-top: 1px solid var(--color-border); } + .rel tbody tr:first-child { border-top: 0; } + .rel td { padding: 0; border: 0; white-space: normal; } + .rel td:nth-child(2) { grid-column: 1 / -1; color: var(--color-text-muted); font-size: var(--text-small); } + .rel td:nth-child(3) { grid-column: 1; } + .rel td.r { grid-row: 1; grid-column: 2; } + .adstxt-nums { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } + .similar { grid-template-columns: 1fr; } + .similar li:nth-child(n) { padding-left: 0; border-left: 0; } + .check-form { grid-template-columns: 1fr; } } + +/* Niche page */ +.page-header > p a { color: var(--color-link); } +.sites .netcol a { color: var(--color-text); text-decoration: none; } +.sites .netcol a:hover { text-decoration: underline; text-underline-offset: 3px; } +#frows td.end::after { content: none; } +.ladder { width: 100%; border-collapse: collapse; font-size: 0.97rem; } +.ladder th { padding: 0 var(--space-5) var(--space-4); border-bottom: 1px solid var(--color-border-strong); color: var(--color-text-muted); font: 500 0.8rem var(--font-mono); text-align: left; white-space: nowrap; } +.ladder td { padding: var(--space-5); border-top: 1px solid var(--color-border); white-space: nowrap; } +.ladder tbody tr:first-child td { border-top: 0; } +.ladder tbody tr:hover td { background: var(--color-surface); } +.ladder .r { text-align: right; } .ladder .mono { font: 0.86rem var(--font-mono); } +.ladder .lnet a { color: var(--color-text); font-weight: var(--weight-semibold); text-decoration: none; } +.ladder .lnet a:hover { text-decoration: underline; text-underline-offset: 3px; } +.ladder .lshare { display: flex; align-items: center; gap: var(--space-4); min-width: 160px; } +.ladder .lshare .minibar { max-width: 200px; } +.ladder .pct { color: var(--color-text-muted); } +.ladder .np { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.sizes { display: grid; gap: var(--space-7); } +.srow { display: grid; grid-template-columns: 130px minmax(0, 1fr); gap: var(--space-7); align-items: center; } +.srow .lab { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.srow .lab b { display: block; color: var(--color-text); font-weight: var(--weight-semibold); } +.stack { position: relative; display: flex; gap: 2px; height: 34px; } +.stack i { display: flex; align-items: center; padding-left: 8px; overflow: hidden; border-radius: 1px; color: var(--color-text); font: var(--weight-semibold) 0.78rem var(--font-mono); font-style: normal; white-space: nowrap; cursor: default; } +.stack i.on-ac { color: var(--viz-on-ac); } +.stack i:hover, .stack i:focus-visible { outline: 2px solid var(--color-text); outline-offset: -2px; } +.stack .viz-tip { transform: translate(-50%, -100%); } +.moves { display: grid; grid-template-columns: auto minmax(60px, 1fr) auto; gap: var(--space-4) var(--space-5); align-items: center; margin: 0; font-size: 0.95rem; } +.moves dt { white-space: nowrap; } +.moves .arrow { margin: 0 6px; color: var(--color-text-muted); } +.moves .track { height: 8px; margin: 0; background: var(--color-surface-2); border-radius: 1px; overflow: hidden; } +.moves .track i { display: block; height: 100%; background: var(--viz-ac); } +.moves .v { margin: 0; color: var(--color-text-muted); font: 0.86rem var(--font-mono); text-align: right; } +.newest { width: 100%; border-collapse: collapse; font-size: 0.95rem; } +.newest td { padding: var(--space-3) 0; border-top: 1px solid var(--color-border); } +.newest tr:first-child td { border-top: 0; } +.newest td + td { padding-left: var(--space-5); } +.newest a { color: var(--color-text); font-weight: var(--weight-semibold); text-decoration: none; } +.newest .r { text-align: right; } .newest .mono { color: var(--color-text-muted); font: 0.86rem var(--font-mono); } +@media (max-width: 640px) { + .ladder th:nth-child(3), .ladder td:nth-child(3) { display: none; } + .ladder td, .ladder th { padding-left: 6px; padding-right: 6px; } + .ladder td.rule { white-space: normal; } + .srow { grid-template-columns: 1fr; gap: var(--space-3); } + .srow .lab b { display: inline; margin-right: 8px; } +} + +/* Stats page */ +.grp.first { margin-top: 0; padding-top: 0; border-top: 0; } +.big3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.big3 b { display: block; font: var(--weight-semibold) 2rem / 1 var(--font-mono); letter-spacing: -0.03em; } +.big3 b.ac { color: var(--color-accent-text); } +.big3 span { color: var(--color-text-muted); font: var(--text-small) var(--font-mono); } +.viz-tip .row { display: flex; justify-content: space-between; gap: 14px; } +.blk > .desc:first-child { margin-top: 0; } diff --git a/dev-space/ad-networks-mockups/assets/mock.js b/dev-space/ad-networks-mockups/assets/mock.js new file mode 100644 index 00000000..b8e11645 --- /dev/null +++ b/dev-space/ad-networks-mockups/assets/mock.js @@ -0,0 +1,56 @@ +/* Shared mockup script: top bar, footer, icons, theme. */ +(() => { + const ICONS = { + network: '', + search: '', + megaphone: '', + calendar: '', + external: '', + shapes: '', + chart: '', + sun: '', + }; + const icon = (name, cls = "icon") => + ``; + + const theme = new URLSearchParams(location.search).get("theme") + || (() => { try { return localStorage.getItem("mk:theme"); } catch (e) { return null; } })() || "dark"; + document.documentElement.dataset.theme = theme; + document.addEventListener("click", (e) => { + if (!e.target.closest("[data-theme-toggle]")) return; + const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; + document.documentElement.dataset.theme = next; + try { localStorage.setItem("mk:theme", next); } catch (err) {} + }); + + const wordmark = ''; + const dot = ''; + + document.addEventListener("DOMContentLoaded", () => { + const content = document.getElementById("page"); + document.body.insertAdjacentHTML("afterbegin", `

`); + // Optional section bar: . + const current = document.body.dataset.sectionCurrent; + if (current) { + const items = ["Directory", "Niches", "Stats", "Checker"]; + document.querySelector(".topbar").insertAdjacentHTML("afterend", ``); + } + content.insertAdjacentHTML("beforeend", ``); + // "/" jumps to the search box, like the site's other search fields. + document.addEventListener("keydown", (e) => { + const box = document.querySelector("[data-search]"); + if (e.key !== "/" || !box || /^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) return; + e.preventDefault(); box.focus(); + }); + document.querySelectorAll("form[data-mock]").forEach((f) => f.addEventListener("submit", (e) => e.preventDefault())); + document.querySelectorAll("[data-icon]").forEach((el) => { el.outerHTML = icon(el.dataset.icon, el.getAttribute("class") || "icon"); }); + }); +})(); diff --git a/dev-space/ad-networks-mockups/assets/stack127-wordmark-on-dark.png b/dev-space/ad-networks-mockups/assets/stack127-wordmark-on-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8a65b794d3cdabbbb702db0b55fc68cc8e4df3c2 GIT binary patch literal 26894 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%0DdA`)RU4o_W-Y_e`D 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 literal 0 HcmV?d00001 diff --git a/dev-space/ad-networks-mockups/assets/stack127-wordmark-on-light.png b/dev-space/ad-networks-mockups/assets/stack127-wordmark-on-light.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1a5a363c58e2b9fbf91057407b1594fc786cca GIT binary patch literal 26834 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 z zuEl-jU8B8#EXw5=9K~aqH0D51#P_xr@QyMv=tR!)gb&Mxd%^ zTGb?U^`b3VTM0{Y@3w2xo~Geweki?6&|{XxutD;wGxc(!bZvooVse+BP>c37j?!W1 z-5P21DXuzgas#t9W0JEw+EAPAseRRrKKX$pn?GodA|4d0T3OwA&3x{47iVA10yUaI z=P<+vWf}xz)ErA&!Us3lXLUBC6%&`dBxH`I{ + + + + +Ad networks for food sites: which networks food blogs use | stack127 + + + + +
+
+
+ +
+ +
+
+

Which food sites run ads?

+
+ + + +
+
+
+ + + +
sitenetworktrafficdomain ageplatformchecked
+
+
+ +
+
+ +
+

Which ad networks do food sites use?

+
+
+

Every network that runs on food sites, with what it takes to join.

+
+ + + +
networkfood sitesshare of food sitesto join
+
+
+ +
+

What do food sites of each size run?

+

Sites grouped by their place in Chrome's list of the most visited sites. Each site counts once, under its main ad network.

+
+
    +
    + +
    +

    Which networks are food sites moving to?

    +

    Food sites that switched networks in the last 90 days, and the newest sites on each network.

    +
    +

    switches, last 90 days

    +

    newest sites

    +
    +
    +
    +
    +
    + + + diff --git a/dev-space/ad-networks-mockups/mediavine.html b/dev-space/ad-networks-mockups/mediavine.html new file mode 100644 index 00000000..9322cbae --- /dev/null +++ b/dev-space/ad-networks-mockups/mediavine.html @@ -0,0 +1,369 @@ + + + + + +Mediavine: requirements and 7,852 sites that use it | stack127 + + + + +
    +
    +
    + +
    +
    +
    +

    Sites using Mediavine

    +
    + + + +
    +
    +
    + + + +
    sitenichetrafficdomain ageplatformads managed bychecked
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +

    What does Mediavine require and pay?

    +
    +
    sessions a month to join
    50,000
    +
    revenue share
    75%
    +
    pay day
    Net 65
    +
    +
    +
    +
    +

    Joining

    +
    +
    Traffic
    50,000 sessions a monthMeasured in Google Analytics
    +
    Visitor location
    No rule
    +
    Content
    Original, long-form content
    +
    Google standing
    Good standing with Google AdSense
    +
    Must connect
    Google Analytics
    +
    Approval time
    Not public
    +
    +
    +
    +

    Pay

    +
    +
    Revenue share
    75%More at higher traffic
    +
    Pay day
    65 days after the month endsNet 65
    +
    Minimum payout
    $25
    +
    Paid by
    Bank transfer, PayPal, wire
    +
    Fees
    Not public
    +
    +
    +
    +

    Rules

    +
    +
    Exclusive
    Yes, no other display ads
    +
    Contract
    30 days notice to leave
    +
    +
    +
    +

    Service

    +
    +
    Type
    Managed ad service
    +
    Ad formats
    Display, video, sticky, in-content
    +
    Extras
    Grow plugin, reporting dashboard
    +
    Company
    Founded 2004, Boca Raton, Florida
    +
    +
    +
    +

    Sources: mediavine.com/requirements, help.mediavine.com · checked Oct 12, 2026

    +
    +
    +
    +

    How do you join Mediavine?

    +

    You apply with your site and give Mediavine read access to your Google Analytics. They check your traffic and content against the rules here. If you're approved, you add their script and their ads.txt lines, and they set up and run the ads for you.

    +
    +
    +

    What if your site is too small for Mediavine?

    +

    Mediavine runs a second program, Journey, for smaller sites. It has a lower traffic rule and the same company behind it. Journey's page lists its rules and the sites that use it.

    +
    +
    +

    What does the pay day mean?

    +

    Mediavine pays each month's earnings a set number of days after the month ends. Earnings from January arrive a couple of months later, not at the start of February. Plan for that gap if you switch from a network that pays faster.

    +
    +
    +

    Can you run other ads with Mediavine?

    +

    Check the exclusive rule under Rules. A network that wants to be your only display ad partner manages every display ad slot on your site. Affiliate links and sponsored posts are usually separate, but confirm with the network before you sign.

    +
    +
    +

    How can you check if a site uses Mediavine?

    +

    Look for Mediavine in the site's ads.txt file, or paste the domain into our ad network checker.

    +
    +
    +
    +
    +
    +
    +
    + +

    Mediavine stats

    From our checks of 7,852 sites · sample data

    + +
    +

    How many sites use Mediavine?

    +
    +
    7,852
    sites listed
    +
    7,459
    confirmed live
    +
    2nd
    of 45 networks
    +
    +948
    sites in a year
    +
    +
    +

    Sites over time

    How many sites ran Mediavine at the end of each month.

    +

    Rank among networks

    Where Mediavine sits among the networks we track, by number of sites.

    Ezoic
    8,144
    Mediavine
    7,852
    Raptive
    7,152
    Journey
    6,953
    Google AdSense
    5,349
    +
    +
    + +
    +

    What kind of sites use Mediavine?

    +
    +

    Top niches

    What the sites are about, as a share of all sites.

    Food
    31%
    Home & DIY
    14%
    Travel
    11%
    Parenting
    9%
    Crafts
    7%
    Health
    6%
    Other
    22%
    +

    Traffic

    Sites grouped by their place in Chrome's list of the most visited sites.

    +

    Biggest publishers

    Companies that run the most Mediavine sites, from Mediavine's seller list. Personal names are left out.

    Tiny Kitchen Lab LLC14 sites
    Nestly Brands LLC11 sites
    Cozy Home Media Inc9 sites
    Slow Notes Media LLC7 sites
    Pantry Press Ltd6 sites
    +
    +
    + +
    +

    Which sites joined or left Mediavine?

    +
    +

    Joined and left

    Sites that started or stopped running Mediavine, per day or per week.

      +

      Switches, last 90 days

      Sites that moved to or from Mediavine, and the network on the other side.

      +
      +
      + +
      +

      How are Mediavine sites built?

      +
      +

      Domain age

      When each site's domain was first registered. A domain can be older than the site on it.

      13 yrsmedian, registered 2013
      +

      Platforms

      What the sites are built with, from clues in their page code.

      • WordPress 86%
      • Squarespace 3%
      • Shopify 2%
      • Next.js 2%
      • Ghost 1%
      • Other 2%
      • Unknown 4%
      +

      Domain endings

      The share of sites on each domain ending, such as .com or .co.uk.

      .com89%.net3%.co.uk2%.org2%.co1%other3%

      +
      +
      + +
      +

      What else is in their ads.txt?

      +
      +

      Ad manager

      Which company each site names as its main ad manager in its ads.txt file.

      88%name Mediavine
      • Mediavine 88%
      • another network 3%
      • not listed 9%
      +

      Also runs

      Other networks from our list that these sites run at the same time.

      Amazon APS
      64%
      Google AdSense
      9%
      Taboola
      4%
      Raptive
      2%
      Ezoic
      1%
      +
      +
      +
      +
      +
      +
      + + + + + diff --git a/dev-space/ad-networks-mockups/niches.html b/dev-space/ad-networks-mockups/niches.html new file mode 100644 index 00000000..d9c5f3ab --- /dev/null +++ b/dev-space/ad-networks-mockups/niches.html @@ -0,0 +1,296 @@ + + + + + +Ad networks by niche: which networks food, travel, and gaming sites use | stack127 + + + + +
      +
      +
      + +
      + +
      +
      +

      Which niches have the most sites?

      +

      Each block is one niche, sized by how many of our sites belong to it.

      +
      +
      + +
      +

      Which ad networks does each niche use?

      +
      + + + +
      +
      +

      The three networks used most in each niche, and the easiest of those three to join.

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      nichesitesshare of all sitestop networkseasiest of these to join
      Food6,320 · 5,878 live18.0%Journey1,000 sessions a month
      Home & DIY3,410 · 3,205 live9.7%Journey1,000 sessions a month
      Travel2,980 · 2,831 live8.5%Journey1,000 sessions a month
      Parenting2,450 · 2,352 live7.0%Journey1,000 sessions a month
      Personal finance2,210 · 2,055 live6.3%Ezoicno traffic minimum
      Health & fitness2,180 · 2,049 live6.2%Ezoicno traffic minimum
      Beauty & fashion1,960 · 1,862 live5.6%Journey1,000 sessions a month
      Gaming1,840 · 1,766 live5.2%Ezoicno traffic minimum
      Tech1,720 · 1,600 live4.9%Ezoicno traffic minimum
      Crafts1,510 · 1,419 live4.3%Journey1,000 sessions a month
      Pets1,280 · 1,216 live3.6%Ezoicno traffic minimum
      Outdoors1,140 · 1,094 live3.2%Journey1,000 sessions a month
      Gardening980 · 911 live2.8%Ezoicno traffic minimum
      Sports940 · 884 live2.7%Ezoicno traffic minimum
      Entertainment910 · 865 live2.6%Ezoicno traffic minimum
      Lifestyle870 · 835 live2.5%Journey1,000 sessions a month
      Education720 · 670 live2.1%AdSenseno traffic minimum
      Automotive640 · 602 live1.8%Ezoicno traffic minimum
      Business590 · 560 live1.7%Ezoicno traffic minimum
      News470 · 451 live1.3%AdSenseno traffic minimum
      +
      +
      + +
      +

      Which networks lead in each niche?

      +

      The share of each niche's sites that run each network. Darker cells mean more sites.

      +
      + + + + + + + + + + + + + + + + + + + + + + + + +
      nicheMediavineRaptiveEzoicJourneyAdSensePlaywireFreestarPubliftNitroPay
      Food412291872221
      Home & DIY33211119102221
      Travel29241417102221
      Parenting30261316102221
      Personal finance17311512142221
      Health & fitness21231812102221
      Beauty & fashion22271315102221
      Gaming43193112413216
      Tech8122612212921
      Crafts36151322102221
      Pets24121721102221
      Outdoors26181317102221
      Gardening31121324102221
      Sports612161213917141
      Entertainment1410201214215131
      Lifestyle25221318102221
      Education1410239242221
      Automotive1292412192221
      Business14142112202221
      News146121217216191
      +
      +

      Numbers are % of the niche's sites.

      +
      + +
      +
      +

      How do we sort sites into niches?

      +

      We give each site one niche, based on what its pages are about. A site that covers several topics goes into its main one. Sites we can't place yet stay unsorted and don't count toward any niche here.

      +
      +
      +

      Why can a niche's shares add up to more than 100%?

      +

      Many sites run more than one ad network at the same time. Each share counts the sites that use that network, so one site can count toward several networks in the same niche.

      +
      +
      +
      +
      +
      + + + diff --git a/dev-space/ad-networks-mockups/site.html b/dev-space/ad-networks-mockups/site.html new file mode 100644 index 00000000..a1b2551e --- /dev/null +++ b/dev-space/ad-networks-mockups/site.html @@ -0,0 +1,136 @@ + + + + + +tinykitchenlab.com ad network: Mediavine | stack127 + + + + +
      +
      +
      + +
      + +
      +
      +
      main ad network
      Mediavine
      +
      traffic
      top 10k
      +
      niche
      Food
      +
      platform
      WordPress
      +
      domain registered
      2014 · 12 yrs
      +
      publisher
      Tiny Kitchen Lab LLC
      +
      + +
      +

      Which ad networks does tinykitchenlab.com use?

      +

      Networks from our list that its ads.txt file names, and whether the network's own seller list agrees.

      +
      + + + + + + + +
      networkwhat its ads.txt saysstatussince
      MediavineNames it as the ad managerConfirmed liveMar 2021
      Amazon APSAllows it to sell ad spaceConfirmed liveJun 2022
      Google AdSenseAllows it to sell ad spaceNot in Google's seller listJan 2019
      +
      +

      Its ads.txt also lists 55 other ad sellers. Most of them work through Mediavine, so we don't count them as separate networks.

      +
      + +
      +

      How have its ad networks changed?

      +

      When each network first and last appeared in its ads.txt file.

      + +
        +
      1. Jun 2022Added Amazon APS
      2. +
      3. Mar 2021Moved from Ezoic to Mediavine
      4. +
      5. Jan 2019Added Google AdSense
      6. +
      7. Mar 2017First ads.txt file, with Ezoic
      8. +
      +

      Dates before Jul 2026 come from archived copies of its ads.txt in the Wayback Machine. A month can be off when the archive skipped a few months.

      +
      + +
      +

      What is in its ads.txt file?

      +

      The public list of companies allowed to sell its ad space, as we read it on Oct 12, 2026.

      +
      +
      +
      +
      142lines
      +
      58companies
      +
      31direct
      +
      111resellers
      +
      + +
      • direct 22%
      • reseller 78%
      +

      companies with the most lines

      +
      +
      google.com
      18
      +
      rubiconproject.com
      9
      +
      pubmatic.com
      8
      +
      openx.com
      7
      +
      appnexus.com
      6
      +
      +
      +
      +
      # tinykitchenlab.com/ads.txt
      +MANAGERDOMAIN=mediavine.com
      +OWNERDOMAIN=tinykitchenlab.com
      +mediavine.com, 4823, DIRECT
      +aps.amazon.com, 9c1a2b, DIRECT
      +google.com, pub-55739, RESELLER, f08c47fec0942fa0
      +rubiconproject.com, 17282, RESELLER, 0bfd66d529a55807
      +pubmatic.com, 157150, RESELLER, 5d62403b186f2ace
      +openx.com, 537145117, RESELLER, 6a698e2ec38604c6
      +appnexus.com, 1356, RESELLER, f5ab79cb980f11d1
      +… 132 more lines
      + View the full ads.txt file +
      +
      +
      + +
      +

      Which similar sites use Mediavine?

      +

      Other food sites of the same size that run Mediavine.

      + +

      See all Mediavine sites or all food sites.

      +
      + +
      +

      Check another site

      +
      + + + +
      +

      We read the site's ads.txt and show which ad networks it uses. Sites we don't list yet get added.

      +
      +
      +
      +
      + + diff --git a/dev-space/ad-networks-mockups/stats.html b/dev-space/ad-networks-mockups/stats.html new file mode 100644 index 00000000..7b0bd5ca --- /dev/null +++ b/dev-space/ad-networks-mockups/stats.html @@ -0,0 +1,178 @@ + + + + + +Ad network market share and stats 2026 | stack127 + + + + +
      +
      +
      + +
      +
      +
      +
      +

      Which ad networks do most sites use?

      +
      38,912
      sites listed
      35,749
      confirmed live
      45
      networks tracked
      39%
      run 2+ networks
      +
      +

      market share

      The share of all sites we list that run each network. A site can run more than one.

      +

      share over time

      Sites on the five biggest networks at the end of each month.

        +
        +
        +
        +

        Which ad networks are growing?

        +
        +

        net change, last 90 days

        Sites gained minus sites lost.

        +

        biggest switches, last 90 days

        The most common moves from one network to another.

        +
        +
        +
        +

        What do sites of each size run?

        +

        Sites grouped by their place in Chrome's list of the most visited sites. Each site counts once, under its main ad network.

        +
          +
          +
          +

          How many ad networks does a site run?

          +
          +

          networks per site

          Networks from our list found on each site.

          +

          most common pairs

          Networks that run together on the same site.

          +
          +
          +
          +

          How do the networks compare?

          +
          +

          joining rule vs sites

          Each dot is a network, placed by its traffic minimum and how many sites run it. Networks with revenue-based rules are left out.

          +

          median domain age

          How long the domains of each network's sites have been registered.

          +

          WordPress share

          The share of each network's sites built on WordPress.

          +
          +
          +
          +

          What do ads.txt files look like?

          +
          +
          71%name an ad manager
          118median lines
          6%have broken lines
          +

          file size

          Sites by the number of lines in their ads.txt file.

          +
          +
          +
          +

          How fresh is this data?

          +
          +
          94%checked in the last 7 days
          91.9%confirmed live
          446new sites last week
          +

          new sites per week

          Sites added to the directory each week, and how many came from people using the checker.

          • found by our scans
          • from the checker
          +
          +
          +
          +
          +
          +

          How do we count a site for a network?

          +

          A site counts for a network when its ads.txt file names the network and the network's own seller list names the site. We re-check sites on a schedule, so these numbers move as sites join and leave.

          +
          +
          +
          +
          +
          + + + diff --git a/dev-space/ad-networks-tracker.md b/dev-space/ad-networks-tracker.md new file mode 100644 index 00000000..e966f69b --- /dev/null +++ b/dev-space/ad-networks-tracker.md @@ -0,0 +1,129 @@ +# Ad networks tracker + +The rule: text that is written once lives in the codebase. Data that changes lives in the database. + +Mockups: `dev-space/ad-networks-mockups/`. Working data: homelab SQLite at `~/.local/share/stack127/ad-networks.sqlite`. + +## Where we are (2026-10-15) + +| Data | Count | Stored in | +| --- | ---: | --- | +| Ad networks | 45 | Supabase (draft) | +| Candidate sites | ~206,000 | Homelab SQLite | +| Sites checked | ~53,000 | Homelab SQLite | +| Sites waiting for a check | ~138,000 | Homelab SQLite | +| Confirmed site and network links | ~40,000 | Homelab SQLite | +| Links that need review | ~6,200 | Homelab SQLite | + +--- + +## Codebase + +### Network details +- [ ] Luna research for all 45 networks: join rules, pay share, payment terms, payment threshold, rules, service, with a source for each fact +- [ ] Opus writes it into one MDX file per network: key numbers, Joining, Pay, Rules, and Service tabs, and sources +- [ ] Guide text and Q&As for each network page, with no numbers in the text + +### Network logos +- [ ] Collect the official logos for all 45 networks +- [ ] Save them in `public/` in one size and format + +### Niches +- [ ] Final list of niches: names, slugs, and one line each +- [ ] Decide whether niche routes become real pages or stay a `?niche=` filter + +### Pages +- [ ] Ad networks directory (`/ad-networks`) +- [ ] Network page (`/ad-networks/`) +- [ ] Niches page +- [ ] Niche page (if we decide yes) +- [ ] Site page +- [ ] Stats page +- [ ] Checker tool (flat URL) + +### Shared parts +- [ ] Tabs component that search engines can read, with every panel in the HTML +- [ ] Chart components with hidden data tables +- [ ] Sites table with search, filter, sort, and pages + +### Docs +- [ ] Update `content_docs/ad-networks/plan.md`: niche routes, site pages, and stats +- [ ] Update `content_docs/ad-networks/brief.md` +- [ ] Decide on the Publisher tools catalog and widen the Domain tools brief for the checker + +--- + +## Database + +### Setup +- [ ] Migration: new site columns (niche, publisher name, platform, domain age, traffic group, ads.txt summary, favicon) +- [ ] Migration: history snapshot table (counts per network per day) +- [ ] Copy reviewed sites and confirmed links from SQLite to Supabase dev +- [ ] Check the dev pages, then copy to production + +### Site checks +- [ ] Clear the ~138,000 sites that are still waiting for a check +- [ ] Review the ~6,200 links that need review + +### Domain age run +- [ ] RDAP lookup for every confirmed site + +### Platform run +- [ ] Find the platform from page code (WordPress, Shopify, Next.js, Astro, Ghost, and so on) + +### Traffic group run +- [ ] Set up the Google Cloud project and BigQuery limits for Chrome rank data +- [ ] Assign a traffic group to every confirmed site + +### Publisher name run +- [ ] Take the business name from sellers.json files + +### Niche run +- [ ] Haiku sorts every confirmed site into one niche +- [ ] Spot check a sample by hand + +### ads.txt summary run +- [ ] Save line counts, direct vs reseller, and top sellers for each site + +### Favicon run +- [ ] Fetch and store a favicon for every confirmed site + +### History +- [ ] Start saving daily counts per network now +- [ ] Luna checks the Wayback Machine terms +- [ ] If allowed, fill older history from archived ads.txt files + +### How often each piece of data updates + +| Data | Source | How often | +| --- | --- | --- | +| New candidate sites | Network sellers.json files, EthicalAds list | Weekly (running) | +| Site checks and links | Each site's own ads.txt | All the time (running) | +| Copy to Supabase | Homelab SQLite | Daily | +| Network counts history | Supabase links | Daily | +| ads.txt summary | Each site's ads.txt | With each site check | +| Publisher names | sellers.json files | Weekly | +| Traffic groups | Chrome rank data | Monthly | +| Platform | Site home page | Monthly | +| Niche | Haiku | Once for each new site, then every 6 months | +| Favicon | Site home page | Once for each new site, then every 6 months | +| Domain age | RDAP | Once for each new site | + +--- + +## Scripts to keep the data fresh + +- [x] `discover`: finds new candidate sites (weekly timer) +- [x] `verify` and `worker`: check each site's ads.txt and update links (always running) +- [x] `backup-local`: backs up the SQLite file (daily timer) +- [ ] `sync`: copies reviewed sites and links from SQLite to Supabase, then refreshes the changed pages +- [ ] `snapshot`: saves the daily count per network +- [ ] `domain-age`: RDAP lookups for new sites +- [ ] `platform`: detects each site's platform +- [ ] `traffic`: pulls Chrome rank data and sets traffic groups +- [ ] `publisher-names`: reads business names from sellers.json files +- [ ] `niche`: Haiku niche sorting for new sites +- [ ] `favicon`: fetches favicons for new sites +- [ ] `ads-txt-summary`: adds the summary to the site check +- [ ] `wayback` (only if the terms allow it): one-time history backfill +- [ ] Timers on homelab for every script above From 2038dd94ccb61aa7179c1401a241e0425d6a8d78 Mon Sep 17 00:00:00 2001 From: RaviTejaKNTS Date: Fri, 16 Oct 2026 12:23:59 +0530 Subject: [PATCH 02/10] Rebuild ad networks pages to match the mockups Rebuild the directory, network pages, niches, niche, stats, and the ad network checker site view to follow the approved mockups in dev-space/ad-networks-mockups. - Shared mockup visuals in app/ui/ad-networks (cards, sites table, report layout, bars, donut, line, joined and left, switches, age strip, heatmap, treemap, ladder, history timeline). - ToolbarSelect rebuilt on react-aria; no native selects. - Live loaders and data gates for every number and chart; blocks without data are hidden and marked data-gate. - Square favicon logos for all networks; joining facts for each. - Four ad network migrations (applied to dev only) plus the Snigel into Publisher Collective merge, with a 301 from /ad-networks/snigel. - Data contract, build plan, and tracker in dev-space. Co-Authored-By: Claude Opus 5.5 --- .../components-ui/[section]/page.tsx | 2 + .../examples/ad-network-demos.tsx | 63 ++ .../components-ui/examples/cards.tsx | 33 + .../components-ui/examples/data-visuals.tsx | 407 +++++++++ .../examples/filters-and-sorting.tsx | 9 + .../components-ui/examples/navigation.tsx | 17 + .../components-ui/examples/page-layout.tsx | 16 + .../components-ui/examples/results.tsx | 125 +++ .../components-ui/examples/tables.tsx | 65 ++ .../(components-ui)/components-ui/sections.ts | 20 +- .../[slug]/content/adapex/article.mdx | 16 +- .../[slug]/content/adapex/intro.mdx | 2 +- .../[slug]/content/adapex/joining.mdx | 5 + .../ad-networks/[slug]/content/adapex/pay.mdx | 3 + .../[slug]/content/adapex/rules.mdx | 5 + .../[slug]/content/adapex/service.mdx | 3 + .../[slug]/content/aditude/article.mdx | 12 +- .../[slug]/content/aditude/intro.mdx | 2 +- .../[slug]/content/aditude/joining.mdx | 3 + .../[slug]/content/aditude/pay.mdx | 3 + .../[slug]/content/aditude/rules.mdx | 3 + .../[slug]/content/aditude/service.mdx | 3 + .../[slug]/content/adnimation/article.mdx | 16 +- .../[slug]/content/adnimation/intro.mdx | 2 +- .../[slug]/content/adnimation/joining.mdx | 7 + .../[slug]/content/adnimation/pay.mdx | 5 + .../[slug]/content/adnimation/rules.mdx | 5 + .../[slug]/content/adnimation/service.mdx | 5 + .../[slug]/content/adpushup/article.mdx | 12 +- .../[slug]/content/adpushup/intro.mdx | 2 +- .../[slug]/content/adpushup/joining.mdx | 3 + .../[slug]/content/adpushup/pay.mdx | 5 + .../[slug]/content/adpushup/rules.mdx | 3 + .../[slug]/content/adpushup/service.mdx | 3 + .../amazon-publisher-services/article.mdx | 16 +- .../amazon-publisher-services/intro.mdx | 2 +- .../amazon-publisher-services/joining.mdx | 5 + .../content/amazon-publisher-services/pay.mdx | 5 + .../amazon-publisher-services/rules.mdx | 3 + .../amazon-publisher-services/service.mdx | 3 + .../content/anymind-anymanager/article.mdx | 12 +- .../content/anymind-anymanager/intro.mdx | 2 +- .../content/anymind-anymanager/joining.mdx | 3 + .../[slug]/content/anymind-anymanager/pay.mdx | 3 + .../content/anymind-anymanager/rules.mdx | 3 + .../content/anymind-anymanager/service.mdx | 3 + .../content/buysellads-carbon-ads/article.mdx | 12 +- .../content/buysellads-carbon-ads/intro.mdx | 2 +- .../content/buysellads-carbon-ads/joining.mdx | 3 + .../content/buysellads-carbon-ads/pay.mdx | 3 + .../content/buysellads-carbon-ads/rules.mdx | 3 + .../content/buysellads-carbon-ads/service.mdx | 3 + .../[slug]/content/clickio/article.mdx | 16 +- .../[slug]/content/clickio/intro.mdx | 2 +- .../[slug]/content/clickio/joining.mdx | 5 + .../[slug]/content/clickio/pay.mdx | 5 + .../[slug]/content/clickio/rules.mdx | 3 + .../[slug]/content/clickio/service.mdx | 3 + .../[slug]/content/concert/article.mdx | 12 +- .../[slug]/content/concert/intro.mdx | 2 +- .../[slug]/content/concert/joining.mdx | 3 + .../[slug]/content/concert/pay.mdx | 3 + .../[slug]/content/concert/rules.mdx | 3 + .../[slug]/content/concert/service.mdx | 3 + .../[slug]/content/ethicalads/article.mdx | 16 +- .../[slug]/content/ethicalads/intro.mdx | 2 +- .../[slug]/content/ethicalads/joining.mdx | 5 + .../[slug]/content/ethicalads/pay.mdx | 3 + .../[slug]/content/ethicalads/rules.mdx | 5 + .../[slug]/content/ethicalads/service.mdx | 3 + .../[slug]/content/ezoic/article.mdx | 16 +- .../[slug]/content/ezoic/intro.mdx | 2 +- .../[slug]/content/ezoic/joining.mdx | 5 + .../ad-networks/[slug]/content/ezoic/pay.mdx | 5 + .../[slug]/content/ezoic/rules.mdx | 3 + .../[slug]/content/ezoic/service.mdx | 3 + .../[slug]/content/freestar/article.mdx | 16 +- .../[slug]/content/freestar/intro.mdx | 2 +- .../[slug]/content/freestar/joining.mdx | 7 + .../[slug]/content/freestar/pay.mdx | 3 + .../[slug]/content/freestar/rules.mdx | 3 + .../[slug]/content/freestar/service.mdx | 3 + .../[slug]/content/geniee-ssp/article.mdx | 12 +- .../[slug]/content/geniee-ssp/intro.mdx | 2 +- .../[slug]/content/geniee-ssp/joining.mdx | 3 + .../[slug]/content/geniee-ssp/pay.mdx | 3 + .../[slug]/content/geniee-ssp/rules.mdx | 3 + .../[slug]/content/geniee-ssp/service.mdx | 3 + .../[slug]/content/google-adsense/article.mdx | 16 +- .../[slug]/content/google-adsense/intro.mdx | 2 +- .../[slug]/content/google-adsense/joining.mdx | 5 + .../[slug]/content/google-adsense/pay.mdx | 5 + .../[slug]/content/google-adsense/rules.mdx | 3 + .../[slug]/content/google-adsense/service.mdx | 3 + .../ad-networks/[slug]/content/index.ts | 281 ++++-- .../[slug]/content/infolinks/article.mdx | 12 +- .../[slug]/content/infolinks/intro.mdx | 2 +- .../[slug]/content/infolinks/joining.mdx | 5 + .../[slug]/content/infolinks/pay.mdx | 5 + .../[slug]/content/infolinks/rules.mdx | 5 + .../[slug]/content/infolinks/service.mdx | 3 + .../content/journey-by-mediavine/article.mdx | 12 +- .../content/journey-by-mediavine/intro.mdx | 2 +- .../content/journey-by-mediavine/joining.mdx | 5 + .../content/journey-by-mediavine/pay.mdx | 3 + .../content/journey-by-mediavine/rules.mdx | 3 + .../content/journey-by-mediavine/service.mdx | 3 + .../[slug]/content/media-net/article.mdx | 12 +- .../[slug]/content/media-net/intro.mdx | 2 +- .../[slug]/content/media-net/joining.mdx | 5 + .../[slug]/content/media-net/pay.mdx | 3 + .../[slug]/content/media-net/rules.mdx | 5 + .../[slug]/content/media-net/service.mdx | 3 + .../[slug]/content/mediavine/article.mdx | 20 +- .../[slug]/content/mediavine/intro.mdx | 2 +- .../[slug]/content/mediavine/joining.mdx | 5 + .../[slug]/content/mediavine/pay.mdx | 5 + .../[slug]/content/mediavine/rules.mdx | 5 + .../[slug]/content/mediavine/service.mdx | 5 + .../[slug]/content/mgid/article.mdx | 12 +- .../ad-networks/[slug]/content/mgid/intro.mdx | 2 +- .../[slug]/content/mgid/joining.mdx | 5 + .../ad-networks/[slug]/content/mgid/pay.mdx | 5 + .../ad-networks/[slug]/content/mgid/rules.mdx | 5 + .../[slug]/content/mgid/service.mdx | 3 + .../content/monetizemore-pubguru/article.mdx | 12 +- .../content/monetizemore-pubguru/intro.mdx | 2 +- .../content/monetizemore-pubguru/joining.mdx | 5 + .../content/monetizemore-pubguru/pay.mdx | 5 + .../content/monetizemore-pubguru/rules.mdx | 3 + .../content/monetizemore-pubguru/service.mdx | 5 + .../[slug]/content/monumetric/article.mdx | 12 +- .../[slug]/content/monumetric/intro.mdx | 2 +- .../[slug]/content/monumetric/joining.mdx | 5 + .../[slug]/content/monumetric/pay.mdx | 5 + .../[slug]/content/monumetric/rules.mdx | 3 + .../[slug]/content/monumetric/service.mdx | 3 + .../[slug]/content/newor-media/article.mdx | 16 +- .../[slug]/content/newor-media/intro.mdx | 2 +- .../[slug]/content/newor-media/joining.mdx | 5 + .../[slug]/content/newor-media/pay.mdx | 5 + .../[slug]/content/newor-media/rules.mdx | 5 + .../[slug]/content/newor-media/service.mdx | 3 + .../[slug]/content/nitropay/article.mdx | 12 +- .../[slug]/content/nitropay/intro.mdx | 2 +- .../[slug]/content/nitropay/joining.mdx | 3 + .../[slug]/content/nitropay/pay.mdx | 5 + .../[slug]/content/nitropay/rules.mdx | 3 + .../[slug]/content/nitropay/service.mdx | 3 + .../[slug]/content/opti-digital/article.mdx | 12 +- .../[slug]/content/opti-digital/intro.mdx | 2 +- .../[slug]/content/opti-digital/joining.mdx | 3 + .../[slug]/content/opti-digital/pay.mdx | 7 + .../[slug]/content/opti-digital/rules.mdx | 3 + .../[slug]/content/opti-digital/service.mdx | 3 + .../[slug]/content/playwire/article.mdx | 16 +- .../[slug]/content/playwire/intro.mdx | 2 +- .../[slug]/content/playwire/joining.mdx | 5 + .../[slug]/content/playwire/pay.mdx | 3 + .../[slug]/content/playwire/rules.mdx | 3 + .../[slug]/content/playwire/service.mdx | 3 + .../[slug]/content/pubfuture/article.mdx | 12 +- .../[slug]/content/pubfuture/intro.mdx | 2 +- .../[slug]/content/pubfuture/joining.mdx | 3 + .../[slug]/content/pubfuture/pay.mdx | 3 + .../[slug]/content/pubfuture/rules.mdx | 3 + .../[slug]/content/pubfuture/service.mdx | 3 + .../[slug]/content/pubgalaxy/article.mdx | 20 +- .../[slug]/content/pubgalaxy/intro.mdx | 2 +- .../[slug]/content/pubgalaxy/joining.mdx | 5 + .../[slug]/content/pubgalaxy/pay.mdx | 5 + .../[slug]/content/pubgalaxy/rules.mdx | 5 + .../[slug]/content/pubgalaxy/service.mdx | 3 + .../[slug]/content/publift/article.mdx | 12 +- .../[slug]/content/publift/intro.mdx | 2 +- .../[slug]/content/publift/joining.mdx | 5 + .../[slug]/content/publift/pay.mdx | 5 + .../[slug]/content/publift/rules.mdx | 3 + .../[slug]/content/publift/service.mdx | 3 + .../[slug]/content/publir/article.mdx | 16 +- .../[slug]/content/publir/intro.mdx | 2 +- .../[slug]/content/publir/joining.mdx | 3 + .../ad-networks/[slug]/content/publir/pay.mdx | 5 + .../[slug]/content/publir/rules.mdx | 3 + .../[slug]/content/publir/service.mdx | 5 + .../content/publisher-collective/article.mdx | 16 +- .../content/publisher-collective/intro.mdx | 2 +- .../content/publisher-collective/joining.mdx | 5 + .../content/publisher-collective/pay.mdx | 5 + .../content/publisher-collective/rules.mdx | 5 + .../content/publisher-collective/service.mdx | 3 + .../[slug]/content/pubnation/article.mdx | 12 +- .../[slug]/content/pubnation/intro.mdx | 2 +- .../[slug]/content/pubnation/joining.mdx | 3 + .../[slug]/content/pubnation/pay.mdx | 3 + .../[slug]/content/pubnation/rules.mdx | 3 + .../[slug]/content/pubnation/service.mdx | 3 + .../[slug]/content/pubpower/article.mdx | 12 +- .../[slug]/content/pubpower/intro.mdx | 2 +- .../[slug]/content/pubpower/joining.mdx | 3 + .../[slug]/content/pubpower/pay.mdx | 5 + .../[slug]/content/pubpower/rules.mdx | 5 + .../[slug]/content/pubpower/service.mdx | 3 + .../[slug]/content/raptive/article.mdx | 12 +- .../[slug]/content/raptive/intro.mdx | 2 +- .../[slug]/content/raptive/joining.mdx | 5 + .../[slug]/content/raptive/pay.mdx | 3 + .../[slug]/content/raptive/rules.mdx | 3 + .../[slug]/content/raptive/service.mdx | 3 + .../[slug]/content/refinery89/article.mdx | 12 +- .../[slug]/content/refinery89/intro.mdx | 2 +- .../[slug]/content/refinery89/joining.mdx | 3 + .../[slug]/content/refinery89/pay.mdx | 5 + .../[slug]/content/refinery89/rules.mdx | 3 + .../[slug]/content/refinery89/service.mdx | 5 + .../[slug]/content/revcontent/article.mdx | 12 +- .../[slug]/content/revcontent/intro.mdx | 2 +- .../[slug]/content/revcontent/joining.mdx | 3 + .../[slug]/content/revcontent/pay.mdx | 5 + .../[slug]/content/revcontent/rules.mdx | 3 + .../[slug]/content/revcontent/service.mdx | 3 + .../[slug]/content/setupad/article.mdx | 12 +- .../[slug]/content/setupad/intro.mdx | 2 +- .../[slug]/content/setupad/joining.mdx | 5 + .../[slug]/content/setupad/pay.mdx | 3 + .../[slug]/content/setupad/rules.mdx | 3 + .../[slug]/content/setupad/service.mdx | 3 + .../[slug]/content/she-media/article.mdx | 12 +- .../[slug]/content/she-media/intro.mdx | 2 +- .../[slug]/content/she-media/joining.mdx | 5 + .../[slug]/content/she-media/pay.mdx | 5 + .../[slug]/content/she-media/rules.mdx | 3 + .../[slug]/content/she-media/service.mdx | 3 + .../[slug]/content/snack-media/article.mdx | 12 +- .../[slug]/content/snack-media/intro.mdx | 2 +- .../[slug]/content/snack-media/joining.mdx | 3 + .../[slug]/content/snack-media/pay.mdx | 5 + .../[slug]/content/snack-media/rules.mdx | 5 + .../[slug]/content/snack-media/service.mdx | 3 + .../[slug]/content/snigel/article.mdx | 1 - .../[slug]/content/snigel/intro.mdx | 1 - .../[slug]/content/sovrn/article.mdx | 12 +- .../[slug]/content/sovrn/intro.mdx | 2 +- .../[slug]/content/sovrn/joining.mdx | 3 + .../ad-networks/[slug]/content/sovrn/pay.mdx | 5 + .../[slug]/content/sovrn/rules.mdx | 3 + .../[slug]/content/sovrn/service.mdx | 3 + .../[slug]/content/sparteo/article.mdx | 12 +- .../[slug]/content/sparteo/intro.mdx | 2 +- .../[slug]/content/sparteo/joining.mdx | 3 + .../[slug]/content/sparteo/pay.mdx | 3 + .../[slug]/content/sparteo/rules.mdx | 3 + .../[slug]/content/sparteo/service.mdx | 5 + .../[slug]/content/taboola/article.mdx | 12 +- .../[slug]/content/taboola/intro.mdx | 2 +- .../[slug]/content/taboola/joining.mdx | 3 + .../[slug]/content/taboola/pay.mdx | 3 + .../[slug]/content/taboola/rules.mdx | 5 + .../[slug]/content/taboola/service.mdx | 3 + .../[slug]/content/teads/article.mdx | 12 +- .../[slug]/content/teads/intro.mdx | 2 +- .../[slug]/content/teads/joining.mdx | 3 + .../ad-networks/[slug]/content/teads/pay.mdx | 5 + .../[slug]/content/teads/rules.mdx | 3 + .../[slug]/content/teads/service.mdx | 3 + .../[slug]/content/the-moneytizer/article.mdx | 12 +- .../[slug]/content/the-moneytizer/intro.mdx | 2 +- .../[slug]/content/the-moneytizer/joining.mdx | 3 + .../[slug]/content/the-moneytizer/pay.mdx | 3 + .../[slug]/content/the-moneytizer/rules.mdx | 3 + .../[slug]/content/the-moneytizer/service.mdx | 3 + .../[slug]/content/venatus/article.mdx | 12 +- .../[slug]/content/venatus/intro.mdx | 2 +- .../[slug]/content/venatus/joining.mdx | 3 + .../[slug]/content/venatus/pay.mdx | 3 + .../[slug]/content/venatus/rules.mdx | 3 + .../[slug]/content/venatus/service.mdx | 3 + .../ad-networks/[slug]/network-details.tsx | 109 +++ .../ad-networks/[slug]/network-stats.tsx | 230 +++++ .../ad-networks/[slug]/network.module.css | 77 ++ .../(directories)/ad-networks/[slug]/page.tsx | 105 ++- .../ad-networks/ad-network-view.tsx | 112 --- .../ad-networks/ad-networks-shared.tsx | 46 + .../ad-networks/ad-networks-sidebar.tsx | 21 - .../ad-networks/ad-networks.module.css | 9 + .../ad-networks/content/article.mdx | 12 + .../ad-networks/content/intro.mdx | 1 - .../ad-networks/directory-browser.tsx | 252 ++++++ .../ad-networks/directory.module.css | 257 ++++++ .../(directories)/ad-networks/join-rules.ts | 79 ++ .../ad-networks/network-facts.ts | 70 ++ .../[niche]/content/automotive/intro.mdx | 1 + .../[niche]/content/beauty-fashion/intro.mdx | 1 + .../niches/[niche]/content/business/intro.mdx | 1 + .../niches/[niche]/content/crafts/intro.mdx | 1 + .../[niche]/content/education/intro.mdx | 1 + .../[niche]/content/entertainment/intro.mdx | 1 + .../niches/[niche]/content/food/intro.mdx | 1 + .../niches/[niche]/content/gaming/intro.mdx | 1 + .../[niche]/content/gardening/intro.mdx | 1 + .../[niche]/content/health-fitness/intro.mdx | 1 + .../niches/[niche]/content/home-diy/intro.mdx | 1 + .../niches/[niche]/content/index.ts | 45 + .../[niche]/content/lifestyle/intro.mdx | 1 + .../niches/[niche]/content/news/intro.mdx | 1 + .../niches/[niche]/content/outdoors/intro.mdx | 1 + .../[niche]/content/parenting/intro.mdx | 1 + .../content/personal-finance/intro.mdx | 1 + .../niches/[niche]/content/pets/intro.mdx | 1 + .../niches/[niche]/content/sports/intro.mdx | 1 + .../niches/[niche]/content/tech/intro.mdx | 1 + .../niches/[niche]/content/travel/intro.mdx | 1 + .../ad-networks/niches/[niche]/layout.tsx | 7 + .../ad-networks/niches/[niche]/niche-view.tsx | 159 ++++ .../ad-networks/niches/[niche]/page.tsx | 27 + .../ad-networks/niches/content/article.mdx | 7 + .../ad-networks/niches/content/intro.mdx | 1 + .../ad-networks/niches/layout.tsx | 6 + .../ad-networks/niches/niches-view.tsx | 112 +++ .../ad-networks/niches/niches.module.css | 21 + .../(directories)/ad-networks/niches/page.tsx | 30 + .../(directories)/ad-networks/page.module.css | 226 ----- app/(site)/(directories)/ad-networks/page.tsx | 125 ++- .../ad-networks/stats/content/article.mdx | 3 + .../ad-networks/stats/content/intro.mdx | 1 + .../ad-networks/stats/layout.tsx | 6 + .../(directories)/ad-networks/stats/page.tsx | 304 +++++++ .../ad-networks/stats/stats.module.css | 5 + .../ad-network-checker-from-url.tsx | 10 + .../ad-network-checker-tool.tsx | 536 ++++++++++++ .../ad-network-checker-view.ts | 191 +++++ .../ad-network-checker.module.css | 170 ++++ .../ad-network-checker/content/article.mdx | 41 + .../ad-network-checker/content/intro.mdx | 1 + .../ad-network-checker/content/usage.mdx | 35 + .../(tools)/ad-network-checker/layout.tsx | 6 + .../(tools)/ad-network-checker/page.tsx | 51 ++ app/api/ad-networks/input.ts | 53 ++ app/api/ad-networks/site/route.ts | 27 + app/api/ad-networks/sites/route.ts | 25 + app/api/cache/refresh/route.ts | 52 +- app/ui/ad-networks/confirmed-ring.tsx | 48 ++ app/ui/ad-networks/details.module.css | 172 ++++ app/ui/ad-networks/details.tsx | 87 ++ app/ui/ad-networks/network-card.module.css | 170 ++++ app/ui/ad-networks/network-card.tsx | 55 ++ app/ui/ad-networks/network-ladder.module.css | 79 ++ app/ui/ad-networks/network-ladder.tsx | 86 ++ app/ui/ad-networks/network-logo-src.ts | 16 + app/ui/ad-networks/network-logo.module.css | 51 ++ app/ui/ad-networks/network-logo.tsx | 22 + app/ui/ad-networks/niches-table.module.css | 196 +++++ app/ui/ad-networks/niches-table.tsx | 127 +++ app/ui/ad-networks/section.module.css | 50 ++ app/ui/ad-networks/section.tsx | 41 + app/ui/ad-networks/site-format.ts | 59 ++ app/ui/ad-networks/site-view.module.css | 378 +++++++++ app/ui/ad-networks/site-view.tsx | 137 +++ app/ui/ad-networks/sites-table.module.css | 230 +++++ app/ui/ad-networks/sites-table.tsx | 342 ++++++++ app/ui/ad-networks/traffic-meter.module.css | 25 + app/ui/ad-networks/traffic-meter.tsx | 22 + app/ui/ad-networks/viz/age-strip.tsx | 72 ++ app/ui/ad-networks/viz/area-line.tsx | 82 ++ app/ui/ad-networks/viz/bars.tsx | 77 ++ app/ui/ad-networks/viz/chart-frame.tsx | 151 ++++ app/ui/ad-networks/viz/columns.tsx | 37 + app/ui/ad-networks/viz/diverging-bars.tsx | 47 + app/ui/ad-networks/viz/donut.tsx | 66 ++ app/ui/ad-networks/viz/dot-plot.tsx | 44 + app/ui/ad-networks/viz/format.ts | 106 +++ app/ui/ad-networks/viz/heatmap.tsx | 63 ++ app/ui/ad-networks/viz/history-gantt.tsx | 68 ++ app/ui/ad-networks/viz/joined-left.tsx | 116 +++ app/ui/ad-networks/viz/lists.tsx | 34 + app/ui/ad-networks/viz/multi-line.tsx | 101 +++ app/ui/ad-networks/viz/parts.tsx | 49 ++ app/ui/ad-networks/viz/publisher-blocks.tsx | 34 + app/ui/ad-networks/viz/scatter-plot.tsx | 102 +++ app/ui/ad-networks/viz/size-stacks.tsx | 80 ++ app/ui/ad-networks/viz/stacked-columns.tsx | 78 ++ app/ui/ad-networks/viz/switch-flow.tsx | 103 +++ app/ui/ad-networks/viz/treemap.tsx | 121 +++ app/ui/ad-networks/viz/type-scale.tsx | 21 + app/ui/ad-networks/viz/viz.module.css | 803 ++++++++++++++++++ app/ui/art/ad-network-checker.spec.json | 11 + app/ui/art/ad-network-checker.svg | 20 + app/ui/art/ad-network-checker.ts | 22 + app/ui/pagination.module.css | 22 + app/ui/pagination.tsx | 19 + app/ui/report.module.css | 253 ++++++ app/ui/report.tsx | 130 +++ app/ui/table-toolbar.module.css | 255 ++++++ app/ui/table-toolbar.tsx | 123 +++ app/ui/tabs.module.css | 86 ++ app/ui/tabs.tsx | 106 +++ app/ui/tools/tool-page-shell.tsx | 4 + content_docs/ad-network-checker/research.md | 52 ++ content_docs/ad-network-checker/tool-spec.md | 77 ++ content_docs/ad-networks/brief.md | 4 +- content_docs/ad-networks/maintenance.md | 47 +- content_docs/ad-networks/plan.md | 12 +- content_docs/ad-networks/research/adapex.md | 29 + content_docs/ad-networks/research/aditude.md | 28 + .../ad-networks/research/adnimation.md | 29 + content_docs/ad-networks/research/adpushup.md | 29 + .../research/amazon-publisher-services.md | 28 + .../research/anymind-anymanager.md | 28 + .../research/buysellads-carbon-ads.md | 28 + content_docs/ad-networks/research/clickio.md | 30 + content_docs/ad-networks/research/concert.md | 28 + .../ad-networks/research/ethicalads.md | 28 + content_docs/ad-networks/research/ezoic.md | 28 + content_docs/ad-networks/research/freestar.md | 28 + .../ad-networks/research/geniee-ssp.md | 28 + .../ad-networks/research/google-adsense.md | 28 + .../ad-networks/research/infolinks.md | 28 + .../research/journey-by-mediavine.md | 28 + .../ad-networks/research/media-net.md | 28 + .../ad-networks/research/mediavine.md | 29 + content_docs/ad-networks/research/mgid.md | 28 + .../research/monetizemore-pubguru.md | 29 + .../ad-networks/research/monumetric.md | 28 + .../ad-networks/research/newor-media.md | 29 + content_docs/ad-networks/research/nitropay.md | 28 + .../ad-networks/research/opti-digital.md | 28 + content_docs/ad-networks/research/playwire.md | 28 + .../ad-networks/research/pubfuture.md | 28 + .../ad-networks/research/pubgalaxy.md | 34 + content_docs/ad-networks/research/publift.md | 29 + content_docs/ad-networks/research/publir.md | 28 + .../research/publisher-collective.md | 28 + .../ad-networks/research/pubnation.md | 28 + content_docs/ad-networks/research/pubpower.md | 28 + content_docs/ad-networks/research/raptive.md | 28 + .../ad-networks/research/refinery89.md | 28 + .../ad-networks/research/revcontent.md | 28 + content_docs/ad-networks/research/setupad.md | 28 + .../ad-networks/research/she-media.md | 28 + .../ad-networks/research/snack-media.md | 28 + content_docs/ad-networks/research/snigel.md | 36 + content_docs/ad-networks/research/sovrn.md | 28 + content_docs/ad-networks/research/sparteo.md | 28 + content_docs/ad-networks/research/taboola.md | 28 + content_docs/ad-networks/research/teads.md | 28 + .../ad-networks/research/the-moneytizer.md | 28 + content_docs/ad-networks/research/venatus.md | 28 + content_docs/domain-tools/README.md | 4 + dev-space/ad-networks-build-plan.md | 79 ++ dev-space/ad-networks-data-contract.md | 200 +++++ dev-space/ad-networks-tracker.md | 68 +- lib/ad-network-niches.ts | 40 + lib/ad-network-site-facts.mjs | 107 +++ lib/ad-networks-cache-verification.ts | 45 + lib/ad-networks-report.ts | 217 +++++ lib/ad-networks-types.ts | 176 ++++ lib/ad-networks.ts | 594 ++++++++++++- lib/card-art.ts | 2 + lib/registered-site-pages.json | 4 + next.config.ts | 1 + package.json | 1 + proxy.ts | 2 +- public/ad-networks/logos/adapex.png | Bin 0 -> 531 bytes public/ad-networks/logos/aditude.png | Bin 0 -> 1591 bytes public/ad-networks/logos/adnimation.png | Bin 0 -> 656 bytes public/ad-networks/logos/adpushup.png | Bin 0 -> 3650 bytes .../logos/amazon-publisher-services.png | Bin 0 -> 658 bytes .../ad-networks/logos/anymind-anymanager.png | Bin 0 -> 766 bytes .../logos/buysellads-carbon-ads.png | Bin 0 -> 603 bytes public/ad-networks/logos/clickio.png | Bin 0 -> 1915 bytes public/ad-networks/logos/concert.png | Bin 0 -> 430 bytes public/ad-networks/logos/ethicalads.png | Bin 0 -> 1336 bytes public/ad-networks/logos/ezoic.png | Bin 0 -> 676 bytes public/ad-networks/logos/freestar.png | Bin 0 -> 615 bytes public/ad-networks/logos/geniee-ssp.png | Bin 0 -> 2090 bytes public/ad-networks/logos/google-adsense.png | Bin 0 -> 3654 bytes public/ad-networks/logos/infolinks.png | Bin 0 -> 2396 bytes .../logos/journey-by-mediavine.png | Bin 0 -> 777 bytes public/ad-networks/logos/media-net.png | Bin 0 -> 576 bytes public/ad-networks/logos/mediavine.png | Bin 0 -> 540 bytes public/ad-networks/logos/mgid.png | Bin 0 -> 584 bytes .../logos/monetizemore-pubguru.png | Bin 0 -> 1007 bytes public/ad-networks/logos/monumetric.png | Bin 0 -> 939 bytes public/ad-networks/logos/newor-media.png | Bin 0 -> 3429 bytes public/ad-networks/logos/nitropay.png | Bin 0 -> 622 bytes public/ad-networks/logos/opti-digital.png | Bin 0 -> 634 bytes public/ad-networks/logos/playwire.png | Bin 0 -> 962 bytes public/ad-networks/logos/pubfuture.png | Bin 0 -> 891 bytes public/ad-networks/logos/pubgalaxy.png | Bin 0 -> 605 bytes public/ad-networks/logos/publift.png | Bin 0 -> 1008 bytes public/ad-networks/logos/publir.png | Bin 0 -> 423 bytes .../logos/publisher-collective.png | Bin 0 -> 960 bytes public/ad-networks/logos/pubnation.png | Bin 0 -> 610 bytes public/ad-networks/logos/pubpower.png | Bin 0 -> 1455 bytes public/ad-networks/logos/raptive.png | Bin 0 -> 670 bytes public/ad-networks/logos/refinery89.png | Bin 0 -> 533 bytes public/ad-networks/logos/revcontent.png | Bin 0 -> 544 bytes public/ad-networks/logos/setupad.png | Bin 0 -> 620 bytes public/ad-networks/logos/she-media.png | Bin 0 -> 1129 bytes public/ad-networks/logos/snack-media.png | Bin 0 -> 631 bytes public/ad-networks/logos/sovrn.png | Bin 0 -> 605 bytes public/ad-networks/logos/sparteo.png | Bin 0 -> 573 bytes public/ad-networks/logos/taboola.png | Bin 0 -> 590 bytes public/ad-networks/logos/teads.png | Bin 0 -> 1587 bytes public/ad-networks/logos/the-moneytizer.png | Bin 0 -> 924 bytes public/ad-networks/logos/venatus.png | Bin 0 -> 426 bytes scripts/ad-networks/local-db.mjs | 3 + scripts/ad-networks/matcher.mjs | 3 +- scripts/ad-networks/merge-identities.mjs | 62 ++ scripts/ad-networks/movements.mjs | 68 ++ scripts/ad-networks/network-identities.mjs | 9 +- scripts/ad-networks/phase2-sources.mjs | 2 +- scripts/ad-networks/ready.sql | 24 + scripts/ad-networks/retired-network-ready.sql | 2 + scripts/ad-networks/site-facts.mjs | 2 + scripts/ad-networks/sources.mjs | 2 +- scripts/ad-networks/sync.mjs | 145 ++++ .../stack127/cache/process-revalidation.mjs | 2 +- scripts/stack127/cache/targets.mjs | 19 +- scripts/stack127/frontend/assets.mjs | 9 + scripts/stack127/frontend/build.mjs | 7 +- scripts/stack127/frontend/content-queue.mjs | 2 +- scripts/stack127/marker-art/figures.mjs | 20 + stack127_docs/shared-ui.md | 70 +- supabase/functions/cache-worker/index.ts | 4 +- ...28_ad_network_site_facts_and_snapshots.sql | 168 ++++ ...twork_observed_movement_classification.sql | 45 + ...d_network_report_evidence_revalidation.sql | 51 ++ ...2558_ad_network_report_source_revision.sql | 106 +++ ...merge_snigel_into_publisher_collective.sql | 90 ++ tests/pages/ad-networks/backend.test.mjs | 184 ++++ .../ad-networks/cache-verification.test.mjs | 114 +++ tests/pages/ad-networks/chart-data.test.mjs | 102 +++ .../ad-networks/checker-domains.test.mjs | 58 ++ .../pages/ad-networks/checker-events.test.mjs | 88 ++ .../pages/ad-networks/identity-merge.test.mjs | 59 ++ tests/pages/ad-networks/loader-harness.mjs | 27 + .../ad-networks/phase4-verifier.test.mjs | 2 +- tests/pages/ad-networks/publishers.test.mjs | 11 + tests/pages/ad-networks/report.test.mjs | 106 +++ tests/stack127/cache-targets.test.mjs | 18 +- tests/stack127/frontend-static.test.mjs | 12 +- tests/stack127/self-host-apps-search.test.mjs | 6 + 543 files changed, 16030 insertions(+), 657 deletions(-) create mode 100644 app/(site)/(components-ui)/components-ui/examples/ad-network-demos.tsx create mode 100644 app/(site)/(components-ui)/components-ui/examples/data-visuals.tsx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx delete mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx delete mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/network-details.tsx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/network-stats.tsx create mode 100644 app/(site)/(directories)/ad-networks/[slug]/network.module.css delete mode 100644 app/(site)/(directories)/ad-networks/ad-network-view.tsx create mode 100644 app/(site)/(directories)/ad-networks/ad-networks-shared.tsx delete mode 100644 app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx create mode 100644 app/(site)/(directories)/ad-networks/ad-networks.module.css delete mode 100644 app/(site)/(directories)/ad-networks/content/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/directory-browser.tsx create mode 100644 app/(site)/(directories)/ad-networks/directory.module.css create mode 100644 app/(site)/(directories)/ad-networks/join-rules.ts create mode 100644 app/(site)/(directories)/ad-networks/network-facts.ts create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx create mode 100644 app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx create mode 100644 app/(site)/(directories)/ad-networks/niches/content/article.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/content/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/niches/layout.tsx create mode 100644 app/(site)/(directories)/ad-networks/niches/niches-view.tsx create mode 100644 app/(site)/(directories)/ad-networks/niches/niches.module.css create mode 100644 app/(site)/(directories)/ad-networks/niches/page.tsx delete mode 100644 app/(site)/(directories)/ad-networks/page.module.css create mode 100644 app/(site)/(directories)/ad-networks/stats/content/article.mdx create mode 100644 app/(site)/(directories)/ad-networks/stats/content/intro.mdx create mode 100644 app/(site)/(directories)/ad-networks/stats/layout.tsx create mode 100644 app/(site)/(directories)/ad-networks/stats/page.tsx create mode 100644 app/(site)/(directories)/ad-networks/stats/stats.module.css create mode 100644 app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx create mode 100644 app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx create mode 100644 app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts create mode 100644 app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css create mode 100644 app/(site)/(tools)/ad-network-checker/content/article.mdx create mode 100644 app/(site)/(tools)/ad-network-checker/content/intro.mdx create mode 100644 app/(site)/(tools)/ad-network-checker/content/usage.mdx create mode 100644 app/(site)/(tools)/ad-network-checker/layout.tsx create mode 100644 app/(site)/(tools)/ad-network-checker/page.tsx create mode 100644 app/api/ad-networks/input.ts create mode 100644 app/api/ad-networks/site/route.ts create mode 100644 app/api/ad-networks/sites/route.ts create mode 100644 app/ui/ad-networks/confirmed-ring.tsx create mode 100644 app/ui/ad-networks/details.module.css create mode 100644 app/ui/ad-networks/details.tsx create mode 100644 app/ui/ad-networks/network-card.module.css create mode 100644 app/ui/ad-networks/network-card.tsx create mode 100644 app/ui/ad-networks/network-ladder.module.css create mode 100644 app/ui/ad-networks/network-ladder.tsx create mode 100644 app/ui/ad-networks/network-logo-src.ts create mode 100644 app/ui/ad-networks/network-logo.module.css create mode 100644 app/ui/ad-networks/network-logo.tsx create mode 100644 app/ui/ad-networks/niches-table.module.css create mode 100644 app/ui/ad-networks/niches-table.tsx create mode 100644 app/ui/ad-networks/section.module.css create mode 100644 app/ui/ad-networks/section.tsx create mode 100644 app/ui/ad-networks/site-format.ts create mode 100644 app/ui/ad-networks/site-view.module.css create mode 100644 app/ui/ad-networks/site-view.tsx create mode 100644 app/ui/ad-networks/sites-table.module.css create mode 100644 app/ui/ad-networks/sites-table.tsx create mode 100644 app/ui/ad-networks/traffic-meter.module.css create mode 100644 app/ui/ad-networks/traffic-meter.tsx create mode 100644 app/ui/ad-networks/viz/age-strip.tsx create mode 100644 app/ui/ad-networks/viz/area-line.tsx create mode 100644 app/ui/ad-networks/viz/bars.tsx create mode 100644 app/ui/ad-networks/viz/chart-frame.tsx create mode 100644 app/ui/ad-networks/viz/columns.tsx create mode 100644 app/ui/ad-networks/viz/diverging-bars.tsx create mode 100644 app/ui/ad-networks/viz/donut.tsx create mode 100644 app/ui/ad-networks/viz/dot-plot.tsx create mode 100644 app/ui/ad-networks/viz/format.ts create mode 100644 app/ui/ad-networks/viz/heatmap.tsx create mode 100644 app/ui/ad-networks/viz/history-gantt.tsx create mode 100644 app/ui/ad-networks/viz/joined-left.tsx create mode 100644 app/ui/ad-networks/viz/lists.tsx create mode 100644 app/ui/ad-networks/viz/multi-line.tsx create mode 100644 app/ui/ad-networks/viz/parts.tsx create mode 100644 app/ui/ad-networks/viz/publisher-blocks.tsx create mode 100644 app/ui/ad-networks/viz/scatter-plot.tsx create mode 100644 app/ui/ad-networks/viz/size-stacks.tsx create mode 100644 app/ui/ad-networks/viz/stacked-columns.tsx create mode 100644 app/ui/ad-networks/viz/switch-flow.tsx create mode 100644 app/ui/ad-networks/viz/treemap.tsx create mode 100644 app/ui/ad-networks/viz/type-scale.tsx create mode 100644 app/ui/ad-networks/viz/viz.module.css create mode 100644 app/ui/art/ad-network-checker.spec.json create mode 100644 app/ui/art/ad-network-checker.svg create mode 100644 app/ui/art/ad-network-checker.ts create mode 100644 app/ui/report.module.css create mode 100644 app/ui/report.tsx create mode 100644 app/ui/table-toolbar.module.css create mode 100644 app/ui/table-toolbar.tsx create mode 100644 app/ui/tabs.module.css create mode 100644 app/ui/tabs.tsx create mode 100644 content_docs/ad-network-checker/research.md create mode 100644 content_docs/ad-network-checker/tool-spec.md create mode 100644 content_docs/ad-networks/research/adapex.md create mode 100644 content_docs/ad-networks/research/aditude.md create mode 100644 content_docs/ad-networks/research/adnimation.md create mode 100644 content_docs/ad-networks/research/adpushup.md create mode 100644 content_docs/ad-networks/research/amazon-publisher-services.md create mode 100644 content_docs/ad-networks/research/anymind-anymanager.md create mode 100644 content_docs/ad-networks/research/buysellads-carbon-ads.md create mode 100644 content_docs/ad-networks/research/clickio.md create mode 100644 content_docs/ad-networks/research/concert.md create mode 100644 content_docs/ad-networks/research/ethicalads.md create mode 100644 content_docs/ad-networks/research/ezoic.md create mode 100644 content_docs/ad-networks/research/freestar.md create mode 100644 content_docs/ad-networks/research/geniee-ssp.md create mode 100644 content_docs/ad-networks/research/google-adsense.md create mode 100644 content_docs/ad-networks/research/infolinks.md create mode 100644 content_docs/ad-networks/research/journey-by-mediavine.md create mode 100644 content_docs/ad-networks/research/media-net.md create mode 100644 content_docs/ad-networks/research/mediavine.md create mode 100644 content_docs/ad-networks/research/mgid.md create mode 100644 content_docs/ad-networks/research/monetizemore-pubguru.md create mode 100644 content_docs/ad-networks/research/monumetric.md create mode 100644 content_docs/ad-networks/research/newor-media.md create mode 100644 content_docs/ad-networks/research/nitropay.md create mode 100644 content_docs/ad-networks/research/opti-digital.md create mode 100644 content_docs/ad-networks/research/playwire.md create mode 100644 content_docs/ad-networks/research/pubfuture.md create mode 100644 content_docs/ad-networks/research/pubgalaxy.md create mode 100644 content_docs/ad-networks/research/publift.md create mode 100644 content_docs/ad-networks/research/publir.md create mode 100644 content_docs/ad-networks/research/publisher-collective.md create mode 100644 content_docs/ad-networks/research/pubnation.md create mode 100644 content_docs/ad-networks/research/pubpower.md create mode 100644 content_docs/ad-networks/research/raptive.md create mode 100644 content_docs/ad-networks/research/refinery89.md create mode 100644 content_docs/ad-networks/research/revcontent.md create mode 100644 content_docs/ad-networks/research/setupad.md create mode 100644 content_docs/ad-networks/research/she-media.md create mode 100644 content_docs/ad-networks/research/snack-media.md create mode 100644 content_docs/ad-networks/research/snigel.md create mode 100644 content_docs/ad-networks/research/sovrn.md create mode 100644 content_docs/ad-networks/research/sparteo.md create mode 100644 content_docs/ad-networks/research/taboola.md create mode 100644 content_docs/ad-networks/research/teads.md create mode 100644 content_docs/ad-networks/research/the-moneytizer.md create mode 100644 content_docs/ad-networks/research/venatus.md create mode 100644 dev-space/ad-networks-build-plan.md create mode 100644 dev-space/ad-networks-data-contract.md create mode 100644 lib/ad-network-niches.ts create mode 100644 lib/ad-network-site-facts.mjs create mode 100644 lib/ad-networks-cache-verification.ts create mode 100644 lib/ad-networks-report.ts create mode 100644 lib/ad-networks-types.ts create mode 100644 public/ad-networks/logos/adapex.png create mode 100644 public/ad-networks/logos/aditude.png create mode 100644 public/ad-networks/logos/adnimation.png create mode 100644 public/ad-networks/logos/adpushup.png create mode 100644 public/ad-networks/logos/amazon-publisher-services.png create mode 100644 public/ad-networks/logos/anymind-anymanager.png create mode 100644 public/ad-networks/logos/buysellads-carbon-ads.png create mode 100644 public/ad-networks/logos/clickio.png create mode 100644 public/ad-networks/logos/concert.png create mode 100644 public/ad-networks/logos/ethicalads.png create mode 100644 public/ad-networks/logos/ezoic.png create mode 100644 public/ad-networks/logos/freestar.png create mode 100644 public/ad-networks/logos/geniee-ssp.png create mode 100644 public/ad-networks/logos/google-adsense.png create mode 100644 public/ad-networks/logos/infolinks.png create mode 100644 public/ad-networks/logos/journey-by-mediavine.png create mode 100644 public/ad-networks/logos/media-net.png create mode 100644 public/ad-networks/logos/mediavine.png create mode 100644 public/ad-networks/logos/mgid.png create mode 100644 public/ad-networks/logos/monetizemore-pubguru.png create mode 100644 public/ad-networks/logos/monumetric.png create mode 100644 public/ad-networks/logos/newor-media.png create mode 100644 public/ad-networks/logos/nitropay.png create mode 100644 public/ad-networks/logos/opti-digital.png create mode 100644 public/ad-networks/logos/playwire.png create mode 100644 public/ad-networks/logos/pubfuture.png create mode 100644 public/ad-networks/logos/pubgalaxy.png create mode 100644 public/ad-networks/logos/publift.png create mode 100644 public/ad-networks/logos/publir.png create mode 100644 public/ad-networks/logos/publisher-collective.png create mode 100644 public/ad-networks/logos/pubnation.png create mode 100644 public/ad-networks/logos/pubpower.png create mode 100644 public/ad-networks/logos/raptive.png create mode 100644 public/ad-networks/logos/refinery89.png create mode 100644 public/ad-networks/logos/revcontent.png create mode 100644 public/ad-networks/logos/setupad.png create mode 100644 public/ad-networks/logos/she-media.png create mode 100644 public/ad-networks/logos/snack-media.png create mode 100644 public/ad-networks/logos/sovrn.png create mode 100644 public/ad-networks/logos/sparteo.png create mode 100644 public/ad-networks/logos/taboola.png create mode 100644 public/ad-networks/logos/teads.png create mode 100644 public/ad-networks/logos/the-moneytizer.png create mode 100644 public/ad-networks/logos/venatus.png create mode 100644 scripts/ad-networks/merge-identities.mjs create mode 100644 scripts/ad-networks/movements.mjs create mode 100644 scripts/ad-networks/ready.sql create mode 100644 scripts/ad-networks/retired-network-ready.sql create mode 100644 scripts/ad-networks/site-facts.mjs create mode 100644 scripts/ad-networks/sync.mjs create mode 100644 supabase/migrations/20261015183928_ad_network_site_facts_and_snapshots.sql create mode 100644 supabase/migrations/20261015193817_ad_network_observed_movement_classification.sql create mode 100644 supabase/migrations/20261016032034_ad_network_report_evidence_revalidation.sql create mode 100644 supabase/migrations/20261016032558_ad_network_report_source_revision.sql create mode 100644 supabase/migrations/20261016060000_merge_snigel_into_publisher_collective.sql create mode 100644 tests/pages/ad-networks/backend.test.mjs create mode 100644 tests/pages/ad-networks/cache-verification.test.mjs create mode 100644 tests/pages/ad-networks/chart-data.test.mjs create mode 100644 tests/pages/ad-networks/checker-domains.test.mjs create mode 100644 tests/pages/ad-networks/checker-events.test.mjs create mode 100644 tests/pages/ad-networks/identity-merge.test.mjs create mode 100644 tests/pages/ad-networks/loader-harness.mjs create mode 100644 tests/pages/ad-networks/report.test.mjs diff --git a/app/(site)/(components-ui)/components-ui/[section]/page.tsx b/app/(site)/(components-ui)/components-ui/[section]/page.tsx index 03cb278b..9b498b24 100644 --- a/app/(site)/(components-ui)/components-ui/[section]/page.tsx +++ b/app/(site)/(components-ui)/components-ui/[section]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"; import { ActionsAndFormsExamples } from "../examples/actions-and-forms"; import { CardsExamples } from "../examples/cards"; import { ContentExamples } from "../examples/content"; +import { DataVisualsExamples } from "../examples/data-visuals"; import { FeedbackAndDialogsExamples } from "../examples/feedback-and-dialogs"; import { FileUploadsExamples } from "../examples/file-uploads"; import { FiltersAndSortingExamples } from "../examples/filters-and-sorting"; @@ -26,6 +27,7 @@ const examples: Record = { "filters-and-sorting": FiltersAndSortingExamples, results: ResultsExamples, tables: TablesExamples, + "data-visuals": DataVisualsExamples, cards: CardsExamples, content: ContentExamples, prose: ProseExamples, diff --git a/app/(site)/(components-ui)/components-ui/examples/ad-network-demos.tsx b/app/(site)/(components-ui)/components-ui/examples/ad-network-demos.tsx new file mode 100644 index 00000000..92c3d119 --- /dev/null +++ b/app/(site)/(components-ui)/components-ui/examples/ad-network-demos.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useState } from "react"; +import type { AdNetworkSitesResponse } from "@/lib/ad-networks-types"; +import { SitesTable } from "@/app/ui/ad-networks/sites-table"; +import { TableToolbar, ToolbarSearch, ToolbarSelect, ToolbarSwitch } from "@/app/ui/table-toolbar"; + +// Working demos for the gallery. The sites table's first page is sample data; searching or +// paging asks the real development API. + +export function ToolbarDemo() { + const [hero, setHero] = useState(""); + const [query, setQuery] = useState(""); + const [sort, setSort] = useState("sites"); + const [niche, setNiche] = useState(""); + const [order, setOrder] = useState("used"); + return ( +
          +
          + +
          + + + + + + +
          + ); +} + +const sample: AdNetworkSitesResponse = { + page: 1, + pageSize: 30, + total: 3, + totalPages: 1, + effectiveSort: "traffic", + availableColumns: ["domain", "niche", "traffic", "domain_age", "platform", "manager", "checked", "publisher"], + availableSorts: ["traffic", "oldest", "newest"], + availableFilters: ["niche"], + filters: { niches: [], networks: [] }, + rows: [ + { domain: "tinykitchenlab.com", publisher_name: "Tiny Kitchen Lab LLC", niche: "food", traffic_group: "top-10k", domain_created_on: "2014-03-02", platform: "WordPress", managed_by: { slug: "mediavine", name: "Mediavine" }, manager_domain: "mediavine.com", last_checked_at: "2026-10-12T08:00:00Z", favicon: null, networks: [{ slug: "mediavine", name: "Mediavine" }] }, + { domain: "wanderwithmara.com", publisher_name: null, niche: "travel", traffic_group: "top-100k", domain_created_on: "2016-07-21", platform: "WordPress", managed_by: { slug: "mediavine", name: "Mediavine" }, manager_domain: "mediavine.com", last_checked_at: "2026-10-11T08:00:00Z", favicon: null, networks: [{ slug: "mediavine", name: "Mediavine" }] }, + { domain: "budgetpantry.net", publisher_name: "Pantry Press Ltd", niche: "food", traffic_group: "top-1m", domain_created_on: "2012-01-10", platform: "Next.js", managed_by: null, manager_domain: null, last_checked_at: "2026-10-10T08:00:00Z", favicon: null, networks: [{ slug: "mediavine", name: "Mediavine" }] }, + ], +}; + +export function SitesTableDemo() { + return ( + + ); +} diff --git a/app/(site)/(components-ui)/components-ui/examples/cards.tsx b/app/(site)/(components-ui)/components-ui/examples/cards.tsx index 9328f8d1..98de5a53 100644 --- a/app/(site)/(components-ui)/components-ui/examples/cards.tsx +++ b/app/(site)/(components-ui)/components-ui/examples/cards.tsx @@ -8,6 +8,8 @@ import { ToolCard } from "@/app/ui/cards/tool-card"; import { DirectoryAppCard } from "@/app/ui/directories/directory-app-card"; import { HardwareFactCardContent, hardwareFactCardClassName } from "@/app/ui/directories/hardware-fact-card"; import type { DirectoryAppCardData } from "@/lib/apps-card"; +import { NetworkCard, NetworkGrid, SortNote } from "@/app/ui/ad-networks/network-card"; +import { NetworkLogo } from "@/app/ui/ad-networks/network-logo"; import { Example } from "../gallery"; import styles from "../components-ui.module.css"; @@ -78,6 +80,37 @@ export function CardsExamples() { + +
          + + + + + + Mediavine + +
          +
          + + Sorted by how many Food sites use each network. + + + + + + + + ); } diff --git a/app/(site)/(components-ui)/components-ui/examples/data-visuals.tsx b/app/(site)/(components-ui)/components-ui/examples/data-visuals.tsx new file mode 100644 index 00000000..f0455977 --- /dev/null +++ b/app/(site)/(components-ui)/components-ui/examples/data-visuals.tsx @@ -0,0 +1,407 @@ +import { ReportFigure, ReportRow, ReportBlock } from "@/app/ui/report"; +import { AgeStrip } from "@/app/ui/ad-networks/viz/age-strip"; +import { AreaLine } from "@/app/ui/ad-networks/viz/area-line"; +import { MoveBars, RankedBars, StackBar } from "@/app/ui/ad-networks/viz/bars"; +import { ColumnChart } from "@/app/ui/ad-networks/viz/columns"; +import { DivergingBars } from "@/app/ui/ad-networks/viz/diverging-bars"; +import { Donut } from "@/app/ui/ad-networks/viz/donut"; +import { DotPlot } from "@/app/ui/ad-networks/viz/dot-plot"; +import { Heatmap } from "@/app/ui/ad-networks/viz/heatmap"; +import { EventsList, NewestList } from "@/app/ui/ad-networks/viz/lists"; +import { HistoryGantt } from "@/app/ui/ad-networks/viz/history-gantt"; +import { JoinedLeftBars, type JoinedLeftPoint } from "@/app/ui/ad-networks/viz/joined-left"; +import { MultiLine } from "@/app/ui/ad-networks/viz/multi-line"; +import { PublisherBlocks } from "@/app/ui/ad-networks/viz/publisher-blocks"; +import { ScatterPlot } from "@/app/ui/ad-networks/viz/scatter-plot"; +import { SizeStacks } from "@/app/ui/ad-networks/viz/size-stacks"; +import { StackedColumns } from "@/app/ui/ad-networks/viz/stacked-columns"; +import { SwitchFlow } from "@/app/ui/ad-networks/viz/switch-flow"; +import { Treemap } from "@/app/ui/ad-networks/viz/treemap"; +import { TypeScale } from "@/app/ui/ad-networks/viz/type-scale"; +import { Example } from "../gallery"; + +// Every example uses the sample numbers from dev-space/ad-networks-mockups, so each one can be +// checked side by side with the mockup element it reproduces. + +const months = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct"]; +const monthPoints = months.map((label, index) => ({ label, fullLabel: `${label} ${index < 2 ? 2025 : 2026}` })); +const sitesOverTime = [6904, 6980, 7051, 7088, 7162, 7240, 7311, 7395, 7480, 7590, 7702, 7852]; + +const dayFormat = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }); +const day = (offset: number, from: string) => dayFormat.format(new Date(Date.parse(`${from}T00:00:00Z`) + offset * 86_400_000)); +const joined30 = [5, 7, 6, 8, 4, 6, 7, 5, 6, 9, 6, 5, 7, 6, 4, 6, 8, 7, 5, 6, 7, 6, 5, 8, 6, 7, 5, 6, 4, 7]; +const left30 = [2, 3, 2, 1, 3, 2, 4, 2, 3, 2, 1, 3, 2, 3, 2, 4, 2, 3, 1, 2, 3, 2, 4, 2, 3, 2, 3, 2, 1, 3]; +const joined90 = [38, 41, 35, 52, 44, 39, 47, 36, 40, 45, 49, 42, 46]; +const left90 = [18, 15, 22, 17, 20, 14, 19, 16, 21, 15, 18, 17, 20]; +const days: JoinedLeftPoint[] = joined30.map((joined, index) => ({ label: day(index - 29, "2026-10-15"), joined, left: left30[index] })); +const weeks: JoinedLeftPoint[] = joined90.map((joined, index) => ({ label: `Week of ${day((index - 12) * 7, "2026-10-12")}`, joined, left: left90[index] })); + +const lineSeries = (from: number, to: number, curve = 1) => months.map((_, index) => Math.round(from + (to - from) * Math.pow(index / 11, curve))); + +const niches = [["Food", 6320], ["Home & DIY", 3410], ["Travel", 2980], ["Parenting", 2450], ["Personal finance", 2210], ["Health & fitness", 2180], ["Beauty & fashion", 1960], ["Gaming", 1840], ["Tech", 1720], ["Crafts", 1510], ["Pets", 1280], ["Outdoors", 1140], ["Gardening", 980], ["Sports", 940], ["Entertainment", 910], ["Lifestyle", 870], ["Education", 720], ["Automotive", 640], ["Business", 590], ["News", 470]] as const; + +export function DataVisualsExamples() { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ({ ...point, value: sitesOverTime[index] }))} /> + + + + + + + + + + + + + + + + + + + + + + ({ year, count: [8, 22, 61, 120, 210, 330, 520, 760, 940, 1010, 980, 890, 760, 640, 531][index] }))} + caption="Domain registration years" + median={2013} + middleHalf={[2008, 2018]} + newest={{ year: 2025, domain: "newcampfire.co" }} + oldest={{ year: 1996, domain: "oldfarmrecipes.com" }} + /> + + + + + + + + + + + + + + + + + + + + ({ label, value, href: label === "Food" ? "/components-ui/data-visuals" : undefined }))} total={35120} /> + + + + ({ key: label, label }))} + corner="niche" + rows={[ + { label: "Food", values: [41, 22, 9, 18, 7, 2, 2, 2, 1] }, + { label: "Home & DIY", values: [33, 21, 11, 19, 10, 2, 2, 2, 1] }, + { label: "Travel", values: [29, 24, 14, 17, 10, 2, 2, 2, 1] }, + { label: "Personal finance", values: [17, 31, 15, 12, 14, 2, 2, 2, 1] }, + { label: "Gaming", values: [3, 4, 15, 2, 14, 21, 5, 9, 18] }, + ]} + unitNote="Numbers are % of the niche's sites." + /> + + + + + + + + + + + + + + + + + + + + + + + ({ label: `Week of ${day((index - 11) * 7, "2026-10-05")}`, total, part: [12, 18, 15, 22, 30, 28, 35, 41, 38, 52, 61, 70][index] }))} + endLabel="last week" + partLabel="from the checker" + restLabel="found by our scans" + startLabel="12 weeks ago" + /> + + + + + +
          + + +
          +
          + + ); +} diff --git a/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx b/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx index 354d6af9..9179c1a5 100644 --- a/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx +++ b/app/(site)/(components-ui)/components-ui/examples/filters-and-sorting.tsx @@ -3,6 +3,7 @@ import { DirectoryFilterController } from "@/app/ui/directories/directory-filter import { DirectoryCheckboxGroup, DirectoryRadioGroup, DirectoryTagSelectGroup } from "@/app/ui/directories/directory-filters"; import { SortDropdown } from "@/app/ui/directories/directory-sort-dropdown"; import { Example } from "../gallery"; +import { ToolbarDemo } from "./ad-network-demos"; import styles from "../components-ui.module.css"; export function FiltersAndSortingExamples() { @@ -35,6 +36,14 @@ export function FiltersAndSortingExamples() { + + + ); } diff --git a/app/(site)/(components-ui)/components-ui/examples/navigation.tsx b/app/(site)/(components-ui)/components-ui/examples/navigation.tsx index eba40776..8c46991c 100644 --- a/app/(site)/(components-ui)/components-ui/examples/navigation.tsx +++ b/app/(site)/(components-ui)/components-ui/examples/navigation.tsx @@ -2,6 +2,7 @@ import { Globe, HardDrive, Keyboard } from "lucide-react"; import { Pagination } from "@/app/ui/pagination"; import { Breadcrumbs } from "@/app/ui/site/breadcrumbs"; import { SidebarList } from "@/app/ui/site/sidebar-list"; +import { UiTabs } from "@/app/ui/tabs"; import { Example } from "../gallery"; import styles from "../components-ui.module.css"; @@ -56,6 +57,22 @@ export function NavigationExamples() { + }, + { id: "pay", title: "Pay", content: }, +]} />` }} + > + Who can apply, the traffic rule, and how to apply.

          }, + { id: "pay", title: "Pay", content:

          Your share, when you get paid, and the payment minimum.

          }, + { id: "rules", title: "Rules", content:

          Ad placement, other ad partners, and the contract.

          }, + ]} /> +
          ); } diff --git a/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx b/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx index c6499e71..a379d498 100644 --- a/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx +++ b/app/(site)/(components-ui)/components-ui/examples/page-layout.tsx @@ -1,4 +1,5 @@ import Link from "next/link"; +import { AdSection, SectionNote } from "@/app/ui/ad-networks/section"; import { HardDrive } from "lucide-react"; import { siteHeroStack127Art } from "@/app/(site)/art/site--hero-stack127"; import { BrandWordmark } from "@/app/ui/site/brand-wordmark"; @@ -111,6 +112,21 @@ export function PageLayoutExamples() { ))} + +
          + + Dates before Jul 2026 come from archived copies of its ads.txt in the Wayback Machine. + + controls sit here} description="Every network that runs on food sites, with what it takes to join." id="section-demo-2" title="Which ad networks do food sites use?"> + The next section starts after a hairline. + +
          +
          ); } diff --git a/app/(site)/(components-ui)/components-ui/examples/results.tsx b/app/(site)/(components-ui)/components-ui/examples/results.tsx index b7eb60a6..d54d8968 100644 --- a/app/(site)/(components-ui)/components-ui/examples/results.tsx +++ b/app/(site)/(components-ui)/components-ui/examples/results.tsx @@ -2,6 +2,12 @@ import { useState } from "react"; import { UiButton, UiCopyAction, UiRawOutput } from "@/app/ui/controls"; +import { Report, ReportBars, ReportBlock, ReportFigure, ReportGroup, ReportNumbers, ReportRow } from "@/app/ui/report"; +import { UiTabs } from "@/app/ui/tabs"; +import { DetailsLayout, FactsList, KeyNumbers, NotPublic, SourcesLine } from "@/app/ui/ad-networks/details"; +import { AdsTxtNumbers, AdsTxtPreview, BlockSubheading, FactMuted, SiteFacts, SimilarSites } from "@/app/ui/ad-networks/site-view"; +import { TrafficMeter } from "@/app/ui/ad-networks/traffic-meter"; +import { RankedBars, StackBar } from "@/app/ui/ad-networks/viz/bars"; import { UiActionRow, UiAllocationBar, UiCodeOutput, UiRangeMarker, UiKeyValueList, UiMetricGroup, UiPrimaryResult, UiProgressBar, UiResultLead, UiStatusWord, UiSteps, UiTimeline, UiUnitGrid } from "@/app/ui/display"; import { Example } from "../gallery"; import styles from "../components-ui.module.css"; @@ -172,6 +178,125 @@ export function ResultsExamples() { /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

          How do you join Mediavine?

          You apply with your site and give Mediavine read access to your Google Analytics. They check your traffic and content against the rules here.

          +

          What does the pay day mean?

          Mediavine pays each month's earnings a set number of days after the month ends.

          + + )} + title="What does Mediavine require and pay?" + titleId="details-demo" + > + + }, + ]} /> }, + { id: "pay", title: "Pay", content: }, + ]} /> + +
          +
          + + }, + { label: "niche", value: "Food" }, + { label: "platform", value: "WordPress" }, + { label: "domain registered", value: <>2014 · 12 yrs }, + { label: "publisher", value: "Tiny Kitchen Lab LLC" }, + ]} /> + + + + + companies with the most lines + + + + + + + + ); } diff --git a/app/(site)/(components-ui)/components-ui/examples/tables.tsx b/app/(site)/(components-ui)/components-ui/examples/tables.tsx index 51b6fecc..70251017 100644 --- a/app/(site)/(components-ui)/components-ui/examples/tables.tsx +++ b/app/(site)/(components-ui)/components-ui/examples/tables.tsx @@ -1,6 +1,10 @@ import { ContentTable } from "@/app/ui/content-elements"; import { UiDataTable } from "@/app/ui/display"; import { Example } from "../gallery"; +import { NetworkLadder } from "@/app/ui/ad-networks/network-ladder"; +import { NichesTable } from "@/app/ui/ad-networks/niches-table"; +import { SiteNetworksTable } from "@/app/ui/ad-networks/site-view"; +import { SitesTableDemo } from "./ad-network-demos"; export function TablesExamples() { return ( @@ -43,6 +47,67 @@ export function TablesExamples() { + + + + + + + + + + + + ); } diff --git a/app/(site)/(components-ui)/components-ui/sections.ts b/app/(site)/(components-ui)/components-ui/sections.ts index 297c27f5..e2466335 100644 --- a/app/(site)/(components-ui)/components-ui/sections.ts +++ b/app/(site)/(components-ui)/components-ui/sections.ts @@ -1,4 +1,5 @@ import { + ChartBarBig, ChartColumn, FileText, Gauge, @@ -65,21 +66,21 @@ export const gallerySections: GallerySection[] = [ label: "Filters and sorting", description: "Search, sort, and filter groups for any long list of records.", icon: ListFilter, - components: ["SortDropdown", "DirectoryCheckboxGroup", "DirectoryRadioGroup", "DirectoryTagSelectGroup", "DirectoryFilterController"], + components: ["SortDropdown", "DirectoryCheckboxGroup", "DirectoryRadioGroup", "DirectoryTagSelectGroup", "DirectoryFilterController", "TableToolbar", "ToolbarSearch", "ToolbarSelect", "ToolbarSwitch"], }, { slug: "results", label: "Results", description: "The main answer, supporting numbers, status words, progress, steps, timelines, and generated output.", icon: Gauge, - components: ["UiPrimaryResult", "UiResultLead", "UiMetricGroup", "UiKeyValueList", "UiStatusWord", "UiProgressBar", "UiAllocationBar", "UiRangeMarker", "UiUnitGrid", "UiSteps", "UiTimeline", "UiCodeOutput", "UiRawOutput"], + components: ["UiPrimaryResult", "UiResultLead", "UiMetricGroup", "UiKeyValueList", "UiStatusWord", "UiProgressBar", "UiAllocationBar", "UiRangeMarker", "UiUnitGrid", "UiSteps", "UiTimeline", "UiCodeOutput", "UiRawOutput", "Report", "ReportGroup", "ReportRow", "ReportBlock", "ReportNumbers", "ReportFigure", "ReportBars", "KeyNumbers", "FactsList", "NotPublic", "DetailsLayout", "SourcesLine", "SiteFacts", "AdsTxtNumbers", "AdsTxtPreview", "SimilarSites", "TrafficMeter"], }, { slug: "tables", label: "Tables", description: "Data tables with row headers, content tables written in MDX, and a scroll frame for wide tables.", icon: Table2, - components: ["UiDataTable", "ContentTable", "TableFrame"], + components: ["UiDataTable", "ContentTable", "TableFrame", "SitesTable", "NichesTable", "NetworkLadder", "SiteNetworksTable"], }, { slug: "charts", @@ -88,12 +89,19 @@ export const gallerySections: GallerySection[] = [ icon: ChartColumn, components: ["UiChart", "UiStaticChart", "UiLiveChart"], }, + { + slug: "data-visuals", + label: "Data visuals", + description: "Bars, stacks, rings, columns, lines, flows, strips, treemaps, grids, and timelines drawn on the server in the site style, with small scripts only for hover, keys, and width. The ad networks pages use these.", + icon: ChartBarBig, + components: ["RankedBars", "StackBar", "MoveBars", "VizLegend", "Donut", "TypeScale", "PublisherBlocks", "ColumnChart", "AreaLine", "JoinedLeftBars", "SwitchFlow", "AgeStrip", "SizeStacks", "NewestList", "Treemap", "Heatmap", "MultiLine", "DivergingBars", "ScatterPlot", "DotPlot", "StackedColumns", "HistoryGantt", "EventsList"], + }, { slug: "cards", label: "Cards", description: "Linked page cards and record cards. Any page can list them.", icon: SquareStack, - components: ["PageCard", "PageCardCounts", "ToolCard", "CatalogCard", "DirectoryCard", "AppFactCardContent", "DirectoryAppCard", "HardwareFactCardContent"], + components: ["PageCard", "PageCardCounts", "ToolCard", "CatalogCard", "DirectoryCard", "AppFactCardContent", "DirectoryAppCard", "HardwareFactCardContent", "NetworkLogo", "NetworkCard", "NetworkGrid", "SortNote"], }, { slug: "content", @@ -128,14 +136,14 @@ export const gallerySections: GallerySection[] = [ label: "Navigation", description: "Breadcrumbs above page titles, page links for long lists, and sidebar link lists.", icon: Signpost, - components: ["Breadcrumbs", "Pagination", "SidebarList"], + components: ["Breadcrumbs", "Pagination", "SidebarList", "UiTabs"], }, { slug: "page-layout", label: "Page layout", description: "Page headers, sidebar positions, the section bar, the entry hero, page metadata, brand pieces, and the tool workbench.", icon: LayoutTemplate, - components: ["PageHeader", "PageTitleIcon", "CategoryIcon", "EntryHero", "PageMeta", "BrandWordmark", "ThemeToggle", "ToolWorkbench", "ToolFieldRow", "ToolResult", "SectionBar"], + components: ["PageHeader", "PageTitleIcon", "CategoryIcon", "EntryHero", "PageMeta", "BrandWordmark", "ThemeToggle", "ToolWorkbench", "ToolFieldRow", "ToolResult", "SectionBar", "AdSection", "SectionHead", "SectionNote"], }, ]; diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx index 7531ce64..49221383 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Who is Adapex a fit for? + +Adapex suits a publisher who already sells ads through Google and wants a team to manage the setup. You keep your own Google Ad Manager account, so you aren't starting from zero. + +## Can you try Adapex without a long contract? + +Yes. Adapex says no long-term contract is needed. It offers better terms on longer contracts, so ask what you'd give up by staying short. + +## What does onboarding through MCM mean? + +MCM is Google's way of letting a partner manage ads for another publisher. Your Ad Manager account stays yours, and Adapex works with it under that link. + +## How can you check if a site uses Adapex? + +Look for Adapex in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Adapex. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx index 1bb3f8d1..45c9ae99 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/intro.mdx @@ -1 +1 @@ -Publisher ad monetization service. A site can use more than one ad network. +Adapex is a managed ad service that runs publishers' ads through Google Ad Manager. It suits sites already on AdSense, Ad Exchange, or Prebid that want a team to manage them. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx new file mode 100644 index 00000000..d8cd9d5b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Adapex? + +Adapex doesn't publish a traffic minimum, site rules, or the regions it accepts. You apply through the contact form on its site. + +If you already run AdSense, Ad Exchange, or Prebid, Adapex can bring that setup in. Onboarding goes through Google Ad Manager's Multiple Customer Management (MCM). diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx new file mode 100644 index 00000000..479ef08a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/pay.mdx @@ -0,0 +1,3 @@ +## How does Adapex pay? + +Adapex pays on Net 60 terms, so each month's earnings arrive 60 days after that month ends. Its share, payment minimum, payment methods, and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx new file mode 100644 index 00000000..6803a0a6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Adapex set? + +Adapex doesn't require a long-term contract. A longer contract can get you a better share or extra features. + +You can block ad categories, advertisers, and domains, and set price floors. Whether Adapex must be your only ad partner, and how much notice you give to leave, are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx new file mode 100644 index 00000000..360d85a6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adapex/service.mdx @@ -0,0 +1,3 @@ +## What does Adapex do for you? + +You get a dashboard that reports revenue and traffic close to real time, split by country and device. You keep your own Google Ad Manager account. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx index 7531ce64..85aa78c8 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Who is Aditude for? + +Aditude works with web and app publishers on their ad technology. It doesn't say who qualifies, so the only way to know is to ask. + +## What should you ask Aditude before you sign? + +Ask for the share you'd get, when you'd be paid, and whether Aditude must be your only ad partner. Ask how much notice you'd need to leave. None of this is on its public pages, so get it in writing. + +## How can you check if a site uses Aditude? + +Look for Aditude in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Aditude. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx index 087fd774..7fb90bd2 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/intro.mdx @@ -1 +1 @@ -Publisher ad technology platform. A site can use more than one ad network. +Aditude is an ad technology company that builds and runs ad setups for web and app publishers. Its joining rules and terms are not public, so you'll need to talk to its team. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx new file mode 100644 index 00000000..01f46e54 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Aditude? + +Not public. Aditude doesn't publish a traffic minimum, site rules, accepted regions, or how to apply. Contact its team to ask whether your site fits. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx new file mode 100644 index 00000000..2b2af237 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/pay.mdx @@ -0,0 +1,3 @@ +## How does Aditude pay? + +Not public. Aditude doesn't publish its share, when it pays, the payment minimum, methods, or currency. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx new file mode 100644 index 00000000..52ebf01a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Aditude set? + +Not public. The terms on Aditude's site cover use of its website, not a publisher agreement. Ask about exclusivity, ad placement, content rules, and notice to leave before you sign. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx new file mode 100644 index 00000000..11633a6e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/aditude/service.mdx @@ -0,0 +1,3 @@ +## What does Aditude do for you? + +Aditude publishes guides on ad technology for publishers. Its dashboard, reporting, and support terms are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx index 7531ce64..b259d509 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Adnimation a fit for a small site? + +Probably not the managed service, since it asks for substantial traffic. Adnimation's MAP product has no pageview minimum, but it's a separate program with its own terms. + +## Can you leave Adnimation? + +Yes. There's no lock-in contract, but you need to give notice. Ask how much before you sign, since it isn't public. + +## What ads will Adnimation run on your site? + +Display, video, and native or recommendation ads, with header bidding behind them. You can ask to change where they sit on your pages. + +## How can you check if a site uses Adnimation? + +Look for Adnimation in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Adnimation. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx index 2e74bcb9..2120ab7b 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/intro.mdx @@ -1 +1 @@ -Publisher ad management service. A site can use more than one ad network. +Adnimation is a managed ad service for publishers with steady traffic. It runs display, video, and native ads for you. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx new file mode 100644 index 00000000..50cdcf62 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/joining.mdx @@ -0,0 +1,7 @@ +## Who can join Adnimation? + +Adnimation asks for substantial traffic but doesn't publish a number. It reviews each site, and you apply by contacting its team. It doesn't say which regions it accepts. + +Sites with gambling, porn, or drug content are restricted. + +Adnimation's separate MAP product says it has no pageview minimum. That's a different program from the managed service on this page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx new file mode 100644 index 00000000..80342fd2 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/pay.mdx @@ -0,0 +1,5 @@ +## How does Adnimation pay? + +Adnimation pays monthly by direct bank payment, on Net 35 terms, and it may accept PayPal. It works on a revenue share, but the rate is not public. The payment minimum and currency are not public either. + +The MAP program has its own payment terms, which don't apply to the managed service. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx new file mode 100644 index 00000000..758723d0 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Adnimation set? + +There's no lock-in contract, but you have to give notice to leave. How much notice is not public. + +You can ask for changes to where ads go on your pages. Adnimation doesn't publish a rule that it must be your only ad partner. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx new file mode 100644 index 00000000..72c475c0 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adnimation/service.mdx @@ -0,0 +1,5 @@ +## What does Adnimation do for you? + +Your dashboard shows gross and net earnings. Adnimation runs display, header bidding, video, CTV and OTT, app, and native or recommendation ads, with lazy loading and ad refresh. + +Support is by email, phone, WhatsApp, Telegram, and Skype. Adnimation says it replies in under two hours on average. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx index 7531ce64..f94fbfdc 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Who is AdPushup for? + +Publishers who want help managing ad placements and price floors. AdPushup doesn't publish who qualifies, so submit your site and see what its review says. + +## Are AdPushup's public terms current? + +Maybe not. The public terms are several years old and may not match today's agreements. Read the agreement you're offered, and go by that. + +## How can you check if a site uses AdPushup? + +Look for AdPushup in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to AdPushup. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx index 48375e7e..944f3f90 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/intro.mdx @@ -1 +1 @@ -Publisher ad revenue optimization platform. A site can use more than one ad network. +AdPushup is an ad revenue platform that helps publishers manage ad placements and price floors. You submit your site for review to get started. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx new file mode 100644 index 00000000..e7c6701d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/joining.mdx @@ -0,0 +1,3 @@ +## Who can join AdPushup? + +AdPushup doesn't publish a traffic minimum, accepted regions, or content rules. You submit your site on its onboarding page, and its team reviews it. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx new file mode 100644 index 00000000..058d2be4 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/pay.mdx @@ -0,0 +1,5 @@ +## How does AdPushup pay? + +The public numbers come from AdPushup's older terms, revised in 2018. They set a default 20% service fee, so you keep 80%, with a $50 minimum and payment on Net 45. Your current agreement may differ, and that agreement is what counts. + +Payment methods and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx new file mode 100644 index 00000000..3247d813 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/rules.mdx @@ -0,0 +1,3 @@ +## What rules does AdPushup set? + +The older terms ban invalid traffic and other policy breaks. Current rules on exclusivity, ad placement, content, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx new file mode 100644 index 00000000..a37d285e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/adpushup/service.mdx @@ -0,0 +1,3 @@ +## What does AdPushup do for you? + +You get a custom dashboard with real-time reports. You can control ad placements and price floors, and its team supports you. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx index 7531ce64..f6302c9c 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Can you run APS with another ad network? + +Yes, for UAM. It's built to run next to your other header bidding partners, so you don't have to drop them. + +## How do you get access to APS? + +Contact APS and ask. UAM is invitation only, and there's no public form. It suits sites that already use Google Ad Manager. + +## What does the UAM fee mean for you? + +APS takes its fee out of each bid before the auction runs. So the bids that compete for your ad space are already net of it. + +## How can you check if a site uses Amazon Publisher Services? + +Look for Amazon Publisher Services in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Amazon Publisher Services. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx index 1a6ea971..62eeefaf 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/intro.mdx @@ -1 +1 @@ -Amazon advertising and header bidding services for web and app publishers. A site can use more than one ad network. +Amazon Publisher Services (APS) sells publishers' ad space to Amazon and other buyers. Its Unified Ad Marketplace is invitation only and suits sites that already run Google Ad Manager. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx new file mode 100644 index 00000000..11f639a0 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Amazon Publisher Services? + +The Unified Ad Marketplace (UAM) is invitation only. It's for web publishers who represent their own sites directly and use Google Ad Manager. APS says it works with publishers worldwide. + +There's no public traffic minimum or application form. Contact APS to ask for access. These rules are for UAM, not every APS product. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx new file mode 100644 index 00000000..7b668a77 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/pay.mdx @@ -0,0 +1,5 @@ +## How does Amazon Publisher Services pay? + +UAM takes a 10% fee from bid prices before the auction runs. Your UAM earnings come in one monthly payment, on Net 60 terms. + +Payment methods and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx new file mode 100644 index 00000000..4ba35433 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Amazon Publisher Services set? + +UAM can run alongside your other header bidding, and its code can sit outside your wrapper. Exclusivity, content rules, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx new file mode 100644 index 00000000..5901d3de --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/amazon-publisher-services/service.mdx @@ -0,0 +1,3 @@ +## What does Amazon Publisher Services do for you? + +Not public. The UAM documents don't describe a dashboard, reporting, or support. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx index 7531ce64..55fe6cec 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Who is AnyManager for? + +Web and app publishers who want header bidding and pricing handled in one platform. AnyMind doesn't publish who qualifies, so contact its team. + +## How does AnyMind set your share? + +You agree on a fixed share before you start. Ask for it in writing, since AnyMind doesn't publish it. + +## How can you check if a site uses AnyManager? + +Look for AnyManager in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to AnyManager. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx index 609dd2c5..ffef3327 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/intro.mdx @@ -1 +1 @@ -AnyMind platform for web and app publisher monetization. A site can use more than one ad network. +AnyManager is AnyMind's monetization platform for web and app publishers. Its joining rules and rates are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx new file mode 100644 index 00000000..7b6b4bd5 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/joining.mdx @@ -0,0 +1,3 @@ +## Who can join AnyManager? + +Not public. AnyMind doesn't publish a traffic minimum, site rules, accepted regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx new file mode 100644 index 00000000..d36eb128 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/pay.mdx @@ -0,0 +1,3 @@ +## How does AnyManager pay? + +AnyMind pays publishers a fixed share agreed before you start. The share itself is not public, and neither are payment timing, the minimum, methods, or currency. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx new file mode 100644 index 00000000..dde9e59e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/rules.mdx @@ -0,0 +1,3 @@ +## What rules does AnyManager set? + +Not public. Exclusivity, ad placement, content rules, contract length, and notice aren't on AnyMind's public pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx new file mode 100644 index 00000000..74c7607a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/anymind-anymanager/service.mdx @@ -0,0 +1,3 @@ +## What does AnyManager do for you? + +AnyManager offers header bidding, automatic price tuning, analytics, and tools for viewability and brand safety. Its dashboard and support levels are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx index 7531ce64..76c49f9e 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Who is Carbon Ads for? + +Sites that want an ad built to match their design. Its terms aren't public, so contact BuySellAds to see if your site fits. + +## What's the difference between BuySellAds and Carbon Ads? + +BuySellAds is the company. Carbon Ads is one of its ad programs for publishers. + +## How can you check if a site uses Carbon Ads? + +Look for Carbon Ads in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Carbon Ads. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx index 06a32587..ee82c07b 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/intro.mdx @@ -1 +1 @@ -Publisher advertising programs including the Carbon Ads network. A site can use more than one ad network. +Carbon Ads, from BuySellAds, places ads built to fit each site's design. Its joining rules and pay terms are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx new file mode 100644 index 00000000..515a9ab5 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Carbon Ads? + +Not public. The Carbon Ads media kit doesn't give a traffic minimum, site rules, regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx new file mode 100644 index 00000000..53ded92c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/pay.mdx @@ -0,0 +1,3 @@ +## How does Carbon Ads pay? + +Not public. The media kit doesn't give the publisher share, when payments go out, the minimum, methods, or currency. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx new file mode 100644 index 00000000..b1d32947 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Carbon Ads set? + +Carbon Ads builds each ad unit to fit the publisher's design. Exclusivity, content rules, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx new file mode 100644 index 00000000..1b5aa8de --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/buysellads-carbon-ads/service.mdx @@ -0,0 +1,3 @@ +## What does Carbon Ads do for you? + +You get performance tracking, analytics, monthly reports, and suggestions to improve results. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx index 7531ce64..4a86b163 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Clickio a fit for a site outside the US? + +It can be. Clickio sets its traffic bar by visitor region, with rules for readers across Europe, Latin America, and elsewhere. The bar is higher outside its main markets. + +## What if your traffic is below Clickio's bar? + +Clickio may still take your site if it already earns enough from ads. Revenue counts as well as pageviews, so mention your earnings when you apply. + +## Can you keep other ad partners with Clickio? + +Clickio's entry rules don't say it must be your only partner. Confirm what your agreement says before you add others. + +## How can you check if a site uses Clickio? + +Look for Clickio in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Clickio. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx index 3f9b5f4c..7d4e8c71 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/intro.mdx @@ -1 +1 @@ -Publisher monetization platform. A site can use more than one ad network. +Clickio is an ad platform that can run a site's ads as a fully managed service. It takes sites from many regions, with a traffic bar that depends on where readers are. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx new file mode 100644 index 00000000..0958b2e4 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Clickio? + +The traffic Clickio wants depends on where your visitors are. Sites with readers in the UK, France, the US, Canada, or Australia have the lowest bar, and other regions need more. A smaller site can still qualify if it earns enough from ads. + +You need six months of history, original content you have rights to, and good standing with Google's policies. Adult, shocking, and racially intolerant content isn't allowed. Clickio also lists sanctioned countries it can't work with, including Cuba, Iran, Syria, Russia, and Sudan. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx new file mode 100644 index 00000000..7ac79a63 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/pay.mdx @@ -0,0 +1,5 @@ +## How does Clickio pay? + +Clickio bills monthly and pays on Net 60, around the 25th to 30th, two months after the month you earn. The minimum depends on how you're paid, and some methods can pay in your local currency. + +Its standard share is not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx new file mode 100644 index 00000000..0279c4f8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Clickio set? + +Clickio's entry rules don't say it must be your only ad partner, and they don't set a notice period. Clickio Prism, its CDN product, has separate rules. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx new file mode 100644 index 00000000..4feeefbf --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/clickio/service.mdx @@ -0,0 +1,3 @@ +## What does Clickio do for you? + +The fully managed service gives you real-time reports, an account manager, and technical and ad operations support. Unified Reports puts your Clickio, AdSense, Ad Manager, and Google Analytics numbers in one place, and you can export them. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx index 7531ce64..d122ea08 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What is Concert? + +Concert is a publisher-led advertising marketplace run by Vox Media. The terms on its site are for advertisers who buy ads through it. + +## Can any site join Concert? + +Concert doesn't publish publisher rules or an application form. Contact its team to ask. + +## How can you check if a site uses Concert? + +Look for Concert in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Concert. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx index 0cee2ade..360d09bf 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/intro.mdx @@ -1 +1 @@ -Vox Media publisher-led advertising marketplace. A site can use more than one ad network. +Concert is a publisher-led advertising marketplace run by Vox Media. Its public terms are for advertisers, and its publisher terms are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx new file mode 100644 index 00000000..6f0bfbd4 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Concert? + +Not public. The Concert terms we found cover advertisers buying ads, not publishers joining. Traffic, content rules, regions, and how to apply are not published. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx new file mode 100644 index 00000000..4dedddb6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/pay.mdx @@ -0,0 +1,3 @@ +## How does Concert pay publishers? + +Not public. Concert doesn't publish a share, timing, minimum, methods, or currency for publishers. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx new file mode 100644 index 00000000..123c777d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Concert set? + +Not public. Concert's advertiser terms don't cover a publisher's exclusivity, ad layout, content rules, contract length, or notice. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx new file mode 100644 index 00000000..e298c1a3 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/concert/service.mdx @@ -0,0 +1,3 @@ +## What does Concert do for publishers? + +Not public. Concert doesn't describe a publisher dashboard, reports, or support on its public pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx index 7531ce64..11f53005 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is EthicalAds a fit for your site? + +It suits developer and open source sites that want one clean ad instead of many. It won't fit a page that needs several ad slots, because EthicalAds must be the only ad on that page. + +## Can you run EthicalAds with another network? + +Yes, as long as they never share a page. EthicalAds lets you split your visitors with another network, or set a fallback ad. + +## Where should the EthicalAds ad go? + +Above the fold, on both desktop and mobile. That's what the policy asks for, along with a minimum click-through rate. + +## How can you check if a site uses EthicalAds? + +Look for EthicalAds in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to EthicalAds. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx index b38e0d94..9cc387c0 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/intro.mdx @@ -1 +1 @@ -Privacy-focused advertising network for developer and open source websites. A site can use more than one ad network. +EthicalAds is a privacy-focused ad network for developer and open source sites. It suits sites that can give its one ad a clear spot near the top of the page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx new file mode 100644 index 00000000..2d5dc0fa --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/joining.mdx @@ -0,0 +1,5 @@ +## Who can join EthicalAds? + +There's no traffic minimum. You need an approved account, and your site has to meet the publisher policy. + +The ad should sit above the fold on desktop and mobile. EthicalAds expects a click-through rate of at least 0.1%. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx new file mode 100644 index 00000000..d530145a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/pay.mdx @@ -0,0 +1,3 @@ +## How does EthicalAds pay? + +EthicalAds pays monthly once your balance reaches the minimum, aiming to pay by the 15th. Methods include Stripe Connect bank transfer, PayPal, OpenCollective, and GitHub Sponsors. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx new file mode 100644 index 00000000..572f0717 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/rules.mdx @@ -0,0 +1,5 @@ +## What rules does EthicalAds set? + +Don't show another ad on the same page as an EthicalAds ad. You can use a fallback ad, or show EthicalAds to some visitors and another network to others. + +Content that breaks the policy can get your ads turned off. The policy doesn't set a contract term or notice period. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx new file mode 100644 index 00000000..56209111 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ethicalads/service.mdx @@ -0,0 +1,3 @@ +## What does EthicalAds do for you? + +The publisher policy explains setup and the rules. Dashboard features and support response times are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx index 7531ce64..8869deec 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Can a small site still join Ezoic? + +Not as a new site on the main program. New sites below the traffic rule can apply to the Ezoic Incubator instead. Sites already earning with Ezoic before the rule changed can stay. + +## Why do Ezoic's payment dates look different in different places? + +Ezoic's legal terms and its payment help page describe the timing differently. The terms are the contract, so we show those. Ask Ezoic to confirm your schedule before you count on a date. + +## What content does Ezoic accept? + +Original content that follows AdSense policies, with visitors who are real people rather than bots. + +## How can you check if a site uses Ezoic? + +Look for Ezoic in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Ezoic. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx index 66637bdf..7c5efc69 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/intro.mdx @@ -1 +1 @@ -Website monetization tools for digital publishers. A site can use more than one ad network. +Ezoic runs ads and site tools for website publishers. New sites need large traffic to join, and smaller sites can apply to its Incubator. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx new file mode 100644 index 00000000..bf0fbba3 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Ezoic? + +Ezoic set its current traffic rule for new sites in February 2026. Sites already earning with Ezoic before February 19, 2026 can keep going under the old rule. Smaller sites can apply to the Ezoic Incubator. + +Your content must be original and follow AdSense policies, and your traffic must come from real people. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx new file mode 100644 index 00000000..36ea27da --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/pay.mdx @@ -0,0 +1,5 @@ +## How does Ezoic pay? + +Ezoic's terms say you get at least 82% of net ad revenue, unless you've signed a different agreement. They say payment comes about 35 days after the advertiser pays Ezoic. + +Ezoic's payment help page describes a Net 30 cycle instead. We go by the legal terms, but ask Ezoic which applies to you. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx new file mode 100644 index 00000000..b2377a6b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Ezoic set? + +Your content must follow AdSense policies. Exclusivity, ad placement controls, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx new file mode 100644 index 00000000..2573c8a6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/ezoic/service.mdx @@ -0,0 +1,3 @@ +## What does Ezoic do for you? + +Ezoic offers a dashboard and publisher tools through its site. Exact reporting and support levels are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx index 7531ce64..33284ead 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Freestar a fit for a growing site? + +Only once it's large. Freestar usually works with high-traffic sites, and it checks where your readers are. A site with most readers outside the US, UK, Canada, New Zealand, and Australia won't meet the rule, even if it's big. + +## How long does it take to start with Freestar? + +Plan for several weeks. An account manager and a technical team guide the setup. + +## What does Freestar check before it accepts you? + +Your content, your standing with ad providers, and where your traffic comes from. It may ask for GA4 access and your traffic and revenue history. + +## How can you check if a site uses Freestar? + +Look for Freestar in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Freestar. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx index b283e52a..ab4e5a15 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/intro.mdx @@ -1 +1 @@ -Publisher advertising and monetization platform. A site can use more than one ad network. +Freestar is a managed ad service for large, brand-safe sites. It wants a big share of readers in the US, UK, Canada, New Zealand, or Australia. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx new file mode 100644 index 00000000..07af2203 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/joining.mdx @@ -0,0 +1,7 @@ +## Who can join Freestar? + +Freestar's traffic rule is what it usually asks for, not a strict cutoff, so ask its team if you're close. At least 50% of your traffic must come from the US, UK, Canada, New Zealand, or Australia. + +Your content must be original and made for people, and you need good standing with major ad providers. Explicit sexual content, piracy, violence, hate speech, and illegal drugs aren't allowed. + +During review, Freestar may ask for GA4 access and your past traffic and revenue. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx new file mode 100644 index 00000000..3d709ff0 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/pay.mdx @@ -0,0 +1,3 @@ +## How does Freestar pay? + +Not public. Freestar doesn't publish its share, payment timing, minimum, methods, or currency. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx new file mode 100644 index 00000000..7d0eb83a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Freestar set? + +Not public. Freestar doesn't publish exclusivity, ad placement controls, contract length, or notice. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx new file mode 100644 index 00000000..ef67472e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/freestar/service.mdx @@ -0,0 +1,3 @@ +## What does Freestar do for you? + +You get a dedicated account manager, a technical team, and publisher consultants. Freestar says onboarding commonly takes three to six weeks. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx index 7531ce64..4c36ee4a 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What is a supply-side platform? + +A supply-side platform (SSP) sells a publisher's ad space to many buyers at once, in real time. + +## How do you join Geniee SSP? + +Geniee doesn't publish an application route for publishers. Contact Geniee to ask. + +## How can you check if a site uses Geniee SSP? + +Look for Geniee SSP in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Geniee SSP. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx index f3e35ac7..3a8ff7be 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/intro.mdx @@ -1 +1 @@ -Publisher supply-side platform operated by Geniee. A site can use more than one ad network. +Geniee SSP is a supply-side platform run by Geniee. It sells publishers' ad space to buyers, and its publisher rules and terms are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx new file mode 100644 index 00000000..8259998f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Geniee SSP? + +Not public. Geniee doesn't publish a traffic minimum, site rules, accepted regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx new file mode 100644 index 00000000..9778233a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/pay.mdx @@ -0,0 +1,3 @@ +## How does Geniee SSP pay? + +Not public. The share, payment timing, minimum, methods, and currency aren't on Geniee's public pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx new file mode 100644 index 00000000..3f137ecf --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Geniee SSP set? + +Not public. Exclusivity, ad placement, content rules, contract length, and notice aren't on Geniee's public pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx new file mode 100644 index 00000000..6dcc673b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/geniee-ssp/service.mdx @@ -0,0 +1,3 @@ +## What does Geniee SSP do for you? + +Not public. Geniee doesn't describe its publisher dashboard, reports, or support. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx index 7531ce64..6e1527d3 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is AdSense a good first ad network? + +For many new sites it's the easiest start. There's no traffic minimum, and you set it up yourself in your AdSense account. + +## Can you use AdSense with another network? + +AdSense doesn't ask to be your only ad partner. Some managed networks do, so check their rules before you add one. + +## Why do you earn less than the share Google shows? + +The share applies after the advertiser platform fee. For ads bought through Google Ads, your cut of what the advertiser paid ends up lower than the headline share. + +## How can you check if a site uses Google AdSense? + +Look for Google AdSense in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Google AdSense. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx index b1ee6b14..8e8db26c 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/intro.mdx @@ -1 +1 @@ -Google advertising program for website publishers. A site can use more than one ad network. +Google AdSense is Google's ad program for website publishers. It has no traffic minimum, so it suits new and small sites. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx new file mode 100644 index 00000000..d861bd81 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Google AdSense? + +You must be at least 18, own original content that follows AdSense policies, and be able to edit your site's HTML. There's no traffic minimum. + +You sign up in an AdSense account. Whether AdSense is open to you, and how you can be paid, depends on your country. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx new file mode 100644 index 00000000..8c4754df --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/pay.mdx @@ -0,0 +1,5 @@ +## How does Google AdSense pay? + +For ads on your content, you get 80% after the advertiser platform fee. When the ad comes from Google Ads, that works out to about 68% of what the advertiser pays. Other AdSense products have different shares. + +Google pays once a month, usually between the 21st and 26th, when your balance passes the minimum and there's no hold on your account. The minimum is set for each currency, and payment methods depend on your country. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx new file mode 100644 index 00000000..4bbe9716 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Google AdSense set? + +Where you place ads must follow the AdSense Program Policies, which also list the content that isn't allowed. AdSense doesn't ask to be your only ad partner. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx new file mode 100644 index 00000000..ba66eadb --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/google-adsense/service.mdx @@ -0,0 +1,3 @@ +## What does Google AdSense do for you? + +You get reports in your AdSense account, and help through the AdSense Help Center and its policy pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/index.ts b/app/(site)/(directories)/ad-networks/[slug]/content/index.ts index 598b32e3..73d8b81c 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/index.ts +++ b/app/(site)/(directories)/ad-networks/[slug]/content/index.ts @@ -1,139 +1,322 @@ -import type { ComponentType } from "react"; +import type { MDXContent } from "mdx/types"; import Intro1 from "./adapex/intro.mdx"; import Article1 from "./adapex/article.mdx"; +import Joining1 from "./adapex/joining.mdx"; +import Pay1 from "./adapex/pay.mdx"; +import Rules1 from "./adapex/rules.mdx"; +import Service1 from "./adapex/service.mdx"; import Intro2 from "./aditude/intro.mdx"; import Article2 from "./aditude/article.mdx"; +import Joining2 from "./aditude/joining.mdx"; +import Pay2 from "./aditude/pay.mdx"; +import Rules2 from "./aditude/rules.mdx"; +import Service2 from "./aditude/service.mdx"; import Intro3 from "./adnimation/intro.mdx"; import Article3 from "./adnimation/article.mdx"; +import Joining3 from "./adnimation/joining.mdx"; +import Pay3 from "./adnimation/pay.mdx"; +import Rules3 from "./adnimation/rules.mdx"; +import Service3 from "./adnimation/service.mdx"; import Intro4 from "./adpushup/intro.mdx"; import Article4 from "./adpushup/article.mdx"; +import Joining4 from "./adpushup/joining.mdx"; +import Pay4 from "./adpushup/pay.mdx"; +import Rules4 from "./adpushup/rules.mdx"; +import Service4 from "./adpushup/service.mdx"; import Intro5 from "./amazon-publisher-services/intro.mdx"; import Article5 from "./amazon-publisher-services/article.mdx"; +import Joining5 from "./amazon-publisher-services/joining.mdx"; +import Pay5 from "./amazon-publisher-services/pay.mdx"; +import Rules5 from "./amazon-publisher-services/rules.mdx"; +import Service5 from "./amazon-publisher-services/service.mdx"; import Intro6 from "./anymind-anymanager/intro.mdx"; import Article6 from "./anymind-anymanager/article.mdx"; +import Joining6 from "./anymind-anymanager/joining.mdx"; +import Pay6 from "./anymind-anymanager/pay.mdx"; +import Rules6 from "./anymind-anymanager/rules.mdx"; +import Service6 from "./anymind-anymanager/service.mdx"; import Intro7 from "./buysellads-carbon-ads/intro.mdx"; import Article7 from "./buysellads-carbon-ads/article.mdx"; +import Joining7 from "./buysellads-carbon-ads/joining.mdx"; +import Pay7 from "./buysellads-carbon-ads/pay.mdx"; +import Rules7 from "./buysellads-carbon-ads/rules.mdx"; +import Service7 from "./buysellads-carbon-ads/service.mdx"; import Intro8 from "./clickio/intro.mdx"; import Article8 from "./clickio/article.mdx"; +import Joining8 from "./clickio/joining.mdx"; +import Pay8 from "./clickio/pay.mdx"; +import Rules8 from "./clickio/rules.mdx"; +import Service8 from "./clickio/service.mdx"; import Intro9 from "./concert/intro.mdx"; import Article9 from "./concert/article.mdx"; +import Joining9 from "./concert/joining.mdx"; +import Pay9 from "./concert/pay.mdx"; +import Rules9 from "./concert/rules.mdx"; +import Service9 from "./concert/service.mdx"; import Intro10 from "./ethicalads/intro.mdx"; import Article10 from "./ethicalads/article.mdx"; +import Joining10 from "./ethicalads/joining.mdx"; +import Pay10 from "./ethicalads/pay.mdx"; +import Rules10 from "./ethicalads/rules.mdx"; +import Service10 from "./ethicalads/service.mdx"; import Intro11 from "./ezoic/intro.mdx"; import Article11 from "./ezoic/article.mdx"; +import Joining11 from "./ezoic/joining.mdx"; +import Pay11 from "./ezoic/pay.mdx"; +import Rules11 from "./ezoic/rules.mdx"; +import Service11 from "./ezoic/service.mdx"; import Intro12 from "./freestar/intro.mdx"; import Article12 from "./freestar/article.mdx"; +import Joining12 from "./freestar/joining.mdx"; +import Pay12 from "./freestar/pay.mdx"; +import Rules12 from "./freestar/rules.mdx"; +import Service12 from "./freestar/service.mdx"; import Intro13 from "./geniee-ssp/intro.mdx"; import Article13 from "./geniee-ssp/article.mdx"; +import Joining13 from "./geniee-ssp/joining.mdx"; +import Pay13 from "./geniee-ssp/pay.mdx"; +import Rules13 from "./geniee-ssp/rules.mdx"; +import Service13 from "./geniee-ssp/service.mdx"; import Intro14 from "./google-adsense/intro.mdx"; import Article14 from "./google-adsense/article.mdx"; +import Joining14 from "./google-adsense/joining.mdx"; +import Pay14 from "./google-adsense/pay.mdx"; +import Rules14 from "./google-adsense/rules.mdx"; +import Service14 from "./google-adsense/service.mdx"; import Intro15 from "./infolinks/intro.mdx"; import Article15 from "./infolinks/article.mdx"; +import Joining15 from "./infolinks/joining.mdx"; +import Pay15 from "./infolinks/pay.mdx"; +import Rules15 from "./infolinks/rules.mdx"; +import Service15 from "./infolinks/service.mdx"; import Intro16 from "./journey-by-mediavine/intro.mdx"; import Article16 from "./journey-by-mediavine/article.mdx"; +import Joining16 from "./journey-by-mediavine/joining.mdx"; +import Pay16 from "./journey-by-mediavine/pay.mdx"; +import Rules16 from "./journey-by-mediavine/rules.mdx"; +import Service16 from "./journey-by-mediavine/service.mdx"; import Intro17 from "./media-net/intro.mdx"; import Article17 from "./media-net/article.mdx"; +import Joining17 from "./media-net/joining.mdx"; +import Pay17 from "./media-net/pay.mdx"; +import Rules17 from "./media-net/rules.mdx"; +import Service17 from "./media-net/service.mdx"; import Intro18 from "./mediavine/intro.mdx"; import Article18 from "./mediavine/article.mdx"; +import Joining18 from "./mediavine/joining.mdx"; +import Pay18 from "./mediavine/pay.mdx"; +import Rules18 from "./mediavine/rules.mdx"; +import Service18 from "./mediavine/service.mdx"; import Intro19 from "./mgid/intro.mdx"; import Article19 from "./mgid/article.mdx"; +import Joining19 from "./mgid/joining.mdx"; +import Pay19 from "./mgid/pay.mdx"; +import Rules19 from "./mgid/rules.mdx"; +import Service19 from "./mgid/service.mdx"; import Intro20 from "./monetizemore-pubguru/intro.mdx"; import Article20 from "./monetizemore-pubguru/article.mdx"; +import Joining20 from "./monetizemore-pubguru/joining.mdx"; +import Pay20 from "./monetizemore-pubguru/pay.mdx"; +import Rules20 from "./monetizemore-pubguru/rules.mdx"; +import Service20 from "./monetizemore-pubguru/service.mdx"; import Intro21 from "./monumetric/intro.mdx"; import Article21 from "./monumetric/article.mdx"; +import Joining21 from "./monumetric/joining.mdx"; +import Pay21 from "./monumetric/pay.mdx"; +import Rules21 from "./monumetric/rules.mdx"; +import Service21 from "./monumetric/service.mdx"; import Intro22 from "./newor-media/intro.mdx"; import Article22 from "./newor-media/article.mdx"; +import Joining22 from "./newor-media/joining.mdx"; +import Pay22 from "./newor-media/pay.mdx"; +import Rules22 from "./newor-media/rules.mdx"; +import Service22 from "./newor-media/service.mdx"; import Intro23 from "./nitropay/intro.mdx"; import Article23 from "./nitropay/article.mdx"; +import Joining23 from "./nitropay/joining.mdx"; +import Pay23 from "./nitropay/pay.mdx"; +import Rules23 from "./nitropay/rules.mdx"; +import Service23 from "./nitropay/service.mdx"; import Intro24 from "./opti-digital/intro.mdx"; import Article24 from "./opti-digital/article.mdx"; +import Joining24 from "./opti-digital/joining.mdx"; +import Pay24 from "./opti-digital/pay.mdx"; +import Rules24 from "./opti-digital/rules.mdx"; +import Service24 from "./opti-digital/service.mdx"; import Intro25 from "./playwire/intro.mdx"; import Article25 from "./playwire/article.mdx"; +import Joining25 from "./playwire/joining.mdx"; +import Pay25 from "./playwire/pay.mdx"; +import Rules25 from "./playwire/rules.mdx"; +import Service25 from "./playwire/service.mdx"; import Intro26 from "./pubfuture/intro.mdx"; import Article26 from "./pubfuture/article.mdx"; +import Joining26 from "./pubfuture/joining.mdx"; +import Pay26 from "./pubfuture/pay.mdx"; +import Rules26 from "./pubfuture/rules.mdx"; +import Service26 from "./pubfuture/service.mdx"; import Intro27 from "./pubgalaxy/intro.mdx"; import Article27 from "./pubgalaxy/article.mdx"; +import Joining27 from "./pubgalaxy/joining.mdx"; +import Pay27 from "./pubgalaxy/pay.mdx"; +import Rules27 from "./pubgalaxy/rules.mdx"; +import Service27 from "./pubgalaxy/service.mdx"; import Intro28 from "./publift/intro.mdx"; import Article28 from "./publift/article.mdx"; +import Joining28 from "./publift/joining.mdx"; +import Pay28 from "./publift/pay.mdx"; +import Rules28 from "./publift/rules.mdx"; +import Service28 from "./publift/service.mdx"; import Intro29 from "./publir/intro.mdx"; import Article29 from "./publir/article.mdx"; +import Joining29 from "./publir/joining.mdx"; +import Pay29 from "./publir/pay.mdx"; +import Rules29 from "./publir/rules.mdx"; +import Service29 from "./publir/service.mdx"; import Intro30 from "./publisher-collective/intro.mdx"; import Article30 from "./publisher-collective/article.mdx"; +import Joining30 from "./publisher-collective/joining.mdx"; +import Pay30 from "./publisher-collective/pay.mdx"; +import Rules30 from "./publisher-collective/rules.mdx"; +import Service30 from "./publisher-collective/service.mdx"; import Intro31 from "./pubnation/intro.mdx"; import Article31 from "./pubnation/article.mdx"; +import Joining31 from "./pubnation/joining.mdx"; +import Pay31 from "./pubnation/pay.mdx"; +import Rules31 from "./pubnation/rules.mdx"; +import Service31 from "./pubnation/service.mdx"; import Intro32 from "./pubpower/intro.mdx"; import Article32 from "./pubpower/article.mdx"; +import Joining32 from "./pubpower/joining.mdx"; +import Pay32 from "./pubpower/pay.mdx"; +import Rules32 from "./pubpower/rules.mdx"; +import Service32 from "./pubpower/service.mdx"; import Intro33 from "./raptive/intro.mdx"; import Article33 from "./raptive/article.mdx"; +import Joining33 from "./raptive/joining.mdx"; +import Pay33 from "./raptive/pay.mdx"; +import Rules33 from "./raptive/rules.mdx"; +import Service33 from "./raptive/service.mdx"; import Intro34 from "./refinery89/intro.mdx"; import Article34 from "./refinery89/article.mdx"; +import Joining34 from "./refinery89/joining.mdx"; +import Pay34 from "./refinery89/pay.mdx"; +import Rules34 from "./refinery89/rules.mdx"; +import Service34 from "./refinery89/service.mdx"; import Intro35 from "./revcontent/intro.mdx"; import Article35 from "./revcontent/article.mdx"; +import Joining35 from "./revcontent/joining.mdx"; +import Pay35 from "./revcontent/pay.mdx"; +import Rules35 from "./revcontent/rules.mdx"; +import Service35 from "./revcontent/service.mdx"; import Intro36 from "./setupad/intro.mdx"; import Article36 from "./setupad/article.mdx"; +import Joining36 from "./setupad/joining.mdx"; +import Pay36 from "./setupad/pay.mdx"; +import Rules36 from "./setupad/rules.mdx"; +import Service36 from "./setupad/service.mdx"; import Intro37 from "./she-media/intro.mdx"; import Article37 from "./she-media/article.mdx"; +import Joining37 from "./she-media/joining.mdx"; +import Pay37 from "./she-media/pay.mdx"; +import Rules37 from "./she-media/rules.mdx"; +import Service37 from "./she-media/service.mdx"; import Intro38 from "./snack-media/intro.mdx"; import Article38 from "./snack-media/article.mdx"; -import Intro39 from "./snigel/intro.mdx"; -import Article39 from "./snigel/article.mdx"; +import Joining38 from "./snack-media/joining.mdx"; +import Pay38 from "./snack-media/pay.mdx"; +import Rules38 from "./snack-media/rules.mdx"; +import Service38 from "./snack-media/service.mdx"; import Intro40 from "./sovrn/intro.mdx"; import Article40 from "./sovrn/article.mdx"; +import Joining40 from "./sovrn/joining.mdx"; +import Pay40 from "./sovrn/pay.mdx"; +import Rules40 from "./sovrn/rules.mdx"; +import Service40 from "./sovrn/service.mdx"; import Intro41 from "./sparteo/intro.mdx"; import Article41 from "./sparteo/article.mdx"; +import Joining41 from "./sparteo/joining.mdx"; +import Pay41 from "./sparteo/pay.mdx"; +import Rules41 from "./sparteo/rules.mdx"; +import Service41 from "./sparteo/service.mdx"; import Intro42 from "./taboola/intro.mdx"; import Article42 from "./taboola/article.mdx"; +import Joining42 from "./taboola/joining.mdx"; +import Pay42 from "./taboola/pay.mdx"; +import Rules42 from "./taboola/rules.mdx"; +import Service42 from "./taboola/service.mdx"; import Intro43 from "./teads/intro.mdx"; import Article43 from "./teads/article.mdx"; +import Joining43 from "./teads/joining.mdx"; +import Pay43 from "./teads/pay.mdx"; +import Rules43 from "./teads/rules.mdx"; +import Service43 from "./teads/service.mdx"; import Intro44 from "./the-moneytizer/intro.mdx"; import Article44 from "./the-moneytizer/article.mdx"; +import Joining44 from "./the-moneytizer/joining.mdx"; +import Pay44 from "./the-moneytizer/pay.mdx"; +import Rules44 from "./the-moneytizer/rules.mdx"; +import Service44 from "./the-moneytizer/service.mdx"; import Intro45 from "./venatus/intro.mdx"; import Article45 from "./venatus/article.mdx"; +import Joining45 from "./venatus/joining.mdx"; +import Pay45 from "./venatus/pay.mdx"; +import Rules45 from "./venatus/rules.mdx"; +import Service45 from "./venatus/service.mdx"; -export const adNetworkContent: Record = { - "adapex": { Intro: Intro1, Article: Article1 }, - "aditude": { Intro: Intro2, Article: Article2 }, - "adnimation": { Intro: Intro3, Article: Article3 }, - "adpushup": { Intro: Intro4, Article: Article4 }, - "amazon-publisher-services": { Intro: Intro5, Article: Article5 }, - "anymind-anymanager": { Intro: Intro6, Article: Article6 }, - "buysellads-carbon-ads": { Intro: Intro7, Article: Article7 }, - "clickio": { Intro: Intro8, Article: Article8 }, - "concert": { Intro: Intro9, Article: Article9 }, - "ethicalads": { Intro: Intro10, Article: Article10 }, - "ezoic": { Intro: Intro11, Article: Article11 }, - "freestar": { Intro: Intro12, Article: Article12 }, - "geniee-ssp": { Intro: Intro13, Article: Article13 }, - "google-adsense": { Intro: Intro14, Article: Article14 }, - "infolinks": { Intro: Intro15, Article: Article15 }, - "journey-by-mediavine": { Intro: Intro16, Article: Article16 }, - "media-net": { Intro: Intro17, Article: Article17 }, - "mediavine": { Intro: Intro18, Article: Article18 }, - "mgid": { Intro: Intro19, Article: Article19 }, - "monetizemore-pubguru": { Intro: Intro20, Article: Article20 }, - "monumetric": { Intro: Intro21, Article: Article21 }, - "newor-media": { Intro: Intro22, Article: Article22 }, - "nitropay": { Intro: Intro23, Article: Article23 }, - "opti-digital": { Intro: Intro24, Article: Article24 }, - "playwire": { Intro: Intro25, Article: Article25 }, - "pubfuture": { Intro: Intro26, Article: Article26 }, - "pubgalaxy": { Intro: Intro27, Article: Article27 }, - "publift": { Intro: Intro28, Article: Article28 }, - "publir": { Intro: Intro29, Article: Article29 }, - "publisher-collective": { Intro: Intro30, Article: Article30 }, - "pubnation": { Intro: Intro31, Article: Article31 }, - "pubpower": { Intro: Intro32, Article: Article32 }, - "raptive": { Intro: Intro33, Article: Article33 }, - "refinery89": { Intro: Intro34, Article: Article34 }, - "revcontent": { Intro: Intro35, Article: Article35 }, - "setupad": { Intro: Intro36, Article: Article36 }, - "she-media": { Intro: Intro37, Article: Article37 }, - "snack-media": { Intro: Intro38, Article: Article38 }, - "snigel": { Intro: Intro39, Article: Article39 }, - "sovrn": { Intro: Intro40, Article: Article40 }, - "sparteo": { Intro: Intro41, Article: Article41 }, - "taboola": { Intro: Intro42, Article: Article42 }, - "teads": { Intro: Intro43, Article: Article43 }, - "the-moneytizer": { Intro: Intro44, Article: Article44 }, - "venatus": { Intro: Intro45, Article: Article45 }, +/** Words for one network page. The four tabs are optional until a writer adds them. */ +export type AdNetworkContent = { + Intro: MDXContent; + Article: MDXContent; + Joining?: MDXContent; + Pay?: MDXContent; + Rules?: MDXContent; + Service?: MDXContent; +}; + +export const adNetworkContent: Record = { + "adapex": { Intro: Intro1, Article: Article1, Joining: Joining1, Pay: Pay1, Rules: Rules1, Service: Service1 }, + "aditude": { Intro: Intro2, Article: Article2, Joining: Joining2, Pay: Pay2, Rules: Rules2, Service: Service2 }, + "adnimation": { Intro: Intro3, Article: Article3, Joining: Joining3, Pay: Pay3, Rules: Rules3, Service: Service3 }, + "adpushup": { Intro: Intro4, Article: Article4, Joining: Joining4, Pay: Pay4, Rules: Rules4, Service: Service4 }, + "amazon-publisher-services": { Intro: Intro5, Article: Article5, Joining: Joining5, Pay: Pay5, Rules: Rules5, Service: Service5 }, + "anymind-anymanager": { Intro: Intro6, Article: Article6, Joining: Joining6, Pay: Pay6, Rules: Rules6, Service: Service6 }, + "buysellads-carbon-ads": { Intro: Intro7, Article: Article7, Joining: Joining7, Pay: Pay7, Rules: Rules7, Service: Service7 }, + "clickio": { Intro: Intro8, Article: Article8, Joining: Joining8, Pay: Pay8, Rules: Rules8, Service: Service8 }, + "concert": { Intro: Intro9, Article: Article9, Joining: Joining9, Pay: Pay9, Rules: Rules9, Service: Service9 }, + "ethicalads": { Intro: Intro10, Article: Article10, Joining: Joining10, Pay: Pay10, Rules: Rules10, Service: Service10 }, + "ezoic": { Intro: Intro11, Article: Article11, Joining: Joining11, Pay: Pay11, Rules: Rules11, Service: Service11 }, + "freestar": { Intro: Intro12, Article: Article12, Joining: Joining12, Pay: Pay12, Rules: Rules12, Service: Service12 }, + "geniee-ssp": { Intro: Intro13, Article: Article13, Joining: Joining13, Pay: Pay13, Rules: Rules13, Service: Service13 }, + "google-adsense": { Intro: Intro14, Article: Article14, Joining: Joining14, Pay: Pay14, Rules: Rules14, Service: Service14 }, + "infolinks": { Intro: Intro15, Article: Article15, Joining: Joining15, Pay: Pay15, Rules: Rules15, Service: Service15 }, + "journey-by-mediavine": { Intro: Intro16, Article: Article16, Joining: Joining16, Pay: Pay16, Rules: Rules16, Service: Service16 }, + "media-net": { Intro: Intro17, Article: Article17, Joining: Joining17, Pay: Pay17, Rules: Rules17, Service: Service17 }, + "mediavine": { Intro: Intro18, Article: Article18, Joining: Joining18, Pay: Pay18, Rules: Rules18, Service: Service18 }, + "mgid": { Intro: Intro19, Article: Article19, Joining: Joining19, Pay: Pay19, Rules: Rules19, Service: Service19 }, + "monetizemore-pubguru": { Intro: Intro20, Article: Article20, Joining: Joining20, Pay: Pay20, Rules: Rules20, Service: Service20 }, + "monumetric": { Intro: Intro21, Article: Article21, Joining: Joining21, Pay: Pay21, Rules: Rules21, Service: Service21 }, + "newor-media": { Intro: Intro22, Article: Article22, Joining: Joining22, Pay: Pay22, Rules: Rules22, Service: Service22 }, + "nitropay": { Intro: Intro23, Article: Article23, Joining: Joining23, Pay: Pay23, Rules: Rules23, Service: Service23 }, + "opti-digital": { Intro: Intro24, Article: Article24, Joining: Joining24, Pay: Pay24, Rules: Rules24, Service: Service24 }, + "playwire": { Intro: Intro25, Article: Article25, Joining: Joining25, Pay: Pay25, Rules: Rules25, Service: Service25 }, + "pubfuture": { Intro: Intro26, Article: Article26, Joining: Joining26, Pay: Pay26, Rules: Rules26, Service: Service26 }, + "pubgalaxy": { Intro: Intro27, Article: Article27, Joining: Joining27, Pay: Pay27, Rules: Rules27, Service: Service27 }, + "publift": { Intro: Intro28, Article: Article28, Joining: Joining28, Pay: Pay28, Rules: Rules28, Service: Service28 }, + "publir": { Intro: Intro29, Article: Article29, Joining: Joining29, Pay: Pay29, Rules: Rules29, Service: Service29 }, + "publisher-collective": { Intro: Intro30, Article: Article30, Joining: Joining30, Pay: Pay30, Rules: Rules30, Service: Service30 }, + "pubnation": { Intro: Intro31, Article: Article31, Joining: Joining31, Pay: Pay31, Rules: Rules31, Service: Service31 }, + "pubpower": { Intro: Intro32, Article: Article32, Joining: Joining32, Pay: Pay32, Rules: Rules32, Service: Service32 }, + "raptive": { Intro: Intro33, Article: Article33, Joining: Joining33, Pay: Pay33, Rules: Rules33, Service: Service33 }, + "refinery89": { Intro: Intro34, Article: Article34, Joining: Joining34, Pay: Pay34, Rules: Rules34, Service: Service34 }, + "revcontent": { Intro: Intro35, Article: Article35, Joining: Joining35, Pay: Pay35, Rules: Rules35, Service: Service35 }, + "setupad": { Intro: Intro36, Article: Article36, Joining: Joining36, Pay: Pay36, Rules: Rules36, Service: Service36 }, + "she-media": { Intro: Intro37, Article: Article37, Joining: Joining37, Pay: Pay37, Rules: Rules37, Service: Service37 }, + "snack-media": { Intro: Intro38, Article: Article38, Joining: Joining38, Pay: Pay38, Rules: Rules38, Service: Service38 }, + "sovrn": { Intro: Intro40, Article: Article40, Joining: Joining40, Pay: Pay40, Rules: Rules40, Service: Service40 }, + "sparteo": { Intro: Intro41, Article: Article41, Joining: Joining41, Pay: Pay41, Rules: Rules41, Service: Service41 }, + "taboola": { Intro: Intro42, Article: Article42, Joining: Joining42, Pay: Pay42, Rules: Rules42, Service: Service42 }, + "teads": { Intro: Intro43, Article: Article43, Joining: Joining43, Pay: Pay43, Rules: Rules43, Service: Service43 }, + "the-moneytizer": { Intro: Intro44, Article: Article44, Joining: Joining44, Pay: Pay44, Rules: Rules44, Service: Service44 }, + "venatus": { Intro: Intro45, Article: Article45, Joining: Joining45, Pay: Pay45, Rules: Rules45, Service: Service45 }, }; diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx index 7531ce64..f1584836 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Infolinks a fit for a small site? + +It can be. Infolinks is open to sites of any size, so it can work before you qualify for a managed network. + +## How current are Infolinks' public terms? + +The public agreement is years old. Ask Infolinks for the current version before you rely on its numbers. + +## How can you check if a site uses Infolinks? + +Look for Infolinks in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Infolinks. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx index 4b722216..41990089 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/intro.mdx @@ -1 +1 @@ -Publisher advertising platform with in-text ad products. A site can use more than one ad network. +Infolinks runs in-text ads and other ad units for websites. Publishers of any size can apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx new file mode 100644 index 00000000..f0c60dc2 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Infolinks? + +Any size of site can apply, and there's no traffic minimum. Infolinks reviews each site and may reject it. + +Illegal, negative, offensive, adult, obscene, and porn content isn't allowed. You sign up with a short web form, and review usually takes 48 hours. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx new file mode 100644 index 00000000..c0af55b9 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/pay.mdx @@ -0,0 +1,5 @@ +## How does Infolinks pay? + +The public agreement gives you 70% for In-Text ads and pays 45 working days after the month ends. Other ad formats have different shares and minimums. + +That agreement was last updated in 2015. If you sign a newer one, the newer terms count. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx new file mode 100644 index 00000000..ac48f346 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Infolinks set? + +The old public agreement starts with a 30-day trial, then renews every 12 months. It can be ended on one business day's notice. + +Adult and illegal content is banned. Check the current terms for exclusivity and ad placement rights. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx new file mode 100644 index 00000000..7da40934 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/infolinks/service.mdx @@ -0,0 +1,3 @@ +## What does Infolinks do for you? + +You track earnings in the Publisher Center. You can change how the ads look and how many show. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx index 7531ce64..d1c359ad 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How is Journey different from Mediavine? + +Journey is for sites still growing, with a much lower bar to join. [Mediavine](/ad-networks/mediavine)'s main program is for sites that already earn steady ad revenue. Journey's pay and rules aren't public, so don't assume they match Mediavine's. + +## What does Mediavine check when you apply to Journey? + +Your traffic sources, the countries your visitors come from, and who your readers are. It also wants original, brand-safe content made by people. + +## How can you check if a site uses Journey by Mediavine? + +Look for Journey by Mediavine in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Journey by Mediavine. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx index 8b6af17e..39953461 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/intro.mdx @@ -1 +1 @@ -Mediavine ad management program for growing publishers. A site can use more than one ad network. +Journey by Mediavine is Mediavine's ad program for smaller, growing sites. It suits bloggers who aren't ready for Mediavine's main program yet. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx new file mode 100644 index 00000000..59e9a7e8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Journey by Mediavine? + +Journey's traffic bar is low. Your content must be original, updated often, made by people, and brand safe, with an engaged audience. + +Mediavine looks at your traffic sources, visitor countries, and reader demographics. You need the Grow plugin on your site. Journey doesn't say which regions it accepts. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx new file mode 100644 index 00000000..e77eaffe --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/pay.mdx @@ -0,0 +1,3 @@ +## How does Journey by Mediavine pay? + +Not public. Journey's share, payment timing, minimum, methods, and currency aren't published. Mediavine's main program terms don't apply to Journey. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx new file mode 100644 index 00000000..f7fbb649 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Journey by Mediavine set? + +Not public. Exclusivity, ad placement rules, contract length, and notice for Journey aren't published. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx new file mode 100644 index 00000000..830b3bda --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/journey-by-mediavine/service.mdx @@ -0,0 +1,3 @@ +## What does Journey by Mediavine do for you? + +Journey works through the Grow plugin. Other service and reporting details are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx index 7531ce64..5d6d16cd 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What is a contextual ad? + +It's an ad picked for the words on the page, not for the person reading it. A page about hiking shows hiking ads. + +## Who is Media.net a fit for? + +Sites you own and run, with real, unpaid traffic. Media.net checks for bought or invalid traffic, and too much of it can get your account suspended. + +## How can you check if a site uses Media.net? + +Look for Media.net in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Media.net. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx index 1ffaadd6..8757ff47 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/intro.mdx @@ -1 +1 @@ -Publisher ad monetization platform. A site can use more than one ad network. +Media.net runs a contextual advertising program for publishers, showing ads based on what each page is about. Publishers apply to join, and the rules are in its public agreement. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx new file mode 100644 index 00000000..d3b3ad7d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Media.net? + +Media.net doesn't publish a traffic minimum or accepted regions. Its agreement covers sites you own, run, or control, and you apply through its publisher program. + +Its inventory policy bans invalid or bought traffic above acceptable levels. Your ad requests and site details must be accurate. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx new file mode 100644 index 00000000..d2adf0bf --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/pay.mdx @@ -0,0 +1,3 @@ +## How does Media.net pay? + +Media.net pays monthly on Net 30 terms. Your pay is based on the valid click revenue it receives, minus its commission and costs. The share, minimum, methods, and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx new file mode 100644 index 00000000..5016400e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Media.net set? + +You agree to keep Media.net's confidential information private. Media.net can suspend or end your account for policy problems. + +The agreement doesn't state an exclusivity rule, placement rules, or a fixed contract term. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx new file mode 100644 index 00000000..68299755 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/media-net/service.mdx @@ -0,0 +1,3 @@ +## What does Media.net do for you? + +The agreement explains how the program runs. Dashboard, reporting, and support levels are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx index 7531ce64..47eefbbc 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/article.mdx @@ -1 +1,19 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How do you join Mediavine? + +You apply with your site, and Mediavine reviews your ad revenue, traffic, and content against the rules here. If you're approved, you add their script and their ads.txt lines, and they set up and run the ads for you. + +## What if your site is too small for Mediavine? + +Mediavine runs a second program, [Journey](/ad-networks/journey-by-mediavine), for smaller sites. It has a lower bar to join and the same company behind it. Journey's page lists its rules and the sites that use it. + +## What does the pay day mean? + +Mediavine pays each month's earnings a set number of days after the month ends. Earnings from January arrive a couple of months later, not at the start of February. Plan for that gap if you switch from a network that pays faster. + +## Can you run other ads with Mediavine? + +Mediavine's help center lists its rules on ad placement and other ad partners. Affiliate links and sponsored posts are usually separate, but check the current rules with Mediavine before you sign. + +## How can you check if a site uses Mediavine? + +Look for Mediavine in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx index e0fb65ff..4c29fb65 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/intro.mdx @@ -1 +1 @@ -Ad management for publishers and creators. A site can use more than one ad network. +Mediavine runs the ads for food, travel, home, and other content sites. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx new file mode 100644 index 00000000..da15ec41 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Mediavine? + +Mediavine Official is for sites that already earn steady ad revenue. When you apply, Mediavine reviews both your traffic and your content. + +Your content needs to be original, high quality, and evergreen. If your site is still growing, [Journey](/ad-networks/journey-by-mediavine) is Mediavine's separate program for newer sites. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx new file mode 100644 index 00000000..6a61560a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/pay.mdx @@ -0,0 +1,5 @@ +## How does Mediavine pay? + +Each month's earnings are paid a set number of days after that month ends, on a fixed day of the month or the next business day. Your first payment arrives a couple of months after you start. + +The minimum payout depends on how you're paid, and some international methods need a higher balance. The currency you're paid in depends on the method you pick. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx new file mode 100644 index 00000000..7b956494 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Mediavine set? + +Mediavine says there's no long-term contract. You control your ad settings yourself from the dashboard. + +Its rules on content and ad placement are in the [Mediavine help center](https://help.mediavine.com/). diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx new file mode 100644 index 00000000..4df2e262 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mediavine/service.mdx @@ -0,0 +1,5 @@ +## What does Mediavine do for you? + +Your dashboard shows earnings by traffic source, country, device, page, and ad unit, with gross and net revenue side by side. + +Support is open every day of the week through the help center and the publisher community. Tools include Grow and the ad management settings. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx index 7531ce64..40ba4da4 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Why does MGID's share depend on the offer? + +MGID sets pay in each publisher's order, not in one public rate card. The share in one regional offer may not be what you're offered, so compare the order you get. + +## Does MGID take small sites? + +It doesn't publish a single traffic minimum, and its Discover program says it has none. Your site still has to pass its quality rules. + +## How can you check if a site uses MGID? + +Look for MGID in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to MGID. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx index 2dfcb12c..dd61f047 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/intro.mdx @@ -1 +1 @@ -Native advertising and website monetization platform for publishers. A site can use more than one ad network. +MGID is a native advertising platform that helps publishers earn from their sites. Its pay terms depend on the offer you sign. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx new file mode 100644 index 00000000..2ea8da0f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/joining.mdx @@ -0,0 +1,5 @@ +## Who can join MGID? + +MGID doesn't publish one traffic minimum or region rule. Your site has to meet its traffic quality and content rules. You apply through its publisher sign-up or contact page. + +MGID's separate Google Discover offer says it has no traffic minimum, but that applies only to that program. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx new file mode 100644 index 00000000..2c44e9c4 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/pay.mdx @@ -0,0 +1,5 @@ +## How does MGID pay? + +A regional MGID offer lists a 65% share, Net 30 payment, and minimums from $50 to $1,000 depending on the method. That's one offer, not a rate for everyone. + +MGID's terms say your pay follows your account and order terms and can be adjusted. The order you sign is what counts. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx new file mode 100644 index 00000000..bf926755 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/rules.mdx @@ -0,0 +1,5 @@ +## What rules does MGID set? + +The terms cover invalid traffic, how content and ads are shown, and allowed use. They don't set one exclusivity rule or contract length for everyone. + +A split mentioned in the terms is for an anti-adblock add-on, not the main service. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx new file mode 100644 index 00000000..82ae8462 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/mgid/service.mdx @@ -0,0 +1,3 @@ +## What does MGID do for you? + +The regional offer includes a dedicated account manager and round-the-clock support. Dashboard and reporting details are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx index 7531ce64..3601facd 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What's the difference between MonetizeMore and PubGuru? + +MonetizeMore is the company. PubGuru is its ad platform, and PubGuru Premium is the higher tier with extra tools and a support team. + +## Is PubGuru Premium a fit for a smaller site? + +Not yet. Premium is for sites with high ad revenue or traffic. Ask MonetizeMore what it offers below that level. + +## How can you check if a site uses MonetizeMore? + +Look for MonetizeMore in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to MonetizeMore. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx index f081d581..2e68b199 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/intro.mdx @@ -1 +1 @@ -Managed ad operations and PubGuru monetization tools for publishers. A site can use more than one ad network. +MonetizeMore manages ads for publishers, and PubGuru is its ad platform. PubGuru Premium suits sites that already earn well from ads or get heavy traffic. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx new file mode 100644 index 00000000..c0cff58a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/joining.mdx @@ -0,0 +1,5 @@ +## Who can join PubGuru Premium? + +PubGuru Premium needs either $5,000 in monthly ad revenue or 2 million monthly pageviews across your sites. MonetizeMore's FAQ mentions only the revenue rule, but the Premium page gives both. + +You start by contacting MonetizeMore. The Premium page doesn't list a location rule. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx new file mode 100644 index 00000000..ac277ce3 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/pay.mdx @@ -0,0 +1,5 @@ +## How does MonetizeMore pay? + +Not public. The Premium page doesn't give the share, payment schedule, minimum, methods, or currency. + +MonetizeMore's Traffic Cop product has its own payment terms, which don't cover PubGuru. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx new file mode 100644 index 00000000..78a36584 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/rules.mdx @@ -0,0 +1,3 @@ +## What rules does PubGuru Premium set? + +Not public. Contract terms, exclusivity, placement limits, and notice aren't on the product page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx new file mode 100644 index 00000000..8642d700 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monetizemore-pubguru/service.mdx @@ -0,0 +1,5 @@ +## What does PubGuru Premium do for you? + +Premium includes custom price floors, match rate automation, smart refresh, A/B testing, ad block recovery, premium demand, Open Bidding, and ads.txt management. A Partner Success Squad covers onboarding, ongoing success, and technical solutions. + +PubGuru Analytics gives you custom reports and checks for revenue gaps between your reports. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx index 7531ce64..17a8f36c 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Monumetric a fit for a new blog? + +It can be, if you write in English and your readers are mainly in the US, UK, Canada, or Australia. Propel is its program for smaller sites. + +## When does Monumetric pay you? + +A couple of months after you earn it, early in the month. Small balances carry over until they pass the minimum. + +## How can you check if a site uses Monumetric? + +Look for Monumetric in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Monumetric. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx index 49365a91..696594c3 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/intro.mdx @@ -1 +1 @@ -Managed advertising for website publishers. A site can use more than one ad network. +Monumetric is a managed ad service for English-language sites. Its Propel program suits smaller blogs that are starting to grow. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx new file mode 100644 index 00000000..e5da3dbd --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Monumetric? + +Your content must be in English, and at least 50% of your traffic must come from the US, UK, Canada, or Australia. Monumetric won't take content that harms its brand, such as porn, drugs, hate speech, or copyrighted material. + +You apply through its program page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx new file mode 100644 index 00000000..63751522 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/pay.mdx @@ -0,0 +1,5 @@ +## How does Monumetric pay? + +Monumetric pays on Net 60, within the first ten business days of the month. If your balance is under the minimum, it rolls into the next month. + +US publishers are paid by direct deposit, and international publishers by PayPal. Payoneer can count as a US bank account in some cases. The share and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx new file mode 100644 index 00000000..a34a48e1 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Monumetric set? + +Not public. Exclusivity, ad layout controls, contract length, and notice aren't on its eligibility or payment pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx new file mode 100644 index 00000000..fbe4acb0 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/monumetric/service.mdx @@ -0,0 +1,3 @@ +## What does Monumetric do for you? + +Monumetric manages and tunes your ads for you. Dashboard and support details are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx index 7531ce64..9e2f478e 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Which Newor Media program fits your site? + +Start with Elevate if your site is new, since it has no stated traffic bar. Catapult and Premier are for sites with more US traffic. + +## Why are there two payment terms for Newor Media? + +The FAQ and the platform agreement disagree. The signed agreement is your contract, so go by that, and ask Newor to confirm your dates. + +## Can you run other ads with Newor Media? + +Not other display networks on Elevate, which is exclusive. Read the agreement for your program before you add partners. + +## How can you check if a site uses Newor Media? + +Look for Newor Media in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Newor Media. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx index d863be3d..825a0cfb 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/intro.mdx @@ -1 +1 @@ -Publisher programs for managing and monetizing website ads. A site can use more than one ad network. +Newor Media runs ads for publishers through a ladder of programs, from Elevate for new sites to Catapult and Premier for larger ones. It suits English-language sites on Google Ad Manager. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx new file mode 100644 index 00000000..1d1f96ff --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Newor Media? + +Elevate, the entry program, has no stated pageview minimum. Your site must be in English, have a navigation bar, follow Google's ad policies, and have room for at least two ads per page. You need Google Ad Manager. Approval usually takes 24 to 48 hours. + +The larger programs set traffic rules: Catapult at 60,000 US pageviews a month, and Premier at 420,000. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx new file mode 100644 index 00000000..863c7567 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/pay.mdx @@ -0,0 +1,5 @@ +## How does Newor Media pay? + +The payment FAQ says Net 30, but the platform agreement says Net 45. If you've signed the agreement, its terms are the ones that count. + +Payment is in US dollars. The share and minimum are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx new file mode 100644 index 00000000..e995137d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Newor Media set? + +The Elevate program is exclusive. The platform agreement starts with a 90-day term, then renews. To leave, give written notice at least 14 days before the term ends. + +OTT inventory is handled separately and isn't exclusive. Some premium private marketplace deals do need exclusive inventory. Your content must follow Google's policies. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx new file mode 100644 index 00000000..21a2062d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/newor-media/service.mdx @@ -0,0 +1,3 @@ +## What does Newor Media do for you? + +The dashboard shows real-time reports. Support is by ticket, and Newor aims to reply within 24 hours. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx index 7531ce64..97e44284 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How fast does NitroPay pay? + +Faster than most networks on this list. Its docs describe payment about a week after the month, with no minimum beyond the transaction fee. + +## How long should you test NitroPay? + +NitroPay recommends a testing period of a few months before you judge results. + +## How can you check if a site uses NitroPay? + +Look for NitroPay in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to NitroPay. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx index af2ba7a8..c0e85b9b 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/intro.mdx @@ -1 +1 @@ -Ad monetization for website publishers. A site can use more than one ad network. +NitroPay is an ad platform for website publishers that pays out quickly after each month. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx new file mode 100644 index 00000000..5d957cac --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/joining.mdx @@ -0,0 +1,3 @@ +## Who can join NitroPay? + +Not public. NitroPay doesn't publish a traffic minimum, content rules, accepted regions, or application steps. Contact its team to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx new file mode 100644 index 00000000..2e71101d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/pay.mdx @@ -0,0 +1,5 @@ +## How does NitroPay pay? + +NitroPay's docs say it pays on Net 7, with no minimum as long as your earnings cover the transaction fee. The base share, full list of methods, and currency are not public. + +A separate bonus page lists share increases for some agreements. That's a bonus, not the starting share. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx new file mode 100644 index 00000000..57c07b0b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/rules.mdx @@ -0,0 +1,3 @@ +## What rules does NitroPay set? + +Not public. Contract terms and how much control you have over ads and layout aren't in its public docs. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx new file mode 100644 index 00000000..453bf3f8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/nitropay/service.mdx @@ -0,0 +1,3 @@ +## What does NitroPay do for you? + +You can reach its success team by email or through the chat in your dashboard. NitroPay suggests a 90-day testing period. Full dashboard and report details aren't in its public docs. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx index 7531ce64..74e6dc9b 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What's the difference between Managed Inventory and Managed Account? + +Both are ways Opti Digital runs your Google Ad Manager setup. They're separate commercial deals with different payment timing. With Managed Account, Google pays its share of revenue on its own schedule. + +## Who is Opti Digital for? + +Publishers who want a partner to run Google Ad Manager. Its fit rules aren't public, so ask its team. + +## How can you check if a site uses Opti Digital? + +Look for Opti Digital in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Opti Digital. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx index 3f9b5f4c..72ba0df0 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/intro.mdx @@ -1 +1 @@ -Publisher monetization platform. A site can use more than one ad network. +Opti Digital is a monetization platform that manages Google Ad Manager setups for publishers. It offers two setups, and each pays on different terms. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx new file mode 100644 index 00000000..82652644 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Opti Digital? + +Not public. Opti Digital doesn't publish a traffic minimum, site rules, regions, or an application checklist. Contact its team to talk about fit. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx new file mode 100644 index 00000000..18e7bd9a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/pay.mdx @@ -0,0 +1,7 @@ +## How does Opti Digital pay? + +Opti Digital works on a revenue share, but the rate is not public. When you're paid depends on your setup. + +With Managed Inventory, Opti Digital pays on Net 45. With Managed Account, it pays Net 60 for the demand it handles, and Google pays its own revenue on its 25-day cycle. Your order sets the exact terms. + +The minimum, methods, and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx new file mode 100644 index 00000000..7675e79f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Opti Digital set? + +Not public. Exclusivity, content rules, contract length, and notice aren't on its help pages. Managed Inventory and Managed Account are different commercial deals, so read the terms for yours. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx new file mode 100644 index 00000000..8304b2e5 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/opti-digital/service.mdx @@ -0,0 +1,3 @@ +## What does Opti Digital do for you? + +The publisher knowledge base explains how payment flows, and invoices are created for you. Dashboard reports and support levels are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx index 7531ce64..b5bfd452 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How much traffic do you need for Playwire? + +Playwire's own pages give different answers, so check the Joining tab and ask its team. Quality matters too: it checks invalid traffic and your ad setup. + +## Can you run other networks with Playwire? + +No. Playwire says other ad network tags can't run with its script. Plan to move your ad slots over to it. + +## Who decides where Playwire's ads go? + +You do. Playwire recommends layouts, but publishers have the final say. + +## How can you check if a site uses Playwire? + +Look for Playwire in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Playwire. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx index e45c693f..e6568ee6 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/intro.mdx @@ -1 +1 @@ -Advertising monetization for independent web publishers. A site can use more than one ad network. +Playwire is a managed ad service for web publishers and apps. It suits established sites with original content and clean traffic. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx new file mode 100644 index 00000000..a0c2a41f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Playwire? + +Playwire's pages disagree on traffic. Its FAQ asks for 500,000 monthly pageviews, or 1,000 daily active users for an app. Its qualification page says publishers are typically advised to have 100,000 pageviews and 20% US traffic. + +You also need original content, invalid traffic at or below 7%, a privacy policy, HTTPS, and room for at least three ads per device. Apply through Playwire's onboarding form. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx new file mode 100644 index 00000000..b04a739e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/pay.mdx @@ -0,0 +1,3 @@ +## How does Playwire pay? + +Playwire pays monthly on Net 60 terms and works on a revenue share. The share, minimum, methods, and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx new file mode 100644 index 00000000..05150f16 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Playwire set? + +Playwire suggests ad layouts, but you have the final say. You can't run other ad network tags alongside the Playwire script. Contract length and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx new file mode 100644 index 00000000..4fba3dd6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/playwire/service.mdx @@ -0,0 +1,3 @@ +## What does Playwire do for you? + +You get a dashboard, campaign tracking, and dedicated teams for publisher success, yield, and technical support. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx index 7531ce64..7a26dd21 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is PubFuture a fit for a small site? + +It may be. PubFuture doesn't publish a traffic minimum, so smaller sites can apply. + +## Can you control which ads PubFuture shows? + +Yes. PubFuture says you can block ads you don't want. + +## How can you check if a site uses PubFuture? + +Look for PubFuture in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubFuture. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx index 48fc1bf9..a3215702 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/intro.mdx @@ -1 +1 @@ -Header bidding and advertising monetization services for publishers. A site can use more than one ad network. +PubFuture is a header bidding and ad monetization service for publishers. It doesn't publish a traffic minimum, so smaller sites can apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx new file mode 100644 index 00000000..8eee674a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/joining.mdx @@ -0,0 +1,3 @@ +## Who can join PubFuture? + +PubFuture doesn't publish a traffic minimum, accepted regions, or a full content checklist. You apply through its sign-up page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx new file mode 100644 index 00000000..f80164fb --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/pay.mdx @@ -0,0 +1,3 @@ +## How does PubFuture pay? + +PubFuture pays on Net 30 terms once you pass the minimum. Its revenue share is not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx new file mode 100644 index 00000000..fefac70d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/rules.mdx @@ -0,0 +1,3 @@ +## What rules does PubFuture set? + +You can block ads you don't want. Exclusivity, content rules, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx new file mode 100644 index 00000000..740f199d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubfuture/service.mdx @@ -0,0 +1,3 @@ +## What does PubFuture do for you? + +An ad operations team supports you, and PubFuture says it replies within 24 hours. It tunes your ads and sends reports. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx index 7531ce64..a112ea47 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/article.mdx @@ -1 +1,19 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Does PubGalaxy take sites below its suggested traffic? + +It can. The traffic level is a suggestion, not a hard rule, and each site is reviewed on its own. + +## Why does PubGalaxy list two shares? + +One covers ads sold through its ad partners, and a lower one covers direct deals. Both are minimums in the published terms. + +## What does PubGalaxy check before it accepts you? + +Your traffic history, your content, and any copyright claims against your site in Google's Transparency Report. + +## Is PubGalaxy still running? + +Yes, as part of Improve Digital. Azerion bought PubGalaxy in 2021 and moved its publisher services under the Improve Digital name in September 2026. The pubgalaxy.com site now opens Improve Digital's site, so new publishers start there. Sites that already run PubGalaxy still name it in their ads.txt files, so we keep listing them here. + +## How can you check if a site uses PubGalaxy? + +Look for PubGalaxy in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubGalaxy. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx index 99be0017..dd81b891 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/intro.mdx @@ -1 +1 @@ -Ad revenue optimization for publishers. A site can use more than one ad network. +PubGalaxy is an ad revenue service that reviews each publisher one by one. It suits sites with original, brand-safe content and some traffic history. It's now part of Improve Digital, which Azerion owns. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx new file mode 100644 index 00000000..2f1bb724 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/joining.mdx @@ -0,0 +1,5 @@ +## Who can join PubGalaxy? + +PubGalaxy has no hard entry rules. It suggests a traffic level but reviews each site on its own. It asks for six months of traffic history and checks the Google Transparency Report for copyright claims against your site. + +Content should be original and brand safe. Stolen or automated content isn't allowed, and neither are violence, nudity, gambling, or banned substances. Apply through its publisher contact page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx new file mode 100644 index 00000000..da1b561d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/pay.mdx @@ -0,0 +1,5 @@ +## How does PubGalaxy pay? + +The published terms give you at least 85% of net revenue from ad partners and at least 65% from direct deals. PubGalaxy pays within 35 days after the month ends, once it receives the advertiser's money. + +The currency is not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx new file mode 100644 index 00000000..4f8fdfba --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/rules.mdx @@ -0,0 +1,5 @@ +## What rules does PubGalaxy set? + +You must own or license your content and let PubGalaxy manage your ad inventory. It can make changes that improve the reader experience, and the terms set rules for direct deals. + +PubGalaxy says terms can vary by publisher. Check exclusivity and notice in the order you sign. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx new file mode 100644 index 00000000..d817d4ae --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubgalaxy/service.mdx @@ -0,0 +1,3 @@ +## What does PubGalaxy do for you? + +You get detailed earnings reports and access to direct and private marketplace deals. Support response times are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx index 7531ce64..839b8581 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Does Publift care more about revenue or traffic? + +Revenue. Publift uses monthly ad revenue as its main test and treats pageviews as a hint. A site with fewer pageviews but strong earnings can still fit. + +## When does Publift pay? + +It depends on where the money comes from. Google revenue is paid sooner than revenue from other networks. Check the dates in your order. + +## How can you check if a site uses Publift? + +Look for Publift in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Publift. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx index 3619d0ae..1fdbf381 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/intro.mdx @@ -1 +1 @@ -Publisher monetization platform using Fuse technology. A site can use more than one ad network. +Publift is a managed ad service built on its Fuse platform. It qualifies sites by ad revenue first, so it suits sites that already earn steadily. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx new file mode 100644 index 00000000..9ca68383 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Publift? + +Publift looks at your monthly ad revenue first. Pageviews are a secondary signal, not a strict rule. Your traffic should be steady and genuine, and your content useful and safe. + +You apply for a site review. Publift doesn't say which regions it accepts. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx new file mode 100644 index 00000000..a3510725 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/pay.mdx @@ -0,0 +1,5 @@ +## How does Publift pay? + +Publift's comparison page lists payment by direct bank transfer, with Google revenue on Net 30 and other network revenue on Net 90. That page is marketing content, so confirm the currency and terms in your order. + +The share is not on its requirements page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx new file mode 100644 index 00000000..e9e22070 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Publift set? + +The requirements page doesn't state exclusivity, layout control, banned topics, contract length, or notice. A Publift blog post describes month-to-month service and a revenue split, but that's general guidance. Confirm both in your order. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx new file mode 100644 index 00000000..6ffa8a5f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publift/service.mdx @@ -0,0 +1,3 @@ +## What does Publift do for you? + +The Fuse platform tunes your ads and reports on them. Publift says you get a dedicated account management team. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx index 7531ce64..5a8b165e 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Which Publir model fits you? + +Pro suits a site that wants Publir to run its ads for a fee. Custom ad deals under the service agreement work on a revenue split instead. Ask which model you'd be on before you sign. + +## Can Publir change its fees? + +Yes. The service agreement allows Publir to change its percentages for costs and fees. + +## What else does Publir offer besides ads? + +Ad block recovery, subscriptions, crowdfunding, and QuickShop. These can add income beyond ad slots. + +## How can you check if a site uses Publir? + +Look for Publir in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Publir. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx index 03592a7f..e586edae 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/intro.mdx @@ -1 +1 @@ -Ad optimization and audience monetization platform for publishers. A site can use more than one ad network. +Publir is an ad optimization and audience monetization platform. It has no traffic or revenue minimum, so sites of any size can try it. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx new file mode 100644 index 00000000..ca23e7c7 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Publir? + +Publir has no traffic or revenue limit. It offers Pro and Enterprise plans, and you apply from its pricing page. Location and content rules are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx new file mode 100644 index 00000000..1f658dc2 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/pay.mdx @@ -0,0 +1,5 @@ +## How does Publir pay? + +Publir has two models. Its Pro service charges a 15% fee. Under its service agreement, custom ad revenue is split 50/50 and paid on Net 60. There's no single share for every publisher. + +The payment minimum, methods, and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx new file mode 100644 index 00000000..9db87427 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Publir set? + +The service agreement lets Publir change its percentages to cover costs and fees. Exclusivity, content rules, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx new file mode 100644 index 00000000..f1f60424 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publir/service.mdx @@ -0,0 +1,5 @@ +## What does Publir do for you? + +Publir offers ad optimization, ad block recovery, subscriptions, crowdfunding, and QuickShop. Pro support is by email during business hours. Enterprise adds real-time analytics and phone support. + +The service agreement says onboarding can take ten business days. Support runs weekdays from 8 a.m. to 5 p.m. Eastern, with a 12-hour reply target outside those hours. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx index 7531ce64..17848d97 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/article.mdx @@ -1 +1,15 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## When does Publisher Collective pay you? + +After the advertiser pays Publisher Collective. The partner terms don't set a fixed interval, so ask how long that usually takes. + +## What do you give up with Publisher Collective? + +You can't edit its ad tags, and you may need to send advertiser leads its way. Read those terms closely if you also sell ads directly. + +## What happened to Snigel? + +Snigel and Publisher Collective merged in February 2025. Publisher Collective now sells under its own name and keeps Snigel as its ad technology. Many sites still name Snigel's domain, snigelweb.com, in their ads.txt files. We count those sites under Publisher Collective. + +## How can you check if a site uses Publisher Collective? + +Look for Publisher Collective or Snigel in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Publisher Collective. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx index 7cf32bff..4a174ce6 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/intro.mdx @@ -1 +1 @@ -Publisher monetization service and network. A site can use more than one ad network. +Publisher Collective is a monetization service and network that sells ads for its publishers. Each publisher signs its own representation agreement. It merged with Snigel in 2025, so we count sites that name Snigel in their ads.txt files here too. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx new file mode 100644 index 00000000..979560f7 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Publisher Collective? + +There's no published traffic minimum. You must own or have rights to your content and follow its terms. Apply through its publisher contact page, then sign an Advertising Representation Agreement. + +Regions aren't fixed in the public terms. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx new file mode 100644 index 00000000..8e205e1d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/pay.mdx @@ -0,0 +1,5 @@ +## How does Publisher Collective pay? + +Publisher Collective pays after the advertiser pays it, by bank transfer in GBP, EUR, or USD. Your share is set in your representation agreement. + +A Publisher Collective blog post lists a different share, timing, and minimum. Those conflict with the partner terms, so go by the terms you sign. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx new file mode 100644 index 00000000..df187b68 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Publisher Collective set? + +Discriminatory, weapons, porn, hateful, and indecent content isn't allowed. You may need to update site materials within seven days, you can't change the ad tags, and you may need to pass ad leads to Publisher Collective. + +It can suspend or end the deal if you don't respond. Exclusivity and contract length are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx new file mode 100644 index 00000000..2b8f98cf --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/publisher-collective/service.mdx @@ -0,0 +1,3 @@ +## What does Publisher Collective do for you? + +Not public. The FAQ points you to apply and sign your own terms, but doesn't describe a dashboard, reports, or support. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx index 7531ce64..877ede9b 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How do you join PubNation? + +PubNation doesn't publish how to apply. Contact the company through its website. + +## What should you ask PubNation before you sign? + +Ask for the share, when you'd be paid, the payment minimum, and whether it must be your only ad partner. Ask how much notice you'd need to leave. + +## How can you check if a site uses PubNation? + +Look for PubNation in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubNation. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx index 8277d893..c4bda632 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/intro.mdx @@ -1 +1 @@ -Publisher monetization company. A site can use more than one ad network. +PubNation is a publisher monetization company. Its rules and terms aren't published on its website. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx new file mode 100644 index 00000000..4f8f096e --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/joining.mdx @@ -0,0 +1,3 @@ +## Who can join PubNation? + +Not public. PubNation doesn't publish a traffic minimum, site rules, accepted regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx new file mode 100644 index 00000000..cc8523db --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/pay.mdx @@ -0,0 +1,3 @@ +## How does PubNation pay? + +Not public. The share, payment timing, minimum, methods, and currency aren't on its website. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx new file mode 100644 index 00000000..b0506a5f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/rules.mdx @@ -0,0 +1,3 @@ +## What rules does PubNation set? + +Not public. Exclusivity, ad control, content rules, contract length, and notice aren't on its website. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx new file mode 100644 index 00000000..c33e8ef1 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubnation/service.mdx @@ -0,0 +1,3 @@ +## What does PubNation do for you? + +Not public. PubNation doesn't describe its reports, dashboard, support, or extra tools. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx index 7531ce64..81d3221a 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Why do some PubPower sites get paid sooner? + +PubPower pays sites above its monthly revenue line as soon as the month ends, with no waiting period. Other sites wait for the standard term. + +## Which PubPower payment method should you pick? + +Pick one with a minimum your earnings clear. Payoneer has the lowest minimum, and bank transfer the highest. + +## How can you check if a site uses PubPower? + +Look for PubPower in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to PubPower. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx index bd0dac31..64c12a2a 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/intro.mdx @@ -1 +1 @@ -Publisher ad management platform. A site can use more than one ad network. +PubPower is an ad management platform for publishers. Sites that earn more get paid faster. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx new file mode 100644 index 00000000..9495dcc1 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/joining.mdx @@ -0,0 +1,3 @@ +## Who can join PubPower? + +Not public. PubPower doesn't publish a traffic minimum, content rules, accepted regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx new file mode 100644 index 00000000..81f92e0b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/pay.mdx @@ -0,0 +1,5 @@ +## How does PubPower pay? + +If you earn more than $3,000 in a month, PubPower pays on Net 0. Otherwise it pays on Net 30. + +The minimum depends on the method: $50 for Payoneer, $100 for PayPal, $100 to $1,000 for crypto, and $1,000 for a bank transfer. The share and payout currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx new file mode 100644 index 00000000..909a51e4 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/rules.mdx @@ -0,0 +1,5 @@ +## What rules does PubPower set? + +Not public. Exclusivity, placement control, content rules, contract length, and notice aren't on its payment page. + +A revenue share article on its help site is for ad networks using its white-label product, not for publishers. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx new file mode 100644 index 00000000..339cb1f8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/pubpower/service.mdx @@ -0,0 +1,3 @@ +## What does PubPower do for you? + +Not public. PubPower doesn't describe its publisher dashboard, support, or reports on the page we checked. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx index 7531ce64..de28bd94 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Raptive a fit for your site? + +It is if you write original content and most of your readers are in the US, Canada, UK, Australia, or New Zealand. Sites with most traffic elsewhere won't qualify. + +## Does your Raptive service change as you grow? + +Yes. A dedicated manager, custom layouts, and fuller reporting depend on your tier. + +## How can you check if a site uses Raptive? + +Look for Raptive in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Raptive. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx index 82a62ed1..9c624913 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/intro.mdx @@ -1 +1 @@ -Ad management and monetization for creators. A site can use more than one ad network. +Raptive is a managed ad service for creators and independent publishers. It suits established sites with most readers in the US, Canada, UK, Australia, or New Zealand. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx new file mode 100644 index 00000000..a7dbf63c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Raptive? + +Most of your traffic must come from the US, Canada, UK, Australia, or New Zealand. You need original content and Google Analytics set up. + +Apply through Raptive's creator application. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx new file mode 100644 index 00000000..6bb39bba --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/pay.mdx @@ -0,0 +1,3 @@ +## How does Raptive pay? + +Raptive pays on the 15th of the month, on Net 45 terms. The minimum is higher if you choose a wire transfer. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx new file mode 100644 index 00000000..65703cf1 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Raptive set? + +Raptive sets up standard or custom ad layouts, depending on your size. Check placement controls in the agreement you sign. Exclusivity, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx new file mode 100644 index 00000000..4b9217ad --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/raptive/service.mdx @@ -0,0 +1,3 @@ +## What does Raptive do for you? + +You get a dashboard and support tickets. Depending on your tier, you also get a dedicated manager, and reporting and layout options differ by tier. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx index 7531ce64..d4bfc998 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## When does Refinery89 pay? + +It depends on your terms. Refinery89 has more than one payment schedule, so ask your Publisher Success Manager which one is yours. + +## What can you see in the Refinery89 dashboard? + +Daily revenue, CPM, RPM, pageviews, fill rate, and viewability, plus invoices and support tickets. You can export your data and manage ads.txt there too. + +## How can you check if a site uses Refinery89? + +Look for Refinery89 in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Refinery89. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx index 1bb3f8d1..6f029520 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/intro.mdx @@ -1 +1 @@ -Publisher ad monetization service. A site can use more than one ad network. +Refinery89 is an ad monetization service for publishers. Each publisher works with a Publisher Success Manager. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx new file mode 100644 index 00000000..4c32720d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Refinery89? + +Not public. Refinery89 doesn't publish a traffic minimum, site rules, eligible regions, or how to apply. Contact its publisher team. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx new file mode 100644 index 00000000..542691d3 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/pay.mdx @@ -0,0 +1,5 @@ +## How does Refinery89 pay? + +The payment minimum depends on your region. Refinery89 publishes more than one set of payment terms, so ask your Publisher Success Manager for your schedule. + +The share, methods, and currency are not fully public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx new file mode 100644 index 00000000..50925783 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Refinery89 set? + +Not public. Exclusivity, placement control, content rules, contract length, and notice aren't on its publisher pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx new file mode 100644 index 00000000..6a6388e9 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/refinery89/service.mdx @@ -0,0 +1,5 @@ +## What does Refinery89 do for you? + +The dashboard shows daily revenue, CPM, RPM, pageviews, fill rate, and viewability. It also has invoice history, tickets, exports, and ads.txt management. + +You can reach your success manager through a Chrome plugin, the dashboard, WhatsApp, email, or phone. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx index 7531ce64..15782d57 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How is your Revcontent share set? + +You negotiate it. Revcontent doesn't publish a standard rate, so compare its offer with what you earn now. + +## What's the payment minimum at Revcontent? + +It depends. The terms set one floor, and each payment method sets its own. Check your dashboard or order for the one that applies to you. + +## How can you check if a site uses Revcontent? + +Look for Revcontent in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Revcontent. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx index 98750459..fc370abf 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/intro.mdx @@ -1 +1 @@ -Content recommendation and native advertising platform for publishers. A site can use more than one ad network. +Revcontent is a content recommendation and native advertising platform for publishers. Its share is negotiated with each publisher. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx new file mode 100644 index 00000000..f6e0c9b3 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Revcontent? + +Revcontent doesn't publish a traffic minimum or region rule. You apply for approval, and your site must follow its publisher terms and content rules. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx new file mode 100644 index 00000000..3e785b9c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/pay.mdx @@ -0,0 +1,5 @@ +## How does Revcontent pay? + +Your share is negotiated. The terms work out your revenue after allowed fees and set a payment floor. + +Each method also has its own minimum: $100 for ACH in the US, $100 for PayPal outside the US, and $1,000 for international wire. These don't add up to one clear threshold, so check your dashboard or order. Payment is in US dollars. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx new file mode 100644 index 00000000..aca1dc46 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Revcontent set? + +The publisher terms set policy and placement rules. They don't state one exclusivity rule or contract term for everyone. Content rules are in the publisher policy. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx new file mode 100644 index 00000000..9950a7f3 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/revcontent/service.mdx @@ -0,0 +1,3 @@ +## What does Revcontent do for you? + +Not public. Revcontent's terms don't describe support, reporting, or dashboard service levels. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx index 7531ce64..406d9fb2 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Why are Setupad's rules unclear? + +Setupad's published rules are in blog articles from different years, and they don't fully agree. Ask its team for current eligibility before you count on them. + +## How can Setupad pay you? + +By bank transfer, PayPal, Payoneer, Wise, Revolut, or Paysera, according to its guidance. + +## How can you check if a site uses Setupad? + +Look for Setupad in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Setupad. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx index 81882588..a7649b7f 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/intro.mdx @@ -1 +1 @@ -Publisher platform for display ad monetization. A site can use more than one ad network. +Setupad is a platform that runs display ads for publishers. Its published rules come from blog articles, so check current terms with its team. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx new file mode 100644 index 00000000..20436579 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/joining.mdx @@ -0,0 +1,5 @@ +## Who can join Setupad? + +Setupad's articles from different years describe a traffic rule, but one counts visitors and another counts sessions. They also mention traffic from the US, UK, Canada, and Australia. We didn't find a current criteria page, so confirm with Setupad. + +You need to follow ad policies and set up ads.txt. Apply through Setupad's publisher contact page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx new file mode 100644 index 00000000..e6e1df99 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/pay.mdx @@ -0,0 +1,3 @@ +## How does Setupad pay? + +Setupad's published guidance lists Net 60 payment. The share and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx new file mode 100644 index 00000000..bb1e5f69 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Setupad set? + +You need to follow ad policies and keep an ads.txt file. Exclusivity, ad placement control, contract length, and notice are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx new file mode 100644 index 00000000..0de1377d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/setupad/service.mdx @@ -0,0 +1,3 @@ +## What does Setupad do for you? + +Not public. Setupad's articles don't describe its dashboard, reports, or support levels. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx index 7531ce64..e62919ed 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## How do you join SHE Media? + +SHE Media doesn't publish a general application checklist. Its referral program lists criteria for referred sites, which is the clearest public guide. + +## Can publishers outside the US work with SHE Media? + +Yes. SHE Media has payment options for publishers outside the US, including PayPal, Wise, ACH, and wire. Payoneer isn't one of them. + +## How can you check if a site uses SHE Media? + +Look for SHE Media in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to SHE Media. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx index 4e89fd3d..baf495ba 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/intro.mdx @@ -1 +1 @@ -Publisher partner network run by SHE Media. A site can use more than one ad network. +SHE Media runs a partner network for publishers. Its joining rules and pay share are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx new file mode 100644 index 00000000..1a1154bd --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/joining.mdx @@ -0,0 +1,5 @@ +## Who can join SHE Media? + +SHE Media doesn't publish a general traffic minimum. Its referral program mentions a traffic level for referred sites, along with quality content and Google Analytics. That's for referrals, not every applicant. + +Regions and a current checklist are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx new file mode 100644 index 00000000..c2e88161 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/pay.mdx @@ -0,0 +1,5 @@ +## How does SHE Media pay? + +The share, payment schedule, and general minimum are not public. + +Outside the US, SHE Media pays by PayPal if you earn under $1,000 a month, and prefers wire transfer above that. Wise and ACH are options too. Payoneer isn't accepted. Currency is not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx new file mode 100644 index 00000000..f9b16605 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/rules.mdx @@ -0,0 +1,3 @@ +## What rules does SHE Media set? + +Not public. Exclusivity, ad placement control, content rules, contract length, and notice aren't on its publisher help pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx new file mode 100644 index 00000000..d93d8473 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/she-media/service.mdx @@ -0,0 +1,3 @@ +## What does SHE Media do for you? + +The Publisher Portal shows reports, at least for referral payouts. General dashboard reports, support times, and tools are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx index 7531ce64..f85a9959 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Can you leave Snack Media? + +Yes. Either side can end the contract with notice, and after the first term it runs month to month. + +## Does Snack Media need to be your only ad partner? + +Only if your order says so. Check the cover sheet before you sign. + +## How can you check if a site uses Snack Media? + +Look for Snack Media in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Snack Media. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx index e585d510..16371e41 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/intro.mdx @@ -1 +1 @@ -Publisher advertising and monetization services. A site can use more than one ad network. +Snack Media is a publisher ad network and monetization service. After the first contract term, it runs month to month. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx new file mode 100644 index 00000000..b099580d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Snack Media? + +Not public. Snack Media doesn't publish a traffic minimum, content rules, regions, or an application process. Contact it through its site. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx new file mode 100644 index 00000000..5d39abcc --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/pay.mdx @@ -0,0 +1,5 @@ +## How does Snack Media pay? + +Snack Media pays within 30 days after the month it receives the advertiser's money. Your share is set on the cover sheet you sign. The standard terms don't give one rate, minimum, method, or currency. + +Older Snack Media terms list a different minimum and timing. We go by the 2025 agreement instead. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx new file mode 100644 index 00000000..ca69d78a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Snack Media set? + +Snack Media is exclusive only if your order says so. The cover sheet sets the first contract term, and its length is not public. After that, the contract renews month to month. + +Either side can end the contract with 30 days' notice. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx new file mode 100644 index 00000000..3decafe8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/snack-media/service.mdx @@ -0,0 +1,3 @@ +## What does Snack Media do for you? + +Not public. The standard terms don't describe a dashboard, reports, support, or extra tools. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx deleted file mode 100644 index 7531ce64..00000000 --- a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/article.mdx +++ /dev/null @@ -1 +0,0 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx deleted file mode 100644 index 4f5a1524..00000000 --- a/app/(site)/(directories)/ad-networks/[slug]/content/snigel/intro.mdx +++ /dev/null @@ -1 +0,0 @@ -Publisher ad management brand. A site can use more than one ad network. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx index 7531ce64..0245d4ec 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What's the difference between Sovrn's Ad Exchange and Sovrn Commerce? + +The Ad Exchange sells ad space on your site. Sovrn Commerce is a separate affiliate product with its own payment terms. The details on this page are for the Ad Exchange. + +## Is Sovrn a fit for a small site? + +It can be. The Ad Exchange doesn't set a traffic minimum, and its payment minimum is low. + +## How can you check if a site uses Sovrn? + +Look for Sovrn in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Sovrn. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx index 6e2eb8d3..5498d669 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/intro.mdx @@ -1 +1 @@ -Advertising and monetization products for publishers. A site can use more than one ad network. +Sovrn offers an ad exchange and other monetization tools for publishers. Its Ad Exchange has no published traffic minimum. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx new file mode 100644 index 00000000..3285008a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Sovrn? + +You need a working business domain and an ad system ID, and you must keep your ads.txt file up to date. Sovrn's approval guide doesn't set a traffic minimum. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx new file mode 100644 index 00000000..ff6505d2 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/pay.mdx @@ -0,0 +1,5 @@ +## How does Sovrn pay? + +The Ad Exchange pays on Net 45, in US dollars, with some options to receive local currency. + +Sovrn Commerce, its separate affiliate product, has different payment terms. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx new file mode 100644 index 00000000..cbfadc0b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Sovrn set? + +The Ad Exchange approval and payment pages don't state exclusivity, placement control, content rules, contract length, or notice. Sovrn Commerce has its own terms. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx new file mode 100644 index 00000000..de13a37f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sovrn/service.mdx @@ -0,0 +1,3 @@ +## What does Sovrn do for you? + +Sovrn's knowledge base covers account upkeep and payments. Dashboard reports and support levels are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx index 7531ce64..a9b9931d 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## What does Sparteo offer besides display ads? + +Video through Viously, consent management through FastCMP, and Actirise, plus analytics on revenue and page speed. + +## How do you work with Sparteo? + +Sparteo doesn't publish how to apply. Contact the company to ask. + +## How can you check if a site uses Sparteo? + +Look for Sparteo in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Sparteo. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx index 118d5d4f..de1aee28 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/intro.mdx @@ -1 +1 @@ -Publisher monetization technology and managed advertising services. A site can use more than one ad network. +Sparteo is an ad technology company with managed ad services for publishers. Its services include Actirise, Viously video, and FastCMP. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx new file mode 100644 index 00000000..0760291b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Sparteo? + +Not public. Sparteo doesn't publish a traffic minimum, site rules, regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx new file mode 100644 index 00000000..9d7d8d42 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/pay.mdx @@ -0,0 +1,3 @@ +## How does Sparteo pay? + +Not public. Sparteo says it earns money when its publishers do, but it doesn't publish a share, payment timing, minimum, methods, or currency. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx new file mode 100644 index 00000000..0ce83338 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Sparteo set? + +Not public. Exclusivity, placement control, content rules, contract length, and notice aren't on its public pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx new file mode 100644 index 00000000..e83cdefa --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/sparteo/service.mdx @@ -0,0 +1,5 @@ +## What does Sparteo do for you? + +Sparteo covers display ads, analytics, Actirise, Viously video, and FastCMP, a consent management tool. Its reports show revenue, viewability, time, URL, and Core Web Vitals. + +Support terms are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx index 7531ce64..064cf4ad 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Can you run Taboola with another recommendation widget? + +No. Taboola must be your only recommendation service during the agreement. The rule covers recommendation services, so check your order for anything else. + +## What content can't run Taboola? + +Fake news, porn, copyright violations, defamation, threats, and criminal material are banned under its Publisher Policy. + +## How can you check if a site uses Taboola? + +Look for Taboola in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Taboola. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx index 4fdcf874..4643d699 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/intro.mdx @@ -1 +1 @@ -Content recommendation and advertising platform for publishers. A site can use more than one ad network. +Taboola is a content recommendation and advertising platform for publishers. It shows recommended stories and ads on your pages. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx new file mode 100644 index 00000000..dbed98c7 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Taboola? + +Taboola doesn't publish a traffic minimum or region rule. Your site must follow the Taboola Publisher Policy. Apply by contacting Taboola. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx new file mode 100644 index 00000000..c3f48c6c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/pay.mdx @@ -0,0 +1,3 @@ +## How does Taboola pay? + +Your share is set in your order. Taboola pays in US dollars within 45 days after the month ends, through Payoneer. A general payment minimum is not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx new file mode 100644 index 00000000..33f82f3d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/rules.mdx @@ -0,0 +1,5 @@ +## What rules does Taboola set? + +While your agreement runs, Taboola must be your only recommendation service. The policy bans fake news, porn, copyright violations, defamation, threats, and criminal content. + +Your order sets the contract length and how to end it. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx new file mode 100644 index 00000000..45cdea36 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/taboola/service.mdx @@ -0,0 +1,3 @@ +## What does Taboola do for you? + +Taboola offers publisher support resources and policy help. Dashboard reports and dedicated support levels are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx index 7531ce64..09b8a416 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Is Teads the same as Outbrain? + +Teads now includes what used to be Outbrain's business. + +## How do you join Teads? + +Teads doesn't publish an application process. Its media owner page is where to start a partnership conversation. + +## How can you check if a site uses Teads? + +Look for Teads in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Teads. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx index 049c4ddc..86e85736 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/intro.mdx @@ -1 +1 @@ -Publisher advertising platform that includes the former Outbrain business. A site can use more than one ad network. +Teads is a publisher advertising platform for web, connected TV, and apps. It now includes the former Outbrain business. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx new file mode 100644 index 00000000..9631c698 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Teads? + +Not public. Teads describes partnerships across web, CTV, and apps, but doesn't publish a traffic minimum, content rules, regions, or how to apply. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx new file mode 100644 index 00000000..87273b6c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/pay.mdx @@ -0,0 +1,5 @@ +## How does Teads pay? + +Your share is set in your contract. A Teads company filing says publisher payment terms usually run 30 to 60 days, but that's not one fixed term. + +The payment minimum, methods, and currency are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx new file mode 100644 index 00000000..4c1e146a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Teads set? + +Teads describes editorial controls and lets you control which ads are blocked. Exclusivity, content rules, contract length, and notice are not public, so confirm them in your contract. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx new file mode 100644 index 00000000..38576db6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/teads/service.mdx @@ -0,0 +1,3 @@ +## What does Teads do for you? + +Teads offers premium ad placements across web, CTV, and apps, plus content extension. Its media owner page mentions partnership support, but not dashboard or reporting levels. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx index 7531ce64..e3366bfc 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## When does The Moneytizer pay you? + +A couple of months after the month you earn it. The payment simulator on its site shows how the timing works. + +## How do you join The Moneytizer? + +Sign up on its site. The Moneytizer doesn't publish entry rules, so its team decides. + +## How can you check if a site uses The Moneytizer? + +Look for The Moneytizer in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to The Moneytizer. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx index a20fbb64..3589454e 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/intro.mdx @@ -1 +1 @@ -Managed website monetization and header bidding for publishers. A site can use more than one ad network. +The Moneytizer is a managed monetization and header bidding service for publishers. Its payment minimum is low. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx new file mode 100644 index 00000000..78382207 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/joining.mdx @@ -0,0 +1,3 @@ +## Who can join The Moneytizer? + +Not public. The Moneytizer doesn't publish a traffic minimum, content rules, regions, or application steps. Its site has a publisher sign-up page. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx new file mode 100644 index 00000000..cc37f93c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/pay.mdx @@ -0,0 +1,3 @@ +## How does The Moneytizer pay? + +After each month ends, The Moneytizer creates your invoice, and payment follows about two months later. The payment method and share are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx new file mode 100644 index 00000000..7f436511 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/rules.mdx @@ -0,0 +1,3 @@ +## What rules does The Moneytizer set? + +Not public. Exclusivity, placement control, content rules, contract length, and notice aren't in its payment information. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx new file mode 100644 index 00000000..5f918eb0 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/the-moneytizer/service.mdx @@ -0,0 +1,3 @@ +## What does The Moneytizer do for you? + +Not public. Its site doesn't describe a dashboard, reports, support, or extra tools in enough detail to list here. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx index 7531ce64..fa4301a4 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/article.mdx @@ -1 +1,11 @@ -A site appears here only when its current ads.txt or live configuration supports the stated relationship. A seller authorization alone does not establish active use. +## Who is Venatus for? + +Publishers in gaming, sports, and entertainment. If your site covers those topics, Venatus is built around that kind of audience. + +## How do you apply to Venatus? + +Fill in the publisher interest form on its site. Venatus doesn't publish entry rules, so its team decides. + +## How can you check if a site uses Venatus? + +Look for Venatus in the site's ads.txt file, or paste the domain into our [ad network checker](/ad-network-checker). We list a site here only when its current ads.txt or live ad setup points to Venatus. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx index 744f98d2..996c4af3 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx +++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/intro.mdx @@ -1 +1 @@ -Advertising and monetization for publisher partners. A site can use more than one ad network. +Venatus is an ad partner for gaming, sports, and entertainment sites. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx new file mode 100644 index 00000000..e68ece34 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/joining.mdx @@ -0,0 +1,3 @@ +## Who can join Venatus? + +Venatus works with gaming, sports, and entertainment sites. It doesn't publish a traffic minimum, region rule, or content checklist. You can send its publisher interest form. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx new file mode 100644 index 00000000..42ff6a9d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/pay.mdx @@ -0,0 +1,3 @@ +## How does Venatus pay? + +Not public. Venatus doesn't publish its share, payment timing, minimum, methods, or currency. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx new file mode 100644 index 00000000..1dcab4fd --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/rules.mdx @@ -0,0 +1,3 @@ +## What rules does Venatus set? + +Not public. Venatus describes curated demand and ad management, but not exclusivity, placement control, content rules, contract length, or notice. diff --git a/app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx new file mode 100644 index 00000000..63fe1469 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/content/venatus/service.mdx @@ -0,0 +1,3 @@ +## What does Venatus do for you? + +Venatus says expert publisher teams manage your ads and bring curated demand. Dashboard and reporting details are not public. diff --git a/app/(site)/(directories)/ad-networks/[slug]/network-details.tsx b/app/(site)/(directories)/ad-networks/[slug]/network-details.tsx new file mode 100644 index 00000000..326c04c7 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/network-details.tsx @@ -0,0 +1,109 @@ +import type { MDXContent } from "mdx/types"; +import type { ReactNode } from "react"; +import { DetailsLayout, FactsList, KeyNumbers, NotPublic, SourcesLine, type Fact, type KeyNumber } from "@/app/ui/ad-networks/details"; +import { formatDay } from "@/app/ui/ad-networks/site-format"; +import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose"; +import { UiTabs, type UiTab } from "@/app/ui/tabs"; +import { NOT_PUBLIC, joinPhrase, joiningNumbers, networkFacts } from "../ad-networks-shared"; +import type { AdNetworkFacts } from "../network-facts"; +import type { AdNetworkContent } from "./content"; +import styles from "./network.module.css"; + +type KeyFact = AdNetworkFacts["keyNumbers"][number]; + +// The mockup's words for the labels research uses. +const keyLabels: Record = { "your share": "revenue share" }; + +/** + * One key number in the mockup's form: the value, then a short label. A joining rule reads as + * `sessions a month to join`; anything after the first `;` in its note stays as a caveat. + */ +function keyNumber(item: KeyFact, joining: boolean): KeyNumber { + const [first = "", ...rest] = (item.note ?? "").split(";"); + if (joining) { + const phrase = joinPhrase(item); + if (phrase.startsWith(item.value) && phrase.length > item.value.length) { + // joinPhrase keeps only the unit; whatever it drops ("or 2 million …", ", for new sites") stays as the caveat. + const alternative = first.match(/\s(or\s+[^,]*\d.*)$/i)?.[1]; + const clause = first.replace(/\s+or\s+(?=[^,]*\d).*$/i, "").split(",").slice(1).join(",").trim(); + const caveat = [alternative, clause, rest.join(";").trim()].filter(Boolean).join("; "); + return { value: item.value, label: `${phrase.slice(item.value.length).trim()} to join`, note: caveat || undefined }; + } + } + const label = item.label.charAt(0).toLowerCase() + item.label.slice(1); + return { value: item.value, label: keyLabels[label] ?? label, note: [first, ...rest].join(";").trim() || undefined }; +} + +/** Tab files start at `##`. They sit under the h3 panel heading UiTabs adds, so they render one level down. */ +const tabComponents = { + h2: ({ children }: { children?: ReactNode }) =>

          {children}

          , + h3: ({ children }: { children?: ReactNode }) =>
          {children}
          , +}; + +/** Article questions sit under the details h2, so their `##` headings render as h3. */ +const articleComponents = { + h2: ({ children }: { children?: ReactNode }) =>

          {children}

          , + h3: ({ children }: { children?: ReactNode }) =>

          {children}

          , +}; + +function TabWords({ Words }: { Words?: MDXContent }) { + return Words ?
          : null; +} + +/** Fact rows for one tab, or nothing when the network publishes none of them. */ +function factRows(rows: readonly { label: string; value: string | null; note?: string }[]): Fact[] { + if (!rows.some((row) => row.value)) return []; + return rows.map((row) => ({ label: row.label, value: row.value ?? , note: row.note })); +} + +/** + * "What does require and pay?" (mockup `.details`): key numbers, the Joining, Pay, + * Rules, and Service tabs, and the sources line, with the Q&A prose beside them. Numbers come from + * network-facts.ts; the words come from the network's MDX files. + */ +export function NetworkDetails({ content, name, slug }: { content: AdNetworkContent; name: string; slug: string }) { + const facts = networkFacts(slug); + const { Article, Joining, Pay, Rules, Service } = content; + + const shown = facts?.keyNumbers.slice(0, 3) ?? []; + const joining = facts ? joiningNumbers(facts) : []; + const keyNumbers = shown.map((item) => keyNumber(item, joining.includes(item))); + + // Researched joining rows in network-facts.ts, in the mockup's order. "Not public" is a finding, not missing data. + const joiningRows: Fact[] = facts ? facts.joining.map((row) => ({ + label: row.label, + value: row.value === NOT_PUBLIC ? : row.value, + note: row.note, + })) : []; + const payRows = facts ? factRows([ + { label: "Revenue share", value: facts.revenueShare }, + { label: "Pay day", value: facts.paymentTerms }, + { label: "Minimum payout", value: facts.paymentThreshold }, + { label: "Paid by", value: facts.paymentMethods.length ? facts.paymentMethods.join(", ") : null }, + ]) : []; + + const tabs: UiTab[] = [ + { id: "joining", title: "Joining", rows: joiningRows, Words: Joining }, + { id: "pay", title: "Pay", rows: payRows, Words: Pay }, + { id: "rules", title: "Rules", rows: [] as Fact[], Words: Rules }, + { id: "service", title: "Service", rows: [] as Fact[], Words: Service }, + ] + .filter((tab) => tab.rows.length || tab.Words) + .map(({ id, rows, title, Words }) => ({ id, title, content: <>{rows.length ? : null} })); + + const checked = facts ? formatDay(facts.checkedAt, true) : null; + + return ( +
          } + title={`What does ${name} require and pay?`} + titleId="details" + > + {keyNumbers.length ? : null} + {tabs.length ? : null} + {facts?.sources.length ? ( + ({ label: source.label, href: source.url }))} /> + ) : null} + + ); +} diff --git a/app/(site)/(directories)/ad-networks/[slug]/network-stats.tsx b/app/(site)/(directories)/ad-networks/[slug]/network-stats.tsx new file mode 100644 index 00000000..da0607b9 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/network-stats.tsx @@ -0,0 +1,230 @@ +import type { ReactNode } from "react"; +import type { AdNetworkListRow, AdNetworkStatsReport } from "@/lib/ad-networks-types"; +import { trafficLabel } from "@/lib/ad-networks-report"; +import { Report, ReportBlock, ReportFigure, ReportGroup, ReportNumbers, ReportRow, type ReportNumber } from "@/app/ui/report"; +import { AgeStrip } from "@/app/ui/ad-networks/viz/age-strip"; +import { AreaLine } from "@/app/ui/ad-networks/viz/area-line"; +import { RankedBars, StackBar, type RankedBar } from "@/app/ui/ad-networks/viz/bars"; +import { ColumnChart } from "@/app/ui/ad-networks/viz/columns"; +import { Donut } from "@/app/ui/ad-networks/viz/donut"; +import { formatNumber, formatSigned } from "@/app/ui/ad-networks/viz/format"; +import { JoinedLeftBars } from "@/app/ui/ad-networks/viz/joined-left"; +import { PublisherBlocks } from "@/app/ui/ad-networks/viz/publisher-blocks"; +import { SwitchFlow } from "@/app/ui/ad-networks/viz/switch-flow"; +import { TypeScale } from "@/app/ui/ad-networks/viz/type-scale"; +import { nicheName } from "@/app/ui/ad-networks/site-format"; +import { ordinal, percent } from "../ad-networks-shared"; + +// Loader shares are 0 to 100. Never round a share that isn't 100% up to 100%. +const pct = (share: number | undefined) => percent(share ?? 0); + +const monthShort = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" }); +const monthLong = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" }); +const day = (iso: string) => Date.parse(`${iso}T00:00:00Z`); + +const dayMs = 86_400_000; +const monday = (time: number) => time - ((new Date(time).getUTCDay() + 6) % 7) * dayMs; + +/** + * The loader's joined and left periods hold only days or weeks with changes. Place each on the + * period's timeline, ending today in UTC like the loader's windows, so gaps read as gaps. + */ +function timeline(periods: NonNullable, now = Date.now()) { + const today = Date.parse(`${new Date(now).toISOString().slice(0, 10)}T00:00:00Z`); + return periods.map((period) => { + const weekly = period.interval === "week"; + const first = weekly ? monday(today - (period.days - 1) * dayMs) : today - (period.days - 1) * dayMs; + const step = weekly ? 7 * dayMs : dayMs; + const slots = Math.round(((weekly ? monday(today) : today) - first) / step) + 1; + return { + ...period, + slots, + startLabel: `${period.days} days ago`, + endLabel: "today", + points: period.points.map((point) => ({ ...point, slot: Math.round((day(point.date) - first) / step) })), + }; + }); +} + +/** Shorter names for tight chart labels on phones. */ +const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, ""); + +/** A row of blocks with only the blocks that have data. Nothing renders when none do. */ +function Row({ blocks, layout }: { blocks: readonly ReactNode[]; layout: "1-1" | "2-1" | "1-1-1" | "2-1-1" }) { + const shown = blocks.filter(Boolean); + return shown.length ? {shown} : null; +} + +/** + * " stats" (mockup `.stats`): question-led groups in the mockup's order. Every block hides + * when its loader field is empty or null, and a group with no blocks left hides. The gates live in + * lib/ad-networks*.ts; each field's data run is in dev-space/ad-networks-data-contract.md. + */ +export function NetworkStats({ network, stats }: { network: AdNetworkListRow; stats: AdNetworkStatsReport }) { + const { name, slug } = network; + if (!stats.listed) return null; + + // How many sites use it? + const summary: ReportNumber[] = [ + { label: "sites listed", value: formatNumber(stats.listed) }, + { label: "confirmed live", value: formatNumber(stats.confirmed), accent: true }, + ...(stats.rank ? [{ label: `of ${stats.networks_tracked} networks`, value: ordinal(stats.rank) }] : []), + ...(stats.change_year !== null ? [{ label: "sites in a year", value: formatSigned(stats.change_year), accent: stats.change_year > 0 }] : []), + ]; + const history = stats.history.map((row) => ({ label: monthShort.format(day(row.date)), fullLabel: monthLong.format(day(row.date)), value: row.listed })); + const rankRows: RankedBar[] = stats.ranking + .map((row, index) => ({ row, index })) + .filter(({ row, index }) => row.count > 0 && (index < 5 || row.slug === slug)) + .map(({ row }) => ({ label: row.name, value: row.count, href: row.slug === slug ? undefined : `/ad-networks/${row.slug}`, highlight: row.slug === slug })); + + // What kind of sites? + const topNiches = stats.niches.slice(0, 6); + const otherNiches = stats.niches.slice(6).reduce((sum, row) => sum + (row.share ?? 0), 0); + const nicheRows: RankedBar[] = [ + ...topNiches.map((row) => ({ label: nicheName(row.key) ?? row.label, value: row.share ?? 0, display: pct(row.share), href: `/ad-networks/niches/${row.key}` })), + ...(otherNiches > 0 ? [{ label: "Other", value: otherNiches, display: pct(otherNiches) }] : []), + ]; + const traffic = stats.traffic.map((row) => ({ + label: trafficLabel(row.key).replace(/^top /, ""), + value: row.count, + tone: row.key.startsWith("not") ? "n2" as const : "ac" as const, + })); + const publishers = stats.publishers.slice(0, 5).map((row) => ({ label: row.label, count: row.count })); + + // Joined, left, and switches. + const periods = timeline(stats.movement_periods ?? []); + const flow = stats.switch_flow; + + // How are the sites built? + const age = stats.domain_age_strip; + const platforms = stats.platform_mix ?? []; + const measuredPlatforms = platforms.filter((row) => row.key !== "unknown"); + const topEndings = stats.domain_endings.slice(0, 5); + const otherEndings = stats.domain_endings.slice(5).reduce((sum, row) => sum + (row.share ?? 0), 0); + const endings = [ + ...topEndings.map((row) => ({ label: row.label, share: (row.share ?? 0) / 100 })), + ...(otherEndings > 0 ? [{ label: "other", share: otherEndings / 100 }] : []), + ]; + + // What else is in their ads.txt? + const manager = (key: string) => stats.managers.find((row) => row.key === key); + const alsoRows: RankedBar[] = stats.also_runs.filter((row) => row.count > 0).slice(0, 5) + .map((row) => ({ label: row.name, value: row.share, display: pct(row.share), href: `/ad-networks/${row.slug}` })); + + const kindBlocks = [ + nicheRows.length ? ( + + + + ) : null, + traffic.length ? ( + + + + ) : null, + publishers.length ? ( + + + + ) : null, + ]; + const moveBlocks = [ + periods.length ? ( + + + + ) : null, + flow && (flow.came_from.length || flow.went_to.length) ? ( + + ({ label: row.label, shortLabel: shortName(row.label), count: row.count }))} + caption={`Where sites came from and went to in the last 90 days`} + center={flow.center.name} + wentTo={flow.went_to.map((row) => ({ label: row.label, shortLabel: shortName(row.label), count: row.count }))} + /> + + ) : null, + ]; + const buildBlocks = [ + age ? ( + + {stats.median_domain_age !== null ? ( + + ) : null} + + + ) : null, + measuredPlatforms.length ? ( + + ({ label: row.label, value: row.count, ...(row.key === "unknown" ? { tone: "track" as const } : {}) }))} + /> + + ) : null, + endings.length ? ( + + + + ) : null, + ]; + const adsTxtBlocks = [ + stats.managers.length ? ( + + + + + ) : null, + alsoRows.length ? ( + + + + ) : null, + ]; + + const groups: { title: string; content: ReactNode; has: boolean }[] = [ + { + title: `How many sites use ${name}?`, + has: true, + content: ( + <> + + + + + ) : null, + rankRows.length ? ( + + + + ) : null, + ]} /> + + ), + }, + { title: `What kind of sites use ${name}?`, has: kindBlocks.some(Boolean), content: }, + { title: `Which sites joined or left ${name}?`, has: moveBlocks.some(Boolean), content: }, + { title: `How are ${name} sites built?`, has: buildBlocks.some(Boolean), content: }, + { title: "What else is in their ads.txt?", has: adsTxtBlocks.some(Boolean), content: }, + ]; + + return ( + + {groups.filter((group) => group.has).map((group) => {group.content})} + + ); +} diff --git a/app/(site)/(directories)/ad-networks/[slug]/network.module.css b/app/(site)/(directories)/ad-networks/[slug]/network.module.css new file mode 100644 index 00000000..2374f647 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/[slug]/network.module.css @@ -0,0 +1,77 @@ +/* One ad network's page (mockup mediavine.html). */ + +.header > p { + max-width: var(--measure-prose); +} + +.metaRow { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font: var(--text-small) / 1.4 var(--font-mono); + gap: var(--space-2) var(--space-7); +} + +.official { + align-items: center; + color: inherit; + display: inline-flex; + gap: var(--space-2); + text-decoration: underline; + text-decoration-color: var(--color-border); + text-underline-offset: 3px; +} + +.official svg { + height: 15px; + width: 15px; +} + +.official:hover, +.official:focus-visible { + color: var(--color-text); + text-decoration-color: currentColor; +} + +/* Body: sections spaced like the mockup's separate canvases, with no rules between them */ + +.body { + display: grid; + min-width: 0; +} + +.body > * { + min-width: 0; +} + +.body > * + * { + margin-top: calc(var(--page-padding-top) + var(--page-padding-bottom)); +} + +/* The Q&A beside the details: flat MDX, spaced like the mockup's question sections */ + +.article { + display: grid; + gap: var(--space-3); +} + +.article h3 { + margin-top: calc(var(--space-8) - var(--space-3)); +} + +.article > h3:first-child { + margin-top: 0; +} + +/* Words under a tab's fact rows */ + +.tabWords { + font-size: var(--text-body); +} + +dl + .tabWords { + border-top: 1px solid var(--color-border); + margin-top: var(--space-5); + padding-top: var(--space-7); +} diff --git a/app/(site)/(directories)/ad-networks/[slug]/page.tsx b/app/(site)/(directories)/ad-networks/[slug]/page.tsx index 8a405ceb..dff6ae83 100644 --- a/app/(site)/(directories)/ad-networks/[slug]/page.tsx +++ b/app/(site)/(directories)/ad-networks/[slug]/page.tsx @@ -1,52 +1,87 @@ -import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; -import { getBreadcrumbs } from "@/lib/breadcrumbs"; -import { PageMeta } from "@/app/ui/site/page-meta"; -import { adNetworkContent } from "./content"; +import { ExternalLink } from "lucide-react"; +import type { ReactNode } from "react"; import { notFound } from "next/navigation"; -import { Network } from "lucide-react"; -import { DirectoryQueryProvider } from "@/app/ui/directories/directory-query"; -import type { DirectoryIndexSource } from "@/app/ui/directories/directory-index"; +import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getRequiredDirectoryPage } from "@/lib/directory-pages"; +import { getAdNetworkPageData, listAdNetworks } from "@/lib/ad-networks"; +import { NetworkLogo } from "@/app/ui/ad-networks/network-logo"; +import { networkLogo } from "@/app/ui/ad-networks/network-logo-src"; +import { SitesTable } from "@/app/ui/ad-networks/sites-table"; +import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; import { AppShell } from "@/app/ui/site/app-shell"; +import { EntryHero } from "@/app/ui/site/entry-hero"; import { PageCanvas } from "@/app/ui/site/page-canvas"; import { PageHeader } from "@/app/ui/site/page-header"; -import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon"; -import { getAdNetwork, listAdNetworks, listAllVerifiedPublishers, publisherPageSize, type VerifiedPublisher } from "@/lib/ad-networks"; -import { getPublishedDirectoryPage, getRequiredDirectoryPage } from "@/lib/directory-pages"; -import { PublisherList } from "../ad-network-view"; -import { AdNetworksSidebar } from "../ad-networks-sidebar"; -import styles from "../page.module.css"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import { Figure, adNetworksSectionNav, joinRule, joinSentence } from "../ad-networks-shared"; +import { adNetworkContent } from "./content"; +import { NetworkDetails } from "./network-details"; +import { NetworkStats } from "./network-stats"; +import styles from "./network.module.css"; // Build every network during the release. Unpublished networks still return 404. export async function generateStaticParams() { - return (await listAdNetworks("")).map((network) => ({ slug: network.slug })); + return (await listAdNetworks()).map((network) => ({ slug: network.slug })); } +/** The intro is one MDX paragraph; it joins the live sentences after it in one lede, like the mockup. */ +const inline = { p: ({ children }: { children?: ReactNode }) => <>{children} }; + export default async function AdNetworkPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; - const directoryPage = await getRequiredDirectoryPage(`ad-networks/${slug}`); const content = adNetworkContent[slug]; - if (!content) throw new Error(`Ad network has no MDX content: ${slug}`); - const { Intro, Article } = content; - const [network, networks, parentPage] = await Promise.all([getAdNetwork(slug), listAdNetworks(""), getPublishedDirectoryPage("ad-networks")]); - if (!network) notFound(); - const route = `/ad-networks/${slug}`; - // The full list ships with the page for search and paging. A network with - // thousands of sites should move it to a cached JSON route ({ url }). - const rows = await listAllVerifiedPublishers(network.id); - const index: DirectoryIndexSource = { records: rows }; + if (!content) notFound(); + const [directoryPage, data, breadcrumbs] = await Promise.all([ + getRequiredDirectoryPage(`ad-networks/${slug}`), + getAdNetworkPageData(slug), + getBreadcrumbs(`ad-networks/${slug}`), + ]); + if (!data) notFound(); + const { network, sites, stats } = data; + const { Intro } = content; + const rule = joinRule(slug); + const officialHost = network.official_url ? new URL(network.official_url).hostname.replace(/^www\./, "") : null; - return - } sidebarLabel="Ad networks"> - - -

          } />{" "}

          - - + return ( + + + +

          +
          + + {network.official_url && officialHost ? ( + + ) : null} +
          +

          + + {rule ? <> {joinSentence(rule)} : null} + {/* data-gate: the sync fills the listed and confirmed counts. */} + {network.listed_count ? ( + network.listed_count === 1 + ? <> We list

          {1}
          site that uses {network.name}, and it {network.verified_count ? "is" : "isn't"} confirmed live from its own ads.txt file or ad code. + : <> We list
          {network.listed_count}
          sites that use {network.name}, and
          {network.verified_count}
          {network.verified_count === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code. + ) : null} +

          - {network.official_url ?

          Official {network.name} website ↗

          : null} - -
          +
          + + {network.listed_count ? ( + + ) : null} + +
          -
          ; + ); } diff --git a/app/(site)/(directories)/ad-networks/ad-network-view.tsx b/app/(site)/(directories)/ad-networks/ad-network-view.tsx deleted file mode 100644 index 4cde36a4..00000000 --- a/app/(site)/(directories)/ad-networks/ad-network-view.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { Search } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useDirectoryIndex, type DirectoryIndexSource } from "@/app/ui/directories/directory-index"; -import { pageParamHref, paramString, type DirectoryParams } from "@/app/ui/directories/directory-params"; -import { hasDirectoryParams, useDirectoryQuery } from "@/app/ui/directories/directory-query"; -import { Pagination } from "@/app/ui/pagination"; -import { StatePanel } from "@/app/ui/state-panel"; -import type { AdNetwork, VerifiedPublisher } from "@/lib/ad-networks"; -import styles from "./page.module.css"; - -const publisherPageSize = 30; - -// Same rules as lib/ad-networks, which can't load in the browser. -function readQuery(params: DirectoryParams) { - return paramString(params, "q").trim().slice(0, 100); -} - -function readPage(params: DirectoryParams) { - const raw = paramString(params, "page"); - return raw && /^[1-9]\d{0,5}$/.test(raw) ? Number(raw) : 1; -} - -function SearchForm({ id, label, placeholder, route }: { id: string; label: string; placeholder: string; route: string }) { - const { params, replace } = useDirectoryQuery(); - const query = readQuery(params); - const [value, setValue] = useState(query); - useEffect(() => setValue(query), [query]); - return
          { - event.preventDefault(); - const next = value.trim().slice(0, 100); - replace(next ? `${route}?q=${encodeURIComponent(next)}` : route); - }} - role="search" - > - -
          -
          ; -} - -type NetworkCard = Pick; - -// All networks ship with the page. ?q= filters them in the browser. -export function AdNetworkList({ networks }: { networks: NetworkCard[] }) { - const { params } = useDirectoryQuery(); - const needle = readQuery(params).toLowerCase(); - const matches = needle ? networks.filter((network) => network.name.toLowerCase().includes(needle) || network.slug.toLowerCase().includes(needle)) : networks; - - return <> - -

          Networks

          {matches.length.toLocaleString("en-US")} {matches.length === 1 ? "network" : "networks"}

          - {matches.length ?
          {matches.map((network) =>
          -

          {network.name}

          {network.summary ?? "Browse verified publisher websites for this network."}

          -
          {network.verified_count.toLocaleString("en-US")} verified {network.verified_count === 1 ? "site" : "sites"}View sites
          -
          )}
          : Show all networks} title="No networks match that search">

          Try a shorter name or browse all networks.

          } - ; -} - -const dateFormat = new Intl.DateTimeFormat("en", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }); - -function PublisherRows({ rows }: { rows: VerifiedPublisher[] }) { - return
            {rows.map((site) =>
          • -
            {site.display_domain} {site.normalized_domain}
            -
            {site.relationship_type === "primary_or_exclusive_manager" ? "Primary or exclusive monetization manager" : "Uses this network"}
            -
          • )}
          ; -} - -// The static page shows the first 30 sites. ?q= and ?page= use the full list in the browser. -export function PublisherList({ firstPage, index, networkName, route, total }: { - firstPage: VerifiedPublisher[]; - index: DirectoryIndexSource; - networkName: string; - route: string; - total: number; -}) { - const { params } = useDirectoryQuery(); - const filtering = hasDirectoryParams(params, ["q", "page"]); - const { failed, records } = useDirectoryIndex(index, filtering); - const query = readQuery(params); - const needle = query.toLowerCase(); - const matches = filtering && records ? records.filter((site) => site.normalized_domain.toLowerCase().includes(needle)) : null; - const count = matches ? matches.length : total; - const totalPages = Math.max(1, Math.ceil(count / publisherPageSize)); - const currentPage = Math.max(1, Math.min(readPage(params), totalPages)); - const rows = matches ? matches.slice((currentPage - 1) * publisherPageSize, currentPage * publisherPageSize) : firstPage; - const hrefForPage = (page: number) => pageParamHref(route, query ? { q: query } : {}, page); - - let body; - if (filtering && !records) { - body = failed - ? Show all verified sites} state="error" title="Sites could not be loaded">

          Reload the page to try again.

          - : ; - } else if (rows.length) { - body = <> - - - ; - } else { - body = Show all verified sites : undefined} title={query ? "No verified sites match that search" : "No publisher sites verified yet"}>

          {query ? "Try another part of the domain." : "This network will show sites here after their current records confirm a relationship."}

          ; - } - - return <> - -

          Verified sites

          {count.toLocaleString("en-US")} {count === 1 ? "site" : "sites"}

          - {body} - ; -} diff --git a/app/(site)/(directories)/ad-networks/ad-networks-shared.tsx b/app/(site)/(directories)/ad-networks/ad-networks-shared.tsx new file mode 100644 index 00000000..52d09961 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/ad-networks-shared.tsx @@ -0,0 +1,46 @@ +import type { SectionNav } from "@/app/ui/site/section-bar"; +import { formatCount } from "@/app/ui/ad-networks/site-format"; +import styles from "./ad-networks.module.css"; + +// Words and helpers shared by the ad network pages. Numbers come from the database or +// network-facts.ts; the MDX words never repeat them. + +export function adNetworksSectionNav(current: string): SectionNav { + return { + label: "ad networks", + href: "/ad-networks", + current, + items: [ + { label: "Directory", href: "/ad-networks" }, + { label: "Niches", href: "/ad-networks/niches" }, + { label: "Stats", href: "/ad-networks/stats" }, + { label: "Checker", href: "/ad-network-checker" }, + ], + }; +} + +export { NOT_PUBLIC, joinLabel, joinOrder, joinPhrase, joinRule, joinSentence, joiningNumbers, networkFacts, trafficRule } from "./join-rules"; + +/** A count in the header lede: mono, and accent-colored for confirmed sites. */ +export function Figure({ children, live = false }: { children: number; live?: boolean }) { + return {formatCount(children)}; +} + +/** The sentence after each intro that says how many sites we list and confirm. */ +export function CountsLine({ confirmed, listed, noun = "sites" }: { confirmed: number; listed: number; noun?: string }) { + if (!listed) return

          We haven't confirmed any {noun} yet. They show up here once we confirm them from their own ads.txt files or ad code.

          ; + return ( +

          + We list

          {listed}
          {listed === 1 ? noun.replace(/\bsites\b/, "site") : noun}, and
          {confirmed}
          {confirmed === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code. +

          + ); +} + +export function ordinal(value: number) { + const tens = value % 100; + if (tens >= 11 && tens <= 13) return `${value}th`; + return `${value}${{ 1: "st", 2: "nd", 3: "rd" }[value % 10] ?? "th"}`; +} + +/** Whole percents, except near the ends: never show 0% or 100% for a value that isn't. */ +export { formatPercent as percent } from "@/app/ui/ad-networks/viz/format"; diff --git a/app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx b/app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx deleted file mode 100644 index 2452ded3..00000000 --- a/app/(site)/(directories)/ad-networks/ad-networks-sidebar.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { SidebarLabel } from "@/app/ui/site/app-shell"; -import { SidebarList } from "@/app/ui/site/sidebar-list"; -import { sidebarPanelClassName, sidebarPanelHeadingClassName } from "@/app/ui/site/sidebar-panel"; -import type { AdNetwork } from "@/lib/ad-networks"; - -export function AdNetworksSidebar({ networks, current }: { networks: AdNetwork[]; current?: string }) { - return
          -
          Ad networks
          - -
          ; -} diff --git a/app/(site)/(directories)/ad-networks/ad-networks.module.css b/app/(site)/(directories)/ad-networks/ad-networks.module.css new file mode 100644 index 00000000..eba218af --- /dev/null +++ b/app/(site)/(directories)/ad-networks/ad-networks.module.css @@ -0,0 +1,9 @@ +/* A count in a header lede (mockup `.fig`): mono, and accent-colored for confirmed sites. */ + +.figure { + font: var(--weight-semibold) 0.95em var(--font-mono); +} + +.figure[data-live] { + color: var(--color-accent-text); +} diff --git a/app/(site)/(directories)/ad-networks/content/article.mdx b/app/(site)/(directories)/ad-networks/content/article.mdx index 8b137891..8fee905b 100644 --- a/app/(site)/(directories)/ad-networks/content/article.mdx +++ b/app/(site)/(directories)/ad-networks/content/article.mdx @@ -1 +1,13 @@ +## What do ad networks require to join? +Most ad networks set a minimum to join, shown on each card under "To join". Many count monthly traffic, and some, such as Mediavine, count the ad revenue your site already earns. Self-serve networks such as Google AdSense take sites of any size. Where a card says "Not public", the network doesn't publish a fixed rule. + +Networks count traffic in different ways. A session is one visit, and a pageview is one page loaded, so one session often includes several pageviews. A minimum of 50,000 sessions is harder to reach than 50,000 pageviews. + +Traffic isn't the only rule. Many networks also check where your visitors come from, whether your content is original, and whether your site follows Google's ad policies. Each network's page lists its full rules. + +## How do we confirm a site uses a network? + +We read each site's own ads.txt file, the public list of companies allowed to sell its ad space. When the file names a network as its ad manager, or the site's pages load that network's ad code, the site counts as confirmed. A network's seller list can point us to a site, but it never confirms one on its own. + +We re-check every site regularly, and a site that drops a network loses its confirmed status. That's why a network's confirmed count can be lower than its total. diff --git a/app/(site)/(directories)/ad-networks/content/intro.mdx b/app/(site)/(directories)/ad-networks/content/intro.mdx deleted file mode 100644 index 56c56744..00000000 --- a/app/(site)/(directories)/ad-networks/content/intro.mdx +++ /dev/null @@ -1 +0,0 @@ -Explore ad networks and the websites verified for each one. Counts include confirmed relationships only. diff --git a/app/(site)/(directories)/ad-networks/directory-browser.tsx b/app/(site)/(directories)/ad-networks/directory-browser.tsx new file mode 100644 index 00000000..6ae5dc9f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/directory-browser.tsx @@ -0,0 +1,252 @@ +"use client"; + +import Link from "next/link"; +import { Search } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from "react"; +import { UiButton } from "@/app/ui/controls"; +import { StatePanel } from "@/app/ui/state-panel"; +import { TableToolbar, ToolbarSelect } from "@/app/ui/table-toolbar"; +import { NetworkGrid, SortNote } from "@/app/ui/ad-networks/network-card"; +import { SectionHead } from "@/app/ui/ad-networks/section"; +import { checkerHref } from "@/app/ui/ad-networks/site-format"; +import type { AdNetworkDirectorySort } from "@/lib/ad-networks-types"; +import styles from "./directory.module.css"; + +export type DirectoryNetwork = { slug: string; name: string }; +export type DirectoryNiche = { slug: string; name: string; /** Network slugs, most sites in this niche first. */ order: readonly string[] }; + +type View = { q: string; sort: AdNetworkDirectorySort; niche: string }; + +const ViewContext = createContext<{ view: View; set: (patch: Partial) => void } | null>(null); + +const visibleAtFirst = 12; +const domainLike = /^(?:https?:\/\/)?(?:[a-z0-9-]+\.)+[a-z]{2,}(?:[/?#].*)?$/i; +const sortLabels: Record = { + "most-sites": "Most sites", + "easiest-to-join": "Easiest to join", + "by-niche": "By niche", +}; + +const cleanQuery = (value: string) => value.trim().slice(0, 100); +const matches = (network: DirectoryNetwork, needle: string) => + network.name.toLowerCase().includes(needle) || network.slug.includes(needle.replace(/\s+/g, "-")); + +/** + * Holds the search, sort, and niche for the directory, so the hero search and the grid below it + * share them. The static page renders the default view; a shared link's `?q=`, `?sort=`, and + * `?niche=` are read in the browser after hydration. + */ +export function DirectoryView({ children, niches, sorts }: { children: ReactNode; niches: readonly DirectoryNiche[]; sorts: readonly AdNetworkDirectorySort[] }) { + const [view, setView] = useState({ q: "", sort: "most-sites", niche: "" }); + + useEffect(() => { + const read = () => { + const search = new URLSearchParams(window.location.search); + const sort = search.get("sort") as AdNetworkDirectorySort | null; + const niche = search.get("niche") ?? ""; + setView({ + q: cleanQuery(search.get("q") ?? ""), + sort: sort && sorts.includes(sort) ? sort : "most-sites", + niche: niches.some((item) => item.slug === niche) ? niche : "", + }); + }; + if (window.location.search) read(); + window.addEventListener("popstate", read); + return () => window.removeEventListener("popstate", read); + }, [niches, sorts]); + + const value = useMemo(() => ({ + view, + set: (patch: Partial) => { + const next = { ...view, ...patch }; + const search = new URLSearchParams(); + if (cleanQuery(next.q)) search.set("q", cleanQuery(next.q)); + if (next.sort !== "most-sites") search.set("sort", next.sort); + if (next.sort === "by-niche" && next.niche) search.set("niche", next.niche); + const suffix = search.toString(); + window.history.replaceState(null, "", suffix ? `${window.location.pathname}?${suffix}` : window.location.pathname); + setView(next); + }, + }), [view]); + + return {children}; +} + +function useView() { + const context = useContext(ViewContext); + if (!context) throw new Error("DirectoryView is missing"); + return context; +} + +/** + * The hero search (mockup `.hero-search`). Typing filters the network cards below. Submitting a + * domain opens the checker, a niche name opens its page, and a single matching network opens its + * page. `/` jumps to the box. + */ +export function HeroSearch({ examples, networks, niches, placeholder }: { + examples: readonly { label: string; href: string }[]; + networks: readonly DirectoryNetwork[]; + /** Published niche pages only. */ + niches: readonly { slug: string; name: string }[]; + placeholder: string; +}) { + const router = useRouter(); + const { view, set } = useView(); + const input = useRef(null); + + useEffect(() => { + const onKey = (event: KeyboardEvent) => { + const active = document.activeElement; + if (event.key !== "/" || event.metaKey || event.ctrlKey || event.altKey) return; + if (active instanceof HTMLElement && (active.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(active.tagName))) return; + event.preventDefault(); + input.current?.focus(); + }; + document.addEventListener("keydown", onKey); + return () => document.removeEventListener("keydown", onKey); + }, []); + + const submit = (event: FormEvent) => { + event.preventDefault(); + const text = cleanQuery(view.q); + if (!text) return; + const lower = text.toLowerCase(); + const named = networks.filter((network) => matches(network, lower)); + if (domainLike.test(text) && !named.length) { + router.push(checkerHref(text.replace(/^https?:\/\//i, "").split(/[/?#]/)[0].replace(/^www\./i, "").toLowerCase())); + return; + } + const niche = niches.find((item) => item.name.toLowerCase() === lower || item.slug === lower); + if (niche) { + router.push(`/ad-networks/niches/${niche.slug}`); + return; + } + if (named.length === 1) router.push(`/ad-networks/${named[0].slug}`); + else document.getElementById("all-networks")?.scrollIntoView({ block: "start" }); + }; + + return ( +
          +
          + + +
          + {examples.length ? ( +

          + Try {examples.map((example) => {example.label})} +

          + ) : null} +
          + ); +} + +/** + * "All ad networks": the sort controls, the sort note, and the cards. Every card is in the server + * HTML in the Most sites order. Cards past the first twelve are `hidden="until-found"`, so search + * engines and find-in-page still reach them, until "Show all" opens them. + */ +export function NetworkBrowser({ cards, easiest, easiestNote, networks, niches, sorts }: { + /** One rendered card per network slug. */ + cards: Record; + /** Network slugs with a known traffic minimum, lowest first. */ + easiest: readonly string[]; + easiestNote: ReactNode; + /** In Most sites order. */ + networks: readonly DirectoryNetwork[]; + niches: readonly DirectoryNiche[]; + sorts: readonly AdNetworkDirectorySort[]; +}) { + const { view, set } = useView(); + const [expanded, setExpanded] = useState(false); + const [hydrated, setHydrated] = useState(false); + const grid = useRef(null); + const needle = cleanQuery(view.q).toLowerCase(); + const niche = view.sort === "by-niche" ? niches.find((item) => item.slug === view.niche) ?? niches[0] : undefined; + + const list = useMemo(() => { + const ranked = (order: readonly string[]) => { + const position = new Map(order.map((slug, index) => [slug, index])); + // Networks missing from the order keep their Most sites order after the ranked ones. + return [...networks].sort((a, b) => (position.get(a.slug) ?? Infinity) - (position.get(b.slug) ?? Infinity)); + }; + const ordered = view.sort === "easiest-to-join" ? ranked(easiest) : niche ? ranked(niche.order) : networks; + return needle ? ordered.filter((network) => matches(network, needle)) : ordered; + }, [easiest, needle, networks, niche, view.sort]); + + const collapsed = !expanded && !needle && list.length > visibleAtFirst; + + // Find-in-page on a hidden card opens the whole list. + useEffect(() => { + const root = grid.current; + if (!root || !collapsed) return; + const open = () => setExpanded(true); + root.addEventListener("beforematch", open); + return () => root.removeEventListener("beforematch", open); + }, [collapsed]); + + // React can't render `hidden="until-found"`, so the server HTML hides the extra cards with CSS + // and the browser swaps that for the attribute before the first paint. + useLayoutEffect(() => setHydrated(true), []); + useLayoutEffect(() => { + if (!hydrated) return; + grid.current?.querySelectorAll("[data-card]").forEach((card, index) => { + if (collapsed && index >= visibleAtFirst) card.setAttribute("hidden", "until-found"); + else card.removeAttribute("hidden"); + }); + }, [collapsed, hydrated, list]); + + return ( +
          + 1 ? ( + + set({ sort: value as AdNetworkDirectorySort, niche: value === "by-niche" ? niche?.slug ?? niches[0]?.slug ?? "" : "" })} + options={sorts.map((sort) => ({ value: sort, label: sortLabels[sort] }))} + value={view.sort} + /> + {niche ? ( + set({ niche: value })} options={niches.map((item) => ({ value: item.slug, label: item.name }))} value={niche.slug} /> + ) : null} + + ) : null} + id="all-networks" + title="All ad networks" + /> + {view.sort === "easiest-to-join" ? {easiestNote} : null} + {niche ? Sorted by how many {niche.name} sites use each network. : null} + {list.length ? ( +
          + + {list.map((network) =>
          {cards[network.slug]}
          )} +
          +
          + ) : ( + set({ q: "" })}>Show all networks} title="No networks match that search"> +

          Try a shorter name, or press Search to check a site's domain.

          +
          + )} + {collapsed ? ( +
          + +
          + ) : null} +
          + ); +} diff --git a/app/(site)/(directories)/ad-networks/directory.module.css b/app/(site)/(directories)/ad-networks/directory.module.css new file mode 100644 index 00000000..ec2012da --- /dev/null +++ b/app/(site)/(directories)/ad-networks/directory.module.css @@ -0,0 +1,257 @@ +/* The ad networks directory (mockup ad-networks.html) and the header parts the network pages share. */ + +/* Header */ + +.header > p { + max-width: var(--measure-prose); +} + +.metaRow { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font: var(--text-small) / 1.4 var(--font-mono); + gap: var(--space-2) var(--space-7); +} + +.official { + align-items: center; + color: inherit; + display: inline-flex; + gap: var(--space-2); + text-decoration: underline; + text-decoration-color: var(--color-border); + text-underline-offset: 3px; +} + +.official svg { + height: 15px; + width: 15px; +} + +.official:hover, +.official:focus-visible { + color: var(--color-text); + text-decoration-color: currentColor; +} + +.visuallyHidden { + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +/* Hero search: one box for networks, sites, and niches */ + +.heroSearch { + display: grid; + gap: var(--space-4); + margin-top: var(--space-5); + max-width: var(--measure-prose); +} + +.heroSearch form { + display: grid; + gap: var(--space-3); + grid-template-columns: minmax(0, 1fr) auto; +} + +.searchBox { + align-items: center; + background: var(--color-surface); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius); + display: flex; + height: 52px; + min-width: 0; +} + +.searchBox:focus-within { + border-color: var(--color-accent-text); + box-shadow: var(--focus-ring); +} + +.searchBox svg { + color: var(--color-text-muted); + flex: none; + height: 20px; + margin-left: 14px; + width: 20px; +} + +.searchBox input { + background: transparent; + border: 0; + color: var(--color-text); + flex: 1; + font: var(--text-body) var(--font-mono); + height: 100%; + min-width: 0; + outline: 0; + padding: 0 14px; +} + +.searchBox input:focus-visible { + box-shadow: none; +} + +.searchBox input::placeholder { + color: var(--color-text-muted); +} + +.searchBox kbd { + border: 1px solid var(--color-border-strong); + border-radius: var(--radius); + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); + margin-right: 12px; + padding: 1px 7px; +} + +.primaryButton, +.secondaryButton { + align-items: center; + border-radius: var(--radius); + cursor: pointer; + display: inline-flex; + font: var(--weight-semibold) var(--text-body) var(--font-sans); + justify-content: center; + padding: 0 22px; + white-space: nowrap; +} + +.primaryButton { + background: var(--color-accent); + border: 1px solid var(--color-accent); + color: var(--color-accent-contrast); + height: 52px; +} + +.primaryButton:hover { + filter: brightness(0.96); +} + +.secondaryButton { + background: var(--color-surface); + border: 1px solid var(--color-border-strong); + color: var(--color-text); + height: var(--control-height-lg); +} + +.secondaryButton:hover { + background: var(--color-surface-2); +} + +.primaryButton:focus-visible, +.secondaryButton:focus-visible { + box-shadow: var(--focus-ring); + outline: 0; +} + +.searchTry { + align-items: baseline; + background: var(--color-bg); + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font: var(--text-small) / var(--line-ui) var(--font-mono); + gap: var(--space-2) var(--space-5); + margin: 0; + width: fit-content; +} + +.searchTry a { + color: var(--color-text); + text-decoration: underline; + text-decoration-color: var(--color-border-strong); + text-underline-offset: 3px; +} + +.searchTry a:hover { + text-decoration-color: currentColor; +} + +@media (max-width: 560px) { + .heroSearch form { + grid-template-columns: 1fr; + } + + .searchBox kbd { + display: none; + } +} + +/* Body: sections spaced like the mockup's separate canvases, with no rules between them */ + +.body { + display: grid; + min-width: 0; +} + +.body > * { + min-width: 0; +} + +.body > * + * { + margin-top: calc(var(--page-padding-top) + var(--page-padding-bottom)); +} + +/* Network cards */ + +.directory { + min-width: 0; +} + +.cell { + display: grid; + min-width: 0; +} + +.cards[data-collapsed] .cell:nth-child(n + 13) { + display: none; +} + +/* `hidden="until-found"` keeps a box, so take hidden cards out of the grid flow. Find-in-page + still reaches them and opens the list. */ +.cell[hidden] { + height: 1px; + overflow: hidden; + position: absolute; + width: 1px; +} + +.cards { + position: relative; +} + +.more { + display: flex; + justify-content: center; + margin-top: var(--space-8); +} + +/* Reading content under the cards (mockup `.prose`) */ + +.article { + display: grid; + gap: var(--space-7); + max-width: var(--measure-prose); +} + +.article h2 { + font: var(--weight-bold) 1.7rem / 1.2 var(--font-display); + letter-spacing: var(--tracking-display); + margin: calc(var(--space-13) - var(--space-7)) 0 0; +} + +.article > h2:first-child { + margin-top: 0; +} + +.article p { + margin: 0; +} diff --git a/app/(site)/(directories)/ad-networks/join-rules.ts b/app/(site)/(directories)/ad-networks/join-rules.ts new file mode 100644 index 00000000..4eff5926 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/join-rules.ts @@ -0,0 +1,79 @@ +// How the ad network pages word joining rules. Pure, so the wording can be checked against every network. +import { adNetworkFacts, type AdNetworkFacts } from "./network-facts"; + +export const NOT_PUBLIC = "Not public"; + +export function networkFacts(slug: string): AdNetworkFacts | null { + return adNetworkFacts[slug] ?? null; +} + +/** The traffic rule as a short phrase, or null when the network publishes none. */ +export function trafficRule(facts: AdNetworkFacts) { + const rule = facts.minTraffic; + if (!rule) return null; + if (rule.amount === 0) return "No traffic minimum"; + return `${rule.amount.toLocaleString("en-US")} ${rule.unit} a month`; +} + +// Key numbers about pay, fees, or contracts. Everything else is a rule for joining. +const payNumber = /pay|share|fee|payout|terms|threshold|notice|timing|schedule|floor|minimums?$/i; + +/** Key numbers that say what it takes to join, such as revenue or traffic to apply. */ +export function joiningNumbers(facts: AdNetworkFacts) { + return facts.keyNumbers.filter((item) => !payNumber.test(item.label)); +} + +// Notes that say the number isn't a rule anyone has to meet. +const notARule = /not a general minimum|not the main rule/i; + +/** + * A joining key number as one phrase: the value, then the first part of its note, worded the + * way people say it. "$5,000" with "in annual ad revenue" reads "$5,000 ad revenue a year". + */ +export function joinPhrase(item: AdNetworkFacts["keyNumbers"][number]) { + let unit = (item.note ?? "").split(";")[0].trim(); + // "monthly ad revenue or 2 million monthly pageviews": keep the first rule. + unit = unit.replace(/\s+or\s+(?=[^,]*\d).*$/i, ""); + unit = unit.replace(/,.*$/, "").replace(/^in\s+/i, ""); + unit = unit.replace(/^monthly\s+(.+)$/i, "$1 a month").replace(/^annual\s+(.+)$/i, "$1 a year"); + const phrase = unit ? `${item.value} ${unit}` : item.value; + return /^(suggested|typical)/i.test(item.label) ? `Typically ${phrase.charAt(0).toLowerCase()}${phrase.slice(1)}` : phrase; +} + +/** What the card shows under "To join": the rule, "Not public", or "Not listed" before research. */ +export function joinLabel(slug: string): { value: string } { + const facts = networkFacts(slug); + if (!facts) return { value: "Not listed" }; + const rule = trafficRule(facts); + if (rule) return { value: rule }; + const first = joiningNumbers(facts).find((item) => !notARule.test(item.note ?? "")); + return { value: first ? joinPhrase(first) : NOT_PUBLIC }; +} + +/** + * The rule a network publishes for joining, as a phrase for the header lede, such as + * `50,000 sessions a month` or `$5,000 ad revenue a year`. Null when it publishes none. + */ +export function joinRule(slug: string): string | null { + const facts = networkFacts(slug); + if (!facts) return null; + const rule = trafficRule(facts); + if (rule) return rule; + const first = joiningNumbers(facts).find((item) => !notARule.test(item.note ?? "")); + return first ? joinPhrase(first) : null; +} + +/** The header sentence for a join rule: `It takes sites with 50,000 sessions a month.` */ +export function joinSentence(rule: string) { + if (rule === "No traffic minimum") return "It takes sites of any size."; + const typical = /^Typically\s+/i.test(rule); + const phrase = rule.replace(/^Typically\s+/i, ""); + const words = /^[A-Z][a-z]/.test(phrase) ? phrase.charAt(0).toLowerCase() + phrase.slice(1) : phrase; + return `It ${typical ? "typically " : ""}takes sites with ${words}.`; +} + +/** Monthly traffic needed, for ordering by how easy a network is to join. Unknown sorts last. */ +export function joinOrder(slug: string) { + return networkFacts(slug)?.minTraffic?.amount ?? null; +} + diff --git a/app/(site)/(directories)/ad-networks/network-facts.ts b/app/(site)/(directories)/ad-networks/network-facts.ts new file mode 100644 index 00000000..101dcf26 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/network-facts.ts @@ -0,0 +1,70 @@ +// Network facts that change rarely: joining rules and pay terms, with sources. +// The page shows these numbers; the MDX words never repeat them. Use null for +// anything the network does not publish, and the page shows "Not public". + +export type AdNetworkKeyNumber = { + label: string; // "Sessions to join", "Your share", "Paid" + value: string; // "50,000", "75%", "Net 65" + note?: string; // "a month", "of ad revenue" + sourceUrl: string; +}; + +export type AdNetworkFacts = { + checkedAt: string; // YYYY-MM-DD, the day the sources were read + keyNumbers: AdNetworkKeyNumber[]; // up to three, most useful first + joining: { label: string; value: string; note?: string; sourceUrl: string | null }[]; + minTraffic: { amount: number; unit: "sessions" | "pageviews" | "visitors" } | null; // monthly; null when there is no public rule + revenueShare: string | null; + paymentTerms: string | null; + paymentThreshold: string | null; + paymentMethods: string[]; + sources: { label: string; url: string }[]; +}; + +// Keyed by network slug. +export const adNetworkFacts: Record = { + adapex: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 60", sourceUrl: "https://adapex.io/faqs/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Google Ad Manager via MCM", sourceUrl: "https://adapex.io/faqs/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Publisher FAQ", url: "https://adapex.io/faqs/" }] }, + aditude: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Official publisher guidance", url: "https://www.aditude.com/blog/the-5-questions-every-publisher-should-ask-their-monetization-partner" }] }, + adnimation: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 35", sourceUrl: "https://www.adnimation.com/faqs/" }], joining: [{ label: "Traffic", value: "Substantial, no figure stated", note: "MAP is a separate program with no minimum", sourceUrl: "https://www.adnimation.com/faqs/" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No gambling, pornography, or drugs", sourceUrl: "https://www.adnimation.com/faqs/" }, { label: "Google standing", value: "Follow Google policy guidelines", sourceUrl: "https://www.adnimation.com/faqs/" }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 35", paymentThreshold: null, paymentMethods: ["Direct bank payment", "PayPal by arrangement"], sources: [{ label: "Publisher FAQ", url: "https://www.adnimation.com/faqs/" }, { label: "MAP program terms, separate program", url: "https://www.adnimation.com/map-terms-conditions/" }] }, + adpushup: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "80%", note: "in older public terms; check your signed agreement", sourceUrl: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }, { label: "Payment minimum", value: "$50", note: "in older public terms", sourceUrl: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }, { label: "Pay day", value: "Net 45", note: "in older public terms", sourceUrl: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "80% in older published terms; current agreement may differ", paymentTerms: "Net 45 in older published terms", paymentThreshold: "$50 in older published terms", paymentMethods: [], sources: [{ label: "Publisher terms, revised 2018", url: "https://www.adpushup.com/wp-content/uploads/2020/02/AdPushup-Terms-of-Service-v2.0-WebSite-Version1.pdf" }, { label: "Publisher onboarding", url: "https://www.adpushup.com/publisher/get-started/" }] }, + "amazon-publisher-services": { checkedAt: "2026-10-16", keyNumbers: [{ label: "UAM fee", value: "10%", note: "taken from SSP bid prices; Unified Ad Marketplace only", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Payment minimum", value: "$5", note: "UAM", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Pay day", value: "Net 60", note: "UAM", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Worldwide", note: "For Unified Ad Marketplace only", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Google Ad Manager", note: "For Unified Ad Marketplace only", sourceUrl: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60 for UAM", paymentThreshold: "$5 for UAM", paymentMethods: [], sources: [{ label: "UAM publisher update", url: "https://aps.amazon.com/aps/wp-content/uploads/2023/01/aps_uam_update_011123-FINAL.pdf" }] }, + "anymind-anymanager": { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "AnyManager overview", url: "https://anymindgroup.com/news/press-release/11388/" }, { label: "AnyMind IR FAQ", url: "https://anymindgroup.com/ir/faq/" }] }, + "buysellads-carbon-ads": { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Carbon Tag or API", sourceUrl: "https://www.buysellads.com/publishers" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Carbon Ads media kit", url: "https://www.buysellads.com/hubfs/Carbon%20Ads%20Media%20Kit.pdf" }, { label: "Carbon Ads publisher page", url: "https://discover.buysellads.com/carbon" }] }, + clickio: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "100,000 to 1,000,000", note: "monthly pageviews; varies by region, and revenue can also qualify a site", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Minimum earnings", value: "$500", note: "a month; may be enforced after the first three months", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Pay day", value: "Net 60", sourceUrl: "https://docs.clickio.com/books/get-started/page/payments-billing" }], joining: [{ label: "Traffic", value: "100,000 to 1,000,000 pageviews by region", note: "Monthly; $500 in monthly revenue may qualify below the threshold", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Visitor location", value: "Sanctioned countries excluded", note: "Pageview thresholds also vary by region", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Content", value: "Follow AdSense policies and third-party rights rules", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Publish ads.txt; provide GA4 or a six-month report if requested", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Site history", value: "Six months of traffic data if external verification is unavailable", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Ad revenue", value: "$500 a month", note: "Possible traffic alternative; ongoing minimum account earnings", sourceUrl: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60", paymentThreshold: "Varies by method: $100 equivalent for ACH/eCheck/SEPA, PayPal and check; $200 wire; some other methods have separate thresholds", paymentMethods: ["ACH", "eCheck", "SEPA", "Wire", "Check", "PayPal", "WebMoney", "Payoneer"], sources: [{ label: "Entry standards", url: "https://docs.clickio.com/books/clickio-entry-standards/page/clickio-entry-standards" }, { label: "Payments and billing", url: "https://docs.clickio.com/books/get-started/page/payments-billing" }] }, + concert: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Concert terms surfaced publicly (advertiser product, not publisher terms)", url: "https://admanager.concert.io/terms" }] }, + ethicalads: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "70%", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Payment minimum", value: "$50", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Pay day", value: "Monthly", note: "target by the 15th", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }], joining: [{ label: "Traffic", value: "No minimum stated", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Meet the publisher policy", note: "Ads must appear above the fold on desktop and mobile", sourceUrl: "https://www.ethicalads.io/publisher-policy/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "70%", paymentTerms: "Monthly; target payment date is the 15th", paymentThreshold: "$50", paymentMethods: ["Stripe Connect / bank transfer", "PayPal", "OpenCollective", "GitHub Sponsors"], sources: [{ label: "Publisher policy", url: "https://www.ethicalads.io/publisher-policy/" }, { label: "Terms of service", url: "https://www.ethicalads.io/terms-of-service/" }] }, + ezoic: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Users to join", value: "250,000", note: "monthly active users, for new sites", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Your share", value: "82%", note: "at least, of net ad revenue", sourceUrl: "https://www.ezoic.com/terms" }, { label: "Payment minimum", value: "$20", sourceUrl: "https://www.ezoic.com/terms" }], joining: [{ label: "Traffic", value: "250,000 monthly active users", note: "Incubator may accept smaller sites", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original content in a supported language", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Google standing", value: "Comply with Google and AdSense policies", note: "AdSense approval is not required for most sites", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Must connect", value: "Google Analytics for traffic verification", sourceUrl: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 250000, unit: "visitors" }, revenueShare: "At least 82% of net ad revenue under published Terms, unless another agreement applies", paymentTerms: "Approximately 35 days after the advertiser pays, under published Terms", paymentThreshold: "$20", paymentMethods: ["PayPal", "Payoneer bank transfer", "Check"], sources: [{ label: "Current eligibility requirements", url: "https://osticket.ezoic.com/kb/article/getting-started-ezoics-requirements" }, { label: "Terms", url: "https://www.ezoic.com/terms" }, { label: "Payment help", url: "https://support.ezoic.com/kb/article/ezoic-payments?lang=en-US&lang=it%3Fid%3Dezoic-payments" }] }, + freestar: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "1,000,000", note: "monthly pageviews; Freestar says usually, so confirm with its team", sourceUrl: "https://freestar.com/contact-us/" }, { label: "From top markets", value: "50%", note: "of traffic from the US, UK, Canada, New Zealand, or Australia", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }], joining: [{ label: "Traffic", value: "Usually 1,000,000 pageviews", note: "Monthly", sourceUrl: "https://freestar.com/contact-us/" }, { label: "Visitor location", value: "At least 50% from the US, UK, Canada, New Zealand, or Australia", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Content", value: "Original, human-focused, brand-safe content", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Google standing", value: "Good standing with major ad providers", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Must connect", value: "Provide GA4 access for review", sourceUrl: "https://freestar.com/publisher-quality-requirements/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 1000000, unit: "pageviews" }, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Publisher quality requirements", url: "https://freestar.com/publisher-quality-requirements/" }, { label: "Publisher contact", url: "https://freestar.com/contact-us/" }] }, + "geniee-ssp": { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "GENIEE corporate site", url: "https://geniee.co.jp/" }, { label: "GENIEE SSP site", url: "https://genieesspv.jp/" }] }, + "google-adsense": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "80%", note: "after the advertiser platform fee; about 68% with Google Ads demand", sourceUrl: "https://support.google.com/adsense/answer/180195?hl=en" }, { label: "Payment minimum", value: "$100", note: "in US dollars", sourceUrl: "https://support.google.com/adsense/answer/1709871?hl=en" }, { label: "Pay days", value: "21st to 26th", note: "of each month", sourceUrl: "https://support.google.com/adsense/answer/7164701?hl=en" }], joining: [{ label: "Traffic", value: "No minimum stated", sourceUrl: "https://support.google.com/adsense/answer/9724?hl=en" }, { label: "Visitor location", value: "Sign-up is available in listed countries", sourceUrl: "https://support.google.com/adsense/answer/13402307?hl=en" }, { label: "Content", value: "Original, high-quality, policy-compliant content", sourceUrl: "https://support.google.com/adsense/answer/9724?hl=en" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Access to the site HTML", note: "Needed to verify and place ad code", sourceUrl: "https://support.google.com/adsense/answer/9724?hl=en" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "80% after the advertiser platform fee for AdSense for Content", paymentTerms: "Monthly; payments are issued between the 21st and 26th when the balance reaches threshold and there are no holds", paymentThreshold: "$100 USD, with equivalent thresholds by account currency", paymentMethods: ["Check", "EFT", "SEPA", "Wire transfer", "PayPal Hyperwallet where available"], sources: [{ label: "Revenue share", url: "https://support.google.com/adsense/answer/180195?hl=en" }, { label: "Payment threshold", url: "https://support.google.com/adsense/answer/1709871?hl=en" }, { label: "Payment schedule", url: "https://support.google.com/adsense/answer/7164701?hl=en" }, { label: "Eligibility", url: "https://support.google.com/adsense/answer/9724?hl=en" }] }, + infolinks: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "70%", note: "for In-Text ads", sourceUrl: "https://sites0001.infolinks.com/service-agreement?email=&name=&url=" }, { label: "Pay day", value: "Net 45", note: "working days", sourceUrl: "https://sites0001.infolinks.com/service-agreement?email=&name=&url=" }, { label: "Payment minimum", value: "$50", note: "for eCheck", sourceUrl: "https://sites0001.infolinks.com/support/payments/can-i-receive-my-payment-via-echeck" }], joining: [{ label: "Traffic", value: "No minimum pageviews or visitors", sourceUrl: "https://sites0001.infolinks.com/support/quality/what-are-the-eligibility-criteria-for-my-site-to-qualify-with-infolinks" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No illegal or offensive content", sourceUrl: "https://sites0001.infolinks.com/support/quality/what-are-the-eligibility-criteria-for-my-site-to-qualify-with-infolinks" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Install the Infolinks script", sourceUrl: "https://sites0001.infolinks.com/support/integration/how-do-i-integrate-infolinks-into-my-website" }, { label: "Approval time", value: "Usually 48 hours", sourceUrl: "https://sites0001.infolinks.com/support/account/how-do-i-sign-up-as-an-infolinks-publisher" }], minTraffic: null, revenueShare: "70% for In-Text ad units; other format terms vary", paymentTerms: "Net 45 working days after the month ends under the public agreement", paymentThreshold: "Varies by method; eCheck $50", paymentMethods: ["ACH", "eCheck", "Payoneer", "PayPal", "Western Union"], sources: [{ label: "Publisher eligibility", url: "https://sites0001.infolinks.com/support/quality/what-are-the-eligibility-criteria-for-my-site-to-qualify-with-infolinks" }, { label: "Service agreement", url: "https://sites0001.infolinks.com/service-agreement?email=&name=&url=" }, { label: "Payment options", url: "https://sites0001.infolinks.com/support/payments/do-i-pay-fees-when-receiving-my-infolinks-payment" }] }, + "journey-by-mediavine": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "More than 1,000", note: "monthly sessions", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }], joining: [{ label: "Traffic", value: "More than 1,000 sessions", note: "Monthly", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original, updated, human-created, brand-safe content", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Google standing", value: "No active AdSense or Ad Exchange issues", sourceUrl: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Must connect", value: "Install the Grow plugin", note: "Journey requirement", sourceUrl: "https://www.journeymv.com/affiliates-faq/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 1000, unit: "sessions" }, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Mediavine approval requirements, including Journey", url: "https://help.mediavine.com/what-does-it-take-to-get-approved-by-mediavine" }, { label: "Journey criteria", url: "https://www.journeymv.com/affiliates-faq/" }] }, + "media-net": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 30", sourceUrl: "https://www.media.net/legal/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original content; prohibited topics are excluded", sourceUrl: "https://www.media.net/legal/inventorypolicies/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Publish an authorized ads.txt file", sourceUrl: "https://www.media.net/legal/inventorypolicies/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 30; monthly revenue payments under the Publisher Agreement", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Publisher Agreement", url: "https://www.media.net/legal/" }, { label: "Inventory policies", url: "https://www.media.net/legal/inventorypolicies/" }] }, + mediavine: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Revenue to join", value: "$5,000", note: "in annual ad revenue", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Your share", value: "75%", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Pay day", value: "Net 65", sourceUrl: "https://help.mediavine.com/mediavine-payment-faq" }], joining: [{ label: "Traffic", value: "No traffic minimum listed", note: "Mediavine sets an ad revenue minimum instead", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Original, quality, evergreen content", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Google standing", value: "No active AdSense ban", note: "You do not need prior AdSense use", sourceUrl: "https://help.mediavine.com/mediavine-official" }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Ad revenue", value: "$5,000 a year", note: "Minimum annual ad revenue", sourceUrl: "https://help.mediavine.com/mediavine-official" }], minTraffic: null, revenueShare: "75%", paymentTerms: "Net 65; paid on the 5th or first business day after it", paymentThreshold: "$100 for most methods; higher for some international methods", paymentMethods: ["PayPal", "Domestic ACH", "International ACH", "US wire", "International wire"], sources: [{ label: "Mediavine Official requirements", url: "https://help.mediavine.com/mediavine-official" }, { label: "Payment FAQ", url: "https://help.mediavine.com/mediavine-payment-faq" }, { label: "Payment methods and minimums", url: "https://help.mediavine.com/mediavine-payment-methods" }] }, + mgid: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Your share", value: "65%", note: "on a published regional offer; current order terms may differ", sourceUrl: "https://pr.mgid.com/monetize" }, { label: "Pay day", value: "Net 30", note: "on the same offer", sourceUrl: "https://pr.mgid.com/monetize" }, { label: "Payment minimums", value: "$50 to $1,000", note: "varies by payment method", sourceUrl: "https://pr.mgid.com/monetize" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No adult, illegal, hateful, or infringing content", sourceUrl: "https://www.mgid.com/services/publishers-tos" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Install MGID ad units and code", sourceUrl: "https://www.mgid.com/services/publishers-tos" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 30 in regional offer; current Publisher Terms and order control", paymentThreshold: "Method-specific; published offer lists $50 to $1,000", paymentMethods: ["Bank transfer", "PayPal", "Tipalti", "Paymaster24"], sources: [{ label: "Regional publisher offer", url: "https://pr.mgid.com/monetize" }, { label: "Publisher terms", url: "https://www.mgid.com/services/publishers-tos" }] }, + "monetizemore-pubguru": { checkedAt: "2026-10-16", keyNumbers: [{ label: "To join Premium", value: "$5,000", note: "monthly ad revenue or 2 million monthly pageviews", sourceUrl: "https://www.monetizemore.com/solutions/pubguru-premium/" }], joining: [{ label: "Traffic", value: "2,000,000 pageviews", note: "Monthly, an alternative to the revenue threshold", sourceUrl: "https://www.monetizemore.com/solutions/pubguru-premium/" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Ad revenue", value: "$5,000 a month", note: "Alternative to the 2 million monthly pageview threshold", sourceUrl: "https://www.monetizemore.com/solutions/pubguru-premium/" }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "PubGuru Premium", url: "https://www.monetizemore.com/solutions/pubguru-premium/" }, { label: "MonetizeMore FAQ", url: "https://www.monetizemore.com/frequently-asked-questions/" }] }, + monumetric: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "10,000", note: "monthly pageviews, Propel program", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Pay day", value: "Net 60", sourceUrl: "https://www.monumetric.com/payment-terms-and-how-you-are-paid/" }, { label: "Payment minimum", value: "$10", sourceUrl: "https://www.monumetric.com/payment-terms-and-how-you-are-paid/" }], joining: [{ label: "Traffic", value: "At least 10,000 pageviews", note: "Monthly", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Visitor location", value: "At least 50% from the US, UK, Canada, or Australia", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Content", value: "English; no pornography, drugs, hate speech, or copyrighted content", sourceUrl: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 10000, unit: "pageviews" }, revenueShare: null, paymentTerms: "Net 60; paid monthly within the first ten business days", paymentThreshold: "$10; balance rolls forward below this amount", paymentMethods: ["US direct deposit", "International PayPal", "Payoneer in some cases"], sources: [{ label: "Eligibility requirements", url: "https://www.monumetric.com/what-are-the-requirements-to-work-with-monumetric/" }, { label: "Payment terms", url: "https://www.monumetric.com/payment-terms-and-how-you-are-paid/" }] }, + "newor-media": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic for Catapult", value: "60,000", note: "US pageviews a month", sourceUrl: "https://newormedia.com/programs" }, { label: "Pay day", value: "Net 45", note: "in the platform agreement; the FAQ says Net 30", sourceUrl: "https://newormedia.com/node/296" }], joining: [{ label: "Traffic", value: "No minimum for Elevate", note: "Catapult: 60,000 US pageviews; Premier: 420,000 US pageviews monthly", sourceUrl: "https://newormedia.com/programs" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "English site with navigation; follow Google ad policies", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Google standing", value: "Follow Google ad policies", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Must connect", value: "Google Ad Manager account", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Approval time", value: "Usually 24 to 48 hours", note: "For Elevate review", sourceUrl: "https://newormedia.com/elevatefaqs" }, { label: "Exclusivity", value: "Required", note: "Newor Media says all publishers must be exclusive", sourceUrl: "https://newormedia.com/elevatefaqs" }], minTraffic: null, revenueShare: null, paymentTerms: "Net 45 in platform agreement; FAQ says Net 30", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Programs", url: "https://newormedia.com/programs" }, { label: "Elevate FAQ", url: "https://newormedia.com/elevatefaqs" }, { label: "Platform agreement", url: "https://newormedia.com/node/296" }, { label: "Payment FAQ", url: "https://newormedia.com/faqs/what-are-your-payment-terms" }] }, + nitropay: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "None", note: "above the transaction fee", sourceUrl: "https://nitropay.com/docs/" }, { label: "Pay day", value: "Net 7", sourceUrl: "https://nitropay.com/docs/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 7 per public docs archive", paymentThreshold: "No stated minimum; earnings must exceed transaction fee", paymentMethods: [], sources: [{ label: "NitroPay docs archive", url: "https://nitropay.com/docs/" }, { label: "Bonus share terms", url: "https://nitropay.com/bonus/" }] }, + "opti-digital": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day, Managed Inventory", value: "Net 45", sourceUrl: "https://knowledge-base.optidigital.com/knowledge/understanding-your-payment-terms" }, { label: "Pay day, Managed Account", value: "Net 60", note: "for applicable demand", sourceUrl: "https://knowledge-base.optidigital.com/knowledge/understanding-your-payment-terms" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 45 for Managed Inventory; Net 60 for applicable demand under Managed Account; exact setup is in the order", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Payment terms by setup", url: "https://knowledge-base.optidigital.com/knowledge/understanding-your-payment-terms" }, { label: "Invoice flow", url: "https://knowledge-base.optidigital.com/knowledge/how-is-the-invoicing-working-at-opti-digital-when-will-i-get-paid" }] }, + playwire: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "100,000 to 500,000", note: "monthly pageviews; Playwire's pages differ", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Pay day", value: "Net 60", sourceUrl: "https://www.playwire.com/faq" }], joining: [{ label: "Traffic", value: "500,000 monthly pageviews or 1,000 daily active users", note: "Qualification page separately suggests applying from 100,000 monthly pageviews", sourceUrl: "https://www.playwire.com/faq" }, { label: "Visitor location", value: "At least 20% of traffic from the US", note: "Qualification guidance", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Content", value: "Unique, original content", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Google standing", value: "Not blacklisted by Google or major ad providers", sourceUrl: "https://www.playwire.com/qualification-criteria" }, { label: "Must connect", value: "Implement the Playwire SDK", note: "Required to access its demand", sourceUrl: "https://www.playwire.com/faq" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Qualification criteria", url: "https://www.playwire.com/qualification-criteria" }, { label: "Publisher FAQ", url: "https://www.playwire.com/faq" }] }, + pubfuture: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 30", sourceUrl: "https://pubfuture.com/?from=chapterpage" }, { label: "Payment minimum", value: "$50", sourceUrl: "https://pubfuture.com/?from=chapterpage" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 30", paymentThreshold: "$50", paymentMethods: ["PayPal", "Payoneer", "Bank transfer", "Bitcoin"], sources: [{ label: "Publisher FAQ", url: "https://pubfuture.com/?from=chapterpage" }, { label: "PubFuture publisher case study", url: "https://blog.pubfuture.com/the-secret-to-400-more-revenue-for-h5-game-sites-pubfutures-winning-formula/1007/" }] }, + pubgalaxy: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Suggested traffic", value: "100,000", note: "monthly pageviews; not a hard minimum", sourceUrl: "https://www.pubgalaxy.com/learn-center/" }, { label: "Your share", value: "85%", note: "at least, of partner net revenue; at least 65% of direct net revenue", sourceUrl: "https://www.pubgalaxy.com/terms-and-conditions/" }, { label: "Payment minimum", value: "$50", note: "PayPal; $100 wire", sourceUrl: "https://www.pubgalaxy.com/terms-and-conditions/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: "At least 85% of net revenue from ad partners and 65% from direct deals under published terms", paymentTerms: "Within 35 days after month end, subject to PubGalaxy receiving advertiser funds", paymentThreshold: "$50 PayPal; $100 wire", paymentMethods: ["PayPal", "Wire transfer"], sources: [{ label: "Publisher Learn Center", url: "https://www.pubgalaxy.com/learn-center/" }, { label: "Publisher terms", url: "https://www.pubgalaxy.com/terms-and-conditions/" }] }, + publift: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Revenue to join", value: "More than $2,000", note: "monthly ad revenue, typically", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Pageview signal", value: "500,000+", note: "a month; a signal, not the main rule", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Payment minimum", value: "$200", note: "in Publift's comparison table", sourceUrl: "https://www.publift.com/blog/the-best-cpm-ad-network-for-publishers" }], joining: [{ label: "Traffic", value: "500,000 pageviews is a secondary signal", note: "Monthly; revenue is the primary qualifying metric", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Visitor location", value: "No location-based limit stated", note: "Publift says it does not discriminate by traffic origin", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Content", value: "Useful, safe, original content", sourceUrl: "https://www.publift.com/blog/publift-requirements" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }, { label: "Ad revenue", value: "More than $2,000 a month", note: "Primary qualifying metric, in USD", sourceUrl: "https://www.publift.com/blog/publift-requirements" }], minTraffic: null, revenueShare: null, paymentTerms: "Google revenue: Net 30; other network revenue: Net 90 in Publift's current comparison article", paymentThreshold: "$200 in Publift's published comparison table", paymentMethods: ["Direct bank transfer"], sources: [{ label: "2026 requirements", url: "https://www.publift.com/blog/publift-requirements" }, { label: "2026 publisher payment comparison", url: "https://www.publift.com/blog/ezoic-vs-adsense-vs-publift" }] }, + publir: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pro fee", value: "15%", note: "Publir's fee on its Pro service; custom ads use a separate split", sourceUrl: "https://publir.com/app/pricing" }, { label: "Custom ad share", value: "50/50", note: "publisher and Publir, Net 60, in the service agreement", sourceUrl: "https://publir.com/service_agreement" }], joining: [{ label: "Traffic", value: "No traffic limit", sourceUrl: "https://publir.com/app/pricing" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60 for custom ad revenue under service agreement", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Pricing", url: "https://publir.com/app/pricing" }, { label: "Service agreement", url: "https://publir.com/service_agreement" }] }, + "publisher-collective": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "€500", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }], joining: [{ label: "Traffic", value: "No numeric minimum stated", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Own or license the media; no prohibited content", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Integrate its systems and ad tags", sourceUrl: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Paid after advertiser payment; no fixed interval is published in the partner terms", paymentThreshold: "€500", paymentMethods: ["Bank transfer; GBP, EUR, or USD"] , sources: [{ label: "Partner terms", url: "https://www.publisher-collective.com/partners-terms-conditions" }, { label: "Publisher FAQ", url: "https://www.publisher-collective.com/faq" }, { label: "Google Ad Exchange article", url: "https://www.publisher-collective.com/blog/google-ad-exchange" }] }, + pubnation: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Official website", url: "https://pubnation.com/" }] }, + pubpower: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 0 or Net 30", note: "Net 0 above $3,000 a month in revenue", sourceUrl: "https://help.pubpower.io/article/payment-methods-terms" }, { label: "Payoneer minimum", value: "$50", sourceUrl: "https://help.pubpower.io/article/payment-methods-terms" }, { label: "Bank transfer minimum", value: "$1,000", sourceUrl: "https://help.pubpower.io/article/payment-methods-terms" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 0 above $3,000 monthly revenue; otherwise Net 30", paymentThreshold: "Method-specific: $50 Payoneer, $100 PayPal, $100 to $1,000 crypto, or $1,000 bank", paymentMethods: ["Bank transfer", "PayPal", "Payoneer", "Cryptocurrency"], sources: [{ label: "Payment methods and terms", url: "https://help.pubpower.io/article/payment-methods-terms" }] }, + raptive: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "25,000", note: "monthly pageviews", sourceUrl: "https://raptive.com/creators/" }, { label: "Your share", value: "75%", sourceUrl: "https://raptive.com/fbp/" }, { label: "Pay day", value: "Net 45", sourceUrl: "https://help.raptive.com/hc/en-us/articles/202052255-When-do-I-get-paid" }], joining: [{ label: "Traffic", value: "At least 25,000 pageviews", note: "Monthly", sourceUrl: "https://raptive.com/creators/" }, { label: "Visitor location", value: "Majority from the US, Canada, UK, Australia, or New Zealand", sourceUrl: "https://raptive.com/creators/" }, { label: "Content", value: "100% original content", sourceUrl: "https://raptive.com/creators/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Set up Google Analytics", sourceUrl: "https://raptive.com/creators/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: { amount: 25000, unit: "pageviews" }, revenueShare: "75%", paymentTerms: "Net 45; paid on the 15th", paymentThreshold: "$25; $100 for wire transfer", paymentMethods: ["ACH", "PayPal", "Wire transfer"], sources: [{ label: "Creator eligibility", url: "https://raptive.com/creators/" }, { label: "Share information", url: "https://raptive.com/fbp/" }, { label: "Payment FAQ", url: "https://help.raptive.com/hc/en-us/articles/202052255-When-do-I-get-paid" }] }, + refinery89: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "€250 or $250", note: "a month, by region", sourceUrl: "https://refinery89.com/docs/faqs/finance/is-there-a-minimum-threshold-for-getting-paid/" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: "€250 or $250, based on region", paymentMethods: [], sources: [{ label: "Payment threshold", url: "https://refinery89.com/docs/faqs/finance/is-there-a-minimum-threshold-for-getting-paid/" }, { label: "Payment timing", url: "https://refinery89.com/docs/faqs/finance/when-do-publishers-get-paid/" }, { label: "Publisher solution", url: "https://refinery89.com/publisher-ad-management-solution-refinery89/" }] }, + revcontent: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "$250", note: "in the publisher terms; payment methods set their own too", sourceUrl: "https://help.revcontent.com/knowledge/publisher-terms" }], joining: [{ label: "Traffic", value: "No universal minimum stated", sourceUrl: "https://help.revcontent.com/knowledge/publisher-terms" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Comply with publisher terms and content policy", sourceUrl: "https://help.revcontent.com/knowledge/publisher-terms" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: "$250 in terms; payment method thresholds range from $100 ACH to $1,000 wire", paymentMethods: ["ACH", "PayPal", "Wire transfer"], sources: [{ label: "Publisher terms", url: "https://help.revcontent.com/knowledge/publisher-terms" }, { label: "Payment requirements and methods", url: "https://help.revcontent.com/knowledge/publisher-payment-requirements-methods" }] }, + setupad: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Traffic to join", value: "100,000", note: "monthly visitors or sessions; dated articles differ, so check current rules", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Payment minimum", value: "$100", note: "in published guidance", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }], joining: [{ label: "Traffic", value: "Not public", note: "Older official articles conflict on visitors versus sessions", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Follow publisher policy", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Maintain ads.txt", sourceUrl: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 60 in published guidance", paymentThreshold: "$100 in published guidance", paymentMethods: ["Bank transfer", "PayPal", "Payoneer", "Wise", "Revolut", "Paysera"], sources: [{ label: "Programmatic advertising guide", url: "https://setupad.com/es/blog/best-programmatic-advertising-platforms/" }, { label: "Platform comparison", url: "https://setupad.com/es/blog/ezoic-vs-adsense-vs-setupad/" }] }, + "she-media": { checkedAt: "2026-10-16", keyNumbers: [{ label: "PayPal cutoff", value: "$1,000", note: "a month, outside the US; wire is preferred above it", sourceUrl: "https://support.shemedia.com/hc/en-us/articles/35987097977627-Payment-Options-for-Publishers-Outside-the-US" }, { label: "Referral traffic", value: "20,000", note: "monthly pageviews, in referral guidance only; not a general minimum", sourceUrl: "https://www.shemedia.com/news-updates/she-media-publisher-referral-program" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Quality content", note: "The referral page describes referral eligibility, not a general applicant checklist", sourceUrl: "https://www.shemedia.com/news-updates/she-media-publisher-referral-program" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: ["PayPal", "WISE/ACH", "Wire transfer"], sources: [{ label: "International payment options", url: "https://support.shemedia.com/hc/en-us/articles/35987097977627-Payment-Options-for-Publishers-Outside-the-US" }, { label: "Publisher referral criteria", url: "https://www.shemedia.com/news-updates/she-media-publisher-referral-program" }] }, + "snack-media": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Notice to leave", value: "30 days", sourceUrl: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "No hateful, pornographic, illegal, or infringing content", sourceUrl: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Share live traffic stats and install network tags", note: "Tags must be live within 14 days of the agreement date", sourceUrl: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Within 30 days after the month when Snack Media receives the advertiser funds", paymentThreshold: null, paymentMethods: [], sources: [{ label: "2025 publisher agreement terms", url: "https://cdn.snack-media.com/wp-content/uploads/2025/08/Snack-Ad-Network-Agreement-TCs.pdf" }, { label: "Terms page", url: "https://www.snack-media.com/Ad_Network_Terms_and_Conditions_for_Publishers/" }] }, + sovrn: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Net 45", sourceUrl: "https://authcore.atlassian.net/wiki/spaces/KB/pages/2693627510846" }, { label: "Payment minimum", value: "$25", note: "$50 for wire", sourceUrl: "https://authcore.atlassian.net/wiki/spaces/KB/pages/2693627510846" }], joining: [{ label: "Traffic", value: "No minimum stated", sourceUrl: "https://knowledge.sovrn.com/kb/what-is-required-for-sovrn-to-approve-my-property" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Operational domain, ad-system ID, and ads.txt", sourceUrl: "https://knowledge.sovrn.com/kb/what-is-required-for-sovrn-to-approve-my-property" }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Net 45 for Ad Exchange", paymentThreshold: "$25 standard; $50 for wire", paymentMethods: ["PayPal", "ACH", "Wire", "eCheck/local transfer", "US check"], sources: [{ label: "Account requirements", url: "https://knowledge.sovrn.com/kb/what-is-required-for-sovrn-to-approve-my-property" }, { label: "Payment help", url: "https://authcore.atlassian.net/wiki/spaces/KB/pages/2693627510846" }] }, + sparteo: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Company and services", url: "https://corporate.sparteo.com/" }, { label: "Publisher platform case study", url: "https://corporate.sparteo.com/post/w3schools-rpm-uplift-unified-monetisation-stack" }] }, + taboola: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "Within 45 days", note: "after the month ends", sourceUrl: "https://www.taboola.com/policies/publisher-revshare-terms-and-conditions-1-3-US" }], joining: [{ label: "Traffic", value: "No numeric minimum stated", sourceUrl: "https://www.taboola.com/policies/publisher-revshare-terms-and-conditions-1-3-US" }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Comply with the Publisher Policy", sourceUrl: "https://pubhelp.taboola.com/hc/en-us/articles/360033751274-Taboola-Publisher-Policy" }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Within 45 days after month end", paymentThreshold: null, paymentMethods: ["Payoneer"], sources: [{ label: "Publisher revenue share terms", url: "https://www.taboola.com/policies/publisher-revshare-terms-and-conditions-1-3-US" }, { label: "Publisher policy", url: "https://pubhelp.taboola.com/hc/en-us/articles/360033751274-Taboola-Publisher-Policy" }] }, + teads: { checkedAt: "2026-10-16", keyNumbers: [{ label: "Pay day", value: "30 to 60 days", note: "typical range in a company filing; contracts are not public", sourceUrl: "https://investors.teads.com/static-files/4f470000-7ee3-4919-9e4d-506e65a44af4" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Teads' company filing describes 30 to 60 days; exact agreement is not public", paymentThreshold: null, paymentMethods: [], sources: [{ label: "Media owner partnership", url: "https://www.teads.com/media-owners" }, { label: "Company filing", url: "https://investors.teads.com/static-files/4f470000-7ee3-4919-9e4d-506e65a44af4" }] }, + "the-moneytizer": { checkedAt: "2026-10-16", keyNumbers: [{ label: "Payment minimum", value: "€50 or $50", sourceUrl: "https://www.themoneytizer.com/payment-simulator" }, { label: "Pay day", value: "About two months", note: "after the month you earn it", sourceUrl: "https://www.themoneytizer.com/payment-simulator" }], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: "Invoice is generated after month end and payment follows about two months later", paymentThreshold: "€50 or $50", paymentMethods: [], sources: [{ label: "Payment simulator", url: "https://www.themoneytizer.com/payment-simulator" }, { label: "US adtech glossary", url: "https://us.themoneytizer.com/adtech-glossary?id=16" }] }, + venatus: { checkedAt: "2026-10-16", keyNumbers: [], joining: [{ label: "Traffic", value: "Not public", sourceUrl: null }, { label: "Visitor location", value: "Not public", sourceUrl: null }, { label: "Content", value: "Not public", sourceUrl: null }, { label: "Google standing", value: "Not public", sourceUrl: null }, { label: "Must connect", value: "Not public", sourceUrl: null }, { label: "Approval time", value: "Not public", sourceUrl: null }], minTraffic: null, revenueShare: null, paymentTerms: null, paymentThreshold: null, paymentMethods: [], sources: [{ label: "Official publisher platform", url: "https://www.venatus.com/" }, { label: "Publisher interest form", url: "https://info.venatus.com/en/signup" }] }, +}; diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx new file mode 100644 index 00000000..0c21a465 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/automotive/intro.mdx @@ -0,0 +1 @@ +The ad networks car and truck sites run, from repair guides to buying advice, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx new file mode 100644 index 00000000..b03d71de --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/beauty-fashion/intro.mdx @@ -0,0 +1 @@ +The ad networks beauty, skin care, and fashion sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx new file mode 100644 index 00000000..c2444159 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/business/intro.mdx @@ -0,0 +1 @@ +The ad networks small business, marketing, and career sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx new file mode 100644 index 00000000..8c680432 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/crafts/intro.mdx @@ -0,0 +1 @@ +The ad networks sewing, knitting, and craft sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx new file mode 100644 index 00000000..3b9d7fc1 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/education/intro.mdx @@ -0,0 +1 @@ +The ad networks learning, school help, and reference sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx new file mode 100644 index 00000000..8bf6c3f1 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/entertainment/intro.mdx @@ -0,0 +1 @@ +The ad networks TV, movie, music, and celebrity sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx new file mode 100644 index 00000000..b62940b2 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/food/intro.mdx @@ -0,0 +1 @@ +The ad networks food blogs and recipe sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx new file mode 100644 index 00000000..685fd93c --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gaming/intro.mdx @@ -0,0 +1 @@ +The ad networks game guides, wikis, and gaming news sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx new file mode 100644 index 00000000..dd6531a8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/gardening/intro.mdx @@ -0,0 +1 @@ +The ad networks gardening and yard care sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx new file mode 100644 index 00000000..88f9fabe --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/health-fitness/intro.mdx @@ -0,0 +1 @@ +The ad networks health, fitness, and wellness sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx new file mode 100644 index 00000000..42db5434 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/home-diy/intro.mdx @@ -0,0 +1 @@ +The ad networks home decor, cleaning, and DIY sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts b/app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts new file mode 100644 index 00000000..1f6a0116 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/index.ts @@ -0,0 +1,45 @@ +import type { MDXContent } from "mdx/types"; +import Intro1 from "./food/intro.mdx"; +import Intro2 from "./home-diy/intro.mdx"; +import Intro3 from "./travel/intro.mdx"; +import Intro4 from "./parenting/intro.mdx"; +import Intro5 from "./personal-finance/intro.mdx"; +import Intro6 from "./health-fitness/intro.mdx"; +import Intro7 from "./beauty-fashion/intro.mdx"; +import Intro8 from "./gaming/intro.mdx"; +import Intro9 from "./tech/intro.mdx"; +import Intro10 from "./crafts/intro.mdx"; +import Intro11 from "./pets/intro.mdx"; +import Intro12 from "./outdoors/intro.mdx"; +import Intro13 from "./gardening/intro.mdx"; +import Intro14 from "./sports/intro.mdx"; +import Intro15 from "./entertainment/intro.mdx"; +import Intro16 from "./lifestyle/intro.mdx"; +import Intro17 from "./education/intro.mdx"; +import Intro18 from "./automotive/intro.mdx"; +import Intro19 from "./business/intro.mdx"; +import Intro20 from "./news/intro.mdx"; + +/** Words for each niche page, keyed by niche slug. */ +export const nicheContent: Record = { + "food": { Intro: Intro1 }, + "home-diy": { Intro: Intro2 }, + "travel": { Intro: Intro3 }, + "parenting": { Intro: Intro4 }, + "personal-finance": { Intro: Intro5 }, + "health-fitness": { Intro: Intro6 }, + "beauty-fashion": { Intro: Intro7 }, + "gaming": { Intro: Intro8 }, + "tech": { Intro: Intro9 }, + "crafts": { Intro: Intro10 }, + "pets": { Intro: Intro11 }, + "outdoors": { Intro: Intro12 }, + "gardening": { Intro: Intro13 }, + "sports": { Intro: Intro14 }, + "entertainment": { Intro: Intro15 }, + "lifestyle": { Intro: Intro16 }, + "education": { Intro: Intro17 }, + "automotive": { Intro: Intro18 }, + "business": { Intro: Intro19 }, + "news": { Intro: Intro20 }, +}; diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx new file mode 100644 index 00000000..f9c4432f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/lifestyle/intro.mdx @@ -0,0 +1 @@ +The ad networks lifestyle and personal blogs run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx new file mode 100644 index 00000000..71180fb8 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/news/intro.mdx @@ -0,0 +1 @@ +The ad networks news and current events sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx new file mode 100644 index 00000000..b8b3316b --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/outdoors/intro.mdx @@ -0,0 +1 @@ +The ad networks camping, hiking, hunting, and fishing sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx new file mode 100644 index 00000000..eccea590 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/parenting/intro.mdx @@ -0,0 +1 @@ +The ad networks parenting, pregnancy, and family sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx new file mode 100644 index 00000000..a56aa21f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/personal-finance/intro.mdx @@ -0,0 +1 @@ +The ad networks personal finance sites run, from budgeting blogs to investing guides, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx new file mode 100644 index 00000000..3ced3d20 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/pets/intro.mdx @@ -0,0 +1 @@ +The ad networks dog, cat, and pet care sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx new file mode 100644 index 00000000..86bd49ec --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/sports/intro.mdx @@ -0,0 +1 @@ +The ad networks sports, scores, and fantasy sports sites run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx new file mode 100644 index 00000000..2e6ce0b5 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/tech/intro.mdx @@ -0,0 +1 @@ +The ad networks tech sites run, from how-to guides to device reviews, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx b/app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx new file mode 100644 index 00000000..66c8d4e6 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/content/travel/intro.mdx @@ -0,0 +1 @@ +The ad networks travel blogs and destination guides run, and the sites that run them. diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx b/app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx new file mode 100644 index 00000000..237d950d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/layout.tsx @@ -0,0 +1,7 @@ +import { registryLayout } from "@/lib/registry-layout"; + +const registry = registryLayout("/ad-networks/niches/[niche]"); +export const generateMetadata = registry.generateMetadata; +export function generateStaticParams() { return []; } +export const revalidate = false; +export default registry.Layout; diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx b/app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx new file mode 100644 index 00000000..67fb8529 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/niche-view.tsx @@ -0,0 +1,159 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; +import type { MDXContent } from "mdx/types"; +import { Shapes } from "lucide-react"; +import type { Breadcrumb } from "@/lib/breadcrumbs"; +import type { AdNetworkNichePageData } from "@/lib/ad-networks-types"; +import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; +import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon"; +import { NetworkLadder } from "@/app/ui/ad-networks/network-ladder"; +import { AdSection } from "@/app/ui/ad-networks/section"; +import { checkerHref, formatDay } from "@/app/ui/ad-networks/site-format"; +import { SitesTable } from "@/app/ui/ad-networks/sites-table"; +import { MoveBars } from "@/app/ui/ad-networks/viz/bars"; +import { NewestList } from "@/app/ui/ad-networks/viz/lists"; +import { SizeStacks } from "@/app/ui/ad-networks/viz/size-stacks"; +import { ReportBlock, ReportRow } from "@/app/ui/report"; +import { AppShell } from "@/app/ui/site/app-shell"; +import { EntryHero } from "@/app/ui/site/entry-hero"; +import { PageCanvas } from "@/app/ui/site/page-canvas"; +import { PageHeader } from "@/app/ui/site/page-header"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import { Figure, NOT_PUBLIC, adNetworksSectionNav, joinLabel, joinOrder } from "../../ad-networks-shared"; +import { trafficLabel, trafficOrder } from "@/lib/ad-networks-report"; +import styles from "../niches.module.css"; + +/** Networks drawn by name in the size stacks. The rest are grouped as Other. */ +const sizeNetworks = 5; + +const inlineIntro = { p: ({ children }: { children?: ReactNode }) => <>{children} }; +/** Short names from the mockup, such as `AdSense` and `Journey`, where space is tight. */ +const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, ""); +const networkHref = (slug: string) => `/ad-networks/${slug}`; + +/** Each size group as 100% split across the niche's biggest networks, with the rest as Other. */ +function sizeStacks(traffic: AdNetworkNichePageData["traffic"]) { + const totals = new Map(); + for (const group of traffic) { + for (const network of group.networks) { + totals.set(network.slug, { name: network.name, count: (totals.get(network.slug)?.count ?? 0) + network.count }); + } + } + const shown = [...totals.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, sizeNetworks); + const groups = [...traffic].sort((a, b) => trafficOrder(a.traffic_group) - trafficOrder(b.traffic_group)).map((group) => { + // Raw shares from counts; Other is the sites left over, so rounding never invents a slice. + const counts = shown.map(([slug]) => group.networks.find((row) => row.slug === slug)?.count ?? 0); + const rest = Math.max(0, group.sites - counts.reduce((sum, value) => sum + value, 0)); + const share = (count: number) => (group.sites ? (count / group.sites) * 100 : 0); + return { label: trafficLabel(group.traffic_group), sites: group.sites, shares: [...counts.map(share), share(rest)] }; + }); + return { groups, series: [...shown.map(([, network]) => ({ label: network.name })), { label: "Other" }] }; +} + +/** + * One niche (mockup `food.html`): its sites, the networks they run with what it takes to join, + * what sites of each size run, and recent moves. Takes loader data so it can be checked with any data. + */ +export function NicheView({ breadcrumbs, data, Intro, title, updatedAt }: { + breadcrumbs: Breadcrumb[]; + data: AdNetworkNichePageData; + Intro: MDXContent; + title: string; + updatedAt: string | Date | null; +}) { + const { niche, sites, switches } = data; + const noun = `${niche.name.toLowerCase()} sites`; + const leader = niche.networks[0]; + const sizes = sizeStacks(data.traffic); + const newest = (data.newest_arrivals ?? []).map((arrival) => ({ + domain: arrival.site.domain, + href: checkerHref(arrival.site.domain), + network: shortName(arrival.network.name), + networkHref: networkHref(arrival.network.slug), + date: formatDay(arrival.observed_at) ?? "", + })); + const moves = switches.slice(0, 6).map((row) => ({ + from: shortName(row.from.name), + fromHref: networkHref(row.from.slug), + to: shortName(row.to.name), + toHref: networkHref(row.to.slug), + count: row.count, + })); + const movesDescription = moves.length && newest.length + ? `${niche.name} sites that switched networks in the last 90 days, and the newest sites on each network.` + : moves.length ? `${niche.name} sites that switched networks in the last 90 days.` : `The newest ${noun} we found, and the network each one joined.`; + + return ( + + + +

          } iconKey={null} />

          + +

          + We list

          {niche.listed}
          {noun}, and
          {niche.confirmed}
          {niche.confirmed === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code. + {leader ? <> {leader.name} runs on more of them than any other network. : null} +

          +
          +
          + + ({ name, slug }))} + noun={noun} + route={`/ad-networks/niches/${niche.slug}`} + scope={{ niche: niche.slug }} + /> + + {niche.networks.length ? ( + { + const rule = joinLabel(network.slug).value; + return { + name: shortName(network.name), + href: networkHref(network.slug), + sites: network.count, + rule: rule === NOT_PUBLIC || rule === "Not listed" ? null : rule, + ruleRank: joinOrder(network.slug), + }; + })} + shareLabel={`share of ${noun}`} + title={`Which ad networks do ${noun} use?`} + total={niche.listed} + /> + ) : null} + + {/* data-gate: the traffic run and resolved main managers fill the size groups. */} + {sizes.groups.length ? ( + + + + ) : null} + + {/* data-gate: the verifier's manager replacements fill switches; observed arrivals (never the starting import) fill newest sites. */} + {moves.length || newest.length ? ( + + + {moves.length ? ( + + + + ) : null} + {newest.length ? ( + + + + ) : null} + + + ) : null} + +
          + ); +} diff --git a/app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx b/app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx new file mode 100644 index 00000000..1cc6c23d --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/[niche]/page.tsx @@ -0,0 +1,27 @@ +import { notFound } from "next/navigation"; +import { adNetworkNiches } from "@/lib/ad-network-niches"; +import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getRequiredDirectoryPage } from "@/lib/directory-pages"; +import { getAdNetworkNichePageData } from "@/lib/ad-networks"; +import { nicheContent } from "./content"; +import { NicheView } from "./niche-view"; + +// Build every niche during the release. Unpublished niches still return 404. +export function generateStaticParams() { + return adNetworkNiches.map((niche) => ({ niche: niche.slug })); +} + +export default async function AdNetworkNichePage({ params }: { params: Promise<{ niche: string }> }) { + const { niche: slug } = await params; + const content = nicheContent[slug]; + if (!content) throw new Error(`Ad network niche has no MDX content: ${slug}`); + const [directoryPage, data, breadcrumbs] = await Promise.all([ + getRequiredDirectoryPage(`ad-networks/niches/${slug}`), + getAdNetworkNichePageData(slug), + getBreadcrumbs(`ad-networks/niches/${slug}`), + ]); + // data-gate: the niche run sorts sites into this niche; until it reaches coverage the loader returns null. + if (!data) notFound(); + + return ; +} diff --git a/app/(site)/(directories)/ad-networks/niches/content/article.mdx b/app/(site)/(directories)/ad-networks/niches/content/article.mdx new file mode 100644 index 00000000..d519f730 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/content/article.mdx @@ -0,0 +1,7 @@ +## How do we sort sites into niches? + +We give each site one niche, based on what its pages are about. A site that covers several topics goes into its main one. Sites we can't place yet stay unsorted and don't count toward any niche here. + +## Why can a niche's shares add up to more than 100%? + +Many sites run more than one ad network at the same time. Each share counts the sites that use that network, so one site can count toward several networks in the same niche. diff --git a/app/(site)/(directories)/ad-networks/niches/content/intro.mdx b/app/(site)/(directories)/ad-networks/niches/content/intro.mdx new file mode 100644 index 00000000..2022af21 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/content/intro.mdx @@ -0,0 +1 @@ +See which ad networks sites use in each niche, from food and travel to gaming. diff --git a/app/(site)/(directories)/ad-networks/niches/layout.tsx b/app/(site)/(directories)/ad-networks/niches/layout.tsx new file mode 100644 index 00000000..0ac770de --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/layout.tsx @@ -0,0 +1,6 @@ +import { registryLayout } from "@/lib/registry-layout"; + +const registry = registryLayout("/ad-networks/niches", { structuredData: "page" }); +export const generateMetadata = registry.generateMetadata; +export const revalidate = false; +export default registry.Layout; diff --git a/app/(site)/(directories)/ad-networks/niches/niches-view.tsx b/app/(site)/(directories)/ad-networks/niches/niches-view.tsx new file mode 100644 index 00000000..7d7aae16 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/niches-view.tsx @@ -0,0 +1,112 @@ +import type { ReactNode } from "react"; +import { Shapes } from "lucide-react"; +import type { Breadcrumb } from "@/lib/breadcrumbs"; +import type { AdNetworkNichesPageData } from "@/lib/ad-networks-types"; +import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; +import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon"; +import { NichesTable, type NicheTableRow } from "@/app/ui/ad-networks/niches-table"; +import { AdSection } from "@/app/ui/ad-networks/section"; +import { Heatmap } from "@/app/ui/ad-networks/viz/heatmap"; +import { Treemap } from "@/app/ui/ad-networks/viz/treemap"; +import { AppShell } from "@/app/ui/site/app-shell"; +import { EntryHero } from "@/app/ui/site/entry-hero"; +import { PageCanvas } from "@/app/ui/site/page-canvas"; +import { PageHeader } from "@/app/ui/site/page-header"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose"; +import { Figure, adNetworksSectionNav, joinLabel } from "../ad-networks-shared"; +import Article from "./content/article.mdx"; +import Intro from "./content/intro.mdx"; +import styles from "./niches.module.css"; + +/** Networks shown as columns in the niche grid: the ones on the most sites. */ +const gridNetworks = 9; + +const inlineIntro = { p: ({ children }: { children?: ReactNode }) => <>{children} }; +/** Short names from the mockup, such as `AdSense` and `Journey`, where space is tight. */ +const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, ""); +const networkHref = (slug: string) => `/ad-networks/${slug}`; + +/** + * The niches index (mockup `niches.html`): sites per niche, each niche's top networks, and the + * niche by network grid. Takes loader data so it can be checked with any data. + */ +export function NichesView({ breadcrumbs, data, nicheHref, title, updatedAt }: { + breadcrumbs: Breadcrumb[]; + data: AdNetworkNichesPageData; + /** The niche page link, or undefined while that page isn't published. */ + nicheHref: (slug: string) => string | undefined; + title: string; + updatedAt: string | Date | null; +}) { + const { niches } = data; + const rows: NicheTableRow[] = niches.map((niche) => ({ + slug: niche.slug, + name: niche.name, + href: nicheHref(niche.slug), + sites: niche.listed, + live: niche.confirmed, + share: niche.share / 100, + top: niche.networks.slice(0, 3).map((network) => ({ name: shortName(network.name), href: networkHref(network.slug), share: network.share / 100 })), + easiest: niche.easiest_to_join + ? { name: shortName(niche.easiest_to_join.name), href: networkHref(niche.easiest_to_join.slug), rule: joinLabel(niche.easiest_to_join.slug).value } + : undefined, + networkShares: Object.fromEntries(niche.networks.map((network) => [network.slug, network.share / 100])), + })); + const columns = data.networks.slice(0, gridNetworks); + + return ( + + + +

          } iconKey={null} />

          + +

          + + {niches.length ? ( + <> We sort

          {data.classified}
          of the
          {data.listed}
          sites we list into
          {niches.length}
          niches.{data.unclassified ? <> The other
          {data.unclassified}
          aren't sorted yet. : null} + ) : null} +

          +
          +
          + + {/* data-gate: the niche run sorts sites into niches; until it reaches coverage the loader returns none and these sections hide. */} + {niches.length ? ( + <> + + ({ label: niche.name, value: niche.listed, href: nicheHref(niche.slug) }))} total={data.classified} /> + + ({ value: network.slug, label: shortName(network.name) }))} + rows={rows} + title="Which ad networks does each niche use?" + /> + {columns.length ? ( + + ({ key: network.slug, label: shortName(network.name) }))} + corner="niche" + rows={niches.map((niche) => ({ + label: niche.name, + href: nicheHref(niche.slug), + values: columns.map((network) => { + const share = niche.networks.find((row) => row.slug === network.slug)?.share; + return share === undefined ? null : Math.round(share); + }), + }))} + unitNote="Numbers are % of the niche's sites." + /> + + ) : null} + + ) : null} +
          +
          +
          + + + ); +} diff --git a/app/(site)/(directories)/ad-networks/niches/niches.module.css b/app/(site)/(directories)/ad-networks/niches/niches.module.css new file mode 100644 index 00000000..cd025969 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/niches.module.css @@ -0,0 +1,21 @@ +/* The method questions below the report (mockup `.nprose`). */ +.article { + margin-top: 64px; + max-width: var(--measure-prose); +} + +.article:first-child { + margin-top: 0; +} + +/* The sites table is not an AdSection, so split every section with the same hairline (mockup `.nsec + .nsec`). */ +.sections { + display: grid; + min-width: 0; +} + +.sections > * + * { + border-top: 1px solid var(--color-border); + margin-top: var(--space-14); + padding-top: var(--space-13); +} diff --git a/app/(site)/(directories)/ad-networks/niches/page.tsx b/app/(site)/(directories)/ad-networks/niches/page.tsx new file mode 100644 index 00000000..1a4d2e1f --- /dev/null +++ b/app/(site)/(directories)/ad-networks/niches/page.tsx @@ -0,0 +1,30 @@ +import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getRequiredDirectoryPage } from "@/lib/directory-pages"; +import { getAdNetworkNichesPageData } from "@/lib/ad-networks"; +import { getExactPublishedPage } from "@/lib/page-registry"; +import { RegistryStructuredData } from "@/lib/registry-layout"; +import { NichesView } from "./niches-view"; + +export default async function AdNetworkNichesPage() { + const [directoryPage, data, breadcrumbs] = await Promise.all([ + getRequiredDirectoryPage("ad-networks/niches"), + getAdNetworkNichesPageData(), + getBreadcrumbs("ad-networks/niches"), + ]); + // Link a niche only once its own page is published. + const published = new Set((await Promise.all(data.niches.map(async (niche) => + (await getExactPublishedPage(`ad-networks/niches/${niche.slug}`)) ? niche.slug : null))).filter(Boolean)); + + return ( + <> + + (published.has(slug) ? `/ad-networks/niches/${slug}` : undefined)} + title={directoryPage.title} + updatedAt={directoryPage.updated_at} + /> + + ); +} diff --git a/app/(site)/(directories)/ad-networks/page.module.css b/app/(site)/(directories)/ad-networks/page.module.css deleted file mode 100644 index 0f4914b1..00000000 --- a/app/(site)/(directories)/ad-networks/page.module.css +++ /dev/null @@ -1,226 +0,0 @@ -.content { - display: grid; - gap: var(--space-9); - grid-template-columns: minmax(0, 1fr); -} - -.search { - display: grid; - gap: var(--space-3); - grid-template-columns: minmax(0, 1fr); - max-width: 620px; - min-width: 0; -} - -.search label { - color: var(--color-text); - font-family: var(--font-display); - font-weight: var(--weight-bold); -} - -.searchRow { - align-items: center; - background: var(--color-surface-2); - border: 1px solid var(--color-border); - border-radius: var(--radius); - display: flex; - gap: var(--space-3); - min-width: 0; - padding-inline-start: var(--space-4); -} - -.searchRow svg { - color: var(--color-text-muted); - flex: none; -} - -.searchRow input { - background: transparent; - border: 0; - color: var(--color-text); - flex: 1; - font: inherit; - min-width: 0; - padding: var(--space-4) 0; -} - -.searchRow input:focus { - outline: none; -} - -.searchRow:focus-within { - outline: 2px solid var(--color-link); - outline-offset: 2px; -} - -.searchRow button { - align-self: stretch; - background: var(--color-accent); - border: 0; - border-radius: 0 var(--radius) var(--radius) 0; - color: var(--color-accent-contrast); - cursor: pointer; - font: inherit; - font-weight: var(--weight-bold); - padding-inline: var(--space-5); -} - -.searchRow button:focus-visible, -.card a:focus-visible, -.siteList a:focus-visible { - outline: 2px solid var(--color-link); - outline-offset: 2px; -} - -.results { - align-items: baseline; - border-bottom: 1px solid var(--color-border); - display: flex; - gap: var(--space-4); - justify-content: space-between; - padding-bottom: var(--space-4); -} - -.results h2 { - color: var(--color-text); - font-family: var(--font-display); - font-size: var(--text-h2); - margin: 0; -} - -.results p, -.cardFoot, -.domain, -.siteMeta, -.method { - color: var(--color-text-muted); - font-family: var(--font-mono); - font-size: var(--text-small); -} - -.results p { - margin: 0; -} - -.grid { - display: grid; - gap: var(--space-4); - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.card { - border: 1px solid var(--color-border); - border-radius: var(--radius); - display: flex; - flex-direction: column; - min-width: 0; -} - -.cardMain { - flex: 1; - padding: var(--space-6); -} - -.card h3 { - font-family: var(--font-display); - font-size: var(--text-h3); - line-height: var(--line-heading); - margin: 0 0 var(--space-3); -} - -.card h3 a, -.cardFoot a, -.siteList a, -.official a { - color: var(--color-link); - text-decoration: none; -} - -.card h3 a:hover, -.cardFoot a:hover, -.siteList a:hover, -.official a:hover { - text-decoration: underline; -} - -.cardMain p { - color: var(--color-text-muted); - line-height: var(--line-body); - margin: 0; -} - -.cardFoot { - align-items: center; - border-top: 1px solid var(--color-border); - display: flex; - flex-wrap: wrap; - gap: var(--space-3); - justify-content: space-between; - padding: var(--space-4) var(--space-6); -} - -.official { - margin: 0; -} - -.siteList { - border-top: 1px solid var(--color-border); - list-style: none; - margin: 0; - padding: 0; -} - -.siteList li { - align-items: center; - border-bottom: 1px solid var(--color-border); - display: flex; - gap: var(--space-4); - justify-content: space-between; - padding: var(--space-5) 0; -} - -.siteList li > div:first-child { - display: grid; - gap: var(--space-2); - min-width: 0; -} - -.siteList a { - font-family: var(--font-display); - font-size: var(--text-h3); - overflow-wrap: anywhere; -} - -.domain { - overflow-wrap: anywhere; -} - -.siteMeta { - display: grid; - flex: none; - gap: var(--space-2); - text-align: right; -} - -.method { - border-top: 1px solid var(--color-border); - line-height: var(--line-body); - margin: var(--space-7) 0 0; - max-width: var(--measure-prose); - padding-top: var(--space-5); -} - -@media (max-width: 680px) { - .grid { - grid-template-columns: minmax(0, 1fr); - } - - .siteList li { - align-items: flex-start; - flex-direction: column; - } - - .siteMeta { - text-align: left; - } -} diff --git a/app/(site)/(directories)/ad-networks/page.tsx b/app/(site)/(directories)/ad-networks/page.tsx index 88662a24..e9e3dc03 100644 --- a/app/(site)/(directories)/ad-networks/page.tsx +++ b/app/(site)/(directories)/ad-networks/page.tsx @@ -1,42 +1,105 @@ -import { getRequiredDirectoryPage } from "@/lib/directory-pages"; +import { Network } from "lucide-react"; import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getRequiredDirectoryPage } from "@/lib/directory-pages"; +import { getAdNetworkDirectoryData } from "@/lib/ad-networks"; +import { getExactPublishedPage, getRequiredPublishedPage } from "@/lib/page-registry"; +import { RegistryStructuredData } from "@/lib/registry-layout"; +import { NetworkCard } from "@/app/ui/ad-networks/network-card"; +import { networkLogo } from "@/app/ui/ad-networks/network-logo-src"; +import { checkerHref } from "@/app/ui/ad-networks/site-format"; import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; -import { PageMeta } from "@/app/ui/site/page-meta"; -import Intro from "./content/intro.mdx"; -import Article from "./content/article.mdx"; -import { Network } from "lucide-react"; -import { DirectoryQueryProvider } from "@/app/ui/directories/directory-query"; +import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon"; import { AppShell } from "@/app/ui/site/app-shell"; import { EntryHero } from "@/app/ui/site/entry-hero"; import { PageCanvas } from "@/app/ui/site/page-canvas"; import { PageHeader } from "@/app/ui/site/page-header"; -import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon"; -import { listAdNetworks } from "@/lib/ad-networks"; -import { RegistryStructuredData } from "@/lib/registry-layout"; -import { getRequiredPublishedPage } from "@/lib/page-registry"; -import { AdNetworkList } from "./ad-network-view"; -import { AdNetworksSidebar } from "./ad-networks-sidebar"; -import styles from "./page.module.css"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import Article from "./content/article.mdx"; +import { Figure, adNetworksSectionNav, joinLabel } from "./ad-networks-shared"; +import { DirectoryView, HeroSearch, NetworkBrowser, type DirectoryNiche } from "./directory-browser"; +import styles from "./directory.module.css"; + +// A real confirmed site, shown as an example the search box can check. +const exampleDomain = "collegefashion.net"; export default async function AdNetworksPage() { - const directoryPage = await getRequiredDirectoryPage("ad-networks"); - await getRequiredPublishedPage("ad-networks"); - const networks = await listAdNetworks(""); + const [directoryPage, data, breadcrumbs] = await Promise.all([ + getRequiredDirectoryPage("ad-networks"), + getAdNetworkDirectoryData(), + getBreadcrumbs("ad-networks"), + getRequiredPublishedPage("ad-networks"), + ]); + + // data-gate: the niche run fills by_niche once classification coverage passes; until then the + // By niche sort, the niche filter, niche search, and the niche example stay hidden. + const niches: DirectoryNiche[] = data.by_niche.map(({ niche, networks }) => ({ + slug: niche.slug, + name: niche.name, + order: [...networks].sort((a, b) => b.niche_count - a.niche_count).map((network) => network.slug), + })); + const publishedNiches = (await Promise.all(data.by_niche.map(async ({ niche }) => + (await getExactPublishedPage(`ad-networks/niches/${niche.slug}`)) ? { slug: niche.slug, name: niche.name } : null))) + .filter((niche) => niche !== null); + + const networks = data.most_sites.map(({ name, slug }) => ({ name, slug })); + const cards = Object.fromEntries(data.most_sites.map((network) => [network.slug, ( + + )])); + const easiest = data.easiest_to_join.map((network) => network.slug); + const anyOpen = data.easiest_to_join.some((network) => network.minTraffic?.amount === 0); + + const examples = [ + ...(data.most_sites[0] ? [{ label: data.most_sites[0].name, href: `/ad-networks/${data.most_sites[0].slug}` }] : []), + { label: exampleDomain, href: checkerHref(exampleDomain) }, + ...(publishedNiches[0] ? [{ label: publishedNiches[0].name, href: `/ad-networks/niches/${publishedNiches[0].slug}` }] : []), + ]; - return - - } sidebarLabel="Ad networks"> - - -

          } />{" "}

          - - -
          -
          - - ({ name, slug, summary, verified_count }))} /> -
          + return ( + <> + + + + + +

          } iconKey={directoryPage.card_icon_key} />

          + +

          + A directory of

          {data.networks_tracked}
          ad networks for publishers, from Ezoic and Mediavine to Google AdSense. See what each one takes to join and how many sites run it. + {/* data-gate: the sync fills the listed and confirmed counts. */} + {data.listed ? <> We list
          {data.listed}
          sites, and
          {data.confirmed}
          {data.confirmed === 1 ? "is" : "are"} confirmed live from their own ads.txt files or ad code. : null} +

          + +
          +
          + + +
          +
          +
          - - ; + + + + ); } diff --git a/app/(site)/(directories)/ad-networks/stats/content/article.mdx b/app/(site)/(directories)/ad-networks/stats/content/article.mdx new file mode 100644 index 00000000..0ce41ced --- /dev/null +++ b/app/(site)/(directories)/ad-networks/stats/content/article.mdx @@ -0,0 +1,3 @@ +## How do we count a site for a network? + +A site counts for a network when its own ads.txt file, or the ad setup on its pages, shows the network. A network's seller list can point us to a site, but it never counts one on its own. We re-check sites on a schedule, so these numbers move as sites join and leave. diff --git a/app/(site)/(directories)/ad-networks/stats/content/intro.mdx b/app/(site)/(directories)/ad-networks/stats/content/intro.mdx new file mode 100644 index 00000000..b823dcb4 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/stats/content/intro.mdx @@ -0,0 +1 @@ +How many sites each ad network runs on, which networks are growing, and what the sites look like. diff --git a/app/(site)/(directories)/ad-networks/stats/layout.tsx b/app/(site)/(directories)/ad-networks/stats/layout.tsx new file mode 100644 index 00000000..a2bb4d41 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/stats/layout.tsx @@ -0,0 +1,6 @@ +import { registryLayout } from "@/lib/registry-layout"; + +const registry = registryLayout("/ad-networks/stats"); +export const generateMetadata = registry.generateMetadata; +export const revalidate = false; +export default registry.Layout; diff --git a/app/(site)/(directories)/ad-networks/stats/page.tsx b/app/(site)/(directories)/ad-networks/stats/page.tsx new file mode 100644 index 00000000..8bd8740a --- /dev/null +++ b/app/(site)/(directories)/ad-networks/stats/page.tsx @@ -0,0 +1,304 @@ +import type { ReactNode } from "react"; +import { ChartColumn } from "lucide-react"; +import { getBreadcrumbs } from "@/lib/breadcrumbs"; +import { getRequiredDirectoryPage } from "@/lib/directory-pages"; +import { getAdNetworkStatsPageData } from "@/lib/ad-networks"; +import type { AdNetworkStatsPageData } from "@/lib/ad-networks-types"; +import { DirectoryPageTitle } from "@/app/ui/directories/directory-page-content"; +import { DirectoryTitleIcon } from "@/app/ui/directories/directory-title-icon"; +import { Report, ReportBars, ReportBlock, ReportGroup, ReportNumbers, ReportRow, type ReportNumber } from "@/app/ui/report"; +import { ColumnChart } from "@/app/ui/ad-networks/viz/columns"; +import { DivergingBars } from "@/app/ui/ad-networks/viz/diverging-bars"; +import { DotPlot } from "@/app/ui/ad-networks/viz/dot-plot"; +import { formatCompact, logStepPosition } from "@/app/ui/ad-networks/viz/format"; +import { MultiLine, type LineSeries } from "@/app/ui/ad-networks/viz/multi-line"; +import { ScatterPlot } from "@/app/ui/ad-networks/viz/scatter-plot"; +import { SizeStacks } from "@/app/ui/ad-networks/viz/size-stacks"; +import { StackedColumns } from "@/app/ui/ad-networks/viz/stacked-columns"; +import { formatCount } from "@/app/ui/ad-networks/site-format"; +import { AppShell } from "@/app/ui/site/app-shell"; +import { EntryHero } from "@/app/ui/site/entry-hero"; +import { PageCanvas } from "@/app/ui/site/page-canvas"; +import { PageHeader } from "@/app/ui/site/page-header"; +import { PageMeta } from "@/app/ui/site/page-meta"; +import { proseClassName, proseFlowClassName } from "@/app/ui/site/prose"; +import { Figure, adNetworksSectionNav, percent } from "../ad-networks-shared"; +import { trafficLabel, trafficOrder } from "@/lib/ad-networks-report"; +import Article from "./content/article.mdx"; +import Intro from "./content/intro.mdx"; +import styles from "./stats.module.css"; + +/** Networks drawn by name in the size stacks. The rest are grouped as Other. */ +const sizeNetworks = 5; +/** Line colors for the five biggest networks, in rank order, as in the mockup. */ +const lineTones: Pick[] = [{ tone: "ac" }, { tone: "ac2" }, { tone: "n1" }, { tone: "ac", dashed: true }, { tone: "n2" }]; + +/** Chart labels use the short names from the mockup, such as `AdSense` and `Journey`. */ +const shortName = (name: string) => name.replace(/^Google /, "").replace(/ by Mediavine$/, ""); +const inlineIntro = { p: ({ children }: { children?: ReactNode }) => <>{children} }; +const monthShort = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC" }); +const monthFull = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" }); +const weekLabel = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }); +const day = (iso: string) => Date.parse(`${iso.slice(0, 10)}T00:00:00Z`); + +/** How we confirmed each link, in words. Keys come from the verifier. */ +const methodLabels: Record = { + ads_txt_managerdomain: "ads.txt MANAGERDOMAIN line", + ads_txt_and_adsense_account: "ads.txt and AdSense account", + ads_txt_and_page_signature: "ads.txt and page ad setup", + page_signature: "Page ad setup only", +}; +const methodLabel = (key: string) => methodLabels[key] ?? key.replace(/_/g, " "); + +/** Month-end counts of the biggest networks, on the dates every line has. */ +function shareOverTime(history: AdNetworkStatsPageData["history"]) { + const dates = [...new Set(history.flatMap((network) => network.history.map((row) => row.date)))] + .filter((date) => history.every((network) => network.history.some((row) => row.date === date))) + .sort(); + return { + points: dates.map((date) => ({ label: monthShort.format(day(date)), fullLabel: monthFull.format(day(date)) })), + series: history.slice(0, lineTones.length).map((network, index) => ({ + label: network.name, + shortLabel: shortName(network.name), + values: dates.map((date) => network.history.find((row) => row.date === date)?.listed ?? 0), + ...lineTones[index], + })), + }; +} + +/** Each size group as 100% split across the biggest networks, with the rest as Other. */ +function sizeStacks(traffic: AdNetworkStatsPageData["traffic"]) { + const totals = new Map(); + for (const group of traffic) { + for (const network of group.networks) { + totals.set(network.slug, { name: network.name, count: (totals.get(network.slug)?.count ?? 0) + network.count }); + } + } + const shown = [...totals.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, sizeNetworks); + const groups = [...traffic].sort((a, b) => trafficOrder(a.traffic_group) - trafficOrder(b.traffic_group)).map((group) => { + // Raw shares from counts; Other is the sites left over, so rounding never invents a slice. + const counts = shown.map(([slug]) => group.networks.find((row) => row.slug === slug)?.count ?? 0); + const rest = Math.max(0, group.sites - counts.reduce((sum, value) => sum + value, 0)); + const share = (count: number) => (group.sites ? (count / group.sites) * 100 : 0); + return { label: trafficLabel(group.traffic_group), sites: group.sites, shares: [...counts.map(share), share(rest)] }; + }); + return { groups, series: [...shown.map(([, network]) => ({ label: network.name })), { label: "Other" }] }; +} + + +// Monthly traffic minimums on the joining rule chart's x axis (mockup `SCX`). +const ruleSteps = [0, 1_000, 10_000, 50_000, 100_000, 500_000, 1_000_000]; +const ruleStepLabels = ruleSteps.map((step) => (step ? formatCompact(step) : "none")); +export default async function AdNetworkStatsPage() { + const [directoryPage, data, breadcrumbs] = await Promise.all([ + getRequiredDirectoryPage("ad-networks/stats"), + getAdNetworkStatsPageData(), + getBreadcrumbs("ad-networks/stats"), + ]); + + const summary: ReportNumber[] = [ + { label: "sites listed", value: formatCount(data.listed) }, + { label: "confirmed live", value: formatCount(data.confirmed), accent: true }, + { label: "networks tracked", value: formatCount(data.networks_tracked) }, + ...(data.multiple_networks_share === null ? [] : [{ label: "run 2+ networks", value: percent(data.multiple_networks_share) }]), + ]; + const shareRows = data.market_share.filter((row) => row.count > 0).slice(0, 12) + .map((row) => ({ label: row.name, value: row.count, display: `${row.share.toFixed(1)}% · ${formatCount(row.count)}`, href: `/ad-networks/${row.slug}` })); + const lines = shareOverTime(data.history); + + const changes = [...data.net_change].sort((a, b) => b.change - a.change); + const changeRows = (changes.length > 10 ? [...changes.slice(0, 5), ...changes.slice(-5)] : changes) + .map((row) => ({ label: row.name, shortLabel: shortName(row.name), value: row.change })); + const switchRows = data.switches.slice(0, 6) + .map((row) => ({ label: `${shortName(row.from.name)} → ${shortName(row.to.name)}`, value: row.count, display: formatCount(row.count) })); + + const sizes = sizeStacks(data.traffic); + + const perSiteTotal = data.networks_per_site.reduce((sum, row) => sum + row.count, 0); + const perSite = data.networks_per_site.map((row, index) => { + const share = perSiteTotal ? (row.count / perSiteTotal) * 100 : 0; + return { label: row.key === "1" ? "1 network" : row.label, value: share, display: percent(share), tone: index ? "ac3" as const : "ac" as const }; + }); + const pairRows = data.pairs.slice(0, 5) + .map((row) => ({ label: `${shortName(row.first.name)} + ${shortName(row.second.name)}`, value: row.count, display: formatCount(row.count) })); + + // The mockup's fixed steps. Each network sits by its real minimum on a log scale between them. + const scatter = data.joining_rules.map((rule) => ({ + label: rule.name, + shortLabel: shortName(rule.name), + x: logStepPosition(rule.amount, ruleSteps), + xDisplay: rule.amount ? `${formatCount(rule.amount)} ${rule.unit}` : "none", + y: rule.sites, + })); + const ageRows = data.median_domain_age.slice(0, 9).map((row) => ({ label: row.name, shortLabel: shortName(row.name), value: Math.round(row.years) })); + const wordpressRows = data.wordpress_share.slice(0, 8) + .map((row) => ({ label: row.name, value: row.share, display: percent(row.share), href: `/ad-networks/${row.slug}` })); + + const ads = data.ads_txt_metrics; + const fileSizeTop = Math.max(0, ...data.ads_txt.file_sizes.map((row) => row.count)); + const fileSizes = data.ads_txt.file_sizes.map((row) => { + const share = data.ads_txt.measured ? (row.count / data.ads_txt.measured) * 100 : 0; + return { label: row.label, value: row.count, display: percent(share), tone: row.count === fileSizeTop ? "ac" as const : "n2" as const }; + }); + + const fresh = data.freshness; + const freshNumbers: ReportNumber[] = [ + ...(fresh.checked_share === null ? [] : [{ label: "checked in the last 7 days", value: percent(fresh.checked_share) }]), + ...(data.confirmed_share === null ? [] : [{ label: "confirmed live", value: `${data.confirmed_share.toFixed(1)}%`, accent: true }]), + ...(fresh.new_last_week === null ? [] : [{ label: "new sites last week", value: formatCount(fresh.new_last_week) }]), + ]; + const weeks = fresh.weekly.map((row) => ({ label: `Week of ${weekLabel.format(day(row.week))}`, total: row.discovered + row.checker, part: row.checker })); + const methodTotal = data.verification_methods.reduce((sum, row) => sum + row.count, 0); + const methodRows = data.verification_methods.map((row) => ({ + label: methodLabel(row.key), + value: row.count, + display: `${percent(methodTotal ? (row.count / methodTotal) * 100 : 0)} · ${formatCount(row.count)}`, + })); + + return ( + + + +

          } iconKey={null} />

          + +

          + From our checks of

          {data.listed}
          sites across
          {data.networks_tracked}
          networks, with
          {data.confirmed}
          confirmed live. +

          +
          +
          + + + + + {shareRows.length || lines.points.length > 1 ? ( + + {shareRows.length ? ( + + + + ) : null} + {/* data-gate: daily snapshots fill a second month end for the five biggest networks. */} + {lines.points.length > 1 ? ( + + + + ) : null} + + ) : null} + + + {/* data-gate: 90 days of snapshots fill net change; the verifier's manager replacements fill switches. */} + {changeRows.length || switchRows.length ? ( + + + {changeRows.length ? ( + + + + ) : null} + {switchRows.length ? ( + + + + ) : null} + + + ) : null} + + {/* data-gate: the traffic run and resolved main managers fill the size groups. */} + {sizes.groups.length ? ( + + + + + + ) : null} + + {perSiteTotal || pairRows.length ? ( + + + {perSiteTotal ? ( + + + + ) : null} + {pairRows.length ? ( + + + + ) : null} + + + ) : null} + + {/* data-gate: the domain-age run fills median ages; the platform run fills WordPress shares. */} + {scatter.length > 1 || ageRows.length || wordpressRows.length ? ( + + + {scatter.length > 1 ? ( + + + + ) : null} + {ageRows.length ? ( + + + + ) : null} + {wordpressRows.length ? ( + + + + ) : null} + + + ) : null} + + {/* data-gate: the ads-txt-summary run fills these numbers and the file sizes. */} + {ads ? ( + + + + + + {fileSizes.length ? ( + + + + ) : null} + + + ) : null} + + {freshNumbers.length || weeks.length || methodRows.length ? ( + + {freshNumbers.length || weeks.length ? ( + + {freshNumbers.length ? : null} + {/* data-gate: real new-site observations with a source for each site fill the weekly columns. */} + {weeks.length ? ( + + + + ) : null} + + ) : null} + {methodRows.length ? ( + + + + ) : null} + + ) : null} + +
          +
          +
          + + + ); +} diff --git a/app/(site)/(directories)/ad-networks/stats/stats.module.css b/app/(site)/(directories)/ad-networks/stats/stats.module.css new file mode 100644 index 00000000..2a465203 --- /dev/null +++ b/app/(site)/(directories)/ad-networks/stats/stats.module.css @@ -0,0 +1,5 @@ +/* The method questions below the report (mockup `.nprose`). */ +.article { + margin-top: 64px; + max-width: var(--measure-prose); +} diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx b/app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx new file mode 100644 index 00000000..38fa7b0c --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker-from-url.tsx @@ -0,0 +1,10 @@ +"use client"; + +import { useInitialQuery } from "@/app/ui/tools/use-initial-query"; +import { AdNetworkCheckerTool, type NetworkLogos } from "./ad-network-checker-tool"; + +// Reads ?site= in the browser so the page stays static. A shared link checks that site right away. +export function AdNetworkCheckerFromUrl({ logos }: { logos: NetworkLogos }) { + const query = useInitialQuery(); + return ; +} diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx b/app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx new file mode 100644 index 00000000..a6c78236 --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker-tool.tsx @@ -0,0 +1,536 @@ +"use client"; + +import Link from "next/link"; +import { CalendarDays, ExternalLink, RotateCcw } from "lucide-react"; +import { useEffect, useRef, useState, type FormEvent, type MouseEvent, type ReactNode, type RefObject } from "react"; +import { AdSection, SectionNote } from "@/app/ui/ad-networks/section"; +import { NetworkLogo } from "@/app/ui/ad-networks/network-logo"; +import { checkerHref, domainAge, nicheName } from "@/app/ui/ad-networks/site-format"; +import { + AdsTxtNumbers, + AdsTxtPreview, + BlockSubheading, + FactMuted, + SimilarSites, + SiteFacts, + SiteNetworksTable, + type SiteFact, +} from "@/app/ui/ad-networks/site-view"; +import { TrafficMeter } from "@/app/ui/ad-networks/traffic-meter"; +import { RankedBars, StackBar } from "@/app/ui/ad-networks/viz/bars"; +import { formatNumber, formatShare, type VizTone } from "@/app/ui/ad-networks/viz/format"; +import { HistoryGantt, type GanttRow } from "@/app/ui/ad-networks/viz/history-gantt"; +import { EventsList } from "@/app/ui/ad-networks/viz/lists"; +import { UiButton, UiShareAction, UiTextField } from "@/app/ui/controls"; +import { UiActionRow, UiStatus } from "@/app/ui/display"; +import { ToolbarSearch } from "@/app/ui/table-toolbar"; +import { ToolResult, ToolWorkbench } from "@/app/ui/tools/tool-workbench"; +import type { AdNetworkCheckerSiteView, AdNetworkRef, AdNetworkSiteResponse } from "@/lib/ad-networks-types"; +import { + EXAMPLE_SITE, + adsTxtUrl, + eventsFor, + evidenceOf, + firstCheck, + formatDay, + formatMonth, + hasChanges, + isManager, + leadFor, + monthKey, + names, + normalizeSite, + saysLabel, + similarNetworks, + sortedRelationships, + statusLabel, +} from "./ad-network-checker-view"; +import styles from "./ad-network-checker.module.css"; + +/** Network slug to its logo path, read from `public/ad-networks/logos` by the server page. */ +export type NetworkLogos = Readonly>; + +type Outcome = + | { kind: "found"; view: AdNetworkCheckerSiteView } + | { domain: string; kind: "missing" } + | { domain: string; kind: "error"; message: string }; + +class FieldError extends Error {} + +async function fetchSite(domain: string, signal: AbortSignal): Promise { + const response = await fetch(`/api/ad-networks/site?domain=${encodeURIComponent(domain)}`, { signal }); + const data = await response.json().catch(() => null); + if (response.status === 400) throw new FieldError(typeof data?.error === "string" ? data.error : "Enter a domain such as example.com."); + if (!response.ok || !data) throw new Error("The check didn't finish. Try again in a moment."); + return data as AdNetworkSiteResponse; +} + +export function AdNetworkCheckerTool({ initialSite = "", logos = {} }: { initialSite?: string; logos?: NetworkLogos }) { + const [site, setSite] = useState(initialSite || EXAMPLE_SITE); + const [error, setError] = useState(null); + const [outcome, setOutcome] = useState(null); + const [loading, setLoading] = useState(false); + const [edited, setEdited] = useState(false); + const inputRef = useRef(null); + const titleRef = useRef(null); + const resultRef = useRef(null); + const request = useRef(null); + const focusTitle = useRef(false); + + async function check(raw: string, focus = true) { + const input = normalizeSite(raw); + if (!input.ok) { + setError(input.message); + requestAnimationFrame(() => inputRef.current?.focus()); + return; + } + request.current?.abort(); + const controller = new AbortController(); + request.current = controller; + setError(null); + setEdited(false); + setLoading(true); + try { + const data = await fetchSite(input.domain, controller.signal); + setOutcome(data.found && data.site ? { kind: "found", view: data.site } : { domain: data.domain, kind: "missing" }); + focusTitle.current = focus; + } catch (caught) { + if (controller.signal.aborted) return; + if (caught instanceof FieldError) { + setError(caught.message); + requestAnimationFrame(() => inputRef.current?.focus()); + } else { + setOutcome({ domain: input.domain, kind: "error", message: caught instanceof Error ? caught.message : "The check didn't finish." }); + focusTitle.current = focus; + } + } finally { + if (request.current === controller) { + request.current = null; + setLoading(false); + } + } + } + + // A shared link checks its site right away. A clean URL waits for the button. + useEffect(() => { + if (initialSite) void check(initialSite, false); + }, [initialSite]); // eslint-disable-line react-hooks/exhaustive-deps + + // Stop a check still running when the tool goes away. A remount starts its own check. + useEffect(() => () => request.current?.abort(), []); + + useEffect(() => { + if (!focusTitle.current || loading) return; + focusTitle.current = false; + titleRef.current?.focus({ preventScroll: true }); + }, [outcome, loading]); + + function submit(event: FormEvent) { + event.preventDefault(); + void check(site); + } + + /** Checks another site from inside the result, then brings the result back into view. */ + function checkFromResult(domain: string) { + setSite(domain); + resultRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }); + void check(domain); + } + + function reset() { + request.current?.abort(); + setSite(EXAMPLE_SITE); + setError(null); + setOutcome(null); + setEdited(false); + setLoading(false); + } + + const stale = loading || (edited && outcome !== null); + + return ( + +
          + { + setSite(value); + setEdited(true); + if (error) setError(null); + }} + placeholder="example.com" + required + spellCheck={false} + value={site} + /> + + {loading ? Checking site... : stale ? Inputs changed : null} + Check site + + + + + + + {outcome?.kind === "found" ? ( + + ) : outcome?.kind === "missing" ? ( + + ) : outcome?.kind === "error" ? ( + void check(outcome.domain)} titleRef={titleRef} /> + ) : ( + + )} + {outcome ? : null} + +
          + ); +} + +/* ---------------- header ---------------- */ + +type TitleRef = RefObject; + +/** The site's name as the result's title, with when we checked it and a link to the site (mockup page header). */ +function SiteHeader({ checkedAt, children, domain, favicon, share, title, titleRef }: { + checkedAt?: string | null; + children?: ReactNode; + domain: string | null; + favicon?: string | null; + share?: boolean; + title?: ReactNode; + titleRef?: TitleRef; +}) { + return ( +
          +
          +

          + {title ?? ( + <> + {domain ? : null} + Ad networks on {domain ?? "—"} + + )} +

          + {share && domain ? : null} +
          +
          + {checkedAt !== undefined ? ( + + ) : null} + {domain ? ( + + + ) : null} +
          + {children} +
          + ); +} + +const networkHref = (network: AdNetworkRef) => `/ad-networks/${network.slug}`; + +/** The mockup's ads.txt wording holds only when every network was confirmed from the file. */ +const allFromAdsTxt = (view: AdNetworkCheckerSiteView) => + view.relationships.every((row) => ["manager", "seller"].includes(evidenceOf(row))); + +function Lead({ view }: { view: AdNetworkCheckerSiteView }) { + const parts = leadFor(view); + return ( +

          + {parts.map((part, index) => ( + + {part.manager ? <>{view.site.domain} runs {part.manager.name} : null} + {part.text} + + ))} +

          + ); +} + +/* ---------------- sections ---------------- */ + +function facts(view: AdNetworkCheckerSiteView): SiteFact[] { + const { site } = view; + const age = domainAge(site); + const niche = nicheName(site.niche); + const items: (SiteFact | null)[] = [ + // Only a MANAGERDOMAIN line names the main ad network. + site.managed_by ? { label: "main ad network", value: {site.managed_by.name} } : null, + // data-gate: the traffic, niche, platform, domain-age, and publisher-names runs fill these facts. + site.traffic_group ? { label: "traffic", value: } : null, + niche && site.niche ? { label: "niche", value: {niche} } : null, + site.platform ? { label: "platform", value: site.platform } : null, + age ? { label: "domain registered", value: <>{age.year}{age.years !== null ? <> · {age.years} {age.years === 1 ? "yr" : "yrs"} : null} } : null, + site.publisher_name ? { label: "publisher", value: site.publisher_name } : null, + ]; + return items.filter((item): item is SiteFact => item !== null); +} + +function NetworksSection({ logos, view }: { logos: NetworkLogos; view: AdNetworkCheckerSiteView }) { + const { domain, managed_by: manager } = view.site; + const rows = sortedRelationships(view); + return ( + + ({ + confirmed: row.status === "confirmed", + href: networkHref(row), + logo: logos[row.slug] ? { src: logos[row.slug] } : null, + name: row.name, + says: saysLabel(view, row), + since: row.first_confirmed_at ? formatMonth(row.first_confirmed_at) : undefined, + status: statusLabel(row), + }))} + /> + {/* data-gate: the ads-txt-summary run saves the full seller list that fills this count. */} + {view.other_sellers ? ( + + Its ads.txt also lists {formatNumber(view.other_sellers)} other ad {view.other_sellers === 1 ? "seller" : "sellers"}. + {manager ? ` Most of them work through ${manager.name}, so we don't count them as separate networks.` : " We don't count them as separate networks."} + + ) : null} + + ); +} + +function ganttTone(view: AdNetworkCheckerSiteView, row: AdNetworkCheckerSiteView["relationships"][number]): VizTone { + if (row.status === "ended") return "n1"; + if (row.status === "stale") return "n2"; + return isManager(view, row) ? "ac" : "ac2"; +} + +function HistorySection({ view }: { view: AdNetworkCheckerSiteView }) { + const rows: GanttRow[] = sortedRelationships(view) + .filter((row) => row.first_confirmed_at) + .map((row) => ({ + from: monthKey(row.first_confirmed_at!), + label: row.name, + note: row.status === "stale" ? "not seen lately" : undefined, + to: row.status === "ended" ? monthKey(row.last_confirmed_at ?? row.ended_at ?? row.first_confirmed_at!) : null, + tone: ganttTone(view, row), + })); + const events = eventsFor(view); + const fromAdsTxt = allFromAdsTxt(view); + const start = firstCheck(view); + const endYear = new Date(view.site.last_checked_at ?? Date.now()).getUTCFullYear(); + return ( + + {rows.length ? : null} + {events.length ? : null} + {start ? Our checks of this site started on {formatDay(start)}. A network found on that first check may have been there for years. : null} + + ); +} + +function AdsTxtSection({ view }: { view: AdNetworkCheckerSiteView }) { + const { domain, last_checked_at: checkedAt } = view.site; + // Seller lines of networks confirmed live read brighter; stale and ended networks don't. + const live = [...new Set(view.relationships.filter((row) => row.status === "confirmed").flatMap((row) => row.ad_system_domains ?? []))]; + const summary = view.ads_txt_summary; + const preview = view.ads_txt_preview ?? summary?.preview ?? null; + const split = summary ? summary.direct + summary.reseller : 0; + const sellers = summary?.top_sellers.slice(0, 5) ?? []; + const shown = preview ? preview.replace(/\r\n?/g, "\n").split("\n").filter((line) => line.trim()).length : 0; + const more = summary && preview ? Math.max(0, summary.lines - shown) : 0; + const numbers = summary ? ( +
          + + {split ? ( + + ) : null} + {sellers.length ? ( + <> + companies with the most lines + ({ label: seller.name, value: seller.lines }))} /> + + ) : null} +
          + ) : null; + const file = preview ? ( +
          + +
          + ) : null; + return ( + + {numbers && file ?
          {numbers}{file}
          : numbers ?? file} +
          + ); +} + +function SimilarSection({ onCheck, view }: { onCheck: (domain: string) => void; view: AdNetworkCheckerSiteView }) { + const matched = similarNetworks(view); + const niche = view.basis === "niche" ? nicheName(view.site.niche) : null; + const sameSize = Boolean(view.site.traffic_group) && view.similar_sites.every((site) => site.traffic_group === view.site.traffic_group); + const what = matched.length === 1 ? matched[0].name : "the same networks"; + const kind = niche ? `${niche.toLowerCase()} sites` : "sites"; + // Links to every network read as a wall when a site runs many, so they stay for one or two. + const links = [ + ...(matched.length <= 2 ? matched.map((network) => ({ href: networkHref(network), label: `${network.name} sites` })) : []), + ...(niche && view.site.niche ? [{ href: `/ad-networks/niches/${view.site.niche}`, label: `${niche.toLowerCase()} sites` }] : []), + ]; + // Clicks check the site here; the link itself still opens the checker for that site. + const intercept = (event: MouseEvent) => { + const link = (event.target as HTMLElement).closest("a"); + if (!link || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) return; + const domain = new URL(link.href).searchParams.get("site"); + if (!domain) return; + event.preventDefault(); + onCheck(domain); + }; + return ( + +
          + ({ domain: site.domain, href: checkerHref(site.domain), trafficGroup: site.traffic_group }))} /> +
          + {links.length ? ( + + See all {links.map((link, index) => ( + + {index ? (index === links.length - 1 ? " or all " : ", ") : null} + {link.label} + + ))}. + + ) : null} +
          + ); +} + +/* ---------------- states ---------------- */ + +function FoundResult({ logos, onCheck, titleRef, view }: { logos: NetworkLogos; onCheck: (domain: string) => void; titleRef: TitleRef; view: AdNetworkCheckerSiteView }) { + const items = facts(view); + return ( + <> + + + + {items.length ? : null} +
          + + {/* data-gate: the API's recorded events (verifier joins and departures, later the Wayback run) fill the history. */} + {hasChanges(view) ? : null} + {/* data-gate: the ads-txt-summary run fills the counts, top sellers, and file preview. */} + {view.ads_txt_summary || view.ads_txt_preview ? : null} + {view.similar_sites.length ? : null} +
          + + ); +} + +function MissingResult({ domain, titleRef }: { domain: string; titleRef: TitleRef }) { + return ( + We don't list {domain} yet} titleRef={titleRef}> +

          + We haven't confirmed an ad network we track on this site.{" "} + Open its ads.txt file to see who it names. +

          +
          + ); +} + +function ErrorResult({ domain, message, onRetry, titleRef }: { domain: string; message: string; onRetry: () => void; titleRef: TitleRef }) { + return ( + Couldn't check {domain}} titleRef={titleRef}> + {message} +
          Try again
          +
          + ); +} + +/** Before the first check: the result's shape with dashes. */ +function EmptyResult() { + return ( + <> + + ({ label, value: "—" }))} + /> + + ); +} + +function CheckAnother({ onCheck }: { onCheck: (domain: string) => void }) { + const [value, setValue] = useState(""); + const [error, setError] = useState(null); + return ( +
          + +
          { + event.preventDefault(); + const input = normalizeSite(value); + if (!input.ok) { + setError(input.message); + return; + } + setError(null); + setValue(""); + onCheck(input.domain); + }} + > + { + setValue(next); + if (error) setError(null); + }} + placeholder="example.com" + size="large" + value={value} + /> + Check + + {error ?

          {error}

          : null} + We show the ad networks we've confirmed on the site, mostly from its ads.txt file. +
          +
          + ); +} diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts b/app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts new file mode 100644 index 00000000..56c3af7b --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker-view.ts @@ -0,0 +1,191 @@ +import type { + AdNetworkCheckerSiteView, + AdNetworkRef, + AdNetworkSiteRelationship, +} from "@/lib/ad-networks-types"; + +export const EXAMPLE_SITE = "collegefashion.net"; +export const NOT_LISTED = "Not listed"; + +export type SiteInput = { domain: string; ok: true } | { message: string; ok: false }; + +const LABEL = /^(?!-)[a-z0-9-]{1,63}(? 253 + || labels.length < 2 + || !labels.every((label) => LABEL.test(label)) + || !ENDING.test(labels[labels.length - 1]) + ) { + return { message: "Enter a domain such as example.com.", ok: false }; + } + return { domain: host, ok: true }; +} + +const monthYear = new Intl.DateTimeFormat("en-US", { month: "short", timeZone: "UTC", year: "numeric" }); +const fullDate = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", timeZone: "UTC", year: "numeric" }); + +/** `Mar 2021`. */ +export function formatMonth(iso: string | null) { + return iso ? monthYear.format(new Date(iso)) : NOT_LISTED; +} + +/** `Oct 12, 2026`. */ +export function formatDay(iso: string | null) { + return iso ? fullDate.format(new Date(iso)) : NOT_LISTED; +} + +/** `2026-10`, the month key the history timeline takes. */ +export const monthKey = (iso: string) => iso.slice(0, 7); + +/** `Mediavine`, `Mediavine and Amazon APS`, `A, B, and C`. Pass `or` for `A, B, or C`. */ +export function names(list: readonly AdNetworkRef[], join: "and" | "or" = "and") { + if (list.length <= 2) return list.map((item) => item.name).join(` ${join} `); + return `${list.slice(0, -1).map((item) => item.name).join(", ")}, ${join} ${list[list.length - 1].name}`; +} + +const statusOrder = { confirmed: 0, stale: 1, ended: 2 } as const; + +/** The manager first, then current networks, then ones not seen lately, then ended ones. */ +export function sortedRelationships(view: AdNetworkCheckerSiteView) { + const manager = view.site.managed_by?.slug; + return [...view.relationships].sort((a, b) => + Number(b.slug === manager) - Number(a.slug === manager) + || statusOrder[a.status] - statusOrder[b.status] + || a.name.localeCompare(b.name)); +} + +/** True when this network is the site's main ad manager. */ +export const isManager = (view: AdNetworkCheckerSiteView, relationship: AdNetworkRef) => + view.site.managed_by?.slug === relationship.slug; + +/** + * How we confirmed a network, from its stored verification method. Unknown methods stay unknown, + * and page-only evidence never reads as an ads.txt statement. + */ +export type Evidence = "manager" | "seller" | "page" | "unknown"; + +export function evidenceOf(relationship: AdNetworkSiteRelationship): Evidence { + switch (relationship.verification_method) { + case "ads_txt_managerdomain": return "manager"; + case "ads_txt_and_adsense_account": + case "ads_txt_and_page_signature": return "seller"; + case "page_signature": return "page"; + default: return "unknown"; + } +} + +/** What the site's ads.txt says about one network (mockup column), worded to match the evidence. */ +export function saysLabel(view: AdNetworkCheckerSiteView, relationship: AdNetworkSiteRelationship) { + const evidence = evidenceOf(relationship); + if (evidence === "page" && relationship.status !== "ended") return "Not listed. Its page ad code shows it"; + if (evidence === "page" || evidence === "unknown") return NOT_LISTED; + if (relationship.status === "ended") return "No longer lists it"; + if (evidence === "seller") return "Allows it to sell ad space"; + return isManager(view, relationship) ? "Names it as the ad manager" : "Names it as an ad manager"; +} + +export function statusLabel(relationship: AdNetworkSiteRelationship) { + if (relationship.status === "confirmed") return "Confirmed live"; + if (relationship.status === "stale") return "Not seen lately"; + return relationship.ended_at ? `Ended ${formatMonth(relationship.ended_at)}` : "Ended"; +} + +/** Who to count: `it`, `both`, or `all 6`. */ +const countWord = (count: number) => (count === 1 ? "it" : count === 2 ? "both" : `all ${count}`); + +/** Each evidence group as a first sentence about the site, or a later one that adds to it. */ +const sentences: Record string> = { + manager: (list, domain, first) => `${first ? `${domain}'s ads.txt file names` : " Its ads.txt file also names"} ${names(list)} as ${list.length === 1 && first ? "its ad manager" : list.length === 1 ? "an ad manager" : "ad managers"}.`, + seller: (list, domain, first) => (first ? `${domain}'s ads.txt file lets ${names(list)} sell its ad space.` : ` It also lets ${names(list)} sell its ad space.`), + page: (list, domain, first) => (first ? `The ad code on ${domain}'s pages shows ${names(list)}.` : ` The ad code on its pages shows ${names(list)}.`), + unknown: (list, domain, first) => (first ? `We confirmed ${names(list)} on ${domain}.` : ` We also confirmed ${names(list)}.`), +}; + +/** + * The lead under the site's name, from the mockup: who runs its ads, who else sells its space, + * and when we confirmed them. Each network is worded by how we confirmed it, and each part + * appears only when the data behind it exists. + */ +export function leadFor(view: AdNetworkCheckerSiteView) { + const { domain, managed_by: manager, manager_domain: managerDomain } = view.site; + const rows = sortedRelationships(view); + const current = rows.filter((row) => row.status === "confirmed"); + const stale = rows.filter((row) => row.status === "stale"); + // "Runs" needs the MANAGERDOMAIN line: the site's own, or the relationship's stored method. + const lead = manager ? current.find((row) => row.slug === manager.slug && (managerDomain || evidenceOf(row) === "manager")) : undefined; + const parts: { manager?: AdNetworkRef; text: string }[] = []; + if (lead) parts.push({ manager: lead, text: `, and its ads.txt file names ${lead.name} as its ad manager.` }); + + for (const evidence of ["manager", "seller", "page", "unknown"] as const) { + const list = current.filter((row) => row !== lead && evidenceOf(row) === evidence); + if (list.length) parts.push({ text: sentences[evidence](list, domain, parts.length === 0) }); + } + if (!parts.length) parts.push({ text: `We haven't confirmed a network we track on ${domain} in our latest checks.` }); + + const dates = current.map((row) => row.last_confirmed_at).filter((value): value is string => Boolean(value)).sort(); + if (dates.length) { + const first = formatDay(dates[0]); + const last = formatDay(dates[dates.length - 1]); + const who = countWord(current.length); + parts.push({ text: first === last ? ` We confirmed ${who} live on ${last}.` : ` We last confirmed ${who} live between ${first} and ${last}.` }); + } + if (stale.length) parts.push({ text: ` ${names(stale)} ${stale.length === 1 ? "hasn't" : "haven't"} shown up in our latest checks.` }); + return parts; +} + +export type HistoryEvent = { date: string; text: string }; + +/** + * The site's network changes, newest first, from the events the API records (mockup events list). + * A move from one network to another comes only from the API's `switched` event. The first + * check reads as our starting point, since the networks on it may have been there long before. + */ +export function eventsFor(view: AdNetworkCheckerSiteView): HistoryEvent[] { + const events = [...(view.events ?? [])].sort((a, b) => + Number(a.kind === "first_check") - Number(b.kind === "first_check") || b.date.localeCompare(a.date)); + return events.map((event) => { + const date = formatMonth(event.date); + if (event.kind === "switched" && event.from_network && event.network) return { date, text: `Moved from ${event.from_network.name} to ${event.network.name}` }; + if (event.kind === "first_check") { + const seen = event.networks?.length ? event.networks : event.network ? [event.network] : []; + return { date, text: seen.length ? `Our first check, with ${names(seen)}` : "Our first check" }; + } + const name = event.network?.name ?? event.from_network?.name; + if (!name) return null; + return { date, text: event.kind === "dropped" ? `Dropped ${name}` : `Added ${name}` }; + }).filter((event): event is HistoryEvent => event !== null); +} + +/** Real changes, not only what our first check found. */ +export const hasChanges = (view: AdNetworkCheckerSiteView) => + (view.events ?? []).some((event) => event.kind !== "first_check") || view.relationships.some((item) => item.status === "ended"); + +/** The day of our first stored check on this site, when the history starts. */ +export function firstCheck(view: AdNetworkCheckerSiteView) { + return view.events?.find((event) => event.kind === "first_check")?.date ?? null; +} + +/** The networks the similar sites were matched on, the same rule as the site view loader. */ +export function similarNetworks(view: AdNetworkCheckerSiteView): AdNetworkRef[] { + const current = sortedRelationships(view).filter((item) => item.status === "confirmed"); + const manager = view.site.managed_by; + return manager && current.some((item) => item.slug === manager.slug) ? [manager] : current; +} + +export function adsTxtUrl(domain: string) { + return `https://${domain}/ads.txt`; +} diff --git a/app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css b/app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css new file mode 100644 index 00000000..b6a9503a --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/ad-network-checker.module.css @@ -0,0 +1,170 @@ +.form { + display: grid; + gap: var(--space-5); + min-width: 0; +} + +/* The result follows the mockup's site view: header, facts, then sections split by hairlines. */ +.result { + gap: 30px; +} + +/* Site header (mockup .page-header): title with the site in a highlight, meta line, lead. */ +.header { + display: grid; + gap: var(--space-5); + min-width: 0; +} + +.titleRow { + align-items: flex-start; + display: flex; + flex-wrap: wrap; + gap: var(--space-5) var(--space-7); + justify-content: space-between; + min-width: 0; +} + +.title { + color: var(--color-text); + font: var(--weight-bold) var(--text-h1) / var(--line-h1) var(--font-display); + isolation: isolate; + letter-spacing: var(--tracking-display); + margin: 0; + max-width: 820px; + min-width: 0; + overflow-wrap: anywhere; +} + +.title:focus { + outline: none; +} + +.title:focus-visible { + box-shadow: var(--focus-ring); +} + +.title mark { + -webkit-box-decoration-break: clone; + background: var(--color-accent); + border-radius: var(--radius); + box-decoration-break: clone; + color: var(--color-accent-contrast); + padding-inline: 0.16em; + position: relative; + z-index: -1; +} + +.meta { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font: var(--text-small) / 1.4 var(--font-mono); + gap: var(--space-2) var(--space-7); +} + +.meta span, +.meta a { + align-items: center; + display: inline-flex; + gap: var(--space-2); + white-space: nowrap; +} + +.meta a { + color: inherit; + text-decoration: underline; + text-decoration-color: var(--color-border); + text-underline-offset: 3px; +} + +.meta a:hover { + color: var(--color-text); + text-decoration-color: currentColor; +} + +.lead { + margin: 0; + max-width: var(--measure-prose); + overflow-wrap: anywhere; +} + +.lead a { + color: var(--color-link); + text-underline-offset: 3px; +} + +.lead a:hover { + text-decoration: underline; +} + +/* Sections (mockup .nsec.site-sec): every one opens with a hairline, including the first. */ +.sections { + display: grid; + min-width: 0; +} + +.sections > section:nth-child(n) { + border-top: 1px solid var(--color-border); + margin-top: 6px; + padding-top: 38px; +} + +.sections > section:nth-child(n + 2) { + margin-top: 44px; +} + +/* ads.txt: numbers and bars beside the file, split by a hairline (mockup .pair). */ +.block { + display: flex; + flex-direction: column; + gap: var(--space-5); + min-width: 0; +} + +.pair { + display: grid; + gap: 34px; +} + +@media (min-width: 900px) { + .pair { + gap: 0; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + + .pair > .block:first-child { + padding-right: 32px; + } + + .pair > .block + .block { + border-left: 1px solid var(--color-border); + padding-left: 32px; + } +} + +/* Check another site (mockup .check-form). */ +.another { + display: grid; + gap: var(--space-3); + grid-template-columns: minmax(0, 520px) max-content; + justify-content: start; +} + +.anotherButton { + height: 52px; + padding-inline: 22px; +} + +.error { + color: var(--color-danger-text); + font-size: var(--text-small); + margin: 0; +} + +@media (max-width: 640px) { + .another { + grid-template-columns: 1fr; + } +} diff --git a/app/(site)/(tools)/ad-network-checker/content/article.mdx b/app/(site)/(tools)/ad-network-checker/content/article.mdx new file mode 100644 index 00000000..93c3372f --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/content/article.mdx @@ -0,0 +1,41 @@ +## How do I find out which ad network a website uses? + +Open the site's ads.txt file by adding `/ads.txt` to its address, such as `example.com/ads.txt`. Look for a line that starts with `MANAGERDOMAIN`. If it's there, it names the company that manages the site's ads. + +Without that line, the file is a list of companies allowed to sell the site's ad space. Spotting the network in that list takes practice, because most lines belong to the network's partners. This checker does that reading for you and matches the lines to the networks we track. + +You can also look at the page's code for ad scripts, but scripts change often and some load only for certain visitors. The ads.txt file is public, stable, and written by the site owner, so we rely on it. + +## What does MANAGERDOMAIN mean in ads.txt? + +`MANAGERDOMAIN` names the primary or exclusive company that sells a site's ad space, as defined in the [IAB ads.txt 1.1 spec](https://iabtechlab.com/wp-content/uploads/2022/04/Ads.txt-1.1.pdf). Most of the network links we confirm come from this line. + +It doesn't prove the deal is exclusive. A site can name a manager and still let other networks sell its ad space, which is why the result can show one main ad network and still list others. + +`OWNERDOMAIN` is a different line. It names the business that owns the site, not an ad network. + +## Why does the ads.txt file list so many companies? + +Each line authorizes one ad account to sell the site's space. `DIRECT` means the site's own account with that company. `RESELLER` means a company that sells the space on someone else's behalf, usually the site's ad network. + +A network such as Mediavine works with dozens of ad exchanges, and each one needs its own line. That's why one network can add dozens of lines. The [IAB's ads.txt page](https://iabtechlab.com/ads-txt/) explains the format in full. + +## Why isn't a site in the results? + +We only show sites our checker has read. We find sites from public lists and each network's own seller list, then read their ads.txt on a schedule. A new or small site may not be in our list yet. + +A site also stays out when we can't confirm a network we track. That includes sites with no ads.txt file, sites that block our checker, and sites that use a network outside our list. The [ad networks directory](/ad-networks) lists every network we track. + +## How current is the result? + +The `Checked` date under the site name is our latest read of its ads.txt. We recheck sites on a schedule. + +The `Since` month is when we first confirmed a network. If that was our first check of the site, the network may have been there for years before. The changes section says when our checks of the site started. + +One missed check never removes a network. A timeout or a blocked request counts as unknown. A network moves to `Not seen lately`, then `Ended`, only after repeated checks find it missing. + +## Does stack127 show traffic or earnings? + +Not yet. Traffic and niche join the site facts as our data runs finish. We don't estimate earnings, because there's no public source we can check them against. + +If you're choosing a network for your own site, the [ad networks directory](/ad-networks) shows each network's sites and how to join. diff --git a/app/(site)/(tools)/ad-network-checker/content/intro.mdx b/app/(site)/(tools)/ad-network-checker/content/intro.mdx new file mode 100644 index 00000000..9f3590ef --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/content/intro.mdx @@ -0,0 +1 @@ +Enter a website to see which ad networks run its ads, whether one of them manages the site, and since when we've seen it. The answer comes from the site's own ads.txt file, checked against the networks we track. diff --git a/app/(site)/(tools)/ad-network-checker/content/usage.mdx b/app/(site)/(tools)/ad-network-checker/content/usage.mdx new file mode 100644 index 00000000..a2a2cf1f --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/content/usage.mdx @@ -0,0 +1,35 @@ +## How do I read the result? + +Enter a domain or any page address on the site, then press `Check site`. The sentence under the site's name gives the answer: who runs its ads, who else can sell its ad space, and when we last confirmed them live. + +Those words follow how we confirmed each network. A network is the site's `main ad network`, and the sentence says the site runs it, only when a `MANAGERDOMAIN` line in its ads.txt names it. For other networks in the file, the sentence says the file lets them sell its ad space, because a listing alone doesn't show which one is in charge. When we confirmed a network from the ad code on the site's pages instead, the sentence says that and doesn't claim the ads.txt file names it. + +The row of facts under it describes the site: main ad network, traffic, niche, platform, the year its domain was registered, and its publisher. Each fact shows only when we know it, so a site we know little about has a short row. The same goes for the sections below it. + +## What do the statuses in the networks table mean? + +The table lists every network we've linked to the site, including ones that stopped. `What its ads.txt says` shows whether the file names the network as an ad manager or allows it to sell ad space. It says `Not listed` when we confirmed the network from the site's page ad code, or when we don't know how we confirmed it. `Since` is the month we first confirmed it. + +| Status | What it means | +| --- | --- | +| Confirmed live | Our last check found the network on the site. | +| Not seen lately | Two checks at least 48 hours apart didn't find it. It may come back. | +| Ended, with a month | Three checks over seven days or more didn't find it. The month is when it ended. | + +## What do the other sections show? + +| Section | What it shows | +| --- | --- | +| How have its ad networks changed? | One bar per network, from the month it first appeared to the month it last did, and a list of each change, newest first. It says a site moved from one network to another only when one ad manager replaced another within seven days. The section shows once a site has a real change, such as a network that left. | +| What is in its ads.txt file? | How many lines, companies, direct lines, and reseller lines the file has, the companies with the most lines, and the start of the file. | +| Similar sites | Other sites on the same main ad network, or on the same networks when there's no `MANAGERDOMAIN` line. Click one to check it here. | + +A food blog with one ad network can still list dozens of companies in its ads.txt, because the network brings its own partners. The companies with the most lines are usually those partners, not separate networks. + + + +Its ads.txt has the line `MANAGERDOMAIN=mediavine.com`, so the sentence reads `collegefashion.net runs Mediavine, and its ads.txt file names Mediavine as its ad manager.` Mediavine is its main ad network and shows as `Confirmed live` in the networks table. + + + +Use `Copy link` to share the result. The link opens this page and checks the same site. diff --git a/app/(site)/(tools)/ad-network-checker/layout.tsx b/app/(site)/(tools)/ad-network-checker/layout.tsx new file mode 100644 index 00000000..ff25ef9d --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/layout.tsx @@ -0,0 +1,6 @@ +import { registryLayout } from "@/lib/registry-layout"; + +const registry = registryLayout("/ad-network-checker"); +export const generateMetadata = registry.generateMetadata; +export const revalidate = false; +export default registry.Layout; diff --git a/app/(site)/(tools)/ad-network-checker/page.tsx b/app/(site)/(tools)/ad-network-checker/page.tsx new file mode 100644 index 00000000..7ca4908a --- /dev/null +++ b/app/(site)/(tools)/ad-network-checker/page.tsx @@ -0,0 +1,51 @@ +import { readdirSync } from "node:fs"; +import path from "node:path"; +import { ScanSearch } from "lucide-react"; +import { PageCanvas } from "@/app/ui/site/page-canvas"; +import type { SectionNav } from "@/app/ui/site/section-bar"; +import { ToolPageArticle, ToolPageHeader } from "@/app/ui/site/tool-page-content"; +import { ToolPageShell } from "@/app/ui/tools/tool-page-shell"; +import { getRequiredToolPage } from "@/lib/tool-pages"; +import { AdNetworkCheckerFromUrl } from "./ad-network-checker-from-url"; +import Article from "./content/article.mdx"; +import Intro from "./content/intro.mdx"; +import Usage from "./content/usage.mdx"; + +const SLUG = "ad-network-checker"; + +/** Every network logo by slug, read once at build time so the browser-side result can show them. */ +function networkLogos() { + const folder = path.join(process.cwd(), "public", "ad-networks", "logos"); + return Object.fromEntries(readdirSync(folder) + .filter((file) => file.endsWith(".png")) + .map((file) => [file.slice(0, -4), `/ad-networks/logos/${file}`])); +} + +const sectionNav: SectionNav = { + label: "Ad networks", + href: "/ad-networks", + current: `/${SLUG}`, + items: [ + { label: "Directory", href: "/ad-networks" }, + { label: "Niches", href: "/ad-networks/niches" }, + { label: "Stats", href: "/ad-networks/stats" }, + { label: "Checker", href: `/${SLUG}` }, + ], +}; + +export default async function AdNetworkCheckerPage() { + const toolPage = await getRequiredToolPage(SLUG); + + return ( + + + } intro={} toolPage={toolPage} /> + + + +
          + + + + ); +} diff --git a/app/api/ad-networks/input.ts b/app/api/ad-networks/input.ts new file mode 100644 index 00000000..a51b09a5 --- /dev/null +++ b/app/api/ad-networks/input.ts @@ -0,0 +1,53 @@ +import { domainToASCII } from "node:url"; +import { isIP } from "node:net"; +import { getDomain } from "tldts"; +import { AD_NETWORK_SITES_PAGE_SIZE } from "../../../lib/ad-networks-report.ts"; +import { getAdNetworkNiche } from "../../../lib/ad-network-niches.ts"; +import type { AdNetworkNicheSlug } from "../../../lib/ad-network-niches.ts"; +import type { AdNetworkSitesQuery, AdNetworkSitesSort } from "../../../lib/ad-networks-types.ts"; + +export class AdNetworkInputError extends Error {} +const sorts = new Set(["domain", "traffic", "oldest", "newest", "checked"]); +const slug = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; +export function parseAdNetworkSitesParams(params: URLSearchParams): AdNetworkSitesQuery { + const allowed = new Set(["network", "niche", "q", "sort", "page", "pageSize"]); + for (const key of params.keys()) { + if (!allowed.has(key) || params.getAll(key).length > 1) throw new AdNetworkInputError(`Invalid parameter: ${key}`); + } + const network = params.get("network") || null; + const niche = params.get("niche") || null; + const q = (params.get("q") ?? "").trim().toLowerCase(); + const sort = params.get("sort") ?? "domain"; + if (network && (network.length > 100 || !slug.test(network))) throw new AdNetworkInputError("Invalid network."); + if (niche && !getAdNetworkNiche(niche)) throw new AdNetworkInputError("Invalid niche."); + if (q.length > 100 || /[\x00-\x1f\x7f]/.test(q)) throw new AdNetworkInputError("Search must be at most 100 characters."); + if (!sorts.has(sort)) throw new AdNetworkInputError("Invalid sort."); + function integer(key: string, fallback: number, max: number) { + const raw = params.get(key); + if (raw === null) return fallback; + if (!/^[1-9]\d*$/.test(raw) || Number(raw) > max) throw new AdNetworkInputError(`Invalid ${key}.`); + return Number(raw); + } + return { network, niche: niche as AdNetworkNicheSlug | null, q, sort: sort as AdNetworkSitesSort, + page: integer("page", 1, 10000), pageSize: integer("pageSize", AD_NETWORK_SITES_PAGE_SIZE, 100) }; +} +export function parseAdNetworkSiteParams(params: URLSearchParams): string { + if ([...params.keys()].some(key => key !== "domain") || params.getAll("domain").length !== 1) { + throw new AdNetworkInputError("Supply one domain."); + } + const input = params.get("domain")?.trim() ?? ""; + if (!input || input.length > 2048 || /[\s@*]/.test(input)) throw new AdNetworkInputError("Enter a public website domain."); + let url: URL; + try { url = new URL(/^https?:\/\//i.test(input) ? input : `https://${input}`); } + catch { throw new AdNetworkInputError("Enter a valid domain."); } + const domain = domainToASCII(url.hostname.toLowerCase().replace(/\.$/, "")).replace(/^www\./, ""); + const labels = domain.split("."); + if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.port || isIP(domain) + || domain.length > 253 || labels.length < 2 + || labels.some(label => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) + || !getDomain(domain, { allowPrivateDomains: true }) + || /\.(?:local|localhost|internal|invalid|test|example)$/.test(domain)) { + throw new AdNetworkInputError("Enter a public website domain."); + } + return domain; +} diff --git a/app/api/ad-networks/site/route.ts b/app/api/ad-networks/site/route.ts new file mode 100644 index 00000000..9b8da8ef --- /dev/null +++ b/app/api/ad-networks/site/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from "next/server"; +import { getAdNetworkCheckerSite } from "@/lib/ad-networks"; +import { getPublishedPage } from "@/lib/page-registry"; +import { AdNetworkInputError, parseAdNetworkSiteParams } from "../input"; +import type { AdNetworkSiteResponse } from "@/lib/ad-networks-types"; + +export const dynamic = "force-dynamic"; +/** Lookup only: unknown domains never trigger a network request or a database write. */ +export async function GET(request: Request) { + const noStore = { "Cache-Control": "no-store" }; + try { + const domain = parseAdNetworkSiteParams(new URL(request.url).searchParams); + if (!await getPublishedPage("ad-network-checker")) { + return NextResponse.json({ error: "Tool not found." }, { status: 404, headers: noStore }); + } + const site = await getAdNetworkCheckerSite(domain); + const result: AdNetworkSiteResponse = { domain, found: site !== null, site }; + return NextResponse.json(result, { headers: site ? { + "Cache-Control": "public, max-age=0, must-revalidate, s-maxage=604800", + "Cache-Tag": "stack127-public,ad-networks-catalog", "X-Robots-Tag": "noindex, follow", + } : noStore }); + } catch (error) { + if (error instanceof AdNetworkInputError) return NextResponse.json({ error: error.message }, { status: 400, headers: noStore }); + console.error("Ad-network checker failed", error); + return NextResponse.json({ error: "Could not load this site." }, { status: 503, headers: noStore }); + } +} diff --git a/app/api/ad-networks/sites/route.ts b/app/api/ad-networks/sites/route.ts new file mode 100644 index 00000000..86b8b1eb --- /dev/null +++ b/app/api/ad-networks/sites/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { getAdNetworkSites } from "@/lib/ad-networks"; +import { getPublishedPage } from "@/lib/page-registry"; +import { AdNetworkInputError, parseAdNetworkSitesParams } from "../input"; + +export const dynamic = "force-dynamic"; +export async function GET(request: Request) { + const noStore = { "Cache-Control": "no-store" }; + try { + const query = parseAdNetworkSitesParams(new URL(request.url).searchParams); + if (!await getPublishedPage("ad-networks") || (query.network && !await getPublishedPage(`ad-networks/${query.network}`)) + || (query.niche && !await getPublishedPage(`ad-networks/niches/${query.niche}`))) { + return NextResponse.json({ error: "Directory not found." }, { status: 404, headers: noStore }); + } + const result = await getAdNetworkSites(query); + return NextResponse.json(result, { headers: { + "Cache-Control": "public, max-age=0, must-revalidate, s-maxage=604800", + "Cache-Tag": "stack127-public,ad-networks-catalog", "X-Robots-Tag": "noindex, follow", + } }); + } catch (error) { + if (error instanceof AdNetworkInputError) return NextResponse.json({ error: error.message }, { status: 400, headers: noStore }); + console.error("Ad-network sites failed", error); + return NextResponse.json({ error: "Could not load sites." }, { status: 503, headers: noStore }); + } +} diff --git a/app/api/cache/refresh/route.ts b/app/api/cache/refresh/route.ts index 7a0517b4..2ee076a8 100644 --- a/app/api/cache/refresh/route.ts +++ b/app/api/cache/refresh/route.ts @@ -13,6 +13,7 @@ import { loadSelfHostAppsSearch } from "@/lib/self-host-apps-search-data"; import { parseSelfHostAppsSearchParams } from "@/lib/self-host-apps-search"; import { loadSelfHostedAppResourcePlannerEntry } from "@/lib/self-hosted-app-resource-planner/data"; import { getPool } from "@/lib/db"; +import { adNetworkPageKind, getAdNetworkApiVerification } from "@/lib/ad-networks-cache-verification"; import { sitePageAvailable } from "@/lib/page-source"; import { getFeeGroup } from "@/lib/platform-fees/groups"; import { LAUNCH_DIRECTORY_REVISION_SQL, launchDirectoryRevisionMatches } from "@/lib/launch-directories-data"; @@ -29,14 +30,17 @@ export async function POST(request: Request) { if (!Array.isArray(paths) || paths.length > 20 || paths.some(path => typeof path !== "string" || !/^\/[a-z0-9/_-]*(?:\.xml)?$/.test(path))) { return NextResponse.json({ error: "Invalid paths" }, { status: 400, headers: noStore }); } - const verified: Array<{ path: string; status: number; revision: string | null; digest: string | null; sourceRevision: string | null }> = []; + const verified: Array<{ path: string; status: number; revision: string | null; digest: string | null; sourceRevision: string | null; requestPath?: string }> = []; for (let i = 0; i < paths.length; i += 4) { await Promise.all(paths.slice(i, i + 4).map(async (path: string) => { + const adNetworkVerification = path === "/api/ad-networks/sites" || path === "/api/ad-networks/site" + ? await getAdNetworkApiVerification(path) : null; + const requestPath=adNetworkVerification?.requestPath??path; const hosts = [...new Set(["127.0.0.1", process.env.HOSTNAME].filter(Boolean))]; let response: Response | undefined; for (const host of hosts) { try { - response = await fetch(`http://${host}:${process.env.PORT || "3333"}${path}`, { + response = await fetch(`http://${host}:${process.env.PORT || "3333"}${requestPath}`, { cache: "no-store", redirect: "manual", signal: AbortSignal.timeout(15_000), }); break; @@ -47,23 +51,26 @@ export async function POST(request: Request) { let contentRevision: string | null = null; let sourceRevision: string | null = null; if (path.startsWith("/api/")) { - const slug = path.split("/").at(-1)!; - const hardwareDirectory = path.startsWith("/api/hardware-directory/") ? slug : null; - const parent = hardwareDirectory ?? (path === "/api/cli-apps/index" ? "cli-apps" - : path.startsWith("/api/self-host-apps/") ? "self-host-apps" : "self-hosted-app-resource-planner"); - const visible = (await getPool().query("select is_published from public.stack127_pages where slug=$1", [parent])).rows[0]?.is_published; let expected: unknown; - if (!visible) expected = null; - else if (hardwareDirectory === "zigbee-devices") { - expected = (await addAmazonMatches(hardwareDirectory, getZigbeeDevices())) - .map(device => zigbeeIndexRecord(device, zigbeeCategoryLabels)); - } else if (hardwareDirectory && isHardwareDirectorySlug(hardwareDirectory)) { - expected = (await getHardwareDirectorySnapshot(hardwareDirectory)).records.map(hardwareIndexRecord); - } else if (path === "/api/cli-apps/index") expected = await loadTerminalApps(); - else if (path === "/api/self-host-apps/search") { - expected = await loadSelfHostAppsSearch(parseSelfHostAppsSearchParams(new URLSearchParams())); - } else if (path.startsWith("/api/self-host-apps/")) expected = (await loadDirectoryAppsBySlugs([slug]))[0] ?? null; - else if (path.startsWith("/api/resource-planner-app/")) expected = await loadSelfHostedAppResourcePlannerEntry(slug); + if (adNetworkVerification) expected=adNetworkVerification.expected; + else { + const slug = path.split("/").at(-1)!; + const hardwareDirectory = path.startsWith("/api/hardware-directory/") ? slug : null; + const parent = hardwareDirectory ?? (path === "/api/cli-apps/index" ? "cli-apps" + : path.startsWith("/api/self-host-apps/") ? "self-host-apps" : "self-hosted-app-resource-planner"); + const visible = (await getPool().query("select is_published from public.stack127_pages where slug=$1", [parent])).rows[0]?.is_published; + if (!visible) expected = null; + else if (hardwareDirectory === "zigbee-devices") { + expected = (await addAmazonMatches(hardwareDirectory, getZigbeeDevices())) + .map(device => zigbeeIndexRecord(device, zigbeeCategoryLabels)); + } else if (hardwareDirectory && isHardwareDirectorySlug(hardwareDirectory)) { + expected = (await getHardwareDirectorySnapshot(hardwareDirectory)).records.map(hardwareIndexRecord); + } else if (path === "/api/cli-apps/index") expected = await loadTerminalApps(); + else if (path === "/api/self-host-apps/search") { + expected = await loadSelfHostAppsSearch(parseSelfHostAppsSearchParams(new URLSearchParams())); + } else if (path.startsWith("/api/self-host-apps/")) expected = (await loadDirectoryAppsBySlugs([slug]))[0] ?? null; + else if (path.startsWith("/api/resource-planner-app/")) expected = await loadSelfHostedAppResourcePlannerEntry(slug); + } if (expected === undefined) throw new Error("Unsupported API refresh"); if (!expected) { if (response.status !== 404) throw new Error(`${path}: removed data is still served`); @@ -90,8 +97,11 @@ export async function POST(request: Request) { ); let published = sitePageAvailable(path, rows[0]); if (published && path.startsWith("/ad-networks/")) { - const source = await getPool().query("select 1 from public.ad_networks where slug=$1", [path.slice("/ad-networks/".length)]); - published = source.rows.length > 0; + const kind=adNetworkPageKind(path); + if (kind === "network") { + const source = await getPool().query("select 1 from public.ad_networks where slug=$1", [path.slice("/ad-networks/".length)]); + published = source.rows.length > 0; + } else if (kind === "missing") published=false; } else if (published && path.startsWith("/platform-fees/")) { const group = getFeeGroup(path.slice("/platform-fees/".length)); if (!group) published = false; @@ -121,7 +131,7 @@ export async function POST(request: Request) { } } } - verified.push({ path, status: response.status, revision: contentRevision, digest: path.startsWith("/api/") && response.status === 200 ? await cacheBodyDigest(html) : null, sourceRevision }); + verified.push({ path, ...(requestPath!==path ? {requestPath} : {}), status: response.status, revision: contentRevision, digest: path.startsWith("/api/") && response.status === 200 ? await cacheBodyDigest(html) : null, sourceRevision }); })); } return NextResponse.json({ verified }, { headers: noStore }); diff --git a/app/ui/ad-networks/confirmed-ring.tsx b/app/ui/ad-networks/confirmed-ring.tsx new file mode 100644 index 00000000..466bd49f --- /dev/null +++ b/app/ui/ad-networks/confirmed-ring.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { formatShare } from "./viz/format"; +import styles from "./network-card.module.css"; + +const radius = 15; +const circumference = 2 * Math.PI * radius; + +/** + * A small ring filled by the share of a network's sites we confirmed live, with a bubble saying it + * in words. Hover or keyboard focus shows the bubble; a tap toggles it on touch screens, and a tap + * elsewhere or Escape closes it. + */ +export function ConfirmedRing({ share }: { share: number }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + const percent = formatShare(share); + + useEffect(() => { + if (!open) return; + const away = (event: PointerEvent) => { if (!ref.current?.contains(event.target as Node)) setOpen(false); }; + const escape = (event: KeyboardEvent) => { if (event.key === "Escape") setOpen(false); }; + document.addEventListener("pointerdown", away); + document.addEventListener("keydown", escape); + return () => { + document.removeEventListener("pointerdown", away); + document.removeEventListener("keydown", escape); + }; + }, [open]); + + return ( + + ); +} diff --git a/app/ui/ad-networks/details.module.css b/app/ui/ad-networks/details.module.css new file mode 100644 index 00000000..1494e48b --- /dev/null +++ b/app/ui/ad-networks/details.module.css @@ -0,0 +1,172 @@ +.details { + display: grid; + gap: var(--space-8); + min-width: 0; +} + +.body { + display: grid; + gap: var(--space-13); +} + +.main { + align-content: start; + display: grid; + gap: var(--space-8); + max-width: 980px; + min-width: 0; +} + +.main > h2 { + font: var(--weight-bold) var(--text-h2) / var(--line-heading) var(--font-display); + letter-spacing: var(--tracking-display); + margin: 0; +} + +.prose { + align-content: start; + display: grid; + gap: var(--space-8); + max-width: var(--measure-prose); +} + +.prose section { + display: grid; + gap: var(--space-3); +} + +.prose h3 { + font: var(--weight-semibold) 1.12rem / 1.3 var(--font-display); + letter-spacing: -0.01em; + margin: 0; +} + +.prose p { + color: var(--color-text); + line-height: var(--line-body); + margin: 0; +} + +.prose a { + color: var(--color-link); + text-underline-offset: 3px; +} + +@media (min-width: 1200px) { + .body { + gap: 56px; + grid-template-columns: minmax(300px, 1fr) minmax(0, 1.45fr); + } + + .prose { + border-right: 1px solid var(--color-border); + order: -1; + padding-right: var(--space-13); + } +} + +/* Key numbers */ +.keyNumbers { + border: 1px solid var(--color-border); + border-radius: var(--radius); + display: grid; + grid-template-columns: repeat(var(--count), minmax(0, 1fr)); + margin: 0; +} + +.keyNumbers > div { + border-left: 1px solid var(--color-border); + display: flex; + flex-direction: column-reverse; + gap: var(--space-2); + justify-content: flex-end; + min-width: 0; + padding: var(--space-7) var(--space-8); +} + +.keyNumbers > div:first-child { + border-left: 0; +} + +.keyNumbers dt { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); +} + +.keyNumbers dt small { + display: block; + font-size: 0.72rem; + line-height: 1.35; + margin-top: 2px; + opacity: 0.85; +} + +.keyNumbers dd { + font: var(--weight-semibold) 1.6rem / 1.1 var(--font-mono); + letter-spacing: -0.02em; + margin: 0; +} + +/* Facts */ +.facts { + margin: 0; +} + +.facts > div { + border-top: 1px solid var(--color-border); + display: grid; + gap: var(--space-2) var(--space-8); + grid-template-columns: minmax(150px, 30%) 1fr; + padding: var(--space-5) 0; +} + +.facts > div:first-child { + border-top: 0; +} + +.facts dt { + color: var(--color-text-muted); +} + +.facts dd { + margin: 0; +} + +.facts dd small { + color: var(--color-text-muted); + display: block; + font-size: var(--text-small); + margin-top: 2px; +} + +.notPublic { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); +} + +.sources { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); + margin: 0; +} + +.sources a { + color: inherit; + text-decoration: underline; + text-decoration-color: var(--color-border-strong); + text-underline-offset: 3px; +} + +@media (max-width: 700px) { + .keyNumbers > div { + padding: var(--space-6) var(--space-5); + } + + .keyNumbers dd { + font-size: 1.25rem; + } + + .facts > div { + grid-template-columns: 1fr; + } +} diff --git a/app/ui/ad-networks/details.tsx b/app/ui/ad-networks/details.tsx new file mode 100644 index 00000000..8fa89826 --- /dev/null +++ b/app/ui/ad-networks/details.tsx @@ -0,0 +1,87 @@ +import type { CSSProperties, ReactNode } from "react"; +import styles from "./details.module.css"; + +export type KeyNumber = { + label: string; + value: ReactNode; + /** A short caveat under the label, such as `in older public terms`. */ + note?: ReactNode; +}; + +/** Up to three headline facts in one bordered row, value above label (mockup `dl.key-nums`). */ +export function KeyNumbers({ items }: { items: readonly KeyNumber[] }) { + return ( +
          + {items.map((item) => ( +
          +
          {item.label}{item.note ? {item.note} : null}
          +
          {item.value}
          +
          + ))} +
          + ); +} + +export type Fact = { + label: string; + value: ReactNode; + /** A small muted line under the value, such as `Measured in Google Analytics`. */ + note?: ReactNode; +}; + +/** Label and value rows split by hairlines, with an optional small note (mockup `dl.facts`). */ +export function FactsList({ items }: { items: readonly Fact[] }) { + return ( +
          + {items.map((item) => ( +
          +
          {item.label}
          +
          {item.value}{item.note ? {item.note} : null}
          +
          + ))} +
          + ); +} + +/** For a fact the network doesn't publish. Research says so; it isn't missing data. */ +export function NotPublic() { + return Not public; +} + +/** + * The network details area (mockup `.details-body`): the facts column with its h2, key numbers, + * tabs, and sources, and the guide prose beside it. On wide screens the prose sits on the left + * behind a hairline; below 1200px it follows the facts. + */ +export function DetailsLayout({ children, prose, title, titleId }: { + /** Key numbers, tabs, and the sources line. */ + children: ReactNode; + /** The Q&A prose, as h3 sections. */ + prose?: ReactNode; + title: ReactNode; + titleId: string; +}) { + return ( +
          +
          +
          +

          {title}

          + {children} +
          + {prose ?
          {prose}
          : null} +
          +
          + ); +} + +/** Where the facts come from and when we checked (mockup `.src`). */ +export function SourcesLine({ checked, sources }: { checked?: string; sources: readonly { label: string; href: string }[] }) { + return ( +

          + Sources: {sources.map((source, index) => ( + {index ? ", " : ""}{source.label} + ))} + {checked ? ` · checked ${checked}` : null} +

          + ); +} diff --git a/app/ui/ad-networks/network-card.module.css b/app/ui/ad-networks/network-card.module.css new file mode 100644 index 00000000..0b226459 --- /dev/null +++ b/app/ui/ad-networks/network-card.module.css @@ -0,0 +1,170 @@ +.grid { + display: grid; + gap: var(--space-5); + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +@media (max-width: 1300px) { + .grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 1000px) { + .grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 560px) { + .grid { + grid-template-columns: 1fr; + } +} + +.card { + align-content: start; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + display: grid; + gap: var(--space-7); + min-width: 0; + padding: var(--space-8); + position: relative; +} + +.card:hover, +.card:focus-within { + border-color: var(--color-accent-text); +} + +.top { + align-items: center; + display: flex; + gap: var(--space-4); + min-width: 0; +} + +.top h3 { + font: var(--weight-bold) 1.06rem / 1.2 var(--font-display); + letter-spacing: -0.01em; + margin: 0; + min-width: 0; + overflow-wrap: anywhere; +} + +.top h3 a { + color: var(--color-text); +} + +/* The name's link covers the whole card. */ +.top h3 a::after { + content: ""; + inset: 0; + position: absolute; +} + +.top h3 a:focus-visible { + box-shadow: none; + outline: 0; +} + +.number { + font: var(--weight-bold) 2.1rem / 1 var(--font-display); + letter-spacing: var(--tracking-display); +} + +.unit { + color: var(--color-text-muted); + font-size: var(--text-small); + margin-top: var(--space-1); +} + +.unit b { + color: var(--color-accent-text); + font: var(--weight-semibold) var(--text-small) var(--font-mono); +} + +.join { + border-top: 1px solid var(--color-border); + display: grid; + gap: 2px; + padding-top: var(--space-5); +} + +.joinLabel { + color: var(--color-text-muted); + font: 0.78rem / 1.3 var(--font-mono); +} + +.joinValue { + font-size: 0.94rem; + font-weight: var(--weight-semibold); + line-height: 1.35; +} + +/* Confirmed ring with its info bubble */ +.ring { + background: none; + border: 0; + border-radius: 50%; + cursor: help; + display: grid; + flex: none; + margin-left: auto; + padding: 2px; + position: relative; + z-index: 1; +} + +.ringTrack { + stroke: var(--color-surface-2); +} + +.ringDone { + stroke: var(--color-accent-text); +} + +.bubble { + background: var(--color-surface); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius); + bottom: calc(100% + 8px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.36); + color: var(--color-text); + font: var(--text-small) / 1.45 var(--font-sans); + opacity: 0; + padding: 8px 10px; + pointer-events: none; + position: absolute; + right: -6px; + text-align: left; + transform: translateY(4px); + transition: opacity 0.12s, transform 0.12s; + width: 230px; +} + +.ring:hover .bubble, +.ring:focus-visible .bubble, +.ring[data-open] .bubble { + opacity: 1; + transform: none; +} + +.bubble b { + color: var(--color-accent-text); + font-family: var(--font-mono); + font-weight: var(--weight-semibold); +} + +.sortNote { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); + margin: calc(var(--space-8) * -1 + var(--space-3)) 0 var(--space-8); +} + +.sortNote b { + color: var(--color-text); + font-weight: var(--weight-semibold); +} diff --git a/app/ui/ad-networks/network-card.tsx b/app/ui/ad-networks/network-card.tsx new file mode 100644 index 00000000..7a2f1224 --- /dev/null +++ b/app/ui/ad-networks/network-card.tsx @@ -0,0 +1,55 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; +import { ConfirmedRing } from "./confirmed-ring"; +import { NetworkLogo } from "./network-logo"; +import type { NetworkLogoFile } from "./network-logo-src"; +import { formatCount } from "./site-format"; +import styles from "./network-card.module.css"; + +export type NetworkCardProps = { + name: string; + href: string; + logo: NetworkLogoFile | null; + /** Sites we list for the network. */ + sites: number; + /** Of those, sites confirmed live from their own ads.txt files or ad code. */ + confirmed: number; + /** The joining rule in words, such as `50,000 sessions a month`. Leave out to hide the footer. */ + join?: ReactNode; +}; + +/** + * One ad network in the directory grid (mockup `.net-card`): logo tile, name, the confirmed-share + * ring, the site count, and what it takes to join. The whole card links to the network's page. + */ +export function NetworkCard({ confirmed, href, join, logo, name, sites }: NetworkCardProps) { + return ( +
          +
          + +

          {name}

          + {sites > 0 ? : null} +
          +
          +
          {formatCount(sites)}
          +
          {sites === 1 ? "site" : "sites"}, {formatCount(confirmed)} confirmed
          +
          + {join ? ( +
          + To join + {join} +
          + ) : null} +
          + ); +} + +/** Network cards four across on wide screens, down to one on phones (mockup `.net-grid`). */ +export function NetworkGrid({ children }: { children: ReactNode }) { + return
          {children}
          ; +} + +/** One mono line under the section heading saying how the cards are sorted (mockup `.sort-note`). */ +export function SortNote({ children }: { children: ReactNode }) { + return

          {children}

          ; +} diff --git a/app/ui/ad-networks/network-ladder.module.css b/app/ui/ad-networks/network-ladder.module.css new file mode 100644 index 00000000..776423a6 --- /dev/null +++ b/app/ui/ad-networks/network-ladder.module.css @@ -0,0 +1,79 @@ +.table { + border-collapse: collapse; + font-size: 0.97rem; + width: 100%; +} + +.table thead th { + border-bottom: 1px solid var(--color-border-strong); + color: var(--color-text-muted); + font: 500 0.8rem var(--font-mono); + padding: 0 var(--space-5) var(--space-4); + text-align: left; + white-space: nowrap; +} + +.table tbody th, +.table tbody td { + border-top: 1px solid var(--color-border); + font-weight: var(--weight-regular); + padding: var(--space-5); + text-align: left; + white-space: nowrap; +} + +.table tbody tr:first-child > * { + border-top: 0; +} + +.table tbody tr:hover > * { + background: var(--color-surface); +} + +.table .right { + text-align: right; +} + +.mono { + font: 0.86rem var(--font-mono); +} + +.name, +.name a { + color: var(--color-text); + font-weight: var(--weight-semibold) !important; +} + +.name a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.share { + align-items: center; + display: flex; + gap: var(--space-4); + min-width: 160px; +} + +.share > span:first-child { + max-width: 200px; +} + +@media (max-width: 640px) { + .table th:nth-child(3), + .table td:nth-child(3) { + display: none; + } + + .table tbody th, + .table tbody td, + .table thead th { + padding-left: 6px; + padding-right: 6px; + } + + .table td.rule { + white-space: normal; + } +} diff --git a/app/ui/ad-networks/network-ladder.tsx b/app/ui/ad-networks/network-ladder.tsx new file mode 100644 index 00000000..73091c19 --- /dev/null +++ b/app/ui/ad-networks/network-ladder.tsx @@ -0,0 +1,86 @@ +"use client"; + +import Link from "next/link"; +import { useState, type ReactNode } from "react"; +import { ToolbarSwitch } from "@/app/ui/table-toolbar"; +import { NotPublic } from "./details"; +import { AdSection } from "./section"; +import { formatCount } from "./site-format"; +import styles from "./niches-table.module.css"; +import ladder from "./network-ladder.module.css"; + +export type LadderRow = { + name: string; + href?: string; + /** Sites in this niche that run the network. */ + sites: number; + /** The joining rule in words, or null when the network doesn't publish one. */ + rule: string | null; + /** Orders "Easiest to join": lower is easier, such as the traffic minimum. Null sorts last. */ + ruleRank: number | null; +}; + +/** + * Every network that runs on a niche's sites, with its count, its share of the niche, and what it + * takes to join (mockup food page ladder). A switch orders by most used or easiest to join; it + * hides when no network has a rule. On phones the share column hides. + */ +export function NetworkLadder({ countLabel, description, id, rows, shareLabel, title, total }: { + /** Header of the count column, such as `food sites`. */ + countLabel: string; + description?: ReactNode; + id: string; + rows: readonly LadderRow[]; + /** Header of the share column, such as `share of food sites`. */ + shareLabel: string; + title: ReactNode; + /** Sites in the niche, for the share. */ + total: number; +}) { + const [order, setOrder] = useState("used"); + const hasRules = rows.some((row) => row.rule); + const canOrder = rows.some((row) => row.ruleRank !== null); + const top = Math.max(1, ...rows.map((row) => row.sites)); + const list = [...rows].sort((a, b) => order === "join" + ? (a.ruleRank ?? Number.MAX_SAFE_INTEGER) - (b.ruleRank ?? Number.MAX_SAFE_INTEGER) || b.sites - a.sites + : b.sites - a.sites); + return ( + : undefined} + description={description} + id={id} + title={title} + > +
          + + + + + + + {hasRules ? : null} + + + + {list.map((row) => { + const share = total ? (row.sites / total) * 100 : 0; + return ( + + + + + {hasRules ? : null} + + ); + })} + +
          network{countLabel}{shareLabel}to join
          {row.href ? {row.name} : row.name}{formatCount(row.sites)} + + + {share > 0 && share < 1 ? "<1" : share.toFixed(0)}% + + {row.rule ?? }
          +
          +
          + ); +} diff --git a/app/ui/ad-networks/network-logo-src.ts b/app/ui/ad-networks/network-logo-src.ts new file mode 100644 index 00000000..f405fd9d --- /dev/null +++ b/app/ui/ad-networks/network-logo-src.ts @@ -0,0 +1,16 @@ +import { existsSync } from "node:fs"; +import path from "node:path"; + +export type NetworkLogoFile = { src: string }; + +/** + * The network's square icon in `public/ad-networks/logos/.png`, checked on the server at + * build time. Null when there is none yet, so NetworkLogo shows the name's first letter. + */ +export function networkLogo(slug: string): NetworkLogoFile | null { + const file = path.join(process.cwd(), "public", "ad-networks", "logos", `${slug}.png`); + return existsSync(file) ? { src: `/ad-networks/logos/${slug}.png` } : null; +} + +/** Just the logo's path. */ +export const networkLogoSrc = (slug: string) => networkLogo(slug)?.src ?? null; diff --git a/app/ui/ad-networks/network-logo.module.css b/app/ui/ad-networks/network-logo.module.css new file mode 100644 index 00000000..8e6dcda4 --- /dev/null +++ b/app/ui/ad-networks/network-logo.module.css @@ -0,0 +1,51 @@ +/* A white tile so any icon reads in both themes. */ +.tile { + background: #fff; + border: 1px solid var(--color-border); + border-radius: var(--radius); + box-sizing: border-box; + color: #141414; + display: inline-grid; + flex: none; + font-family: var(--font-display); + font-weight: var(--weight-bold); + line-height: 1; + place-items: center; +} + +.tile img { + display: block; + object-fit: contain; +} + +.tile[data-size="card"] { + font-size: 15px; + height: 32px; + width: 32px; +} + +.tile[data-size="card"] img { + height: 20px; + width: 20px; +} + +.tile[data-size="title"] { + height: 0.9em; + margin-right: 0.28em; + vertical-align: -0.1em; + width: 0.9em; +} + +.tile[data-size="title"] img { + height: 66%; + width: 66%; +} + +.tile[data-letter]::before { + content: attr(data-letter); +} + +.tile[data-size="title"][data-letter]::before { + font-size: 0.6em; + letter-spacing: 0; +} diff --git a/app/ui/ad-networks/network-logo.tsx b/app/ui/ad-networks/network-logo.tsx new file mode 100644 index 00000000..f39508a8 --- /dev/null +++ b/app/ui/ad-networks/network-logo.tsx @@ -0,0 +1,22 @@ +import type { NetworkLogoFile } from "./network-logo-src"; +import styles from "./network-logo.module.css"; + +/** + * A network's square icon on a white tile, so any logo reads in both themes. `card` is the 32px + * tile used in cards and table rows. `title` sizes it to sit in an h1 in place of the title icon. + * Decorative: the network's name is always next to it. Pass `logo` from `networkLogo(slug)`; + * without one the tile shows the name's first letter. + */ +export function NetworkLogo({ logo, name, size = "card" }: { logo: NetworkLogoFile | null; name: string; size?: "card" | "title" }) { + return ( + // The letter is drawn by CSS, so it never becomes part of a heading's text. + + ); +} diff --git a/app/ui/ad-networks/niches-table.module.css b/app/ui/ad-networks/niches-table.module.css new file mode 100644 index 00000000..e005974b --- /dev/null +++ b/app/ui/ad-networks/niches-table.module.css @@ -0,0 +1,196 @@ +.scroll { + min-width: 0; + overflow-x: auto; +} + +.table { + border-collapse: collapse; + font-size: 0.97rem; + width: 100%; +} + +.table thead th { + border-bottom: 1px solid var(--color-border-strong); + color: var(--color-text-muted); + font: 500 0.8rem var(--font-mono); + padding: 0 var(--space-5) var(--space-4); + text-align: left; + text-transform: lowercase; + white-space: nowrap; +} + +.table tbody th, +.table tbody td { + border-top: 1px solid var(--color-border); + font-weight: var(--weight-regular); + padding: var(--space-6) var(--space-5); + text-align: left; + vertical-align: middle; + white-space: nowrap; +} + +.table tbody tr:first-child > * { + border-top: 0; +} + +.table tbody tr:hover > * { + background: var(--color-surface); +} + +.table .right { + text-align: right; +} + +.mono { + font: 0.86rem var(--font-mono); +} + +.muted { + color: var(--color-text-muted); +} + +.niche, +.niche a { + color: var(--color-text); + font-weight: var(--weight-semibold) !important; +} + +.niche a:hover, +.tops a:hover, +.easy a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.shareWrap { + align-items: center; + display: flex; + gap: var(--space-4); + min-width: 150px; +} + +.minibar { + background: var(--color-surface-2); + border-radius: 1px; + flex: 1; + height: 8px; + max-width: 120px; + overflow: hidden; +} + +.minibar i { + background: var(--color-accent-text); + display: block; + height: 100%; +} + +.pct { + color: var(--color-text-muted); + font: 0.86rem var(--font-mono); +} + +.tops ul { + display: flex; + gap: var(--space-2) var(--space-7); + list-style: none; + margin: 0; + padding: 0; +} + +.tops a, +.easy a { + color: var(--color-text); +} + +.tops span { + color: var(--color-text-muted); + font: 0.82rem var(--font-mono); +} + +.easy span { + display: block; + font-size: var(--text-small); + margin-top: 2px; +} + +.empty { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); + margin: 0; +} + +/* Phones: each row becomes a small block, no sideways scroll. */ +@media (max-width: 760px) { + .table thead { + display: none; + } + + .table, + .table tbody { + display: block; + } + + .table tbody tr { + border-top: 1px solid var(--color-border); + display: grid; + gap: 6px 12px; + grid-template-columns: 1fr auto; + padding: var(--space-6) 0; + } + + .table tbody tr:first-child { + border-top: 0; + } + + .table tbody th, + .table tbody td { + border: 0; + padding: 0; + white-space: normal; + } + + .table tbody tr:hover > * { + background: none; + } + + .niche { + grid-column: 1; + } + + .count { + grid-column: 2; + grid-row: 1; + } + + .shareCell, + .tops, + .easy { + grid-column: 1 / -1; + } + + .shareWrap { + min-width: 0; + } + + .tops ul { + flex-wrap: wrap; + } + + .easy { + color: var(--color-text-muted); + font-size: var(--text-small); + } + + .easy:not(:empty)::before { + content: "Easiest to join: "; + } + + .easy span { + display: inline; + margin-left: 4px; + } + + .easy span::before { + content: "· "; + } +} diff --git a/app/ui/ad-networks/niches-table.tsx b/app/ui/ad-networks/niches-table.tsx new file mode 100644 index 00000000..8eb844b9 --- /dev/null +++ b/app/ui/ad-networks/niches-table.tsx @@ -0,0 +1,127 @@ +"use client"; + +import Link from "next/link"; +import { useMemo, useState, type ReactNode } from "react"; +import { TableToolbar, ToolbarSearch, ToolbarSelect } from "@/app/ui/table-toolbar"; +import { AdSection } from "./section"; +import { formatCount } from "./site-format"; +import styles from "./niches-table.module.css"; + +export type NicheTableNetwork = { name: string; href?: string; /** 0 to 1 of the niche's sites. */ share: number }; + +export type NicheTableRow = { + slug: string; + name: string; + href?: string; + sites: number; + /** Confirmed live sites. Leave out to show only the count. */ + live?: number; + /** 0 to 1 of all sorted sites. */ + share: number; + /** The networks used most, largest first. The table shows three. */ + top: readonly NicheTableNetwork[]; + /** The easiest of the top networks to join, with its rule in words. */ + easiest?: { name: string; href?: string; rule: string }; + /** Share of the niche's sites per network slug, 0 to 1, for the network filter. */ + networkShares?: Readonly>; +}; + +const percent = (share: number, digits = 0) => `${(share * 100).toFixed(digits)}%`; + +function ShareCell({ max, share, digits }: { max: number; share: number; digits?: number }) { + return ( + + + + {percent(share, digits)} + + + ); +} + +/** + * Every niche with its site count, share of all sites, top three networks, and the easiest of + * those to join (mockup niches table). Search narrows by name. The network filter swaps the share + * column for that network's share in each niche and sorts by it. Columns with no data hide. + */ +export function NichesTable({ description, id, networks = [], rows, title }: { + description: ReactNode; + id: string; + /** Networks for the filter. Leave empty to hide it. */ + networks?: readonly { value: string; label: string }[]; + rows: readonly NicheTableRow[]; + title: ReactNode; +}) { + const [query, setQuery] = useState(""); + const [network, setNetwork] = useState(""); + const [sort, setSort] = useState("sites"); + const networkLabel = networks.find((item) => item.value === network)?.label ?? ""; + const hasTop = rows.some((row) => row.top.length); + const hasEasiest = rows.some((row) => row.easiest); + const filterable = networks.length > 0 && rows.some((row) => row.networkShares); + + const list = useMemo(() => { + const term = query.trim().toLowerCase(); + const shown = rows.filter((row) => row.name.toLowerCase().includes(term)); + const shareOf = (row: NicheTableRow) => row.networkShares?.[network] ?? 0; + return [...shown].sort((a, b) => network ? shareOf(b) - shareOf(a) : sort === "az" ? a.name.localeCompare(b.name) : b.sites - a.sites); + }, [network, query, rows, sort]); + const maxShare = Math.max(0, ...rows.map((row) => row.share)); + const maxNetwork = Math.max(0, ...rows.map((row) => row.networkShares?.[network] ?? 0)); + + return ( + + + {filterable ? : null} + + + )} + description={{network ? `Niches sorted by how many of their sites use ${networkLabel}.` : description}} + id={id} + title={title} + > + {list.length ? ( +
          + + + + + + + {hasTop ? : null} + {hasEasiest ? : null} + + + + {list.map((row) => ( + + + + {network + ? + : } + {hasTop ? ( + + ) : null} + {hasEasiest ? ( + + ) : null} + + ))} + +
          nichesites{network ? `${networkLabel} share` : "share of all sites"}top networkseasiest of these to join
          {row.href ? {row.name} : row.name}{formatCount(row.sites)}{row.live !== undefined ? · {formatCount(row.live)} live : null} +
            + {row.top.slice(0, 3).map((item) => ( +
          • {item.href ? {item.name} : item.name} {percent(item.share)}
          • + ))} +
          +
          + {row.easiest ? <>{row.easiest.href ? {row.easiest.name} : row.easiest.name}{row.easiest.rule} : null} +
          +
          + ) :

          No niches match. Try another word.

          } +
          + ); +} diff --git a/app/ui/ad-networks/section.module.css b/app/ui/ad-networks/section.module.css new file mode 100644 index 00000000..9dfdf90f --- /dev/null +++ b/app/ui/ad-networks/section.module.css @@ -0,0 +1,50 @@ +.head { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: var(--space-3) var(--space-7); + justify-content: space-between; + margin-bottom: var(--space-8); +} + +.head h2, +.title { + font: var(--weight-bold) var(--text-h2) / var(--line-heading) var(--font-display); + letter-spacing: var(--tracking-display); + margin: 0; +} + +.section { + display: grid; + gap: var(--space-6); + min-width: 0; + scroll-margin-top: calc(var(--top-bar-height) + var(--space-5)); +} + +.section + .section { + border-top: 1px solid var(--color-border); + margin-top: var(--space-14); + padding-top: var(--space-13); +} + +.section .head { + margin-bottom: 0; +} + +.description { + color: var(--color-text-muted); + font-size: var(--text-small); + margin: -4px 0 var(--space-3); +} + +.note { + color: var(--color-text-muted); + font-size: var(--text-small); + margin: 0; +} + +.note a, +.description a { + color: var(--color-link); + text-underline-offset: 3px; +} diff --git a/app/ui/ad-networks/section.tsx b/app/ui/ad-networks/section.tsx new file mode 100644 index 00000000..0f09f32e --- /dev/null +++ b/app/ui/ad-networks/section.tsx @@ -0,0 +1,41 @@ +import type { ReactNode } from "react"; +import styles from "./section.module.css"; + +/** + * A section heading with controls beside it, such as a search and sorts (mockup `.sec-head`). + * The controls drop below the heading when there isn't room. + */ +export function SectionHead({ actions, id, title }: { actions?: ReactNode; id: string; title: ReactNode }) { + return ( +
          +

          {title}

          + {actions} +
          + ); +} + +/** + * One section of an ad networks page (mockup `.nsec`): an h2 asked as a question, optional controls + * beside it, a one-line muted description, then the content. Sections that follow each other are + * split by a hairline. + */ +export function AdSection({ actions, children, description, id, title }: { + actions?: ReactNode; + children: ReactNode; + description?: ReactNode; + id: string; + title: ReactNode; +}) { + return ( +
          + {actions ? :

          {title}

          } + {description ?

          {description}

          : null} + {children} +
          + ); +} + +/** A small muted line under a block, such as where dates come from (mockup `.note`). */ +export function SectionNote({ children }: { children: ReactNode }) { + return

          {children}

          ; +} diff --git a/app/ui/ad-networks/site-format.ts b/app/ui/ad-networks/site-format.ts new file mode 100644 index 00000000..5c35edc2 --- /dev/null +++ b/app/ui/ad-networks/site-format.ts @@ -0,0 +1,59 @@ +import { getAdNetworkNiche } from "@/lib/ad-network-niches"; +import type { AdNetworkSiteRow } from "@/lib/ad-networks-types"; + +// Shared by the sites table and the ad network checker, so a site reads the same everywhere. +// Fixed en-US formats and UTC dates keep the server and browser output identical. + +export const NOT_LISTED = "Not listed"; + +const dayFormat = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", timeZone: "UTC" }); +const fullDayFormat = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }); + +export const formatCount = (value: number) => value.toLocaleString("en-US"); + +export function formatDay(iso: string | null | undefined, withYear = false) { + if (!iso) return null; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return null; + return (withYear ? fullDayFormat : dayFormat).format(date); +} + +/** Chrome's popularity groups, most visited first. Each fills one more step of a five-step meter. */ +const trafficSteps: Record = { + "top 1k": 5, + "top 10k": 4, + "top 100k": 3, + "top 1m": 2, + "not ranked": 1, +}; + +/** `top-100k` or `top 100k` reads as `top 100k`. */ +export const trafficText = (group: string) => group.toLowerCase().replace(/-/g, " ").replace(/(\d)m\b/, "$1M"); + +export function trafficStep(group: string | null) { + return group ? trafficSteps[group.toLowerCase().replace(/-/g, " ")] ?? null : null; +} + +/** Registration year and age in whole years at the last check. Age needs both dates. */ +export function domainAge(row: Pick) { + if (!row.domain_created_on) return null; + const created = new Date(row.domain_created_on); + if (Number.isNaN(created.getTime())) return null; + const year = created.getUTCFullYear(); + const checked = row.last_checked_at ? new Date(row.last_checked_at) : null; + if (!checked || Number.isNaN(checked.getTime())) return { year, years: null }; + let years = checked.getUTCFullYear() - year; + if (checked.getUTCMonth() < created.getUTCMonth() || (checked.getUTCMonth() === created.getUTCMonth() && checked.getUTCDate() < created.getUTCDate())) years -= 1; + return { year, years: Math.max(0, years) }; +} + +export function nicheName(slug: string | null) { + return getAdNetworkNiche(slug)?.name ?? null; +} + +/** The company named as the site's main ad manager in ads.txt, or the bare domain when it isn't one of ours. */ +export function managerName(row: Pick) { + return row.managed_by?.name ?? row.manager_domain ?? null; +} + +export const checkerHref = (domain: string) => `/ad-network-checker?site=${encodeURIComponent(domain)}`; diff --git a/app/ui/ad-networks/site-view.module.css b/app/ui/ad-networks/site-view.module.css new file mode 100644 index 00000000..21730564 --- /dev/null +++ b/app/ui/ad-networks/site-view.module.css @@ -0,0 +1,378 @@ +.srOnly { + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +/* Site facts */ +.facts { + border-bottom: 1px solid var(--color-border); + border-top: 1px solid var(--color-border); + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + margin: 0 0 8px; +} + +.facts > div { + border-left: 1px solid var(--color-border); + display: flex; + flex-direction: column-reverse; + gap: var(--space-2); + justify-content: flex-end; + min-width: 0; + padding: 16px 16px 15px; +} + +.facts > div:first-child { + border-left: 0; + padding-left: 0; +} + +.facts dt { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); +} + +.facts dd { + font: var(--weight-semibold) 1.05rem / 1.3 var(--font-sans); + margin: 0; + overflow-wrap: anywhere; +} + +.facts dd a { + color: var(--color-text); + text-decoration: underline; + text-decoration-color: var(--color-border-strong); + text-underline-offset: 3px; +} + +.muted { + color: var(--color-text-muted); + font-weight: var(--weight-regular); +} + +@media (max-width: 1099px) { + .facts { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + + .facts > div:nth-child(4) { + border-left: 0; + padding-left: 0; + } + + .facts > div:nth-child(n + 4) { + border-top: 1px solid var(--color-border); + } +} + +@media (max-width: 640px) { + .facts { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .facts > div { + padding: 14px 12px; + } + + .facts > div:nth-child(4) { + border-left: 1px solid var(--color-border); + padding-left: 12px; + } + + .facts > div:nth-child(2n + 1) { + border-left: 0; + padding-left: 0; + } + + .facts > div:nth-child(n + 3) { + border-top: 1px solid var(--color-border); + } +} + +/* Networks on a site */ +.scroll { + min-width: 0; + overflow-x: auto; +} + +.rel { + border-collapse: collapse; + font-size: 0.97rem; + width: 100%; +} + +.rel th { + border-bottom: 1px solid var(--color-border-strong); + color: var(--color-text-muted); + font: 500 0.8rem var(--font-mono); + padding: 0 var(--space-5) var(--space-4); + text-align: left; + white-space: nowrap; +} + +.rel td { + border-top: 1px solid var(--color-border); + padding: var(--space-6) var(--space-5); + white-space: nowrap; +} + +.rel tbody tr:first-child td { + border-top: 0; +} + +.rel .right { + text-align: right; +} + +.mono { + color: var(--color-text-muted); + font: 0.86rem var(--font-mono); +} + +.net { + align-items: center; + display: flex; + gap: var(--space-5); +} + +.net a, +.net b { + color: var(--color-text); + font-weight: var(--weight-semibold); +} + +.net a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.state { + align-items: center; + color: var(--color-text-muted); + display: inline-flex; + font-size: var(--text-small); + gap: var(--space-3); +} + +.state::before { + border: 1.5px solid var(--color-text-muted); + border-radius: 50%; + content: ""; + height: 7px; + width: 7px; +} + +.state[data-ok] { + color: var(--color-text); +} + +.state[data-ok]::before { + background: var(--color-accent-text); + border-color: var(--color-accent-text); +} + +@media (max-width: 640px) { + .rel thead { + display: none; + } + + .rel, + .rel tbody, + .rel tr { + display: block; + } + + .rel tr { + border-top: 1px solid var(--color-border); + display: grid; + gap: 6px 10px; + grid-template-columns: 1fr auto; + padding: 12px 0; + } + + .rel tbody tr:first-child { + border-top: 0; + } + + .rel td { + border: 0; + padding: 0; + white-space: normal; + } + + .rel td:nth-child(2) { + color: var(--color-text-muted); + font-size: var(--text-small); + grid-column: 1 / -1; + } + + .rel td:nth-child(3) { + grid-column: 1; + } + + .rel td.right { + grid-column: 2; + grid-row: 1; + } +} + +/* ads.txt */ +.adsNumbers { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 0; +} + +.adsNumbers > div { + display: flex; + flex-direction: column-reverse; + gap: 2px; + justify-content: flex-end; +} + +.adsNumbers dd { + font: var(--weight-semibold) 1.6rem / 1 var(--font-mono); + letter-spacing: -0.02em; + margin: 0; +} + +.adsNumbers dt { + color: var(--color-text-muted); + font: var(--text-small) var(--font-mono); +} + +.sub { + color: var(--color-text-muted); + font: var(--weight-semibold) var(--text-small) var(--font-mono); + margin: var(--space-5) 0 0; +} + +.adsTxt { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + color: var(--color-text-muted); + font: 0.82rem / 1.7 var(--font-mono); + margin: 0; + overflow-x: auto; + padding: 14px 16px; +} + +.adsTxt code { + font: inherit; +} + +.comment { + color: var(--color-text-muted); + opacity: 0.7; +} + +.adsTxt mark { + background: rgba(58, 112, 0, 0.26); + border-radius: 1px; + color: var(--color-text); + padding: 1px 3px; +} + +:global([data-theme="dark"]) .adsTxt mark { + background: rgba(186, 252, 60, 0.3); +} + +.bright { + color: var(--color-text); +} + +.fileLink { + align-items: center; + color: var(--color-text-muted); + display: inline-flex; + font: var(--text-small) var(--font-mono); + gap: var(--space-2); + text-underline-offset: 3px; +} + +.fileLink:hover { + color: var(--color-text); + text-decoration: underline; +} + +@media (max-width: 640px) { + .adsNumbers { + gap: 14px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +/* Similar sites */ +.similar { + border-top: 1px solid var(--color-border); + display: grid; + gap: 0; + grid-template-columns: repeat(3, minmax(0, 1fr)); + list-style: none; + margin: 0; + padding: 0; +} + +.similar li { + align-items: center; + border-bottom: 1px solid var(--color-border); + display: flex; + gap: var(--space-5); + justify-content: space-between; + padding: 14px 16px 14px 0; +} + +.similar li:not(:nth-child(3n + 1)) { + border-left: 1px solid var(--color-border); + padding-left: 16px; +} + +.similar a, +.similar b { + color: var(--color-text); + font-weight: var(--weight-semibold); + overflow-wrap: anywhere; +} + +.similar a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.similar li > span { + color: var(--color-text-muted); + flex: none; + font: 0.82rem var(--font-mono); +} + +@media (max-width: 1099px) { + .similar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .similar li:not(:nth-child(3n + 1)) { + border-left: 0; + padding-left: 0; + } + + .similar li:nth-child(2n) { + border-left: 1px solid var(--color-border); + padding-left: 16px; + } +} + +@media (max-width: 640px) { + .similar { + grid-template-columns: 1fr; + } + + .similar li:nth-child(n) { + border-left: 0; + padding-left: 0; + } +} diff --git a/app/ui/ad-networks/site-view.tsx b/app/ui/ad-networks/site-view.tsx new file mode 100644 index 00000000..f7b56edb --- /dev/null +++ b/app/ui/ad-networks/site-view.tsx @@ -0,0 +1,137 @@ +import Link from "next/link"; +import { ExternalLink } from "lucide-react"; +import type { ReactNode } from "react"; +import { NetworkLogo } from "./network-logo"; +import type { NetworkLogoFile } from "./network-logo-src"; +import { TrafficMeter } from "./traffic-meter"; +import styles from "./site-view.module.css"; + +export type SiteFact = { label: string; value: ReactNode }; + +/** A site's facts in one ruled row of up to six, wrapping to three and then two (mockup `.site-facts`). */ +export function SiteFacts({ items }: { items: readonly SiteFact[] }) { + return ( +
          + {items.map((item) => ( +
          +
          {item.label}
          +
          {item.value}
          +
          + ))} +
          + ); +} + +/** Muted words inside a site fact, such as `· 12 yrs`. */ +export function FactMuted({ children }: { children: ReactNode }) { + return {children}; +} + +export type SiteNetworkRow = { + name: string; + href?: string; + logo: NetworkLogoFile | null; + /** What the site's ads.txt says about it, such as `Names it as the ad manager`. */ + says: string; + /** Status in words, such as `Confirmed live` or `Not in Google's seller list`. */ + status: string; + /** Confirmed statuses get a filled accent dot; others an empty one. */ + confirmed: boolean; + /** Such as `Mar 2021`. */ + since?: string; +}; + +/** The networks on one site, with what its ads.txt says and whether the network agrees (mockup `table.rel`). */ +export function SiteNetworksTable({ caption, rows }: { caption: string; rows: readonly SiteNetworkRow[] }) { + const since = rows.some((row) => row.since); + return ( +
          + + + + {since ? : null} + + + {rows.map((row) => ( + + + + + {since ? : null} + + ))} + +
          {caption}
          networkwhat its ads.txt saysstatussince
          {row.href ? {row.name} : {row.name}}{row.says}{row.status}{row.since ?? ""}
          +
          + ); +} + +export type AdsTxtNumber = { label: string; value: ReactNode }; + +/** Up to four ads.txt counts, such as lines, companies, direct, and resellers (mockup `.adstxt-nums`). */ +export function AdsTxtNumbers({ items }: { items: readonly AdsTxtNumber[] }) { + return ( +
          + {items.map((item) => ( +
          {item.label}
          {item.value}
          + ))} +
          + ); +} + +/** A small mono subheading inside a block, such as `companies with the most lines` (mockup `.sub`). */ +export function BlockSubheading({ children }: { children: ReactNode }) { + return

          {children}

          ; +} + +/** + * The first lines of an ads.txt file as we read them (mockup `pre.adstxt`). Comments are dimmed, + * the MANAGERDOMAIN line is marked, and lines for `highlight` domains are brighter. `more` adds a + * closing "… N more lines" comment. `fileHref` adds a link to the full file. + */ +export function AdsTxtPreview({ fileHref, highlight = [], label, more, text }: { + fileHref?: string; + /** Seller domains to brighten, such as `mediavine.com`. */ + highlight?: readonly string[]; + label: string; + more?: number; + text: string; +}) { + const bright = new Set(highlight.map((domain) => domain.toLowerCase())); + const lines = text.replace(/\r\n?/g, "\n").replace(/\n+$/, "").split("\n"); + const line = (value: string) => { + const trimmed = value.trim(); + if (trimmed.startsWith("#")) return {value}; + if (/^managerdomain\s*=/i.test(trimmed)) return {value}; + const domain = trimmed.split(",")[0]?.trim().toLowerCase(); + if (domain && bright.has(domain)) return {value}; + return value; + }; + return ( + <> +
          +        
          +          {lines.map((value, index) => {line(value)}{"\n"})}
          +          {more ? … {more.toLocaleString("en-US")} more {more === 1 ? "line" : "lines"} : null}
          +        
          +      
          + {fileHref ? : null} + + ); +} + +export type SimilarSite = { domain: string; href?: string; trafficGroup: string | null }; + +/** Similar sites in three columns of ruled rows, with their traffic meter (mockup `ul.similar`). */ +export function SimilarSites({ sites }: { sites: readonly SimilarSite[] }) { + return ( +
            + {sites.map((site) => ( +
          • + {site.href ? {site.domain} : {site.domain}} + +
          • + ))} +
          + ); +} diff --git a/app/ui/ad-networks/sites-table.module.css b/app/ui/ad-networks/sites-table.module.css new file mode 100644 index 00000000..87fb59f3 --- /dev/null +++ b/app/ui/ad-networks/sites-table.module.css @@ -0,0 +1,230 @@ +/* Mockup table.sites */ +.section { + display: grid; + min-width: 0; + scroll-margin-top: calc(var(--top-bar-height) + var(--space-5)); +} + +.head { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: var(--space-3) var(--space-7); + justify-content: space-between; + margin-bottom: var(--space-8); +} + +.head h2 { + font: var(--weight-bold) var(--text-h2) / var(--line-heading) var(--font-display); + letter-spacing: var(--tracking-display); + margin: 0; +} + +.frame { + min-width: 0; + overflow-x: auto; +} + +.table { + border-collapse: collapse; + font-size: 0.97rem; + width: 100%; +} + +.table[data-loading] tbody { + opacity: 0.55; +} + +.table thead th { + border-bottom: 1px solid var(--color-border-strong); + color: var(--color-text-muted); + font: 500 0.8rem var(--font-mono); + padding: 0 var(--space-5) var(--space-4); + text-align: left; + text-transform: lowercase; + white-space: nowrap; +} + +.table tbody th, +.table tbody td { + border-top: 1px solid var(--color-border); + font-weight: var(--weight-regular); + padding: var(--space-6) var(--space-5); + text-align: left; + vertical-align: middle; + white-space: nowrap; +} + +.table tbody tr:first-child > * { + border-top: 0; +} + +.table tbody tr:hover > * { + background: var(--color-surface); +} + +.table .right { + text-align: right; +} + +.mono { + font: 0.86rem var(--font-mono); +} + +.muted, +.none { + color: var(--color-text-muted); +} + +.none { + font: var(--text-small) var(--font-mono); +} + +.site { + min-width: 0; +} + +.domain { + color: var(--color-text); + font-weight: var(--weight-semibold); +} + +.domain:hover, +.domain:focus-visible { + text-decoration: underline; + text-underline-offset: 3px; +} + +.publisher { + color: var(--color-text-muted); + display: block; + font-size: var(--text-small); + font-weight: var(--weight-regular); + margin-top: 2px; +} + +.networks a { + color: var(--color-text); +} + +.networks a:hover { + text-decoration: underline; + text-underline-offset: 3px; +} + +.foot { + align-items: center; + color: var(--color-text-muted); + display: flex; + flex-wrap: wrap; + font: var(--text-small) var(--font-mono); + gap: var(--space-5); + justify-content: space-between; + margin-top: var(--space-8); +} + +/* The shared pager, drawn like the mockup's: 36px boxes in strong borders. */ +.pager nav { + gap: var(--space-2); + justify-content: flex-end; + margin: 0; +} + +.pager nav a, +.pager nav span { + border-color: var(--color-border-strong); + color: var(--color-text); + font: var(--text-small) var(--font-mono); + min-height: 36px; + min-width: 36px; + padding: 0 10px; +} + +.pager nav a:hover { + background: var(--color-surface-2); +} + +.pager nav span[aria-hidden="true"] { + border-color: transparent; + color: var(--color-text-muted); + min-width: auto; + padding: 0 var(--space-1); +} + +.pager nav span[aria-disabled="true"] { + color: var(--color-text-muted); +} + +.pager nav span[aria-current="page"] { + background: var(--color-accent); + border-color: var(--color-accent); + color: var(--color-accent-contrast); + font-weight: var(--weight-semibold); +} + +/* Phones: each row becomes a two-line block, no sideways scroll. */ +@media (max-width: 700px) { + .table thead { + display: none; + } + + .table, + .table tbody, + .table tr { + display: block; + } + + .table tbody tr { + border-top: 1px solid var(--color-border); + padding: var(--space-6) 0; + } + + .table tbody tr:first-child { + border-top: 0; + } + + .table tbody tr:hover > * { + background: none; + } + + .table tbody th, + .table tbody td { + border: 0; + color: var(--color-text-muted); + display: inline; + font: var(--text-small) / 1.7 var(--font-mono); + padding: 0; + white-space: normal; + } + + .table tbody th.site { + color: var(--color-text); + display: block; + font: var(--text-body) var(--font-sans); + margin-bottom: var(--space-3); + } + + .table tbody td + td::before { + content: " · "; + } + + .table tbody td[data-column="checked"] { + display: none; + } + + .table tbody td + td[data-column="manager"]::before { + content: " · managed by "; + } + + .table tbody td + td[data-column="manager"][data-none]::before { + content: " · manager "; + } + + .table tbody td[data-column="manager"][data-none] { + text-transform: lowercase; + } + + .networks a { + color: inherit; + } +} diff --git a/app/ui/ad-networks/sites-table.tsx b/app/ui/ad-networks/sites-table.tsx new file mode 100644 index 00000000..c608a064 --- /dev/null +++ b/app/ui/ad-networks/sites-table.tsx @@ -0,0 +1,342 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useRef, useState, type MouseEvent, type ReactNode } from "react"; +import { adNetworkNiches, type AdNetworkNicheSlug } from "@/lib/ad-network-niches"; +import type { AdNetworkRef, AdNetworkSiteRow, AdNetworkSitesResponse, AdNetworkSitesSort } from "@/lib/ad-networks-types"; +import { UiButton } from "@/app/ui/controls"; +import { Pagination } from "@/app/ui/pagination"; +import { StatePanel } from "@/app/ui/state-panel"; +import { TableToolbar, ToolbarSearch, ToolbarSelect } from "@/app/ui/table-toolbar"; +import { NOT_LISTED, checkerHref, domainAge, formatCount, formatDay, managerName, nicheName } from "./site-format"; +import { TrafficMeter } from "./traffic-meter"; +import styles from "./sites-table.module.css"; + +export type SitesTableColumn = "niche" | "networks" | "traffic" | "age" | "platform" | "manager" | "checked"; + +export type SitesTableProps = { + /** The page path. Filters, sort, and page live in its query, such as `?q=&niche=&page=2`. */ + route: string; + heading: ReactNode; + headingId: string; + /** The first page with the default view, rendered on the server. */ + initial: AdNetworkSitesResponse; + /** The sort `initial` was loaded with. */ + defaultSort: AdNetworkSitesSort; + /** Fixed filters for the whole page, such as this network or this niche. */ + scope: { network?: string; niche?: AdNetworkNicheSlug }; + columns: readonly SitesTableColumn[]; + /** Show the niche filter. Leave it off until sites have niches. */ + nicheFilter?: boolean; + /** Show a network filter with these networks. */ + networkFilter?: readonly AdNetworkRef[]; + /** Words for the count line, such as `food sites`. */ + noun?: string; + /** + * Sort options, in order. Leave out sorts whose data isn't collected yet. The select hides when + * one option is left. Defaults to the mockup's Traffic, Oldest domain, and Newest domain, plus + * `defaultSort` when it isn't one of them. + */ + sorts?: readonly AdNetworkSitesSort[]; +}; + +type View = { q: string; niche: string; network: string; sort: AdNetworkSitesSort; page: number }; + +const sortOptions: { value: AdNetworkSitesSort; label: string }[] = [ + { value: "domain", label: "A to Z" }, + { value: "traffic", label: "Traffic" }, + { value: "oldest", label: "Oldest domain" }, + { value: "newest", label: "Newest domain" }, + { value: "checked", label: "Last checked" }, +]; +const allSorts = new Set(sortOptions.map((option) => option.value)); +const mockupSorts: readonly AdNetworkSitesSort[] = ["traffic", "oldest", "newest"]; +const apiColumn: Record = { + niche: "niche", + networks: "network", + traffic: "traffic", + age: "domain_age", + platform: "platform", + manager: "manager", + checked: "checked", +}; +const columnLabels: Record = { + niche: "niche", + networks: "network", + traffic: "traffic", + age: "domain age", + platform: "platform", + manager: "ads managed by", + checked: "checked", +}; + +function readView(defaultSort: AdNetworkSitesSort, props: Pick): View { + const search = new URLSearchParams(window.location.search); + const sort = search.get("sort") ?? ""; + const page = Number(search.get("page")); + const niche = search.get("niche") ?? ""; + const network = search.get("network") ?? ""; + return { + q: (search.get("q") ?? "").slice(0, 100), + niche: props.nicheFilter && adNetworkNiches.some((item) => item.slug === niche) ? niche : "", + network: props.networkFilter?.some((item) => item.slug === network) ? network : "", + sort: allSorts.has(sort) ? sort as AdNetworkSitesSort : defaultSort, + page: Number.isInteger(page) && page > 1 && page <= 10000 ? page : 1, + }; +} + +function viewHref(route: string, view: View, defaultSort: AdNetworkSitesSort, page = view.page) { + const params = new URLSearchParams(); + if (view.q.trim()) params.set("q", view.q.trim()); + if (view.niche) params.set("niche", view.niche); + if (view.network) params.set("network", view.network); + if (view.sort !== defaultSort) params.set("sort", view.sort); + if (page > 1) params.set("page", String(page)); + const query = params.toString(); + return query ? `${route}?${query}` : route; +} + +const isDefault = (view: View, defaultSort: AdNetworkSitesSort) => + !view.q.trim() && !view.niche && !view.network && view.sort === defaultSort && view.page === 1; + +function TrafficCell({ group }: { group: string | null }) { + return group ? : {NOT_LISTED}; +} + +function AgeCell({ row }: { row: AdNetworkSiteRow }) { + const age = domainAge(row); + if (!age) return {NOT_LISTED}; + return age.years === null + ? <>{age.year} + : <>{age.years} {age.years === 1 ? "yr" : "yrs"} · {age.year}; +} + +function Cell({ column, row }: { column: SitesTableColumn; row: AdNetworkSiteRow }) { + switch (column) { + case "niche": { + const name = nicheName(row.niche); + return {name ?? {NOT_LISTED}}; + } + case "networks": + return ( + + {row.networks.length + ? row.networks.map((network, index) => {index ? ", " : ""}{network.name}) + : {NOT_LISTED}} + + ); + case "traffic": + return ; + case "age": + return ; + case "platform": + return {row.platform ?? {NOT_LISTED}}; + case "manager": { + const name = managerName(row); + return {name ?? NOT_LISTED}; + } + case "checked": + return {formatDay(row.last_checked_at) ?? "—"}; + } +} + +/** + * A long list of sites. The server renders the first page in the default view; search, filters, + * sort, and pages load from the cached sites API and keep their state in the URL. On phones each + * row becomes a two-line block, so the table never scrolls sideways. + */ +export function SitesTable(props: SitesTableProps) { + const { columns, defaultSort, heading, headingId, initial, networkFilter, nicheFilter, noun = "sites", route, scope, sorts = mockupSorts } = props; + const [view, setView] = useState({ q: "", niche: "", network: "", sort: defaultSort, page: 1 }); + const [data, setData] = useState(initial); + const [status, setStatus] = useState<"idle" | "loading" | "error">("idle"); + const [attempt, setAttempt] = useState(0); + const section = useRef(null); + + // The current response says which sorts, filters, and columns have data. Anything without data + // stays hidden, except an active filter, which stays so it can be cleared. + const availableSorts = data.availableSorts ?? sorts; + const shownSorts = [...(sorts.includes(defaultSort) ? [] : [defaultSort]), ...sorts] + .filter((value) => value === defaultSort || availableSorts.includes(value)) + .flatMap((value) => sortOptions.filter((option) => option.value === value)); + // Show the sort the API used. A shared link's unavailable sort falls back to the default there. + const shownSort = status === "loading" ? view.sort : data.effectiveSort ?? view.sort; + const withActive = (options: { value: string; label: string }[], active: string, label: string) => + active && !options.some((option) => option.value === active) ? [...options, { value: active, label }] : options; + const nicheOptions = withActive( + (data.filters?.niches ?? []).map((niche) => ({ value: niche.slug, label: niche.name })), + view.niche, + nicheName(view.niche || null) ?? view.niche, + ); + const networkChoices = data.filters?.networks?.length ? data.filters.networks : networkFilter ?? []; + const networkOptions = withActive( + networkChoices.map((network) => ({ value: network.slug, label: network.name })), + view.network, + networkFilter?.find((network) => network.slug === view.network)?.name ?? view.network, + ); + const showNicheFilter = Boolean(nicheFilter) && ((data.availableFilters?.includes("niche") && nicheOptions.length > 0) || Boolean(view.niche)); + const showNetworkFilter = Boolean(networkFilter) && ((data.availableFilters?.includes("network") && networkOptions.length > 0) || Boolean(view.network)); + const filterRef = useRef({ nicheFilter, networkFilter }); + + // Read a shared link after hydration, and follow the back button. + useEffect(() => { + const sync = () => setView(readView(defaultSort, filterRef.current)); + if (window.location.search) sync(); + window.addEventListener("popstate", sync); + return () => window.removeEventListener("popstate", sync); + }, [defaultSort]); + + const scopeNetwork = scope.network; + const scopeNiche = scope.niche; + useEffect(() => { + if (isDefault(view, defaultSort)) { + setData(initial); + setStatus("idle"); + return; + } + const controller = new AbortController(); + const timer = window.setTimeout(async () => { + setStatus("loading"); + const params = new URLSearchParams(); + const network = scopeNetwork ?? view.network; + const niche = scopeNiche ?? view.niche; + if (network) params.set("network", network); + if (niche) params.set("niche", niche); + if (view.q.trim()) params.set("q", view.q.trim()); + params.set("sort", view.sort); + params.set("page", String(view.page)); + params.set("pageSize", String(initial.pageSize)); + try { + const response = await fetch(`/api/ad-networks/sites?${params}`, { signal: controller.signal }); + if (!response.ok) throw new Error(`Sites request failed: ${response.status}`); + setData(await response.json() as AdNetworkSitesResponse); + setStatus("idle"); + } catch (error) { + if (!controller.signal.aborted) { + console.error(error); + setStatus("error"); + } + } + }, view.q ? 250 : 0); + return () => { + controller.abort(); + window.clearTimeout(timer); + }; + }, [attempt, defaultSort, initial, scopeNetwork, scopeNiche, view]); + + const update = useCallback((patch: Partial, push = false) => { + const next = { ...view, page: 1, ...patch }; + const href = viewHref(route, next, defaultSort); + if (push) window.history.pushState(null, "", href); + else window.history.replaceState(null, "", href); + setView(next); + }, [defaultSort, route, view]); + + // Page links change the view in place instead of loading the static page again. + const onPagerClick = (event: MouseEvent) => { + const link = (event.target as Element).closest("a[href]"); + if (!(link instanceof HTMLAnchorElement) || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + event.preventDefault(); + const page = Number(new URL(link.href).searchParams.get("page") ?? "1"); + update({ page: Number.isInteger(page) && page > 0 ? page : 1 }, true); + section.current?.scrollIntoView({ block: "start" }); + }; + + // A column with no data yet is left out, instead of a column of "Not listed". The API's + // availableColumns decides when present; otherwise the rows on this page do. + const hasValue: Partial unknown>> = { + niche: (row) => row.niche, + traffic: (row) => row.traffic_group, + age: (row) => row.domain_created_on, + platform: (row) => row.platform, + }; + const available = data.availableColumns; + const shown = columns.filter((column) => available + ? available.includes(apiColumn[column]) + : !hasValue[column] || data.rows.some((row) => hasValue[column]!(row))); + + const reset = () => update({ q: "", niche: "", network: "", sort: defaultSort }); + const filtered = Boolean(view.q.trim() || view.niche || view.network); + const first = data.total ? (data.page - 1) * data.pageSize + 1 : 0; + const last = Math.min(data.total, data.page * data.pageSize); + + let body: ReactNode; + if (status === "error") { + body = ( + setAttempt((count) => count + 1)}>Try again} state="error" title="Sites could not be loaded"> +

          Check your connection and try again.

          +
          + ); + } else if (!data.rows.length) { + body = filtered + ? Show all sites} title="No sites match">

          Try another part of the domain, or clear the filters.

          + :

          Sites show up here once we confirm them from their own ads.txt files or ad code.

          ; + } else { + body = ( + <> +
          + + + + + {shown.map((column) => )} + + + + {data.rows.map((row) => ( + + + {shown.map((column) => )} + + ))} + +
          site{columnLabels[column]}
          + {row.domain} + {row.publisher_name ? {row.publisher_name} : null} +
          +
          +
          + {formatCount(first)}-{formatCount(last)} of {formatCount(data.total)} {noun} +
          + viewHref(route, view, defaultSort, page)} + totalPages={data.totalPages} + variant="words" + /> +
          +
          + + ); + } + + return ( +
          +
          +

          {heading}

          + {initial.total ? + update({ q: q.slice(0, 100) })} placeholder="Search sites" value={view.q} /> + {showNicheFilter ? ( + update({ niche })} + options={[{ value: "", label: "All" }, ...nicheOptions]} + value={view.niche} + /> + ) : null} + {showNetworkFilter ? ( + update({ network })} + options={[{ value: "", label: "All" }, ...networkOptions]} + value={view.network} + /> + ) : null} + {shownSorts.length > 1 ? update({ sort: sort as AdNetworkSitesSort })} options={shownSorts} value={shownSort} /> : null} + : null} +
          + {body} +
          + ); +} diff --git a/app/ui/ad-networks/traffic-meter.module.css b/app/ui/ad-networks/traffic-meter.module.css new file mode 100644 index 00000000..a4a3ec87 --- /dev/null +++ b/app/ui/ad-networks/traffic-meter.module.css @@ -0,0 +1,25 @@ +.wrap { + white-space: nowrap; +} + +.meter { + display: inline-flex; + gap: 2px; + margin-right: var(--space-3); + vertical-align: -1px; +} + +.meter i { + background: var(--color-surface-2); + border-radius: 1px; + height: 12px; + width: 5px; +} + +.meter i[data-on] { + background: var(--color-accent-text); +} + +.label { + color: var(--color-text-muted); +} diff --git a/app/ui/ad-networks/traffic-meter.tsx b/app/ui/ad-networks/traffic-meter.tsx new file mode 100644 index 00000000..ed0e9c50 --- /dev/null +++ b/app/ui/ad-networks/traffic-meter.tsx @@ -0,0 +1,22 @@ +import { trafficStep, trafficText } from "./site-format"; +import styles from "./traffic-meter.module.css"; + +/** + * Five small steps, filled by a site's place in Chrome's list of the most visited sites, then the + * group in muted words (mockup `.meter` + `top 10k`). Renders nothing for a missing group. + */ +export function TrafficMeter({ group, muted = true, showLabel = true }: { + group: string | null; + /** Muted group words, as in tables and lists. Set false inside a site fact, where the value reads plain. */ + muted?: boolean; + showLabel?: boolean; +}) { + if (!group) return null; + const step = trafficStep(group); + return ( + + {step ? : null} + {showLabel ? {trafficText(group)} : null} + + ); +} diff --git a/app/ui/ad-networks/viz/age-strip.tsx b/app/ui/ad-networks/viz/age-strip.tsx new file mode 100644 index 00000000..4cee455a --- /dev/null +++ b/app/ui/ad-networks/viz/age-strip.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { MeasuredChart, svgSize, useChartWidth } from "./chart-frame"; +import { formatNumber } from "./format"; +import { VizDataTable } from "./parts"; +import styles from "./viz.module.css"; + +export type AgeBin = { + /** First registration year in the bin. */ + year: number; + count: number; +}; + +export type AgeEnd = { year: number; domain?: string }; + +const base = 66; + +/** + * When the domains were registered: a small histogram over a year line, the middle half as a + * band, the median as an accent line, and the oldest and newest domains at the ends (mockup + * domain age). Bins are evenly spaced, `binYears` apart. + */ +export function AgeStrip({ bins, binYears, caption, median, middleHalf, newest, oldest }: { + bins: readonly AgeBin[]; + /** Years per bin. Defaults to the gap between the first two bins, or 1. */ + binYears?: number; + caption: string; + /** Median registration year. */ + median: number; + /** The 25th and 75th percentile years, drawn as a band. */ + middleHalf?: readonly [number, number]; + newest: AgeEnd; + oldest: AgeEnd; +}) { + const { measured, ref, width } = useChartWidth(720); + const first = oldest.year - 1; + const last = Math.max(newest.year + 1, first + 2); + const x = (year: number) => ((year - first) / (last - first)) * (width - 8) + 4; + const top = Math.max(1, ...bins.map((bin) => bin.count)); + const years = binYears ?? (bins.length > 1 ? Math.abs(bins[1].year - bins[0].year) : 1); + const barWidth = ((width - 8) / (last - first)) * years; + const tickStep = width < 420 ? 10 : 5; + const ticks: number[] = []; + for (let tick = Math.ceil((first + 1) / tickStep) * tickStep; tick < last; tick += tickStep) ticks.push(tick); + const two = width < 480; + const height = two ? 130 : 112; + const medianX = x(median); + const medianLeft = medianX + 110 > width; + + return ( + <> + + + + [years > 1 ? `${bin.year} to ${bin.year + years - 1}` : String(bin.year), formatNumber(bin.count)])} /> + + ); +} diff --git a/app/ui/ad-networks/viz/area-line.tsx b/app/ui/ad-networks/viz/area-line.tsx new file mode 100644 index 00000000..0dcf252a --- /dev/null +++ b/app/ui/ad-networks/viz/area-line.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { InteractiveChart, svgSize, useChartWidth } from "./chart-frame"; +import { formatCompact, formatNumber, lineScale } from "./format"; +import { VizDataTable } from "./parts"; +import styles from "./viz.module.css"; + +export type AreaLinePoint = { + /** Short axis label, such as `Nov`. */ + label: string; + /** Full label in the hover note and data table, such as `Nov 2025`. */ + fullLabel: string; + value: number; +}; + +const height = 170; +const left = 44; +const bottom = 22; +const top = 8; +const right = 10; + +/** + * One count over time as a filled line, ending in a dot (mockup sites over time). Hover, tap, or + * the arrow keys show the exact count and the change from the point before. `unit` follows the + * count in the note, such as `sites`. + */ +export function AreaLine({ caption, points, unit = "sites" }: { caption: string; points: readonly AreaLinePoint[]; unit?: string }) { + const { measured, ref, width } = useChartWidth(640); + const count = points.length; + const { high, low, ticks } = lineScale(points.map((point) => point.value), 3); + const plotWidth = Math.max(1, width - left - right); + const x = (index: number) => (count > 1 ? left + (index * plotWidth) / (count - 1) : left + plotWidth / 2); + const y = (value: number) => top + (1 - (value - low) / (high - low || 1)) * (height - top - bottom); + const path = points.map((point, index) => `${index ? "L" : "M"}${x(index).toFixed(1)} ${y(point.value).toFixed(1)}`).join(" "); + const step = Math.max(1, Math.ceil((count * 40) / plotWidth)); + const last = points[count - 1]; + + return ( + <> + (count > 1 ? Math.round((px - left) / (plotWidth / (count - 1))) : 0)} + label={caption} + measureRef={ref} + pointAt={(index) => { + const point = points[index]; + const change = index ? point.value - points[index - 1].value : null; + return { + x: x(index), + y: y(point.value), + dot: true, + tip: ( + <> + {point.fullLabel} · {formatNumber(point.value)} {unit} + {change === null ? null : <> · = 0 ? styles.up : undefined}>{change >= 0 ? "+" : "−"}{formatNumber(Math.abs(change))}} + + ), + }; + }} + width={width} + > + + + [point.fullLabel, formatNumber(point.value)])} /> + + ); +} diff --git a/app/ui/ad-networks/viz/bars.tsx b/app/ui/ad-networks/viz/bars.tsx new file mode 100644 index 00000000..0f5b8e5e --- /dev/null +++ b/app/ui/ad-networks/viz/bars.tsx @@ -0,0 +1,77 @@ +import Link from "next/link"; +import { Fragment, type ReactNode } from "react"; +import { clamp, formatNumber, toneColor, type VizTone } from "./format"; +import { VizLegend } from "./parts"; +import styles from "./viz.module.css"; + +export type RankedBar = { + label: ReactNode; + value: number; + /** The number at the end of the row, such as `31%` or `7,852`. Defaults to the value. */ + display?: ReactNode; + href?: string; + /** Marks the row the page is about, such as this network in a ranking. */ + highlight?: boolean; +}; + +/** + * Ranked bars, label left and exact value right (mockup `dl.bars`). Bars scale to the largest value + * unless `max` is set. `tone="all"` fills every bar with the accent; `tone="highlight"` keeps the + * bars neutral and fills only highlighted rows. Reads as a description list, so it needs no table. + */ +export function RankedBars({ ariaLabel, max, rows, tone = "all" }: { + ariaLabel?: string; + max?: number; + rows: readonly RankedBar[]; + tone?: "all" | "highlight"; +}) { + const top = max ?? Math.max(1, ...rows.map((row) => row.value)); + return ( +
          + {rows.map((row, index) => ( +
          +
          {row.href ? {row.label} : row.label}
          + +
          {row.display ?? formatNumber(row.value)}
          +
          + ))} +
          + ); +} + +export type StackPart = { label: ReactNode; value: number; tone: VizTone; /** Legend value, such as `88%`. */ display?: ReactNode }; + +/** + * One thin bar split into parts of a whole, with a legend below (mockup `.stackbar`). Parts keep + * their order. Set `legend={false}` when the parts are named elsewhere. + */ +export function StackBar({ legend = true, parts }: { legend?: boolean; parts: readonly StackPart[] }) { + const shown = parts.filter((part) => part.value > 0); + return ( + <> + + {legend ? ({ label: part.label, tone: part.tone, value: part.display }))} /> : null} + + ); +} + +export type Move = { from: ReactNode; to: ReactNode; fromHref?: string; toHref?: string; count: number }; + +/** Switches between networks: `from → to`, a bar, and the count (mockup `dl.moves`). */ +export function MoveBars({ ariaLabel, moves }: { ariaLabel?: string; moves: readonly Move[] }) { + const top = Math.max(1, ...moves.map((move) => move.count)); + const name = (label: ReactNode, href?: string) => href ? {label} : label; + return ( +
          + {moves.map((move, index) => ( + +
          {name(move.from, move.fromHref)}→{name(move.to, move.toHref)}
          + +
          {formatNumber(move.count)}
          +
          + ))} +
          + ); +} diff --git a/app/ui/ad-networks/viz/chart-frame.tsx b/app/ui/ad-networks/viz/chart-frame.tsx new file mode 100644 index 00000000..adc46ec9 --- /dev/null +++ b/app/ui/ad-networks/viz/chart-frame.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent, type PointerEvent, type ReactNode, type RefObject } from "react"; +import { clamp } from "./format"; +import styles from "./viz.module.css"; + +// Runs before paint in the browser; the server never calls it. +const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; + +/** + * The chart box's real pixel width. The server and the first browser render use `initial`, with + * the SVG scaled to fit, so the chart is in the HTML before scripts run. After that the chart + * redraws at the real width on every resize, so its text stays 12px. + */ +export function useChartWidth(initial: number) { + const ref = useRef(null); + const [width, setWidth] = useState(initial); + const [measured, setMeasured] = useState(false); + useBrowserLayoutEffect(() => { + const element = ref.current; + if (!element) return; + const read = () => { + const next = Math.floor(element.clientWidth); + if (!next) return; + setWidth(next); + setMeasured(true); + }; + read(); + const observer = new ResizeObserver(read); + observer.observe(element); + return () => observer.disconnect(); + }, []); + return { ref, width, measured }; +} + +/** SVG size attributes: exact once measured, scaled to the box before that. */ +export function svgSize(width: number, height: number, measured: boolean) { + return measured + ? { width, height, viewBox: `0 0 ${width} ${height}` } + : { width: "100%", viewBox: `0 0 ${width} ${height}`, preserveAspectRatio: "xMinYMin meet" as const }; +} + +export type ChartPoint = { + /** Position in chart pixels. */ + x: number; + y: number; + /** Draw a dot at the point, as on a line. */ + dot?: boolean; + tip: ReactNode; +}; + +/** + * A chart box that shows the exact numbers for one value on hover, tap, or arrow keys, with an + * optional guide line and dot (mockup sites over time and joined and left). Escape or a tap + * elsewhere hides the label. + */ +export function InteractiveChart({ children, count, guide, indexAt, label, measureRef, onActive, pointAt, width }: { + children: ReactNode; + /** How many values the keys step through. */ + count: number; + /** Top and bottom of the guide line in chart pixels. Leave out for bar charts. */ + guide?: readonly [number, number]; + /** The value index under a pointer, from its x position in chart pixels. */ + indexAt: (x: number) => number; + /** The chart's name, read before the key hint. */ + label: string; + measureRef: RefObject; + /** Called with the active index, or -1 when nothing is active. */ + onActive?: (index: number) => void; + pointAt: (index: number) => ChartPoint; + /** The width the chart is drawn at, to map pointer and label positions. */ + width: number; +}) { + const [active, setActive] = useState(null); + const [visible, setVisible] = useState(false); + // Pointer down runs before focus, so focus reads the index it set from here. + const activeRef = useRef(null); + const tipRef = useRef(null); + const scale = () => (measureRef.current?.clientWidth || width) / width; + + const show = useCallback((index: number) => { + const next = clamp(Math.round(index), 0, Math.max(0, count - 1)); + activeRef.current = next; + setActive(next); + setVisible(true); + onActive?.(next); + }, [count, onActive]); + const hide = useCallback(() => { + setVisible(false); + onActive?.(-1); + }, [onActive]); + + useEffect(() => { + const away = (event: globalThis.PointerEvent) => { + if (measureRef.current && !measureRef.current.contains(event.target as Node)) hide(); + }; + document.addEventListener("pointerdown", away); + return () => document.removeEventListener("pointerdown", away); + }, [hide, measureRef]); + + const point = visible && active !== null && active < count ? pointAt(active) : null; + + // Keep the label inside the box. + useBrowserLayoutEffect(() => { + const tip = tipRef.current; + const box = measureRef.current; + if (!tip || !box || !point) return; + const half = tip.offsetWidth / 2; + tip.style.left = `${clamp(point.x * scale(), half, Math.max(half, box.clientWidth - half))}px`; + tip.style.top = `${point.y * scale() - 12}px`; + }); + + const at = (event: PointerEvent) => { + const box = event.currentTarget.getBoundingClientRect(); + return indexAt((event.clientX - box.left) / scale()); + }; + const onKeyDown = (event: KeyboardEvent) => { + const current = activeRef.current ?? count - 1; + const next = { ArrowRight: current + 1, ArrowLeft: current - 1, Home: 0, End: count - 1 }[event.key]; + if (next !== undefined) { + event.preventDefault(); + show(next); + } else if (event.key === "Escape") hide(); + }; + + return ( +
          show(activeRef.current ?? count - 1)} + onKeyDown={onKeyDown} + onPointerDown={(event) => show(at(event))} + onPointerLeave={(event) => { if (event.pointerType === "mouse" && document.activeElement !== event.currentTarget) hide(); }} + onPointerMove={(event) => { if (event.pointerType === "mouse") show(at(event)); }} + ref={measureRef} + role="group" + tabIndex={0} + > + {children} + {point && guide ?
          : null} + {point?.dot ?
          : null} + +
          + ); +} + +/** A chart box that only measures its width, for charts with no hover. */ +export function MeasuredChart({ children, measureRef }: { children: ReactNode; measureRef: RefObject }) { + return
          {children}
          ; +} diff --git a/app/ui/ad-networks/viz/columns.tsx b/app/ui/ad-networks/viz/columns.tsx new file mode 100644 index 00000000..25175568 --- /dev/null +++ b/app/ui/ad-networks/viz/columns.tsx @@ -0,0 +1,37 @@ +import { formatNumber, toneColor, type VizTone } from "./format"; +import { VizDataTable } from "./parts"; +import styles from "./viz.module.css"; + +export type ColumnItem = { + label: string; + value: number; + /** The number above the column, such as `61%`. Defaults to the value. */ + display?: string; + /** Defaults to the accent. Use `n2` for an "unknown" or "not ranked" group. */ + tone?: VizTone; +}; + +/** + * One column per group with its value above and label below (mockup traffic, networks per site, + * and file size charts). Drawn in HTML, so text keeps its size at every width with no script. + */ +export function ColumnChart({ caption, height = 170, items }: { caption: string; height?: number; items: readonly ColumnItem[] }) { + const top = Math.max(1, ...items.map((item) => item.value)); + const plot = height - 22 - 18; + return ( +
          + + [item.label, item.display ?? formatNumber(item.value)])} /> +
          + ); +} diff --git a/app/ui/ad-networks/viz/diverging-bars.tsx b/app/ui/ad-networks/viz/diverging-bars.tsx new file mode 100644 index 00000000..6e2d916f --- /dev/null +++ b/app/ui/ad-networks/viz/diverging-bars.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { MeasuredChart, svgSize, useChartWidth } from "./chart-frame"; +import { formatSigned } from "./format"; +import { VizDataTable } from "./parts"; +import styles from "./viz.module.css"; + +export type DivergingRow = { label: string; shortLabel?: string; value: number }; + +const labelWidth = 112; +const rowHeight = 24; + +/** + * Gains right of a center line in accent, losses left in neutral, with the signed number at the + * bar end (mockup net change, last 90 days). Pass rows in the order to show, usually biggest gain + * first. + */ +export function DivergingBars({ caption, rows }: { caption: string; rows: readonly DivergingRow[] }) { + const { measured, ref, width } = useChartWidth(560); + const gain = Math.max(1, ...rows.map((row) => row.value)); + const loss = Math.max(1, ...rows.map((row) => -row.value)); + const middle = labelWidth + (width - labelWidth) * (gain >= loss * 2 ? 0.4 : gain * 2 <= loss ? 0.6 : 0.5); + const scale = Math.max(0.0001, Math.min((width - middle - 44) / gain, (middle - labelWidth - 40) / loss)); + const height = rows.length * rowHeight + 4; + return ( + <> + + + + [row.label, formatSigned(row.value)])} /> + + ); +} diff --git a/app/ui/ad-networks/viz/donut.tsx b/app/ui/ad-networks/viz/donut.tsx new file mode 100644 index 00000000..10f6a577 --- /dev/null +++ b/app/ui/ad-networks/viz/donut.tsx @@ -0,0 +1,66 @@ +import type { ReactNode } from "react"; +import { formatShare, partTones, toneColor, type VizTone } from "./format"; +import { VizDataTable, VizLegend } from "./parts"; +import styles from "./viz.module.css"; + +export type DonutPart = { label: string; value: number; tone?: VizTone }; + +const size = 132; +const radius = 52; +const circumference = 2 * Math.PI * radius; + +/** + * Parts of one whole as a ring, with the biggest part's share in the middle and a legend beside + * it (mockup platforms donut). Parts default to the accent steps, then the neutrals. Give a part + * the `track` tone for "Unknown". + */ +export function Donut({ caption, centerLabel, centerValue, parts }: { + /** What the ring shows, for screen readers, such as `Platforms`. */ + caption: string; + /** Defaults to the first part's share and name. */ + centerValue?: ReactNode; + centerLabel?: ReactNode; + parts: readonly DonutPart[]; +}) { + const total = parts.reduce((sum, part) => sum + Math.max(0, part.value), 0) || 1; + const toned = parts.map((part, index) => ({ ...part, tone: part.tone ?? partTones[Math.min(index, partTones.length - 1)] })); + let offset = 0; + const arcs = toned.map((part, index) => { + const length = (Math.max(0, part.value) / total) * circumference; + const arc = ( + 1 ? 1.5 : 0), 0.6)} ${circumference}`} + strokeDashoffset={-offset} + strokeWidth={14} + style={{ stroke: toneColor(part.tone) }} + transform={`rotate(-90 ${size / 2} ${size / 2})`} + /> + ); + offset += length; + return arc; + }); + const first = toned[0]; + return ( +
          +
          + +
          + ({ label: part.label, tone: part.tone, value: formatShare(part.value / total) }))} /> + [part.label, formatShare(part.value / total)])} /> +
          + ); +} diff --git a/app/ui/ad-networks/viz/dot-plot.tsx b/app/ui/ad-networks/viz/dot-plot.tsx new file mode 100644 index 00000000..a1c72837 --- /dev/null +++ b/app/ui/ad-networks/viz/dot-plot.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { MeasuredChart, svgSize, useChartWidth } from "./chart-frame"; +import { niceScale } from "./format"; +import { VizDataTable } from "./parts"; +import styles from "./viz.module.css"; + +export type DotRow = { label: string; shortLabel?: string; value: number }; + +const labelWidth = 112; +const rowHeight = 24; + +/** + * One dot per row on a shared scale from zero, with a stem and the value beside it (mockup median + * domain age). The first row's dot is accent, so pass the leader first. `unit` follows each value. + */ +export function DotPlot({ caption, rows, unit = "" }: { caption: string; rows: readonly DotRow[]; unit?: string }) { + const { measured, ref, width } = useChartWidth(360); + const { high, ticks } = niceScale(rows.map((row) => row.value), 3, true); + const x = (value: number) => labelWidth + (value / high) * (width - labelWidth - 40); + const height = rows.length * rowHeight + 20; + const suffix = unit ? ` ${unit}` : ""; + return ( + <> + + + + [row.label, `${row.value}${suffix}`])} /> + + ); +} diff --git a/app/ui/ad-networks/viz/format.ts b/app/ui/ad-networks/viz/format.ts new file mode 100644 index 00000000..fc76c989 --- /dev/null +++ b/app/ui/ad-networks/viz/format.ts @@ -0,0 +1,106 @@ +// Shared by every ad network visual. Fixed en-US formats keep the server and browser output identical. + +/** The visual's fill steps: accent at three strengths, neutral at three, and the empty track. */ +export type VizTone = "ac" | "ac2" | "ac3" | "n1" | "n2" | "n3" | "track"; + +/** The CSS color for a tone. The values live in `viz.module.css` under `.tokens`. */ +export const toneColor = (tone: VizTone) => (tone === "track" ? "var(--color-surface-2)" : `var(--viz-${tone})`); + +/** Strong accent fills take dark text in dark mode and white text in light mode. */ +export const onAccent = (tone: VizTone) => tone === "ac" || tone === "ac2"; + +/** The default order of tones for parts of a whole, largest first. */ +export const partTones: readonly VizTone[] = ["ac", "ac2", "ac3", "n1", "n2", "n3"]; + +export const formatNumber = (value: number) => value.toLocaleString("en-US"); + +/** `7,000` reads `7k`, `7,500` reads `7.5k`, `1,200,000` reads `1.2M`. */ +export function formatCompact(value: number) { + const abs = Math.abs(value); + if (abs >= 1_000_000) return `${trim(value / 1_000_000)}M`; + if (abs >= 1_000) return `${trim(value / 1_000)}k`; + return trim(value); +} + +const trim = (value: number) => String(Number(value.toFixed(1))); + +/** + * A percent from 0 to 100 for labels. Whole percents, except near the ends: a tiny nonzero value + * reads `<1%`, and only a complete 100 reads `100%` (99.96 reads `99.9%`). Round only for the + * label; keep raw values for sums, widths, and remainders. + */ +export function formatPercent(value: number) { + if (value > 0 && value < 1) return "<1%"; + if (value >= 99.5 && value < 100) return `${Math.min(99.9, Math.floor(value * 10) / 10)}%`; + return `${Math.round(value)}%`; +} + +/** A share from 0 to 1 as a percent label, with the same rules as `formatPercent`. */ +export const formatShare = (share: number) => formatPercent(share * 100); + +/** `+12`, `−4` (true minus sign), `0`. */ +export const formatSigned = (value: number) => (value > 0 ? `+${formatNumber(value)}` : value < 0 ? `−${formatNumber(-value)}` : "0"); + +/** A round step that splits `span` into about `count` parts: 1, 2, 2.5, or 5 times a power of ten. */ +export function niceStep(span: number, count: number) { + const raw = Math.max(span, 1e-9) / Math.max(count, 1); + const power = 10 ** Math.floor(Math.log10(raw)); + const unit = raw / power; + return (unit <= 1 ? 1 : unit <= 2 ? 2 : unit <= 2.5 ? 2.5 : unit <= 5 ? 5 : 10) * power; +} + +/** Axis bounds and ticks around the data. `zero` keeps the axis starting at 0. */ +export function niceScale(values: readonly number[], count = 3, zero = false) { + const min = zero ? 0 : Math.min(...values); + const max = Math.max(...values, zero ? 1 : -Infinity); + const span = max - min || Math.abs(max) || 1; + const step = niceStep(span, count); + const low = zero ? 0 : Math.floor(min / step) * step; + const high = Math.max(Math.ceil(max / step) * step, low + step); + const ticks: number[] = []; + for (let tick = low; tick <= high + step / 2; tick += step) ticks.push(Number(tick.toFixed(6))); + return { low, high, ticks }; +} + +export const clamp = (value: number, low: number, high: number) => Math.min(Math.max(value, low), high); + +/** Charts with this many values or fewer carry a visually hidden data table. */ +export const DATA_TABLE_LIMIT = 60; + +/** + * Axis bounds for lines: some room below the lowest point, and the top snapped to the next round + * tick when the data comes close to it. Ticks are the round steps inside the bounds. + */ +export function lineScale(values: readonly number[], count = 3) { + const min = Math.min(...values); + const max = Math.max(...values); + const span = max - min || Math.abs(max) || 1; + const step = niceStep(span, count); + const ceiling = Math.ceil(max / step) * step; + const low = min - span * 0.12; + const high = ceiling - max < step * 0.35 ? ceiling : max + span * 0.12; + const ticks: number[] = []; + for (let tick = Math.ceil(low / step) * step; tick <= high + 1e-9; tick += step) if (tick > low) ticks.push(Number(tick.toFixed(6))); + return { low, high, ticks }; +} + +/** + * Where a value sits on fixed steps such as `0, 1k, 10k, 50k, 100k, 500k, 1M`: zero and below on + * the first step, others placed on a log scale between the two steps around them, so 25k sits + * a little past halfway from 10k to 50k. Values past the last step stay on it. + */ +export function logStepPosition(value: number, steps: readonly number[]) { + if (!steps.length) return 0; + if (value <= steps[0]) return 0; + for (let index = 1; index < steps.length; index += 1) { + if (value > steps[index]) continue; + const low = steps[index - 1]; + const high = steps[index]; + if (value === high) return index; + // A zero step has no log; values below the next step's half-way read as just past it. + const from = low > 0 ? Math.log10(low) : Math.log10(high) - 1; + const share = Math.max(0, (Math.log10(value) - from) / (Math.log10(high) - from)); + return index - 1 + Math.min(1, share); + } + return steps.length - 1; +} diff --git a/app/ui/ad-networks/viz/heatmap.tsx b/app/ui/ad-networks/viz/heatmap.tsx new file mode 100644 index 00000000..e1a4ebc6 --- /dev/null +++ b/app/ui/ad-networks/viz/heatmap.tsx @@ -0,0 +1,63 @@ +"use client"; + +import Link from "next/link"; +import { useState, type CSSProperties } from "react"; +import styles from "./viz.module.css"; + +export type HeatmapColumn = { key: string; label: string }; +export type HeatmapRow = { label: string; href?: string; values: readonly (number | null)[] }; + +/** + * A grid of rows by columns where darker accent cells mean bigger values (mockup niche by network + * grid). Values are whole percents or counts, printed in each cell. Hovering a cell outlines its + * row and column. The first column stays put while the grid scrolls sideways on phones. + */ +export function Heatmap({ caption, columns, corner, rows, unitNote }: { + caption: string; + columns: readonly HeatmapColumn[]; + /** Header of the row-name column, such as `niche`. */ + corner: string; + rows: readonly HeatmapRow[]; + /** A short mono line under the grid, such as `Numbers are % of the niche's sites.` */ + unitNote?: string; +}) { + const [cross, setCross] = useState<{ row: number; column: number } | null>(null); + const top = Math.max(1, ...rows.flatMap((row) => row.values.map((value) => value ?? 0))); + return ( + <> +
          + setCross(null)}> + + + + + {columns.map((column) => )} + + + + {rows.map((row, rowIndex) => ( + + + {row.values.map((value, columnIndex) => { + const alpha = value === null ? 0 : 0.05 + 0.9 * (value / top); + return ( + + ); + })} + + ))} + +
          {caption}
          {corner}{column.label}
          {row.href ? {row.label} : row.label}= 0.5 || undefined} + data-x={cross?.column === columnIndex || undefined} + key={columns[columnIndex]?.key ?? columnIndex} + onMouseEnter={() => setCross({ row: rowIndex, column: columnIndex })} + style={{ "--a": alpha.toFixed(2) } as CSSProperties} + > + {value ?? ""} +
          +
          + {unitNote ?

          {unitNote}

          : null} + + ); +} diff --git a/app/ui/ad-networks/viz/history-gantt.tsx b/app/ui/ad-networks/viz/history-gantt.tsx new file mode 100644 index 00000000..4bd7091a --- /dev/null +++ b/app/ui/ad-networks/viz/history-gantt.tsx @@ -0,0 +1,68 @@ +import type { CSSProperties } from "react"; +import { clamp, onAccent, toneColor, type VizTone } from "./format"; +import styles from "./viz.module.css"; + +export type GanttRow = { + label: string; + /** `YYYY-MM`, the month the network first appeared. */ + from: string; + /** `YYYY-MM`, the last month it appeared, or null while it still runs. */ + to: string | null; + tone: VizTone; + /** Added after the dates, such as `not confirmed`. */ + note?: string; +}; + +const monthFormat = new Intl.DateTimeFormat("en-US", { month: "short", year: "numeric", timeZone: "UTC" }); +const monthText = (value: string) => monthFormat.format(new Date(`${value}-01T00:00:00Z`)); +const monthLong = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", timeZone: "UTC" }); +const monthLongText = (value: string) => monthLong.format(new Date(`${value}-01T00:00:00Z`)); +// Where a label goes on wide screens, from the bar's share of the track. The track is at least +// this wide above 640px; below that every label sits on its own line under its bar. +const narrowestTrack = 560; +const charWidth = 7.2; +const barPadding = 16; + +const position = (value: string) => { + const [year, month] = value.split("-").map(Number); + return year + ((month || 1) - 1) / 12; +}; + +/** + * One bar per network from the month it first appeared to the month it last did, across whole + * years (mockup history timeline). Pass `endYear` as the current year so open bars run to "now". + * The whole chart is one image with a plain sentence for screen readers. + */ +export function HistoryGantt({ endYear, rows, startYear }: { endYear: number; rows: readonly GanttRow[]; startYear?: number }) { + const first = startYear ?? Math.min(endYear, ...rows.map((row) => Math.floor(position(row.from)))); + const years = Array.from({ length: endYear - first + 1 }, (_, index) => first + index); + const span = years.length; + const at = (value: number) => clamp(((value - first) / span) * 100, 0, 100); + const summary = rows.map((row) => `${row.label} ${row.to === row.from ? `in ${monthLongText(row.from)}` : `from ${monthLongText(row.from)} to ${row.to ? monthLongText(row.to) : "now"}`}${row.note ? `, ${row.note}` : ""}.`).join(" "); + return ( +
          +
          {years.map((year) => {year})}
          + {rows.map((row) => { + const left = at(position(row.from)); + const right = row.to ? at(position(row.to) + 1 / 12) : 100; + const width = Math.max(right - left, 1); + // One month reads as that month, not "Sep 2026 to Sep 2026". + const dates = row.to === row.from ? monthText(row.from) : `${monthText(row.from)} to ${row.to ? monthText(row.to) : "now"}`; + const text = `${dates}${row.note ? ` · ${row.note}` : ""}`; + const textShare = ((text.length * charWidth + barPadding) / narrowestTrack) * 100; + // Inside the bar when it fits, else after it, else before it. A label never gets cut short. + const place = width >= textShare ? "inside" : right + textShare <= 100 ? "after" : left >= textShare ? "before" : "inside"; + const textStyle = { "--left": `${left}%`, "--right": `${right}%`, "--width": `${width}%` } as CSSProperties; + return ( +
          + {row.label} +
          + + {text} +
          +
          + ); + })} +
          + ); +} diff --git a/app/ui/ad-networks/viz/joined-left.tsx b/app/ui/ad-networks/viz/joined-left.tsx new file mode 100644 index 00000000..8ce38810 --- /dev/null +++ b/app/ui/ad-networks/viz/joined-left.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useState } from "react"; +import { InteractiveChart, svgSize, useChartWidth } from "./chart-frame"; +import { formatNumber, formatSigned } from "./format"; +import { VizDataTable, VizLegend } from "./parts"; +import styles from "./viz.module.css"; + +export type JoinedLeftPoint = { + /** The day or week, such as `Oct 3` or `Week of Sep 28`. */ + label: string; + joined: number; + left: number; + /** Position on the period's timeline, 0 for the first day or week. Needed with `slots`. */ + slot?: number; +}; + +export type JoinedLeftPeriod = { + /** Stable key, such as `30`. */ + key: string; + /** Switch label, such as `30 days`. */ + label: string; + points: readonly JoinedLeftPoint[]; + /** Words under the first bar, such as `30 days ago` or `13 weeks ago`. */ + startLabel: string; + /** Words under the last bar. Defaults to `today`. */ + endLabel?: string; + /** + * Days or weeks in the period. Set it when `points` holds only the days with changes: each bar + * then sits at its `slot`, and days with no changes stay empty instead of the bars stretching. + */ + slots?: number; +}; + +const height = 120; +const middle = 74; + +/** + * Sites that joined (accent, above the line) and left (neutral, below) per day or week, with a + * 30 days / 90 days switch and a legend of totals and the net change (mockup joined and left). + * Hover, tap, or the arrow keys show one bar pair. The switch hides when there is one period. + */ +export function JoinedLeftBars({ caption, periods }: { caption: string; periods: readonly JoinedLeftPeriod[] }) { + const [key, setKey] = useState(periods[0]?.key ?? ""); + const [active, setActive] = useState(-1); + const { measured, ref, width } = useChartWidth(560); + const period = periods.find((item) => item.key === key) ?? periods[0]; + if (!period) return null; + + const { points } = period; + const count = points.length; + const slotted = Boolean(period.slots) && points.every((point) => point.slot !== undefined); + const barWidth = width / Math.max(1, slotted ? period.slots! : count); + const slotOf = (index: number) => (slotted ? points[index].slot! : index); + // With slots, a pointer over an empty day reads the nearest day with changes. + const indexAt = (px: number) => { + const slot = Math.floor(px / barWidth); + if (!slotted) return slot; + let best = 0; + points.forEach((point, index) => { if (Math.abs(point.slot! - slot) < Math.abs(points[best].slot! - slot)) best = index; }); + return best; + }; + const topMax = Math.max(1, ...points.map((point) => point.joined)); + const bottomMax = Math.max(1, ...points.map((point) => point.left)); + const scale = Math.min((middle - 4) / topMax, (height - 16 - middle - 2) / bottomMax); + const joined = points.reduce((sum, point) => sum + point.joined, 0); + const left = points.reduce((sum, point) => sum + point.left, 0); + + return ( + <> + {periods.length > 1 ? ( +
          + {periods.map((item) => ( + + ))} +
          + ) : null} + ({ + x: slotOf(index) * barWidth + barWidth / 2, + y: middle - points[index].joined * scale, + tip: <>{points[index].label} · {formatNumber(points[index].joined)} joined · {formatNumber(points[index].left)} left, + })} + width={width} + > + + + + [point.label, point.joined, point.left])} /> + + ); +} diff --git a/app/ui/ad-networks/viz/lists.tsx b/app/ui/ad-networks/viz/lists.tsx new file mode 100644 index 00000000..764b699a --- /dev/null +++ b/app/ui/ad-networks/viz/lists.tsx @@ -0,0 +1,34 @@ +import Link from "next/link"; +import type { ReactNode } from "react"; +import styles from "./viz.module.css"; + +export type NewestSite = { domain: string; href?: string; network: string; networkHref?: string; date: string }; + +/** The newest sites with their network and the day we found them (mockup `table.newest`). */ +export function NewestList({ caption, sites }: { caption: string; sites: readonly NewestSite[] }) { + return ( + + + + {sites.map((site) => ( + + + + + + ))} + +
          {caption}
          {site.href ? {site.domain} : site.domain}{site.networkHref ? {site.network} : site.network}{site.date}
          + ); +} + +/** Dated events, newest first, such as network changes on one site (mockup `ol.events`). */ +export function EventsList({ events }: { events: readonly { date: string; text: ReactNode }[] }) { + return ( +
            + {events.map((event, index) => ( +
          1. {event.date}{event.text}
          2. + ))} +
          + ); +} diff --git a/app/ui/ad-networks/viz/multi-line.tsx b/app/ui/ad-networks/viz/multi-line.tsx new file mode 100644 index 00000000..066a0e02 --- /dev/null +++ b/app/ui/ad-networks/viz/multi-line.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { InteractiveChart, svgSize, useChartWidth } from "./chart-frame"; +import { formatCompact, formatNumber, lineScale, toneColor, type VizTone } from "./format"; +import { VizDataTable, VizLegend } from "./parts"; +import styles from "./viz.module.css"; + +export type LineSeries = { + label: string; + /** Shorter name for the line-end label, such as `AdSense`. */ + shortLabel?: string; + /** One value per point, in the order of `points`. */ + values: readonly number[]; + tone: VizTone; + /** Dashed line, to tell two lines of the same tone apart. */ + dashed?: boolean; +}; + +const height = 220; +const left = 40; +const bottom = 20; +const top = 8; + +/** + * A few series over time, with names at the line ends on wide screens and a legend below (mockup + * share over time). Hover, tap, or the arrow keys list every series' value at that point, biggest + * first. + */ +export function MultiLine({ caption, points, series }: { + caption: string; + /** The x axis: short label such as `Nov`, and full label such as `Nov 2025`. */ + points: readonly { label: string; fullLabel: string }[]; + series: readonly LineSeries[]; +}) { + const { measured, ref, width } = useChartWidth(640); + const right = width < 480 ? 8 : 92; + const count = points.length; + const { high, low, ticks } = lineScale(series.flatMap((item) => item.values), 4); + const plotWidth = Math.max(1, width - left - right); + const x = (index: number) => (count > 1 ? left + (index * plotWidth) / (count - 1) : left + plotWidth / 2); + const y = (value: number) => top + (1 - (value - low) / (high - low || 1)) * (height - top - bottom); + const step = Math.max(1, Math.ceil((count * 40) / plotWidth)); + const ends = series + .map((item) => ({ name: item.shortLabel ?? item.label, y: y(item.values[count - 1] ?? low) + 4 })) + .sort((a, b) => a.y - b.y); + for (let index = 1; index < ends.length; index += 1) ends[index].y = Math.max(ends[index].y, ends[index - 1].y + 14); + + return ( + <> + (count > 1 ? Math.round((px - left) / (plotWidth / (count - 1))) : 0)} + label={caption} + measureRef={ref} + pointAt={(index) => ({ + x: x(index), + y: top, + tip: ( + <> + {points[index].fullLabel} + {[...series].sort((a, b) => (b.values[index] ?? 0) - (a.values[index] ?? 0)).map((item) => ( +
          {item.label}{formatNumber(item.values[index] ?? 0)}
          + ))} + + ), + })} + width={width} + > + +
          + ({ label: item.label, tone: item.tone, dashed: item.dashed }))} /> + item.label)]} + rows={points.map((point, index) => [point.fullLabel, ...series.map((item) => formatNumber(item.values[index] ?? 0))])} + /> + + ); +} diff --git a/app/ui/ad-networks/viz/parts.tsx b/app/ui/ad-networks/viz/parts.tsx new file mode 100644 index 00000000..df1330b8 --- /dev/null +++ b/app/ui/ad-networks/viz/parts.tsx @@ -0,0 +1,49 @@ +import type { ReactNode } from "react"; +import { DATA_TABLE_LIMIT, toneColor, type VizTone } from "./format"; +import styles from "./viz.module.css"; + +/** Leave `tone` out for a plain entry with no swatch, such as a net total. */ +export type LegendItem = { label: ReactNode; tone?: VizTone; value?: ReactNode; dashed?: boolean }; + +/** A row of swatches with names and optional bold values. `column` stacks them, as beside a donut. */ +export function VizLegend({ column, items }: { column?: boolean; items: readonly LegendItem[] }) { + return ( +
            + {items.map((item, index) => ( +
          • + {item.tone ?