Building a Marketing Landing Page: Astro, GSAP, and a 14-Task Deploy Pipeline
The puzzle app was live at puzzle.kidsgamesapp.com. The engineering blog was live at blog.kidsgamesapp.com. The one domain we’d had since day one — kidsgamesapp.com itself — was returning nothing.
Not great for SEO. Not great for user acquisition. Not great for the future version of me who’s going to look back at this from his yacht.
Time to fix that.
Why a Marketing Site?
The app lives behind a subdomain. puzzle.kidsgamesapp.com is where kids actually play — but it’s not what parents find when they search for “kids puzzle app” or “Puzzle Time app”. The apex domain, kidsgamesapp.com, is the SEO anchor. It’s the link you paste when you tell someone about the app. It’s the first thing an App Store reviewer sees when they check your website field.
Leaving it blank was leaving money on the table. Or at least, leaving future money on the future table.
The goal for the landing page is simple:
- Rank for the app name — when someone searches “Puzzle Time kids app”, this page should come up
- Convert parents into downloads — clear value prop, screenshots of the real app, a download button
- Look legit — an app with no website feels like a hobby project. An app with a polished marketing site feels like a product.
We don’t have an App Store URL yet (submission is the next milestone), so the CTA says “Coming Soon” for now. The infrastructure is ready the moment the link exists.
What We Built
A fully animated marketing landing page for Puzzle Time! — the kids sliding tile puzzle app. One session, start to finish:
- Static Astro 6 site with Tailwind CSS v4 and GSAP 3 + ScrollTrigger
- Deep purple gradient background with floating theme character images at the corners
- 7 sections: Navbar, Hero, Features, Screenshots, How It Works, Download CTA, Footer
- Full hero entrance animations + scroll-triggered reveals throughout
- Multi-stage Docker build → nginx:alpine
- GitHub Actions CI/CD → ECR → ArgoCD auto-sync
- DNS + TLS — site live at
https://kidsgamesapp.com
The Design Challenge
The hardest part wasn’t the code — it was the background.
The spec called for floating theme character images (Frozen, Paw Patrol, Bluey, Sonic, etc.) drifting at the corners of the hero section. The intent: make the dark background feel fun and kid-friendly rather than adult and moody. The target audience is parents of toddlers — it needs to read as “playful” in about 200 milliseconds.
Several iterations to get there:
Attempt 1: Images at 8–25% opacity. Too subtle. On any real screen they were invisible — just dark shapes in the corners. Parents wouldn’t notice them; kids definitely wouldn’t care.
Attempt 2: Light/pastel backgrounds. Tried going the opposite direction — soft whites and pastels. Looked like a generic SaaS landing page. Wrong vibe entirely.
Final: Dark purple, high-opacity characters. Cranked the images to 70%+ opacity with hard drop shadows and a border-radius: 22px card treatment. Added a multi-color bokeh layer behind everything. Now the characters actually read — they’re clearly Frozen, clearly Paw Patrol — while the gradient background keeps the hero from feeling washed out.
The final treatment:
- Base gradient:
#2d0a5e→#1a0a3e→#0a1060 - Multi-color bokeh layer: pink, indigo, amber, teal radial gradients at 30–45% opacity
- 6 character floater images at the corners and mid-edges, 70% opacity, slight rotation per card
- Sparkle dots and ✦ symbols scattered across the hero at varying sizes and opacities
- GSAP slow Y-axis float loop on each character, offset start times so they feel alive and organic
The 7 Sections
The page is a single-scroll experience — no sub-routes, no routing, just one long page designed to answer every question a parent might have before downloading.
Navbar — sticky, transparent until scroll, blurs on scroll. App icon + “Puzzle Time!” left, gradient “Download” CTA right.
Hero — full viewport height. Badge → headline → subtext → CTA → 4 app screenshots floating below with alternating vertical offset and slight rotation. The character images float in the background throughout.
Features — 4 glassmorphism cards in a 2×2 grid on mobile, 4-column on desktop:
- 🤖 AI-Generated Puzzles — “Never the same puzzle twice”
- 🌍 10 Magical Worlds — “Frozen, Paw Patrol, Bluey & more”
- ⭐ Track Progress — “Stars & badges they earn”
- 🏆 Level Up — “3×3 to 5×5 grids, grows with your child”
Screenshots — 4 real app screenshots horizontal-scrollable on mobile, 4-up grid on desktop. These are the actual screenshots from the React Native project: home carousel, my collection, puzzle complete, profile.
How It Works — 3 steps with a connecting gradient line: Pick a theme → AI creates a puzzle → Solve & earn stars. For parents who want to understand the mechanic before handing their phone to a 3-year-old.
Download CTA — full-width section, centered radial glow, big button. Currently “Coming Soon” — one line change when the App Store URL exists.
Footer — copyright, privacy link, support link. Both point to puzzle.kidsgamesapp.com/privacy and /support since the legal pages are already served there.
Reusing the Existing Platform
This is the part I like most about how this platform is built.
We already had:
- An EKS cluster with ArgoCD watching the infra repo
- A shared
helm-charts/web-appHelm chart used by the puzzle app and the blog - ingress-nginx + cert-manager with Let’s Encrypt already configured
- A GitHub Actions pattern: build Docker image → push to ECR → update image tag in infra repo → ArgoCD auto-syncs
Deploying a completely new site to this infrastructure is:
Task 1: Add ECR repo — one line in terraform.tfvars, tofu apply
Task 2: K8s values.yaml + ArgoCD Application manifest — copy from blog, change 3 values
Tasks 3–11: Build the actual Astro site
Task 12: Dockerfile — multi-stage node:22-alpine → nginx:alpine
Task 13: GitHub Actions deploy workflow — copy from kids-games, change ECR repo name
Task 14: Route53 ALIAS record + wait for TLS cert
Total time from “let’s build this” to https://kidsgamesapp.com returning HTTP/2 200: one session. This is why I spent the time building the platform properly instead of throwing things on a VPS.
GSAP Animations
Every section has an animation. None of them are gratuitous — each one serves to draw attention to content as the user scrolls into it.
Hero entrance (page load, sequential):
- Badge fades + slides up
- Headline fades + slides up (slight delay)
- Subtext fades in
- CTA button scales up from 0.8
- Screenshots stagger float up from below
Character float (continuous, runs forever):
Each of the 6 character images has an independent GSAP yoyo tween — a slow up/down drift of about 20px. Offset start times (0.3s apart) prevent them from moving in sync, which would look robotic. The combined effect is that the hero section feels alive even when nothing else is animating.
Scroll reveals:
- Feature cards: fade + slide up, staggered 0.15s apart
- Screenshot cards: same pattern, scoped to
#screenshots .screenshot-card(important — see below) - How It Works steps: sequential reveal, each step waits for the previous
- CTA button: scale pulse + glow shadow increase on scroll into view
A Few Technical Notes
Tailwind v4 has no config file. It uses a @tailwindcss/vite Vite plugin and a single @import "tailwindcss" in your CSS. No tailwind.config.mjs. This caught the initial plan off guard — had to adapt on the fly.
Astro uses PUBLIC_ not VITE_ for client-exposed env vars. The S3 base URL for theme images needs to be passed as a build-time ARG in Docker and accessed as PUBLIC_ASSETS_BASE_URL in Astro components. Using VITE_ silently fails — the variable just evaluates to undefined in the browser with no warning.
GSAP in Astro runs in <script> blocks, not component logic. Astro components are server-rendered. ScrollTrigger and GSAP must run client-side after hydration. All animation code lives in src/scripts/animations.ts, imported via a <script> block in index.astro — Vite bundles it correctly from there.
Scope your ScrollTrigger selectors. The .screenshot-card class exists in both the Hero section (the 4 floating app screenshots below the CTA) and the Screenshots section. Without scoping, the scroll trigger would re-animate the hero screenshots when the user scrolled into the Screenshots section. Fix: use #screenshots .screenshot-card as the selector.
The TLS cert was stuck for 15 hours. cert-manager had created the ingress and tried to issue the Let’s Encrypt certificate immediately — but there was no DNS record pointing kidsgamesapp.com to the cluster, so the HTTP-01 ACME challenge kept failing silently. Once the Route53 ALIAS record was created, I deleted the stale CertificateRequest to force a retry. The cert issued in under 60 seconds.
The Stack
| Layer | Choice |
|---|---|
| Framework | Astro 6 (static output) |
| Styling | Tailwind CSS v4 |
| Animation | GSAP 3 + ScrollTrigger |
| Container | node:22-alpine builder → nginx:alpine |
| Registry | AWS ECR |
| Deploy | ArgoCD (GitOps) |
| CI/CD | GitHub Actions (OIDC auth to AWS) |
| DNS | Route53 ALIAS record |
| TLS | cert-manager + Let’s Encrypt |
What’s Next
The CTA currently says “Coming Soon on the App Store.” The next milestone is submitting the app to the App Store — once that URL exists, updating the marketing site is a two-line change in Hero.astro and DownloadCTA.astro, a git push, and the pipeline does the rest.
Then comes the real work: SEO content, getting the page to actually rank, and converting those rankings into downloads.
But the infrastructure is ready. The page is live. And when I’m a millionaire from a kids puzzle app, this post will be exhibit A.