Legal audit of a real blog: what we found on codigoagente and how we fixed it
In part one we covered which US, EU and Ecuador laws affect an AI-built app or site and put together an audit prompt. Here we apply it to a real case: this very blog, a Next.js 16 and Sanity site deployed on Vercel and built largely with Claude Code. You'll see the inventory, the curl checks, before and after screenshots and the code we changed. The screenshots show the Spanish version of the site.
The inventory: what the site actually does
Before fixing anything, we listed what the site does with visitors' data:
| Question | Answer for codigoagente |
|---|---|
| Sign-up, forms or comments? | No |
| Newsletter or marketing emails? | No |
| Subscriptions or payments? | No |
| Analytics, ads or session replay? | No |
| Third-party resources? | Images from `cdn.sanity.io` (Sanity's CDN) |
| Affiliate links? | Yes, to Contabo in the Paymenter guide |
| Contact? | One email address, `contacto@codigoagente.com` |
| Audience? | Ecuador, the rest of Latin America, Spain and English-speaking readers |
| AI features for readers? | No (content is written with AI assistance, but readers don't interact with any AI) |
That rules several laws out: COPPA (not a children's service and no data collected), CIPA (no recording or chat), CAN-SPAM and Article 50 of Ecuador's Electronic Commerce Act (no emails), ROSCA (no subscriptions) and the DMCA (readers can't publish anything). Still in play: Ecuador's LOPDP, the GDPR because of readers in Spain, and the FTC's guidance on affiliate links.
Check 1: fonts and third-party resources
The site uses the Geist fonts via next/font/google. That module downloads the fonts at build time and serves them from the site's own domain, so the reader's browser never contacts Google. We checked it on the live site:
curl -s https://codigoagente.vercel.app/ | grep -c "fonts.googleapis.com\|fonts.gstatic.com"
# 0
curl -s https://codigoagente.vercel.app/ | grep -oE '[^"]*\.woff2' | head -2
# /_next/static/immutable/media/797e433ab948586e-s.p.1v5bejj26fx9h.woff2
# /_next/static/immutable/media/caa3a2e1cccd8315-s.p.0zr6hhvz-h9nw.woff2Result: fine. Zero calls to Google; fonts come from /_next/static/.
Listing every external domain on an article with images turned up another third party:
curl -s https://codigoagente.vercel.app/blog/instalar-paymenter-vps-coolify-docker \
| grep -oE '(src|href)="https?://[^"/]+' | sort | uniq -c
# 3 href="https://contabo.com
# 1 href="https://github.com
# 2 href="https://safebuildlabs.com
# 9 src="https://cdn.sanity.ioThe hrefs are links the reader chooses to click. The srcs on cdn.sanity.io are not: the browser loads them automatically and Sanity receives the reader's IP, just like Google with fonts. There are two options: serve images through the site's own domain (for example with next/image, at the cost of Vercel image optimisation quota) or disclose it transparently. We chose the latter for now: Sanity was already a data processor, and the policy now states exactly what it receives.
Check 2: cookies
curl -sI https://codigoagente.vercel.app/ | grep -ci "set-cookie"
# 0The public site sets no cookies and doesn't use localStorage (confirmed by searching src/ for document.cookie, localStorage and cookies()). The cookie policy said otherwise: it described a consent banner that doesn't exist, AdSense cookies that were never enabled, and left gaps like [PROVEEDOR PENDIENTE] ("provider pending").

Fix: we rewrote src/app/(es)/cookies/page.tsx with a real inventory (pages and fonts from Vercel, images from Sanity, no analytics or ads) and explained why there is no banner. If analytics is added tomorrow, the page already says consent will be requested first.
Check 3: the privacy policy
Same problem, but more serious, because the LOPDP (Art. 12) requires fair and transparent information. The old version mentioned "aggregate analytics" and "advertising through Google AdSense" based on consent, a banner, and keeping logs "for a maximum of [N] days".

Fix in src/app/(es)/privacidad/page.tsx:
- Data processed: only connection data and the emails we receive.
- Named providers: Vercel (hosting), Sanity (content and image CDN) and the domain's email forwarding.
- A concrete deadline for rights requests: 15 days, as set by the LOPDP.
- The right to complain to the Superintendencia de Protección de Datos Personales.
- A commitment to update the policy before adding analytics, ads or forms.
We also removed the "[N] business days" from the contact page and the mention of "advertising space" from the terms, because the site shows no ads.
Check 4: affiliate links
The Paymenter guide links to Contabo with an affiliate link. The site already marked those links with rel="sponsored" for Google and showed a "Tools mentioned" box with the disclosure, but at the end of the article. A reader clicking the link in the third paragraph had seen no notice, and the FTC expects the disclosure close to the link.
Fix 1: a notice at the top of every article with affiliate links.

Fix 2: an "affiliate" tag next to every paid link in the body.

The change is in src/components/organisms/post-body.tsx: the component that renders Portable Text links already knew whether a link was sponsored, so adding the tag next to it was enough:
link: ({ value, children }) => {
const href: string = value?.href ?? "#";
const external = href.startsWith("http");
const sponsored = Boolean(value?.sponsored);
const rel = sponsored ? "sponsored noopener noreferrer" : "noopener noreferrer";
return (
<>
<a href={href} {...(external ? { target: "_blank", rel } : {})}>
{children}
</a>
{sponsored ? (
<span className="ml-1 rounded border px-1 align-middle font-mono text-[0.7em] text-muted-foreground no-underline">
{affiliateMark}
</span>
) : null}
</>
);
},The tag and notice text come from the src/lib/i18n.ts dictionary, so the English site shows "affiliate". The top notice is added in src/components/templates/post-template.tsx when the article has affiliateLinks.
What's still pending (and why we didn't fix it ourselves)
An honest audit also writes down what can't be closed in code:
- Controller details. The LOPDP (Art. 12(8)) requires disclosing the controller's address, phone and email. codigoagente is an anonymous brand and currently only publishes the email and city. Meeting this without exposing an individual means, for example, running the blog through a company or with a business address. That's a business decision, not a code change.
- English policies. The legal pages exist only in Spanish, and the English footer says so. For US and EU readers, an English version is worth having.
- Images from Sanity's CDN. It's disclosed, but if the site grows with EU readers, serving them from its own domain removes the issue entirely.
- AI-written content. The AI Act requires disclosing AI-generated text published to inform the public on matters of public interest, unless it undergoes human review with editorial responsibility. This blog is technical and reviewed before publishing, but a note on how articles are written adds transparency.
Summary of changes
| Risk | Law | Applies? | Change | File |
|---|---|---|---|---|
| Google Fonts | GDPR | Yes | Already compliant via `next/font` | `src/app/layout.tsx` |
| Third-party images | GDPR / LOPDP | Yes | Disclosed in privacy and cookie policies | `privacidad/page.tsx`, `cookies/page.tsx` |
| Policy not matching reality | LOPDP Art. 12 | Yes | Rewritten | `privacidad/page.tsx`, `cookies/page.tsx` |
| Gaps in contact page and terms | LOPDP | Yes | 15-day deadline, no ads | `contacto/page.tsx`, `terminos/page.tsx` |
| Affiliate links without nearby notice | FTC | Yes | Top notice and tag next to each link | `post-body.tsx`, `post-template.tsx`, `i18n.ts` |
| Minors | COPPA / LOPDP Art. 21 | No | — | — |
| Session replay | CIPA | No | — | — |
| Marketing emails | CAN-SPAM / ECA Art. 50 | No | — | — |
| Subscriptions | ROSCA / California ARL | No | — | — |
| User content | DMCA | No | — | — |
Before pushing, pnpm lint and pnpm build passed without errors, and we checked on a local production server that the notice and tags appear only on articles with affiliate links.
FAQ
How long does an audit like this take?
For a blog like this one, the inventory and curl checks take under an hour. The code fixes, another hour. What takes longest is deciding the non-technical pending items.
Can I use the same prompt on my project?
Yes, it's the one from part one. The key is step 1: Claude takes inventory before touching anything and asks you what it can't infer.
Why not add a cookie banner "just in case"?
Because a banner with no cookies to manage confuses readers and adds a component to maintain. If there are no non-essential cookies, the right move is to say so in the policy.
Conclusion
Most issues weren't technical but about consistency: template-generated legal pages describing a site that doesn't exist. The fixes were small: two rewritten policies, two gaps removed and an affiliate notice where readers actually see it. If your project is AI-built, start with the inventory from part one and review your own legal pages with the same eye.
// implementation
More on AI agents

- Skills for your agents
- Claude Code

- Skills for your agents
- Claude Code

- Skills for your agents
- Claude Code