Skip to content
Back to all work
Website Migration · PHPPublic WebsiteBackendREST API

Tattoo Panda: WordPress to Plain PHP with Contentful

A Miami tattoo studio's site, rebuilt without a framework and checked page by page against the original.

Tattoo Panda is a Miami tattoo studio run by artists who wanted a website as distinctive as their work, and one that felt quick to use. I rebuilt it from WordPress as a lean, framework-free PHP site with animated pages, online booking, a gear showcase, giveaways and a public voting contest, all powered by Contentful, and checked every page against the original.

Client

Tattoo Panda

Tattoo Panda

Role

Lead Full-Stack Engineer

Year

2026

Category

Website Migration · PHP

The challenge#

Tattoo Panda is a Miami tattoo studio, and its owners are artists first. They wanted a website that felt unique, with its own look, motion and personality instead of a template feel, and one that was fast and simple to run. Their site had grown on WordPress: a custom child theme on top of a commercial parent theme, with thousands of lines of CSS, a 2,500-line home page template and a multi-step booking modal.

The studio wanted that exact design and behaviour kept, but on a coded site with far less machinery behind it, hosted on SiteGround shared hosting, where PHP is the only runtime and there is nothing to compile.

The brief was a faithful port with the design, animations and behaviour unchanged. Booking, contest voting and artist content all had to keep working, and visitors and the studio's existing tracking could not notice the switch.

The approach#

When I audited the theme, I found it was big but its WordPress surface was tiny. Across every template there were only about 15 distinct WordPress functions, and almost all page content was hardcoded HTML. That changed the plan. A rewrite into a framework would have added risk for no benefit, so I chose plain PHP.

  • Plain PHP over Astro or static HTML. The templates were already PHP, so removing the WordPress calls was mechanical rather than a rewrite. Booking, one-time-code voting and Contentful writes also need a server runtime, and PHP was the only one the host offered. Astro would have meant a second runtime and a build toolchain the host cannot run. Pure HTML would have meant pasting the header, footer and booking modal into every page.
  • The live pages are the spec. I captured the rendered HTML of all nine pages and both theme stylesheets as a read-only baseline, then wrote a verify.sh script that renders every page locally and compares it to its capture. For each page it reports word-level text differences, CSS class differences, PHP warnings and any leftover old-domain URLs, and it exits non-zero on any drift. That turned "does it look the same?" into a repeatable check.
  • Keep the cascade intact. The two theme stylesheets stay separate and load in their original order, because merging them would change specificity. Each template's large inline style block moved verbatim into its own page stylesheet, with no reformatting or minifying.

What I built#

A small shell with nine pages#

A thin shared layer handles configuration, escaping helpers, asset URLs with cache-busting, and the head, header and footer includes. The nine pages (Home, Gallery, Gear, Contact, Giveaway, Booking, Tattoo Fight, Privacy Policy and Terms) each share one shape: a small $PAGE array followed by the includes. Apache rewrites give pretty URLs, a custom 404 and a redirect from the old /give/ path, and keep internal folders away from the web.

Old URLs that keep working#

The Tattoo Fight page's JavaScript posts to WordPress's admin-ajax.php. Rather than rewrite that code, I routed the old path to a small PHP dispatcher that sends each action to its new handler. The same idea covers the contact form and booking endpoints. The browser-side code carried over unchanged while the server side was replaced.

Contentful integration#

  • Reading: artist grids on the home and gallery pages come from Contentful through a cache with a five-minute lifetime. Cache files are written to a temporary name and then renamed, so a failed write can never leave a corrupted cache.
  • Writing: the booking form runs the full Contentful Management flow on the server. It uploads the reference image, creates and processes the asset, polls until it is ready, publishes it, then creates and publishes the appointment entry.
  • Safe by design: the browser only ever sends an artist slug. The server maps it to the Contentful entry itself, so a tampered value is rejected with a 400. The write token is used only on the server and never reaches the browser.

Booking modal#

The multi-step booking modal, built with Alpine.js and about 2,650 lines in the PHP port, carried over as it was, ending in a confirmation step.

Tattoo Fight: contest and voting#

The studio's Tattoo Fight contest has its own MySQL schema with three tables for contests, the leaderboard and votes. Voting is protected by email one-time codes: a six-digit code generated with random_int, valid for five minutes and compared with hash_equals. Daily per-email and per-IP limits are enforced before a vote counts, and every query uses PDO prepared statements. The nomination form checks file type with finfo, caps uploads at 5 MB and sanitises file names. If the database is unreachable, the page falls back to its empty state instead of an error.

Everything a studio site needs#

  • Animated, on-brand pages. Scroll-triggered reveals, animate.css effects, counters and image lightboxes (Fancybox) are carried over exactly, and I compared the animation markers against the original on every page.
  • Artist showcase. Artist grids on the home and gallery pages are pulled live from Contentful, so the studio edits its roster in one place.
  • Online booking. A step-by-step modal collects the idea, the artist and reference images, and sends them straight to the studio's Contentful space as a published appointment, with "no preference" supported.
  • Gear page and gallery. Dedicated pages for the studio's equipment and its portfolio of work.
  • Giveaways. A giveaway page with an empty state, and a redirect so older links still land on it.
  • Tattoo Fight contest. Artists are nominated through a form with photo upload, and the public votes by email code. Countdown timers and contest windows are calculated on the server, with TikTok and YouTube embeds on the page.
  • Contact and lead capture. Embedded forms and a booking calendar from GoHighLevel, kept as they were.
  • Legal pages and clean URLs. Privacy Policy and Terms pages, pretty URLs and a custom 404.

Tracking and credentials#

Google Tag Manager, GA4 and the Meta Pixel are loaded from three small partials in a fixed order, with IDs read from the environment. Keeping the same pixel ID keeps Meta's server-side event gateway working. All credentials load through one env() helper from a git-ignored .env, with a documented .env.example. A real server environment variable always wins over the file.

Results#

Tattoo Panda now runs on a lean PHP site at pandatattoo.com that keeps the studio's distinctive look and motion, matches the original page by page, and keeps booking, contests and giveaways working. It has no framework, no Composer or npm dependencies and no build step, and it deploys by SFTP to shared hosting. Artist content stays editable in Contentful, bookings land there directly, and the Tattoo Fight contest runs on three database tables and six server endpoints. Removing the heavy theme also leaves the studio with far less code to maintain.

Lessons learned#

  • A big theme can hide a small dependency. Counting the WordPress functions actually used showed the right size of solution.
  • A capture-and-diff harness is a cheap, strong guard when the goal is "identical to the original".
  • Shims for old endpoints let me replace the server side without touching working browser code.

Tech stack

PHP logoPHP
Apache (.htaccess) logoApache (.htaccess)
MySQL (PDO) logoMySQL (PDO)
Contentful logoContentful
Alpine.js logoAlpine.js
GoHighLevel logoGoHighLevel
Google Tag Manager logoGoogle Tag Manager
GA4 logoGA4
Meta Pixel logoMeta Pixel
JavaScript logoJavaScript
SiteGround logoSiteGround

Timeline & Milestones

Audit

Catalogued every WordPress call and template, and found only about 15 distinct functions to replace.

Baseline

Captured all nine pages and both stylesheets, and built the diff script to compare against them.

Port

Built the PHP shell and pages, moved styles verbatim and kept the old endpoint URLs working through rewrites.

Integrations

Added Contentful reads and writes, the booking flow, one-time-code voting and tracking partials.

Outcomes & Impact

  • Pages ported icon9

    Pages ported

    Result description

  • Framework or build dependencies icon0

    Framework or build dependencies

    Result description

  • Server API endpoints icon6

    Server API endpoints

    Result description

  • Pages diffed against originals icon9 / 9

    Pages diffed against originals

    Result description