Bootstrapping a shared affiliate-link library across two sites
I run affiliate links on vdaluz.com (Amazon Associates, Proton Partners, an AdGuard referral), and imperfectsystems.com’s dev-log blog was about to need the same thing. I’d already extracted a shared blog-components package, @vdaluz/astro-blog, when I split the two sites apart. Rather than write a second, slightly different affiliate system for the new site, I pulled affiliate handling into its own package too: @vdaluz/astro-affiliate.
Problem
vdaluz.com’s affiliate links had drifted. A spike measured 72 posts using 4+ different wordings for the same Amazon disclosure sentence, and one program’s disclosure text was never actually rendered anywhere despite being defined in config. Hand-typed inline disclosures don’t scale, and I was about to duplicate the whole mess on a second site.
Investigation
The fix isn’t just “write one component,” it’s making it structurally impossible to publish an affiliate link without its disclosure. That means the disclosure requirement has to live in the build, not in a person remembering to add a sentence.
I settled on a key format like amazon.atomicHabits or proton.pass: program name, dot, item key. In markdown, a link becomes [Atomic Habits](affiliate:amazon.atomicHabits). A remark plugin walks the AST at build time, resolves that into the real URL, and cross-checks every program actually used against the post’s affiliates: [amazon] frontmatter. Use an affiliate link without declaring the program, and the build fails with a message telling you exactly what to add. Compliance by construction instead of compliance by memory.
Solution
The package mirrors astro-blog’s conventions almost exactly: raw .ts/.astro source with no build step, per-path exports, dependency-free, distributed as a tag-pinned tarball (npm’s GitHub-shorthand resolution rewrites to git+ssh, which fails in CI without SSH keys, so the tarball URL is the only form that survives a CI runner). Each site supplies its own catalog, tracking tag, and disclosure text via config; the package carries zero affiliate data itself.
Two program kinds: amazon constructs the URL from an ASIN and a site’s Associates tag, links is just a flat key-to-URL map for everything else (Proton, AdGuard, whatever comes next). An <AffiliateDisclosure> component reads a post’s affiliates array and renders the right text at the top of the post body, above the fold, which is what the FTC actually wants: disclosure before the links, not buried in a footer.
Reflection
Verification took a slightly odd shape. I packed the tarball for real and installed it into vdaluz.com as an actual consumer, then ran astro check and astro build against a throwaway page exercising every export, which caught real type and runtime issues a mocked test wouldn’t have. For the remark plugin specifically, I hit a small but instructive snag: Node’s native TypeScript support strips types but still enforces plain ESM resolution, which requires explicit .ts extensions on relative imports, while the package’s source deliberately omits them since that’s correct for bundler consumption. A quick script against the real source failed with a module-not-found error that had nothing to do with the actual logic. Fix was to test the extension-sensitive parts through the real Astro pipeline, and only reach for a fast isolated Node script when the code has zero Astro surface, copying files with fixed-up extensions rather than touching the real source.
The package’s live at github.com/vdaluz/astro-affiliate, v0.1.0. Next up is actually wiring it into both sites and doing the disclosure sweep on vdaluz.com’s existing posts.
Related reading
Clearing a backlog: five small fixes in one release
A dead-decoration a11y fix, a schema field that validated but never rendered, and a formatter run that silently flipped quote style across four files.
The escaping bug that only shows up with a second query param
A rewrite function that worked in every test and every real URL it had ever seen, and would still have broken the moment someone added a second query param.
Testing the part of the codebase that documents its own footgun
A README warning about a subtle failure mode is a confession that the code isn't tested against it yet.