Appearance
Ads on a Laravel site: AdSense, your own banners and a product feed, without a plugin
A local bookshop wants the space after the second paragraph of your posts, from Monday to Friday. Your sidebar should carry AdSense, but only for visitors who said yes to marketing cookies. And the shop's products should run in Google Shopping, which wants them in a file it can fetch.
On a Laravel site, each of these is a small project: a table for banners, something to rotate them, a script tag that respects consent, a route for ads.txt, a job for the feed. And the rotation fights the page cache, which keeps one page for every visitor. kitloom/ads does all of it as one module, from the admin.
Ask the agent
With kitloom/ai connected, an agent works on your ads with the rights of the user whose key it holds:
- "Put this new banner after the second paragraph of posts, until Friday." (
ads_create,ad_zones_create) - "Pause the spring sale banners." (
ads_list,ads_update) - "Which ad got the most clicks this month?" (
ads_stats) - "Which products did the Merchant Center feed leave out, and why?" (
ads_feed_status)
Nothing is deleted: an agent pauses instead. An HTML ad runs on every visitor's page, so it needs its own permission, for an agent's user as for anyone else. The AdSense publisher ID stays on the Settings page for people to change, because it decides who is paid. Every change goes through the same writer as the admin's, with the agent's user as the actor.
An ad and a zone
An ad is what is shown: an image with a link, your own HTML, an AdSense unit or a Google Ad Manager unit. It has a weight, and it can have dates.
A zone is where: the Ad zone widget in a sidebar, [ad zone="sidebar"] in a text, a block, or by itself before a record's text, after it, or after its Nth paragraph, on the post types you tick. A paragraph inside a quote, a list or a table does not count, so an ad never breaks one in two.
Several ads in a zone take turns. A banner of weight 3 is shown three times as often as one of weight 1.
It works with the page cache
The usual way to rotate banners is on the server, and a page cache then serves one banner to everyone until it expires. A Vue or React theme adds a second problem: it puts a widget's HTML in with v-html, where a script never runs.
So the server draws every candidate of a zone in a <template>, and one script in the footer picks. It checks the dates and the screen width, asks about consent, and takes one by weight, for this visitor and this moment. A banner with a start date appears on time on a page cached yesterday. When an ad is paused or a zone changes, the pages that show it are drawn anew. A visitor seeing an ad draws nothing anew: counts stay out of the cache's way.
AdSense, Ad Manager and consent
Enter your publisher ID or network code on Ads → Settings, and make units with the IDs from your account. With auto ads on, AdSense loads on every page and Google places the ads.
With the Cookie Consent module, the networks and your own code wait for the visitor's consent to marketing, and a zone fills the moment they say yes. An image of your own sets no cookies, so it waits for nothing. Signed-in administrators and editors see no ads by default, and No ads on this page keeps a privacy policy free of them, auto ads included.
/ads.txt serves the lines your networks give you, and adds AdSense's own line once the publisher ID is set. If you already use Site Kit by Google, php artisan ads:import takes the publisher ID from it.
Views and clicks
The list of ads shows the views and clicks of the last 7 days, with the click rate. A view counts an ad at least half on the screen. A click counts the links of your own banners and code, and the link itself goes straight to the advertiser, marked rel="sponsored nofollow". For AdSense and Ad Manager, trust their own reports: the money is there.
The counts are per ad per zone per day and say nothing about who saw an ad, so there is no personal data to export or erase.
A feed for Google Merchant Center and Meta Catalog
With the Shop module, Ads → Settings gives you the address of a feed: the file Google Merchant Center, Meta Catalog, Pinterest and Microsoft Merchant read. Paste it in as a scheduled feed.
Each simple product is one line and each enabled variation another, grouped under its product, with its price as the catalogue shows it, its stock, its pictures, and its GTIN when it has one. The scheduler builds it every six hours, or you press Build the feed now. What cannot be listed is left out with the reason, such as a product without a picture, so you can put it right before a network refuses it. The address holds a key, so your prices are not at an address anyone guesses.
For a Laravel developer
The module keeps its ads and zones in tables of its own, with Eloquent models you can swap in the ModelRegistry. AdWriter and ZoneWriter take the acting user and announce AdSaved and AdZoneSaved. It comes with both admins, Filament and Nova.
php
$ad = app(AdWriter::class)->create([
'name' => 'Spring sale',
'kind' => 'image',
'weight' => 3,
'settings' => ['attachment_id' => 12, 'url' => 'https://example.com/sale'],
], $user);
$zone = app(ZoneWriter::class)->create(['name' => 'After the intro', 'placement' => 'after_paragraph', 'paragraph' => 2], $user);
app(ZoneWriter::class)->setAds($zone, [$ad->id], $user);With kitloom/security's Content Security Policy, the module names the hosts of the networks that are running. Google says the hosts its ad code uses change and supports only a strict, nonce-based policy for AdSense, so try a site with ads on the report-only header first.
Watch it
Install it
The module comes with kitloom, for either admin. On a Filament site:
bash
composer require kitloom/ads kitloom/ads-filament
php artisan migrateOn a Nova site, the second package is kitloom/ads-nova. Every detail of the module is in the Ads guide.