Ewout
Door Ewout
12 oktober 202614 min. leestijd

Google Ads als code: van klikken naar een diff

Company As CodeDevOpsAI
Gegenereerde laptoppresentatie met een gerenderd voorbeeld van onze Google Search-advertentie voor software op maat

Google Search is voor een softwarebedrijf als het onze nog steeds een belangrijke bron van leads. Iemand die zoekt op 'software op maat laten maken' heeft vaak al een concreet probleem. Daar willen we vindbaar zijn. Ook nu een steeds groter deel van onze eigen werkdag zich afspeelt in een gesprek met een Woordenboek · In het kortLLMLLM staat voor large language model: een AI-model dat op grote hoeveelheden tekst is getraind. Het kan taal verwerken en tekst genereren, zoals antwoorden, samenvattingen en code.Lees meer⁠.

Alleen: vraag een developer wie er zin heeft om een middag door de Google Ads-console te klikken, en het wordt opvallend stil. Bij ons in ieder geval wel. Campagnes, advertentiegroepen, zoekwoorden, uitsluitingen, biedstrategieën. We weten heel goed hoe we software moeten bouwen. Van advertenties weten we een stuk minder.

Onze gebruikelijke reactie op zo'n situatie laat zich raden: kan dit ook als code?

Inmiddels staan onze Google Ads-campagnes in Woordenboek · In het kort42KB42KB is de centrale repository van 10KB voor code, bedrijfskennis, documentatie en interne tooling. Het is de plek waar we onze aanpak van company as code in de praktijk brengen.Lees meer⁠, de repository waarin we steeds meer van ons bedrijf beheren. We beheren ze met leesbare bestanden, validatie, een plan voordat er iets verandert en een statefile op S3. Dat past bij Woordenboek · In het kortcompany as codeCompany as code is een aanpak waarbij je bedrijfskennis, werkwijzen en instellingen vastlegt in tekstbestanden en code. Zo kun je ze beheren met versiebeheer, controles en automatisering.Lees meer⁠, ons idee om het werk rond ons bedrijf te organiseren met dezelfde gereedschappen als onze softwareontwikkeling.

Eerst de website, toen de advertenties

Onze hele website schrijven we in Woordenboek · In het kortMDXMDX is een bestandsformaat waarin je Markdown kunt combineren met JSX, JavaScript-expressies en import- en exportinstructies.Lees meer⁠, een combinatie van Woordenboek · In het kortMarkdownMarkdown is een tekstnotatie waarmee je gewone tekst structuur geeft, bijvoorbeeld met koppen, lijsten, nadruk en links.Lees meer⁠ en Woordenboek · In het kortcomponentEen component is een afgebakend onderdeel van software met een eigen taak en een duidelijke manier om met andere onderdelen samen te werken.Lees meer⁠. Tekst, metadata en aanwijzingen voor de opmaak staan in bestanden. Qwik en onze plugins bouwen daar de pagina's van. Een wijziging kunnen we zoeken, vergelijken en reviewen in dezelfde editor waarin we software ontwikkelen. Hoe dat werkt, vertellen we in de case over onze eigen website.

Dat wilden we ook voor onze advertenties. De Google Ads-interface vinden we onoverzichtelijk. Je klikt van een campagne naar een advertentiegroep en dan weer naar een instelling ergens anders. Even over alle advertenties Woordenboek · In het kortgreppenGreppen is tekst doorzoeken op een woord of patroon, meestal in meerdere bestanden of in de uitvoer van een programma. De term komt van het Unix-programma grep.Lees meer⁠ om een formulering terug te vinden zit er niet in. Een simpele global find and replace, zoals we die in onze editor doen, evenmin. In tekstbestanden kunnen we dat gewoon doen en daarna de wijzigingen naast elkaar bekijken.

Omdat de bestanden in Woordenboek · In het kortversiebeheerVersiebeheer legt wijzigingen in code vast, inclusief wie wat heeft aangepast en wanneer. Zo kunnen ontwikkelaars samenwerken, veranderingen vergelijken en teruggaan naar een eerdere versie.Lees meer⁠ staan, krijgen we er automatisch een Woordenboek · In het kortaudit trailEen audit trail is een chronologisch overzicht van handelingen en wijzigingen in een systeem. Het maakt achteraf zichtbaar wie of wat iets deed, wanneer dat gebeurde en wat er veranderde.Lees meer⁠ bij: wie heeft wat wanneer gewijzigd? Bevalt een wijziging niet, dan kunnen we eenvoudig een Woordenboek · In het kortrollbackEen rollback is het terugdraaien van een wijziging, zodat software, configuratie of gegevens terugkeren naar een eerdere toestand. Teams gebruiken dit wanneer een wijziging fouten of andere ongewenste gevolgen veroorzaakt.Lees meer⁠ doen: de eerdere configuratie terugzetten en daar opnieuw een plan voor laten maken. Na goedkeuring past de tool die instellingen weer toe in Google.

Dus kreeg iedere landingspagina een campagnebestand ernaast. De pagina en de advertentie gaan over dezelfde zoekintentie en dienst. Door ze bij elkaar te bewaren, kunnen we ze ook samen aanpassen:

apps/website/
├── ads.yaml
└── pages/landingspagina's/software-op-maat/
    ├── nl.mdx
    ├── en.mdx
    └── campaign.mdx

MDX is een wat ongebruikelijke keuze voor advertentieconfiguratie. Het paste wel bij de rest van onze website. De instellingen staan bovenaan in YAML, de advertentieteksten eronder als Markdown. Zo kunnen we dezelfde bestandsindeling blijven gebruiken voor pagina's en advertenties.

In deze indeling bevat ads.yaml de gedeelde instellingen, zoals het doelland en de taal. nl.mdx en en.mdx zijn de twee versies van de landingspagina. campaign.mdx bevat de campagne en wordt alleen door onze Ads-tooling gelezen. Die haalt de instellingen en teksten uit het bestand zonder MDX-code uit te voeren.

Onze eigen Terraform voor Google Ads

Bij de website kunnen we iedere keer alle HTML opnieuw genereren en uploaden. Bij advertenties zou dat een probleem opleveren. Een campagne in Google heeft een eigen ID, instellingen en opgebouwde historie. Als we bij iedere wijziging een nieuwe campagne aanmaken, verliezen we het verband met wat er al draait. En hebben we er opeens twee die geld kunnen uitgeven.

We wilden Google Ads op precies dezelfde manier beheren als onze infrastructuur via Terraform. De logische oplossing was dus natuurlijk: we implementeren onze eigen Terraform voor Google Ads.

Met Terraform beschrijf je in bestanden hoe je infrastructuur eruit moet zien. Bijvoorbeeld welke servers er moeten zijn en hoe die zijn ingesteld. Dat heet Woordenboek · In het kortdeclaratiefDeclaratief betekent dat je beschrijft wat de gewenste uitkomst is, zonder alle stappen vast te leggen waarmee de software die moet bereiken.Lees meer⁠ werken. De tool vergelijkt die beschrijving met wat er al bestaat en maakt een plan met de benodigde wijzigingen. Na goedkeuring voert apply dat plan uit. De volgende keer begint dezelfde vergelijking opnieuw.

Voor onze advertenties werkt dat net zo. Een voorbeeld met fictieve bedragen: we verlagen in ads.yaml het maandbudget voor maatwerk nieuwbouw van € 900 naar € 600. Voor het gemak rekenen we hier met 30 dagen per maand. De tool vindt het bestaande gedeelde budget in Google en stelt voor om het dagbudget van € 30 naar € 20 te verlagen. Dat bedrag geldt voor alle campagnes in de groep samen. Na goedkeuring voert de tool de wijziging uit. Bij de volgende vergelijking is er niets meer te doen: in het bestand en in Google staat nu hetzelfde budget.

We hebben hiervoor een tool in TypeScript gebouwd die de gewenste en bestaande instellingen vergelijkt en wijzigingen uitvoert. De meeste opdrachten gaan via Zernio's Google Ads-API. Zernio biedt de koppeling waarmee ons programma campagnes, groepen en advertenties kan beheren. Voor onder meer netwerkinstellingen en afbeeldingen gebruiken we aanvullend een directe Google Ads-client.

Met 'onze eigen Terraform' bedoelen we die werkwijze van configuratie, plan en apply. Onze tool is een zelfstandig programma en beheert de Google Search-campagnes die we ermee aanmaken. Bestaande, handmatig aangemaakte campagnes blijven buiten zijn beheer.

De campagne past in een tekstbestand

Een Google Ads-campagne bevat onder meer een budget en een planning. Binnen de campagne zitten advertentiegroepen. Die brengen de zoekwoorden en bijbehorende advertenties bij elkaar. Diezelfde indeling gebruiken we in ons bestand.

Hieronder staat een ingekorte versie van onze campagne voor software op maat. Bovenaan staat de Woordenboek · In het kortfrontmatterFrontmatter is een blok metadata aan het begin van een contentbestand, meestal met waarden die de gebruikte tool uitleest.Lees meer⁠, het blok met instellingen tussen de twee regels met streepjes:

yaml
---
id: software-op-maat
name: Software op maat
deploy: true
status: active
budgetGroup: maatwerk-nieuwbouw
schedule: weekdays
adGroups:
  maatwerk:
    keywords:
      - text: software op maat
        matchType: exact
---

De belangrijkste instellingen zijn:

  • deploy: true neem deze campagne mee in de vergelijking.
  • status: active de gewenste status wanneer we de campagne met activatie uitrollen.
  • budgetGroup gebruik het gedeelde budget en de CPC-limiet uit ads.yaml. In ons fictieve voorbeeld krijgt de hele groep € 600 per maand, oftewel € 20 per dag, met een limiet van € 15 per klik.
  • schedule: weekdays laat de advertenties op werkdagen draaien.
  • adGroups groepeer de zoekwoorden en advertenties. Hier bevat de groep maatwerk één zoekwoord.

Daaronder staan de advertentieteksten. De kop ad-group: maatwerk koppelt ze aan de groep hierboven. Binnen die groep geven we de advertentie de naam technisch-maatwerk:

markdown
## ad-group: maatwerk
### ad: technisch-maatwerk
#### headlines
- Software op maat
- Bedrijfskritische software
- Direct contact met developers
#### descriptions
- 10+ jaar maatwerkervaring. Bouw je bedrijfssoftware samen met onze developers.
- Kies de techniek samen met de developers die je software bouwen en onderhouden.

Onze parser leest die vaste koppen en lijstjes. Hij controleert of de groepen bestaan, of er genoeg teksten zijn en of die binnen de toegestane lengtes passen. De bestemmings-URL haalt hij uit de Nederlandse landingspagina in dezelfde map. Daar voegt de tooling Woordenboek · In het kortUTM-tagsUTM-tags zijn parameters in een URL die aangeven bij welke bron, welk kanaal en welke campagne een link hoort. Webstatistieksoftware kan daarmee bezoeken aan die campagne toeschrijven.Lees meer⁠ aan toe, zodat we in onze webstatistieken kunnen terugzien via welke campagne een bezoeker binnenkwam.

We leveren meerdere headlines en descriptions aan. Bij een responsive search ad kiest Google daar bij iedere vertoning een combinatie uit. Eén campagnebestand kan zo verschillende advertenties opleveren.

Gegenereerde telefoonpresentatie met een gerenderde Google Search-advertentie op basis van de Nederlandse campagnecopy

Voor dit artikel hebben we onze advertentie als voorbeeld op een telefoon en laptop laten renderen.

Van configuratie naar een plan

Het campagnebestand beschrijft wat we willen. Om te bepalen wat er moet gebeuren, moet de tool ook weten welke campagne daarbij hoort en hoe die nu in Google is ingesteld. Daarom gebruikt de tool drie bronnen:

  • desired is de gewenste configuratie, ingelezen uit onze campagnebestanden.
  • state is onze administratie van eerder aangemaakte campagnes, groepen en advertenties. Hierin staan hun Google-IDs.
  • live bevat de actuele instellingen die de tool met die IDs bij Google heeft opgevraagd.

Die drie gaan naar de functie die de verschillen berekent. De actuele gedeelde budgetten en biedstrategieën worden apart uitgelezen en gaan mee als budget. Zo'n overzicht van verschillen noemen we een Woordenboek · In het kortdiffEen diff is een overzicht van de verschillen tussen twee versies van een bestand of twee situaties. Het laat zien wat is toegevoegd, verwijderd of gewijzigd.Lees meer⁠. De functie krijgt haar invoer als benoemde velden in één object:

typescript
const operations = diff({ desired, state, live, budget });

operations is de lijst met opdrachten die nodig zijn om van de bestaande naar de gewenste situatie te komen. Denk aan een campagne aanmaken, een budget aanpassen of een advertentie pauzeren. Deze lijst vormt het plan dat we beoordelen voordat er iets wordt uitgevoerd.

Door Google steeds opnieuw uit te lezen, vinden we ook wijzigingen die buiten de repo zijn gedaan. Stel dat de maandruimte in ads.yaml een gedeeld dagbudget van € 20 oplevert, maar iemand dat budget in de console naar € 40 heeft gezet. De tool stelt dan voor om het terug te brengen naar € 20. De repo is leidend voor de velden die we beheren. Zo'n externe afwijking heet Woordenboek · In het kortdriftDrift is een afwijking die ontstaat doordat een beheerde omgeving buiten de vastgelegde configuratie om verandert. De werkelijke instellingen komen dan niet meer overeen met wat in de bestanden staat.Lees meer⁠.

We bedienen de tool met een Woordenboek · In het kortCLICLI staat voor command-line interface: een manier om een programma te bedienen met tekstcommando's, meestal vanuit een terminal.Lees meer⁠, een programma dat we vanuit de terminal starten. Die controleert de bestanden, haalt de actuele instellingen op en toont het plan. Wij beoordelen de voorgestelde wijzigingen voordat de tool ze uitvoert. Campagnes activeren is een bewuste keuze; standaard bereidt de tool ze gepauzeerd voor. Een lege diff betekent dat alles al staat zoals bedoeld.

Een plan kan ook verouderen. Terwijl wij de wijzigingen bekijken, kan een collega bijvoorbeeld een budget aanpassen. Daarom leest de tool vlak voor apply de configuratie, state en live instellingen opnieuw. Als er iets veranderd is, stopt de uitvoering en moeten we een nieuw plan beoordelen. Ons akkoord blijft zo gekoppeld aan de wijzigingen die we hebben gezien.

S3 onthoudt welke campagne welke is

Ons bestand gebruikt het lokale ID software-op-maat voor de campagne. Google kent diezelfde campagne als een numeriek ID. De tool moet dat verband onthouden om bij een volgende uitvoering de juiste campagne terug te vinden. Dat doen we in een zogenaamde statefile, net zoals Terraform dat doet.

We bewaren die administratie als Woordenboek · In het kortJSONJSON staat voor JavaScript Object Notation. Het is een tekstformaat waarmee software gestructureerde gegevens kan uitwisselen en opslaan.Lees meer⁠ in Amazon S3, de bestandsopslag van AWS. Zo gebruikt iedere uitvoering dezelfde state, ook als we vanaf een andere laptop werken. Een uitsnede ziet er zo uit. De Google-IDs in dit voorbeeld zijn fictief:

json
{
  "campaigns": {
    "software-op-maat": {
      "googleId": "12345678901",
      "adGroups": {
        "maatwerk": {
          "googleId": "23456789012"
        }
      }
    }
  }
}

Bij de volgende uitvoering weet de tool dus dat wijzigingen aan software-op-maat naar campagne 12345678901 moeten. Het lokale ID blijft gelijk als we het bestand naar een andere map verplaatsen. Ook dan blijft het dezelfde campagne in Google.

De volledige state bevat ook de advertenties, beheerde afbeeldingen, het account en een revisienummer dat bij iedere write oploopt. Verwijderen we een campagnebestand, dan stelt de tool voor om de bijbehorende campagne te pauzeren. Na goedkeuring voert de tool dat uit. De IDs blijven in de state staan en de campagnehistorie blijft in Google beschikbaar. Hetzelfde geldt voor verwijderde groepen en advertenties.

In de S3-bucket staan state.json, de opgeslagen plannen en onze goedkeuringen of afwijzingen. Die bucket is privé en versleuteld. S3 bewaart ook eerdere versies van de bestanden. Plannen en beslissingen worden eenmalig aangemaakt, zodat we later kunnen terugzien wat er is voorgesteld en goedgekeurd.

Voorkomen dat twee uitvoeringen elkaar overschrijven

Met gedeelde state ontstaat een volgend probleem. Twee developers kunnen op hetzelfde moment de CLI starten. Als ze allebei een eigen versie van state.json opslaan, zou de laatste write de wijzigingen van de eerste kunnen kwijtraken.

Daarom onthouden we bij het lezen de ETag, een kenmerk waarmee S3 de gelezen inhoud kan herkennen. Bij het opslaan sturen we die mee. S3 accepteert de write alleen als het bestand ondertussen hetzelfde is gebleven. Hieronder staat die controle, ingekort uit onze opslagcode:

typescript
await client.send(new PutObjectCommand({
  Bucket: bucket,
  Key: 'state.json',
  Body: JSON.stringify(nextState),
  ...(stateEtag
    ? { IfMatch: stateEtag }
    : { IfNoneMatch: '*' }),
}));

PutObjectCommand schrijft de nieuwe JSON naar S3. Met IfMatch vragen we S3 om eerst de ETag te vergelijken. Bij de allereerste write is er nog geen ETag. Dan zorgt IfNoneMatch: '*' ervoor dat we alleen een nieuw bestand kunnen aanmaken. Deze conditionele writes laten de opslag mislukken zodra een andere uitvoering ons voor is geweest.

We willen ook voorkomen dat twee uitvoeringen tegelijk aan Google gaan sleutelen. Voor apply maakt de tool daarom een lock.json aan met zo'n conditionele write. Wie dat bestand als eerste aanmaakt, krijgt de beurt. Een tweede uitvoering stopt zolang de lock bestaat. Na afloop verwijdert de eerste uitvoering zijn eigen lock weer.

Die lock blijft bij een vastgelopen proces soms achter. Hij verloopt bewust niet automatisch. Een traag proces kan immers nog bezig zijn. Bij herstel controleren we eerst of de eerdere uitvoering is gestopt. Wijzigingen via de Google Ads-console blijven mogelijk. Daarvoor hebben we de live controles vóór en na apply.

Nieuwe campagnes worden eerst gepauzeerd aangemaakt. De tool vult de instellingen en advertenties in en vraagt ze daarna opnieuw op bij Google. Activering volgt alleen als we daarvoor gekozen hebben en die controles slagen. Na apply vergelijken we alles nog een keer met de gewenste configuratie. Een lege diff betekent dat de beheerde instellingen overeenkomen en de uitvoering geslaagd is.

Wat als Google klaar is, maar het antwoord wegvalt?

Stel dat we Google vragen een campagne aan te maken. Google doet dat, maar de verbinding valt weg voordat het antwoord ons bereikt. Onze client krijgt een timeout en heeft het nieuwe campagne-ID nooit ontvangen. Nog een keer proberen kan dan een tweede campagne opleveren.

Om zo'n onzekere situatie te kunnen herkennen, houden we in de state een klein logboek bij. Vóór iedere API-opdracht slaan we op welke wijziging we gaan uitvoeren. Zolang die opdracht openstaat, bevat de state een pending-veld:

typescript
state.pending = {
  journalVersion: 1,
  calls: [],
  credentialFingerprint: api.credentialFingerprint,
  planId: plan.id,
  operation: op,
  idempotencyKey: randomUUID(),
  startedAt: new Date().toISOString(),
};
await store.saveState(state);
await journalMutation(api, state, desired, () => store.saveState(state));
delete state.pending;
await store.saveState(state);

De volgorde is hier het belangrijkste. Eerst bewaren we de opdracht in S3. Dan voert journalMutation de wijziging uit. Vóór iedere API-aanroep die iets wijzigt, bewaart die een hash van het verzoek en na ontvangst het antwoord, voordat eventuele nieuwe Google-IDs in de state worden verwerkt. Pas daarna verwijderen we pending en slaan we de state opnieuw op.

De idempotencyKey geeft iedere opdracht een uniek kenmerk. Bij aanvragen aan Zernio sturen we die sleutel mee om herhaalde aanvragen herkenbaar te maken. Bij een onzekere API-uitkomst of mislukte opslag blijft de opdracht als pending in S3 staan. De volgende uitvoering biedt inspectie en herstel aan voordat er een nieuw plan kan worden uitgevoerd. Ontvangen antwoorden verwerken we zonder de wijziging nogmaals uit te voeren. Zonder antwoord moet de herstelroute vaststellen of herhalen veilig is of dat eerst de aangemaakte resources bij Google moeten worden teruggelezen.

S3 en Google verwerken hun wijzigingen afzonderlijk. Daardoor kan een deel van een plan al uitgevoerd zijn wanneer een latere stap faalt. Met het logboek kunnen we zien bij welke opdracht het misging en verder werken vanaf wat er daadwerkelijk is uitgevoerd.

Wat het LLM-tijdperk hieraan verandert

LLMs lezen tekst en produceren tekst. Dat maakt deze manier van werken zo geschikt voor AI. Onze bedrijfskennis, keywordresearch, landingspagina's en campagnes staan allemaal in tekstbestanden. Een Woordenboek · In het kortagentEen AI-agent is software die een taalmodel en gereedschappen gebruikt om een opdracht uit te voeren. De agent kiest vervolgstappen op basis van het doel en de resultaten van eerdere stappen.Lees meer⁠ kan die als context lezen en zijn werk als wijzigingen aan diezelfde bestanden teruggeven. De tooling vertaalt die wijzigingen vervolgens naar een website of een campagne in Google Ads.

Daarmee kan een LLM het werk van een marketeer uitvoeren. We vertellen welke diensten we willen verkopen, aan wie en met welk budget. De agent kan zoekwoorden onderzoeken, campagnes indelen, landingspagina's schrijven en bijpassende advertenties maken. Omdat hij de pagina en de campagne samen kan lezen, kan hij de belofte in de advertentie laten aansluiten op wat een bezoeker na de klik aantreft.

Ook de resultaten zijn weer tekst. pnpm ads report levert Markdown, JSON en een CSV met zoektermen op. Die kan de agent lezen om uitgaven en resultaten te analyseren, irrelevante zoektermen te herkennen en wijzigingen voor te stellen. Vervolgens past hij de campagnebestanden aan en vraagt hij een nieuw plan op. Zo gebruiken we dezelfde werkwijze voor het opzetten én het bijhouden van campagnes.

Door zowel de input als de resultaten in tekst beschikbaar te maken, kan een LLM dit werk van begin tot eind uitvoeren: context lezen, bestanden aanpassen en op basis van rapportages verbeteringen voorstellen. Wij geven richting en beoordelen de wijzigingen. De tooling zorgt dat de goedgekeurde bestanden ook daadwerkelijk een website of campagne worden.

Key takeaways

  • Onze Google Ads staan als tekstbestanden naast de landingspagina’s. Daardoor kunnen we wijzigingen vergelijken, reviewen en met plan/apply uitrollen.

  • Een LLM verzorgt voor ons de keywordresearch, campagneopzet, landingspagina’s en advertenties. Wij bepalen de doelen en het budget en keuren wijzigingen goed.

  • Voor onze kleinschalige campagnes kunnen we nu gemakkelijk zelf het beheer doen, zonder een online marketeer in te schakelen.

Ewout

Contactpersoon: Ewout

Samenwerken?

Heb je een softwarevraag of een project in gedachten? Neem contact op met Ewout. Samen bespreken we wat je nodig hebt en hoe we kunnen helpen.

CONTACT

Neem contact met ons op

Heb je een vraag of wil je sparren over je software? Laat je gegevens achter, dan nemen we snel contact met je op.