diff --git a/apps/web/public/styles.css b/apps/web/public/styles.css
index ee12079..d7916aa 100644
--- a/apps/web/public/styles.css
+++ b/apps/web/public/styles.css
@@ -157,6 +157,21 @@ pre code {
.small {
font-size: 0.82rem;
}
+/*
+ * The visible half of an affiliate disclosure. It sits inside the link it is
+ * disclosing, on a plain text link and on the primary order button alike, so
+ * it inherits colour and steps down only in size and weight.
+ */
+.aff-mark {
+ margin-left: 0.4em;
+ font-size: 0.7em;
+ font-weight: 400;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ opacity: 0.7;
+ vertical-align: 0.08em;
+ cursor: help;
+}
.visually-hidden,
.skip {
position: absolute;
diff --git a/apps/web/src/views/parts.jsx b/apps/web/src/views/parts.jsx
index 23c3c57..2c20e79 100644
--- a/apps/web/src/views/parts.jsx
+++ b/apps/web/src/views/parts.jsx
@@ -7,6 +7,7 @@ import {
fmtGb,
fmtMoney,
KIND_LABELS,
+ outbound,
paramsFrom,
toggle,
toggleBucket,
@@ -16,6 +17,36 @@ import { raw } from 'hono/html';
/** Shared pieces: the rack-unit row, facet rail, chips, agent panel, sparkline. */
+/**
+ * A link off the site to a provider.
+ *
+ * Routed through a referral endpoint where we have a deal with that provider,
+ * and labelled as one when it is, because a reader is entitled to know which
+ * links pay us. `rel` carries `sponsored` in that case, which is what search
+ * engines ask for on a paid link.
+ */
+export function Outbound({ url, domain, class: cls, children }) {
+ const out = outbound(url, domain);
+ if (!out.href) return null;
+ return (
+
+ {children}
+ {out.affiliate ? : null}
+
+ );
+}
+
+export function AffiliateMark() {
+ return (
+
+ affiliate
+
+ );
+}
+
export const flag = (code) =>
/^[A-Z]{2}$/.test(code)
? String.fromCodePoint(...[...code].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65))
diff --git a/apps/web/src/views/providers.jsx b/apps/web/src/views/providers.jsx
index 664d9a2..1090af6 100644
--- a/apps/web/src/views/providers.jsx
+++ b/apps/web/src/views/providers.jsx
@@ -1,7 +1,7 @@
import { config } from '@r4ck/config';
import { countryName, fmtMoney } from '@r4ck/core';
import { Layout } from './Layout.jsx';
-import { AgentPanel, Automation, flag, initials, ProviderCard, Unit } from './parts.jsx';
+import { AgentPanel, Automation, flag, initials, Outbound, ProviderCard, Unit } from './parts.jsx';
const csv = (v) =>
String(v ?? '')
@@ -249,11 +249,9 @@ export function Provider({ user, detail }) {
- {p.url ? (
-
- {p.domain ?? 'Website'} ↗
-
- ) : null}
+
+ {p.domain ?? 'Website'} ↗
+
{p.links.api_docs ? API docs : null}
{p.links.cli ? CLI : null}
{p.links.terraform ? Terraform : null}
diff --git a/apps/web/src/views/servers.jsx b/apps/web/src/views/servers.jsx
index 1c04663..e1c0825 100644
--- a/apps/web/src/views/servers.jsx
+++ b/apps/web/src/views/servers.jsx
@@ -9,6 +9,7 @@ import {
FacetRail,
flag,
KindBadge,
+ Outbound,
Pager,
Price,
Sparkline,
@@ -227,16 +228,9 @@ export function Server({ user, detail }) {
{s.price.hourly_usd ? (
≈ {fmtMoney(s.price.hourly_usd)}/hour
) : null}
- {s.url ? (
-
- Order at {s.provider.domain ?? s.provider.name} ↗
-
- ) : null}
+
+ Order at {s.provider.domain ?? s.provider.name} ↗
+
Add to compare
@@ -350,9 +344,9 @@ export function Compare({ user, result }) {
'Order',
(s) =>
s.url ? (
-
+
↗
-
+
) : (
'·'
),
diff --git a/packages/core/src/affiliates.js b/packages/core/src/affiliates.js
new file mode 100644
index 0000000..521c8f4
--- /dev/null
+++ b/packages/core/src/affiliates.js
@@ -0,0 +1,107 @@
+/**
+ * Referral links.
+ *
+ * A few providers pay a commission on a referred order. Where one does, the
+ * outbound link to that provider goes through their referral endpoint instead
+ * of straight to their site, and says so where a reader can see it.
+ *
+ * ## Why this table lives in source and not in the database
+ *
+ * `providers` is a mirror. `sync` pulls the hosting collection from NicheDB
+ * and upserts it with `coalesce(excluded.x, providers.x)` on every column, so
+ * a referral URL stored there would arrive as null on each sync and survive
+ * only by the grace of that coalesce — one upsert written differently and the
+ * deal is silently gone. It would also be invisible: nothing in the repository
+ * would record who pays us. A table in source makes the commercial
+ * relationship reviewable in a diff, and it means a provider who is not in the
+ * catalogue yet is already wired up for the day they appear.
+ *
+ * ## Landing-only links, which is what Dedirock's is
+ *
+ * Most referral endpoints set a cookie and then forward the visitor to a URL
+ * of the caller's choosing. Dedirock's is WHMCS `aff.php`, and it was measured
+ * on 2026-09-25: it answers 301 to `https://dedirock.com`, sets
+ * `WHMCSAffiliateID` for ninety days, and ignores both `url` and `goto`.
+ * Passing `aff=960` inline on a billing page sets no cookie at all. There is
+ * therefore no way to earn the referral *and* land on a named plan.
+ *
+ * So a landing-only link replaces a link to the provider's front door, where
+ * nothing is lost because that is where it lands anyway, and never replaces a
+ * deep link to a particular offer. Sending somebody who clicked a specific
+ * plan to a homepage instead, to earn a commission, would be charging the
+ * reader for our revenue — which is the thing that makes a directory worth
+ * nothing. An entry without `landingOnly` is free to replace any link.
+ */
+
+/** domain -> referral endpoint. Keys are registrable domains, lowercase. */
+export const AFFILIATES = {
+ 'dedirock.com': {
+ url: 'https://billing.dedirock.com/aff.php?aff=960',
+ landingOnly: true,
+ network: 'whmcs',
+ },
+};
+
+/** The rel for a paid link. `sponsored` is the value search engines ask for. */
+export const AFFILIATE_REL = 'noopener nofollow sponsored';
+export const PLAIN_REL = 'noopener nofollow';
+
+export function hostOf(url) {
+ try {
+ return new URL(String(url)).hostname.replace(/^www\./, '').toLowerCase();
+ } catch {
+ return null;
+ }
+}
+
+/** A domain however it was written — bare, with www, or as a whole URL. */
+function key(domain) {
+ if (!domain) return null;
+ const d = String(domain).trim().toLowerCase();
+ if (!d) return null;
+ if (d.includes('://')) return hostOf(d);
+ return d.replace(/^www\./, '').replace(/[/?#].*$/, '') || null;
+}
+
+/** The deal for a domain, or null. A subdomain still belongs to its provider. */
+export function affiliateFor(domain) {
+ const k = key(domain);
+ if (!k) return null;
+ if (AFFILIATES[k]) return AFFILIATES[k];
+ for (const [d, entry] of Object.entries(AFFILIATES)) {
+ if (k.endsWith(`.${d}`)) return entry;
+ }
+ return null;
+}
+
+/**
+ * Is this URL the provider's front door rather than a particular page?
+ * Only a front door may be swapped for a landing-only referral link.
+ */
+export function isFrontDoor(url) {
+ let u;
+ try {
+ u = new URL(String(url));
+ } catch {
+ return false;
+ }
+ return (u.pathname === '' || u.pathname === '/') && u.search === '';
+}
+
+/**
+ * Where an outbound link should actually point, and how to mark it.
+ * Returns the original URL untouched when no deal applies, so this is safe to
+ * wrap around every outbound link in the site.
+ */
+export function outbound(url, domain) {
+ // An empty string is no link at all, and would render an pointing at
+ // the current page, so it is normalised away rather than passed through.
+ const plain = { href: url || null, affiliate: false, rel: PLAIN_REL };
+ if (!url) return plain;
+
+ const entry = affiliateFor(domain ?? hostOf(url));
+ if (!entry) return plain;
+ if (entry.landingOnly && !isFrontDoor(url)) return plain;
+
+ return { href: entry.url, affiliate: true, rel: AFFILIATE_REL };
+}
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index 2c34476..78739a9 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -1,3 +1,4 @@
+export * from './affiliates.js';
export * from './countries.js';
export * from './facets.js';
export * from './fx.js';
diff --git a/test/affiliates.test.js b/test/affiliates.test.js
new file mode 100644
index 0000000..43e0010
--- /dev/null
+++ b/test/affiliates.test.js
@@ -0,0 +1,141 @@
+import { describe, expect, test } from 'bun:test';
+import {
+ AFFILIATE_REL,
+ AFFILIATES,
+ affiliateFor,
+ hostOf,
+ isFrontDoor,
+ outbound,
+ PLAIN_REL,
+} from '@r4ck/core';
+
+const DEDIROCK = 'https://billing.dedirock.com/aff.php?aff=960';
+
+describe('the table itself', () => {
+ test('Dedirock is in it, with the referral URL we were given', () => {
+ expect(AFFILIATES['dedirock.com'].url).toBe(DEDIROCK);
+ });
+
+ test('every key is a bare lowercase domain, because that is what lookups use', () => {
+ for (const key of Object.keys(AFFILIATES)) {
+ expect(key).toBe(key.toLowerCase());
+ expect(key).not.toContain('://');
+ expect(key).not.toContain('/');
+ expect(key.startsWith('www.')).toBe(false);
+ }
+ });
+
+ test('every entry points somewhere absolute and https', () => {
+ for (const entry of Object.values(AFFILIATES)) {
+ expect(() => new URL(entry.url)).not.toThrow();
+ expect(new URL(entry.url).protocol).toBe('https:');
+ }
+ });
+});
+
+describe('affiliateFor', () => {
+ test('finds a provider by its bare domain', () => {
+ expect(affiliateFor('dedirock.com')?.url).toBe(DEDIROCK);
+ });
+
+ test('does not care about www, case or a stray path', () => {
+ expect(affiliateFor('www.DediRock.com')?.url).toBe(DEDIROCK);
+ expect(affiliateFor('dedirock.com/vps')?.url).toBe(DEDIROCK);
+ expect(affiliateFor('https://www.dedirock.com/')?.url).toBe(DEDIROCK);
+ });
+
+ test('a subdomain still belongs to the provider', () => {
+ expect(affiliateFor('billing.dedirock.com')?.url).toBe(DEDIROCK);
+ });
+
+ test('a domain that merely ends in the same letters is not a match', () => {
+ // The guard is on the dot: "notdedirock.com" must not borrow the deal.
+ expect(affiliateFor('notdedirock.com')).toBeNull();
+ expect(affiliateFor('dedirock.com.evil.example')).toBeNull();
+ });
+
+ test('is null for a provider we have no deal with, and for nothing at all', () => {
+ expect(affiliateFor('hetzner.com')).toBeNull();
+ expect(affiliateFor('')).toBeNull();
+ expect(affiliateFor(null)).toBeNull();
+ expect(affiliateFor(undefined)).toBeNull();
+ });
+});
+
+describe('isFrontDoor', () => {
+ test('a bare origin is, with or without the trailing slash', () => {
+ expect(isFrontDoor('https://dedirock.com')).toBe(true);
+ expect(isFrontDoor('https://dedirock.com/')).toBe(true);
+ });
+
+ test('a path or a query is not', () => {
+ expect(isFrontDoor('https://dedirock.com/vps')).toBe(false);
+ expect(isFrontDoor('https://dedirock.com/?plan=9')).toBe(false);
+ });
+
+ test('a hash alone still counts as the front door', () => {
+ expect(isFrontDoor('https://dedirock.com/#pricing')).toBe(true);
+ });
+
+ test('nonsense is not a front door rather than throwing', () => {
+ expect(isFrontDoor('not a url')).toBe(false);
+ expect(isFrontDoor(null)).toBe(false);
+ });
+});
+
+describe('outbound', () => {
+ test('a provider with no deal is returned exactly as it came in', () => {
+ const out = outbound('https://hetzner.com/', 'hetzner.com');
+ expect(out.href).toBe('https://hetzner.com/');
+ expect(out.affiliate).toBe(false);
+ expect(out.rel).toBe(PLAIN_REL);
+ });
+
+ test('the front door is swapped for the referral link and marked sponsored', () => {
+ const out = outbound('https://dedirock.com/', 'dedirock.com');
+ expect(out.href).toBe(DEDIROCK);
+ expect(out.affiliate).toBe(true);
+ expect(out.rel).toBe(AFFILIATE_REL);
+ expect(out.rel).toContain('sponsored');
+ });
+
+ test('it works out the provider from the URL when no domain is stored', () => {
+ expect(outbound('https://www.dedirock.com/').href).toBe(DEDIROCK);
+ });
+
+ /*
+ * The one that matters. aff.php was measured on 2026-09-25: it 301s to
+ * https://dedirock.com and ignores `url` and `goto`, so swapping it in for a
+ * deep link would silently drop somebody who clicked a named plan onto the
+ * homepage. Earning a commission is not worth that, so a deep link is left
+ * alone and simply earns nothing.
+ */
+ test('a deep link to a named offer is left alone, commission or not', () => {
+ const deep = 'https://dedirock.com/kvm-vps/ryzen-4gb';
+ const out = outbound(deep, 'dedirock.com');
+ expect(out.href).toBe(deep);
+ expect(out.affiliate).toBe(false);
+ expect(out.rel).toBe(PLAIN_REL);
+ });
+
+ test('a deep link into the billing subdomain is left alone too', () => {
+ const cart = 'https://billing.dedirock.com/index.php?rp=/store/vps';
+ expect(outbound(cart, 'dedirock.com').href).toBe(cart);
+ });
+
+ test('a missing URL stays missing rather than becoming a referral link', () => {
+ expect(outbound(null, 'dedirock.com').href).toBeNull();
+ expect(outbound(undefined, 'dedirock.com').affiliate).toBe(false);
+ expect(outbound('', 'dedirock.com').href).toBeNull();
+ });
+});
+
+describe('hostOf', () => {
+ test('strips www and lowercases', () => {
+ expect(hostOf('https://WWW.Example.com/x')).toBe('example.com');
+ });
+
+ test('is null for something that is not a URL', () => {
+ expect(hostOf('nope')).toBeNull();
+ });
+});