Ads and sponsors
Ads are static markup: sold by email, written into site.config.ts, rendered at build time, with
no script and no ad network. Each placement is one boolean under ads, and ads.page.enabled
builds the /advertise/ page that lists the rates.
-
Turn on the page and one placement.
site.config.ts ads: {contactEmail: 'ads@acme.com',page: { enabled: true, note: 'Placements run for a calendar month.' },topBanner: { enabled: true, text: 'Sponsor Acme — your product in front of every reader.', url: 'https://sponsor.example' },rates: [{ slot: 'topBanner', price: '$99 / month', blurb: 'One line under the header, on every page.' }],},pnpm check:configanswerssite.config.ts OK — <name> (<url>). Aurlorlogomust be anhttps://URL or a/-rooted path. -
Build and deploy.
Terminal pnpm deployThe build’s
zero-js:line still ends— ok: nothing here adds a script.
Verify
Section titled “Verify”| What to check | Where | You should see |
|---|---|---|
| The placement | curl -s https://<your domain>/ | grep -o 'data-ad="[a-z-]*"' |
data-ad="top-banner"; the others are sponsors, inline-card, corner |
| The rates | https://<your domain>/advertise/ |
the table with a live badge on each enabled placement, and an Email button |
| The footer | any page | an Advertise link |
| The rel | page source | every ad link carries rel="sponsored noopener" |
How it works
Section titled “How it works”| Placement | Where it renders | What marks it |
|---|---|---|
topBanner |
one line between the header and the content, on every public page | the label pill (default Ad), the text, a linkLabel button |
sponsors |
a logo wall under the home-page hero, above the featured listings; renders nothing while items is empty |
the heading (default “Supported by these partners”), each logo titled with its tagline, and an Advertise with us link when the page is on |
inlineCard |
a card shaped like a listing inside every listing grid, after every every listings, never as the last cell |
“Sponsored” under the name, an Ad pill, a dashed border |
corner |
a small card fixed bottom-right on wide screens (the md breakpoint and up) |
an Ad pill and a × that hides it for that page through a CSS checkbox, no script |

Every ad anchor is rel="sponsored noopener", so search engines read it as what it is. A logo
renders with an empty alt and lazy loading; the in-grid card without a logo shows the
name’s first letter.
/advertise/ is built only while ads.page.enabled is true — the route has an empty
getStaticPaths() otherwise, so there is no stub — and advertise is a reserved segment no
listing or category base can take. It renders page.intro, a table of rates (placement, where
it appears plus the rate’s blurb, price) with a live badge on every placement currently
on, page.note, and a mailto: button to ads.contactEmail, falling back to
site.contactEmail, with the subject Advertising on <site name>. The footer links to it from
the same switch, and so does the sponsor wall.

Selling one. A prospect writes to the contact address naming a placement and a duration.
You add them to site.config.ts — a sponsor is name, url, optional logo and tagline; the
other placements take name, tagline, url, logo — and deploy. There is no runtime state:
the placement runs until you edit the file again.
The demo turns every placement on with placeholder copy pointing at /advertise/, priced per
month, so a buyer sees each slot before selling it. Treat those values as an example, not a
price list.
When it is off or degraded
Section titled “When it is off or degraded”| State | Visitor sees | /admin/ reports |
Fix |
|---|---|---|---|
| Everything off (the default) | no ads, no footer link, /advertise/ is a 404 |
nothing — ads have no runtime | — |
A placement on, page.enabled off |
the ad links to /advertise/, which does not exist, unless url is set |
nothing | set url, or enable the page |
sponsors.enabled with empty items |
nothing renders; not marked live on the rates table | nothing | add an item |
A grid no longer than every |
no in-grid card | nothing | expected |
A url without https:// or a leading / |
pnpm check:config fails naming the field |
— | fix the value |
Configuration
Section titled “Configuration”| Field | Default | What it changes |
|---|---|---|
ads.contactEmail |
site.contactEmail |
the mailto: on /advertise/ |
ads.page.enabled |
false |
whether /advertise/ and its footer link exist |
ads.page.intro, ads.page.note |
a stock intro, no note | the copy above and below the rates |
ads.topBanner, ads.sponsors, ads.inlineCard, ads.corner |
enabled: false |
one placement each; inlineCard.every is 2 to 50, default 6 |
ads.rates |
one On request row per placement |
the pricing table: slot, price, blurb |
See ads.
Directorysrc/
Directorycomponents/ads/
- AdBanner.astro
topBanner - Sponsors.astro
sponsors - AdCard.astro
inlineCard - CornerAd.astro
corner
- AdBanner.astro
- lib/ads.ts
AD_SLOTS,activeSlots(),withInlineSlots() - pages/[advertise].astro the rates page
- layouts/Base.astro mounts the banner and the corner card
- templates/HomePage.astro mounts the sponsor wall
- components/listing/Grid.astro mounts the in-grid card
- components/site/Footer.astro the Advertise link
- config/defaults.ts
DEFAULT_ADS
- site.config.ts the demo’s
adsblock