Built with AI / How this site was made — Laney Turner

I built the whole thing.
Then I built the CMS to run it.

Most portfolios sit there and look pretty. Mine tracks who’s reading, what they click, and which version of my resume is landing. When editing eight pages by hand got old, I wrote a plugin to do it for me. Here’s what’s underneath.

Project My portfolio
Role Designer, builder, strategist
Tools WordPress, GTM, GA4
Stack HTML, CSS, JS, PHP
Meta
case study
2026
⟶ / 01 — Receipts

What’s actually here.

10
Live case studies, all hand-written. Real clients, real numbers, real outcomes.
8×
Tracked events firing on every page. GTM picks them up. GA4 reports them.
0
Templates, themes, or page builders. Designed from a blank file.
4×
UTM dimensions per resume link. Different versions A/B-tested by source.
⟶ / 02 — Lines of code

The CMS is bigger than the site it runs.

I wrote ~8,700 lines to ship the static site. Then I wrote ~14,000 more to build the plugin that runs it. Custom architecture costs more upfront. It pays back every edit.

/ The CMS plugin
13,800
Lines of code
A real WordPress plugin running every page on this site. 20+ section types, 24 migrations, an admin dashboard I designed myself.
8,215 PHP 5,509 CSS 118 JS
vs.
/ The portfolio site
8,700
Lines of code
Eight pages of hand-written HTML, CSS, and JS. Design system, tracking, accessibility, schema — no template, no page builder, no theme.
Custom markup Custom styles Custom tracking
↳ Every line written and reviewed by me. Nothing copied from a template.
⟶ / 03 — How I work

Building with Claude.

claude.ai — laney’s portfolio build
L
Laney
I want my portfolio to feel like an editorial magazine, not a Squarespace template. Fraunces for headlines, Space Mono for the UI, hot pink and acid yellow. Wire up GTM, GA4, and tag the resume by source. Mobile-first. Ship it.
C
Claude
Got it. Hard borders, offset shadows, rotated stickers. Starting the design system.

CSS custom properties for the palette so every page stays in sync. Tracking script pushes structured events to dataLayer — eight of them:

// firing on every page
page_loadedemail_clickphone_click
resume_download // key event
linkedin_clickspotify_click
contact_form_submit
contact_form_confirmed // key event

Mobile-first with fluid type. WCAG AA contrast. JSON-LD on every page so Google connects the work to the candidate.

First draft going up now.

↳ Lightly edited. Real exchange.
⟶ / 04 — The next loop

Then I built a CMS for it.

Static HTML is fast. Editing eight pages by hand after the third rewrite is not. So I designed a custom WordPress plugin — every page on this site renders through it, every section is editable from a real admin dashboard. No Elementor. No theme. No shortcode soup. Just PHP and a clean data model.

/ v1

Static HTML

Eight pages from scratch. Design system, typography, motion, accessibility — all written by hand and pushed to Bluehost. Worked. Didn’t scale.

/ v2

WordPress upload

Moved everything to WordPress on the Hello Elementor theme. Full-width template, raw HTML inside each page. Live, but every edit was still a copy-paste.

/ v3

Custom plugin

Real WordPress plugin. Admin editor, versioned migrations, 20+ reusable section types. I edit the site through a dashboard I designed. Currently at v3.0.24.

What’s inside.

A real CMS, not a wrapper.
01

One data model, ten case studies

Every case study uses the same schema. Sections snap on or off per page — hero, numbers, gallery, video, campaign callout, before/after, takeaway. Add a new section type once, every page can use it.

02

Migrations that don’t break things

Every plugin update runs a versioned migration that patches existing data without overwriting edits. 24 of them shipped. The site never went down once.

03

Animated DM proof

A real Messenger conversation that types itself out on a loop — pure CSS, no JavaScript, no third-party widget. I used it to recreate an actual booking thread for Tami’s Tasty Tables.

04

SVG dashboards

Real campaign metrics rendered as interactive SVG — animated line charts, summary tiles, hover states. Used on Holding Them Accountable to show GroundTruth daily reach without embedding a screenshot that’ll rot.

05

Banners in context

Three IAB banner sizes shown inside actual phone and browser frames — so a recruiter sees the ad the way it ran, not as a flat 320×50 floating in space. Animated slide-in, scrolling skeleton page.

06

Native analytics cards

Email, LinkedIn, social, and web stats rendered as native HTML cards — not screenshots from a dashboard that expires next month. Editable. Always sharp. Never broken.

07

Video fallbacks that just work

When LinkedIn, Facebook, or Instagram block their iframe embeds, the renderer falls back to a styled thumbnail card that links to the post. Reliable over fragile. Always.

08

Per-page color systems

Each case study gets its own brand palette — hero, accents, callouts all painted from one variable set. Set the color once, the page renders itself. No global theme switch.

20+
Section types
10
Case study pages
24
Migrations shipped
0
Templates used
⟶ / 05 — Why bother

A portfolio that can’t track itself can’t prove anything. So I tracked everything.

— Before I wrote line one
⟶ / 06 — Under the hood

Seven things that actually run.

/ 01
Design system
Hot pink, acid yellow, electric blue, cream, ink

Five colors. Two typefaces. Hard borders. Offset shadows. CSS custom properties so every page stays in sync — change one variable, every page updates. Fraunces for headlines because nothing else moves like it. Space Mono for the UI because labels need to feel like labels.

CSS variablesType systemBrand voice
/ 02
Custom CMS plugin
v3.0.24 ✦ 20+ section types ✦ 24 migrations shipped

Real WordPress plugin running the whole site. Hero, numbers, gallery, video, campaign callout, before/after, takeaway — every section is its own type, editable from an admin dashboard I designed. Sections snap on or off per page. No Elementor.

PHPWordPressSchemaAdmin UI
/ 03
SEO that connects
JSON-LD on every page

Person, WebSite, WebPage, ProfilePage, CreativeWork — every page ships with structured data linked to one Person ID so Google connects the work to the candidate. Open Graph and Twitter Card meta everywhere. No Yoast required.

JSON-LDOpen GraphEEAT
/ 04
GTM + GA4
Container live across every page

Google Tag Manager fires on every page load with a noscript fallback. One GA4 config tag, plus custom event tags pulling every dataLayer push. Form submissions and resume downloads marked as Key Events — the conversions that actually matter for a job hunt.

GTMGA4Key EventsDebugView
/ 05
Event tracking
8 dataLayer events ✦ every interaction worth measuring

One tracking script handles email clicks, phone taps, resume downloads, LinkedIn opens, Spotify plays, form submits, and Formspree confirmations. Each event carries link location, click text, page path. Segmentation in GA4 means something — not just a pile of clicks.

dataLayerEvent paramsForm tracking
/ 06
UTM-tagged resume
Every link knows where it came from

Every resume placement gets its own UTM-tagged URL — source, medium, campaign, content. The PDF in my email signature is tagged differently than the one on LinkedIn, which is tagged differently than the one on this site. Different versions can be A/B-tested by source. Marketing applied to my own job hunt.

UTM paramsAttributionA/B variants
/ 07
Mobile-first & accessible
Tested at 1000px, 700px, 380px

Recruiters open portfolios on their phones on the way home from work. Every page is responsive by default — fluid typography, tap targets sized for thumbs, lazy-loaded iframes, system-font fallbacks while the web fonts load. ARIA labels on form fields, semantic landmarks for screen readers, focus-visible states for keyboard nav, contrast that clears WCAG AA. The site has to work everywhere, or it doesn’t work anywhere.

Mobile-firstWCAG AAFluid typeLazy loading
⟶ / 07 — What’s firing

Events under the hood.

Every interaction worth tracking pushes a structured event into the GTM dataLayer. GTM routes them to GA4. The ones that matter — form submits, resume downloads — are flagged as Key Events. That’s GA4’s word for conversion.

The script is about 120 lines. GA4-compliant: parameter values clipped to 100 characters, snake_case throughout, and a separate contact_form_confirmed event that only fires after Formspree’s success redirect. The conversion count reflects delivered messages, not just submit clicks.

What’s firing, right column.

// dataLayer events firing on this site
window.dataLayer.push({
  event: 'contact_form_submit',
  form_topic: 'Hiring',
  link_location: 'contact-form',
  page_path: '/contact'
});

// All tracked event names:
page_loaded
email_click
phone_click
resume_download      // ★ Key Event
linkedin_click
spotify_click
contact_form_submit
contact_form_confirmed  // ★ Key Event
↳ Fired automatically on every page across the site
⟶ / 08 — Tagging my own resume

Where the clicks come from.

Every place my resume lives gets its own UTM-tagged URL. When a hiring manager clicks, GA4 tells me where it came from — and which version they got. Different resumes can be A/B-tested by source.

/resume.pdf?utm_source=linkedin
&utm_medium=profile&utm_campaign=job_search_2026
&utm_content=v3_brand_focused
  • utm_source — where the click came from (linkedin, email, instagram, site)
  • utm_medium — the format (profile, signature, bio, dm)
  • utm_campaign — what the push is (job_search_2026 keeps it clean)
  • utm_content — which resume version (v1_strategy, v2_creative, v3_brand) for the A/B
⟶ / 09 — What I’d tell you

The marketers who pull ahead are the ones who learned how to build, not just how to brief. Strategy is sharper when you’ve seen how it gets shipped.

Designed it. Built it. Track it. Owned by Laney Tagged, tracked, shipped Designed it. Built it. Track it. Owned by Laney Tagged, tracked, shipped
⟶ / Next

More case studies.