astro

The landing pages belong to the designer now

The landing pages belong to the designer now

At the start of June we wanted landing pages. The main website already exists - it is Next.js with Sanity behind it - but we wanted a separate page for each audience we are going after: one for yoga studios first, then others, each with its own copy, its own imagery, its own subdomain, and in English and Bulgarian.

I knew two things going in. I did not want these pages inside the main site, and I did not want to be the person who builds them. We have a designer. He already decides what the pages look like. The part in between - turning his design into HTML, CSS and a deployment - is the part that usually makes a developer the bottleneck, and I wanted to see whether I could remove myself from it entirely.

This is how that went, three weeks in.

Why Astro, a few months after choosing React

In January I wrote that the front-end choice is made, and that it is React and Next.js. That is still true for applications. A landing page is not an application. It is a document with some motion on it. It has no session, no data of its own, nothing to authenticate, and it should arrive as HTML and be readable before a single script runs.

Astro is built for exactly that. A page is an .astro file: some frontmatter at the top, then markup that looks like HTML because it mostly is. The build produces static files. There is no server to run and nothing to keep warm.

The deciding point was that it does not make me give up React. Astro renders React components as islands: the page is static, and the few pieces that need JavaScript - a carousel, a scroll animation - are React components hydrated on their own, only where they are used. So the reasoning from January still holds. The assistant is at its most fluent in React, and the parts of a landing page that are hard to get right are exactly the parts where it gets to write React. Everything else is markup and Tailwind.

The stack is deliberately the one we use elsewhere: Astro 6, Tailwind 4, shadcn components for the shared primitives, Motion for the animations that need more than a CSS transition, and Vercel for hosting. Nothing the designer learns here is specific to this repository.

And it is separate from the main site on purpose. The Next.js site has its own build pipeline - types generated from the Sanity schema, a generated sitemap and llms.txt, a CMS behind every page - and none of that should stand between a designer and a changed headline. A broken landing page should not be able to break the main website, and a change to the main website should not redeploy a campaign.

One folder is one page is one subdomain

A folder under src/campaigns/ is a landing page, and its name is its subdomain. The folder yoga is served at yoga.1club.ai, in /en and /bg.

src/campaigns/yoga/
  config.ts        origin, default locale, OG image
  page.astro       the page
  assets/          images, optimised at build time
  locales/
    en.json
    bg.json

My first version, on the morning of June 5, was a single Vercel project with a /{campaign}/{locale} path for every page and host-based rewrites in vercel.json to hide the path segment. It worked, and I replaced it before lunch. Every campaign would have redeployed on every change, every page's URL structure depended on a rewrite table, and it was the one project in the organisation shaped differently from all the others.

The second version builds one campaign per Vercel project. Every project builds from the same repository; the only difference between them is an environment variable, PUBLIC_CAMPAIGN, and the Astro config turns it into two import aliases:

const CAMPAIGN = process.env.PUBLIC_CAMPAIGN || 'yoga'
const campaignDir = fileURLToPath(
  new URL(`./src/campaigns/${CAMPAIGN}`, import.meta.url),
)

export default defineConfig({
  integrations: [react()],
  vite: {
    plugins: [tailwindcss()],
    resolve: {
      alias: {
        'virtual:active-campaign-page': `${campaignDir}/page.astro`,
        'virtual:active-campaign-config': `${campaignDir}/config.ts`,
      },
    },
  },
})

The routes, the sitemap and robots.txt import from those aliases, so each build contains exactly one campaign, served from the root of its own domain, with no rewrites and nothing from the other pages in the bundle.

The rest is automation, so that adding a page never involves me. On a merge to main, a GitHub Actions workflow runs a provisioning script: for every campaign folder it makes sure a Vercel project called lp-{slug} exists, sets its PUBLIC_CAMPAIGN, attaches {slug}.1club.ai and triggers the first deployment. Our domain is on Vercel's nameservers, so DNS and the certificate happen by themselves. The script is idempotent and never deletes anything. Each project's Ignored Build Step is a short script that diffs the commit and only builds if the change touched that campaign's folder or the shared code, so editing the yoga page does not rebuild anything else.

Two more details make the pages behave well without anyone thinking about them. The root of each subdomain picks a language from navigator.languages, with a meta refresh to English for visitors without JavaScript. And every call to action goes through one CTAButton component that builds the link with UTM parameters and the locale, so the page cannot ship a button that loses track of where the visitor came from.

The instructions are written for Claude, not for me

The most important file in the repository is AGENTS.md, and it opens by telling the agent who it is working for:

- This project is primarily developed by a designer, not a full-time engineer.
- The designer will use Claude together with Figma/MCP to generate and refine
  landing pages.
- Treat Figma as the visual source of truth, but adapt designs into responsive,
  accessible web layouts instead of copying fixed-frame positions blindly.
- Keep generated code easy for a designer to inspect: clear Astro markup,
  readable Tailwind classes, small components, and no clever abstractions.

I have written before about writing for a reader that forgets. This one was different, because the person the agent reports to cannot catch what it gets wrong in the code. So most of the file is about the mistakes that a designer would not notice in review, and that I would not be there to notice:

  • Copy lives in the locale files, never in the markup.
  • Designs arrive in English only, and the agent is responsible for every other locale - same keys, the same shape, natural phrasing rather than word for word, image alt text included. A page that is English-only does not ship.
  • Do not invent claims, pricing, testimonials, statistics or partner logos.
  • CSS transitions first; Motion only inside small React islands.
  • No new dependencies, no Next.js, no analytics yet.
  • Where images go, how large a video may be before it belongs somewhere else, and which image gets fetchpriority="high".

Around that file are a handful of Claude Code commands that turn the common tasks into one line. /new-page scaffolds a campaign from the template and checks that it builds. /sync takes a campaign and a Figma frame, reads the frame through the Figma MCP server, applies the images, text, spacing, type and colour to the page, and then compares the two side by side - listing the deviations it made on purpose and the mismatches it could not resolve, so it cannot paper over a difference. pnpm page yoga scaffolds if needed and opens the page in the browser, for when he just wants to look.

And then there is git. A designer should not have to learn how to resolve a merge conflict to change a heading, so there is a /push command that looks at the changes, writes a Conventional Commit message, stages files by name and refuses anything that looks like a secret, and a /pull command that brings in main, resolves conflicts by keeping the intent of both sides, and stops and asks when it cannot tell. Both forbid force-pushes, reset --hard and skipped hooks.

Handing it over

I set up the repository on the afternoon of June 4, spent the morning of June 5 on the campaign machinery, and merged it at half past twelve. The designer's first commit, titled "Hero section", landed at five to five the same afternoon.

His commit history shows the handover. The first days have messages like "UI Tweaks and scroll animation" and, my favourite, "amazing great job commit", with a couple of hand-made merges of origin in between. The /push command arrived on June 8, and after a few days of him using it some of the time, the history reads like anyone else's: feat(yoga): add FAQ + footer, smooth sticky pins, scroll blur. He never learned Conventional Commits; the command writes them for him.

There was one real snag, and it was not his. On June 10 his pushes stopped deploying. Vercel's Git integration refuses to build a commit whose author is not a member of the Vercel team, and his GitHub account was not one. We had already met this in the monorepo, so the fix was the same: deploy from GitHub Actions with the Vercel CLI and a token, with .git removed from the build directory, so the deployment belongs to the token's owner and there is no commit author for Vercel to object to. The same workflow works out which campaigns a push touched and deploys only those. I also stopped the provisioning job from running on every edit to a campaign; it now runs only when a campaign's config.ts changes, which is when there is something new to provision. That was my last commit to the repository.

How much of a success it was

The yoga page is live, at yoga.1club.ai, in both languages. Since June 10, everything on it has been the designer's work.

As of today he has made twelve commits and added a little over five thousand lines. Of the 647 lines in the yoga page.astro, 627 are his; the other twenty are what was left of my template. There are fifteen React islands in the components folder and he wrote fourteen of them: the highlights carousel, sticky sections that pin while you scroll, a typewriter reveal, a logo marquee, push notifications sliding in, a video that scales as it comes into view, a ring of connected systems, an FAQ, and an animated demo of our AI assistant that is on its own four hundred lines long. The one I wrote was the example.

None of that would have been built if it had gone through me, because I would not have spent the time. A sticky section that blurs as it scrolls is exactly the kind of work I would have negotiated down to a fade.

What I cannot tell you yet is whether the page works as marketing. Analytics is deliberately phase two - the AGENTS.md tells the agent not to add PostHog yet - so I have no conversion numbers, and I am not going to pretend the deployment pipeline is a growth metric. The success I can claim is narrower: a designer is producing our web pages, live, in two languages, without an engineer between his design and the internet.

The first trade-off is review. Nobody reviews his code the way I would review an engineer's. He works on main, and some of those components are longer than I would have made them. I accepted that deliberately, because of what the architecture limits: a campaign is a static page in its own project on its own subdomain, with no server, no credentials and no data. The worst thing a bad commit can do is break one landing page, visibly, until the next commit fixes it. The rules that do matter - no invented claims, both languages, tracked calls to action - are the ones the agent is told to hold, and the ones I check when I look.

The other trade-off is ownership. Those fourteen components are his, and the agent's. If they need to change, the designer changes them. That was the aim, but it does mean I have to resist the urge to tidy them.

What I actually built

Looking back at the three weeks, the code I wrote is the rails the page runs on: a campaign convention, a provisioning script, an Ignored Build Step, a deploy workflow, and above all a set of instructions that let an agent work safely for someone who is not going to read what it writes.

The handover used to be a Figma file I would implement. Now it is a folder he owns, and the next landing page is a slug and a Figma link away. I would like to be as unnecessary to the second one as I was to most of the first.

Deyan Peev

Founding Engineer in Sofia, Bulgaria. Currently at 1club.

© 2026 Deyan Peev