Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/web/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/views/parts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
fmtGb,
fmtMoney,
KIND_LABELS,
outbound,
paramsFrom,
toggle,
toggleBucket,
Expand All @@ -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 (
<a class={cls} href={out.href} rel={out.rel} target="_blank">
{children}
{out.affiliate ? <AffiliateMark /> : null}
</a>
);
}

export function AffiliateMark() {
return (
<span
class="aff-mark"
title="Affiliate link: r4ck.dev may earn a commission on an order. It buys no ranking and costs you nothing."
>
affiliate
</span>
);
}

export const flag = (code) =>
/^[A-Z]{2}$/.test(code)
? String.fromCodePoint(...[...code].map((c) => 0x1f1e6 + c.charCodeAt(0) - 65))
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/views/providers.jsx
Original file line number Diff line number Diff line change
@@ -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 ?? '')
Expand Down Expand Up @@ -249,11 +249,9 @@ export function Provider({ user, detail }) {
<Automation list={p.automation} />
</p>
<p class="links">
{p.url ? (
<a href={p.url} rel="noopener nofollow" target="_blank">
{p.domain ?? 'Website'} ↗
</a>
) : null}
<Outbound url={p.url} domain={p.domain}>
{p.domain ?? 'Website'} ↗
</Outbound>
{p.links.api_docs ? <a href={p.links.api_docs}>API docs</a> : null}
{p.links.cli ? <a href={p.links.cli}>CLI</a> : null}
{p.links.terraform ? <a href={p.links.terraform}>Terraform</a> : null}
Expand Down
18 changes: 6 additions & 12 deletions apps/web/src/views/servers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
FacetRail,
flag,
KindBadge,
Outbound,
Pager,
Price,
Sparkline,
Expand Down Expand Up @@ -227,16 +228,9 @@ export function Server({ user, detail }) {
{s.price.hourly_usd ? (
<span class="muted small">≈ {fmtMoney(s.price.hourly_usd)}/hour</span>
) : null}
{s.url ? (
<a
class="button primary"
href={s.url}
rel="noopener nofollow sponsored"
target="_blank"
>
Order at {s.provider.domain ?? s.provider.name} ↗
</a>
) : null}
<Outbound class="button primary" url={s.url} domain={s.provider.domain}>
Order at {s.provider.domain ?? s.provider.name} ↗
</Outbound>
<label class="compare-toggle">
<input type="checkbox" data-compare={s.id} />
<span>Add to compare</span>
Expand Down Expand Up @@ -350,9 +344,9 @@ export function Compare({ user, result }) {
'Order',
(s) =>
s.url ? (
<a href={s.url} rel="noopener nofollow" target="_blank">
<Outbound url={s.url} domain={s.provider.domain}>
↗
</a>
</Outbound>
) : (
'·'
),
Expand Down
107 changes: 107 additions & 0 deletions packages/core/src/affiliates.js
Original file line number Diff line number Diff line change
@@ -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 <a> 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 };
}
1 change: 1 addition & 0 deletions packages/core/src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './affiliates.js';
export * from './countries.js';
export * from './facets.js';
export * from './fx.js';
Expand Down
141 changes: 141 additions & 0 deletions test/affiliates.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
Loading