---
type: plan
project: leaguelaw
title: League & Williams website rebuild — build plan
status: active
created: 2026-08-10
target: November 2026 launch
---

# League & Williams rebuild — build plan

Read `brief.md` first. This file holds the locked decisions, the phase order, and
the gates. It is forward-looking; once something is delivered its record moves to
`.logs/diary.md`.

## Locked decisions

| Decision | Value | Why |
|---|---|---|
| Scope | Option 2, Design Modernization | Client accepted 2026-08 |
| Framing | Modernization, **not** redesign | Client reacts badly to "redesign"; they want their site, brought current |
| Theme | Kadence + child theme | House standard; pattern tooling is proven on it |
| Build method | Unsynced pattern library | Carried from Brentwood; see §Patterns |
| **Environment** | **Keep this DB, swap the theme in place** | Only way passwords, GF entries, IDs and slugs survive; see §Content conversion |
| **Content** | **Strip to plain blocks, rebuild presentation** | Preserves every page/post/entry while discarding old formatting |
| Photography | Labelled placeholders until November | Client reshoots office + staff in November |
| Launch target | November 2026 | Client's stated aim; lands with the new photos |
| Hosting | Stays on Flywheel | Not in scope to move |
| AI add-on | `bw-ai-schema-pro` **configured, never custom-built** | Plugin already does it; needs a mapped team CPT |

## Content conversion — measured, not assumed

Measured against the fresh pull on 2026-08-10. **The content is in far better
shape than a decade-old site suggests, and this materially shrinks the job.**

| Finding | Number |
|---|---|
| WPBakery / `[vc_*]` shortcode content | **0** — none anywhere |
| Total-theme or other proprietary blocks | **0** |
| Pages already in blocks | 46 of 47 (1 empty) |
| Published posts already in blocks | 171 of 272 |
| Core paragraph / heading / image blocks | 210 / 186 / 180 items |
| ACF blocks | **43 items** |
| Shortcodes present | **32 items** |
| Raw HTML blocks | **9 items** |
| Columns / spacer wrappers | 9 / 7 items |
| Group, cover, buttons wrappers | 0 |

**What this means.** The site is already on core Gutenberg blocks with almost no
layout scaffolding. "Convert everything to plain block text" is largely already
true. So do **not** run a blanket rewrite over all 319 published items — that is
all risk for no gain.

**Do this instead: a targeted strip pass over roughly 70 items** — the 43 ACF
blocks, 32 shortcodes, 9 raw HTML blocks, and the 9 columns / 7 spacers. Those
are the only things carrying formatting worth removing. Everything else is
already the plain paragraph-and-heading content we want to rebuild around.

### Non-negotiables for the conversion

1. **Snapshot the database first**, compressed, before touching content. This is
   destructive and one-way. (Brentwood's `.snapshots/<project>-pre-*.sql.gz`
   convention.)
2. **Done-test: text in equals text out.** For every converted item, strip all
   markup from the before and after and compare. No words may be lost. Formatting
   disappearing is the goal; content disappearing is a bug.
3. **Never touch `wp_users`, `wp_usermeta`, or the `gf_*` tables.** 3,929 form
   entries and every login live there. The conversion is `post_content` only.
4. **Do not renumber or re-slug anything.** Post IDs and slugs are what protect
   the URLs and rankings. If a URL must change, it goes in the redirect map.
5. **Table prefix is `wp_eopoktulnc_`.** A script written against `wp_` will
   silently do nothing, or worse, hit the vestigial empty `wp_users` table.

### Also carried over untouched

ACF field data (18 field groups, 104 fields) lives in postmeta, not
`post_content`, so staff bios, testimonials and law-facts survive the strip pass
without special handling. Gravity Forms definitions and entries live in their own
tables. Media library is untouched.

## Stack cleanup — theme and plugin swap

**Decided 2026-08-10.** Target stack is **Kadence + ACF + Yoast + Gravity Forms**.
Everything else goes. No template clone — a clean start on the new theme, on this
database.

### ⚠️ The Total theme owns the lawyer post type — read before removing it

`staff` and `testimonials` are registered by the **Total theme**, not by any
plugin:

- `themes/Total/framework/classes/PostTypes/Staff.php`
- `themes/Total/framework/classes/PostTypes/Testimonials.php`

**Deleting Total without replacing these first makes the 13 lawyers disappear**
from wp-admin and the front end. The rows stay in the database, orphaned and
invisible, which reads as data loss even though nothing was deleted.

**So the order is fixed:** build the Kadence child theme and stand up the
replacement post type **first**, confirm all 13 lawyers still list in wp-admin,
and only then remove Total.

**`staff` is not re-registered under its old name — it becomes `lawyers`.**
Decided 2026-08-10: a new `lawyers` CPT with position, phone and email fields,
migrated by changing `post_type` on the existing rows so IDs, slugs and URLs are
untouched. **Full spec, field-extraction rules, and two URL traps:
[`lawyers-cpt-migration.md`](lawyers-cpt-migration.md) — read it before starting.**

`testimonials` (10 entries) is also Total-owned and still needs its own decision.

### Plugins: keep four, remove the rest

| Keep | Why |
|---|---|
| `advanced-custom-fields-pro` | Lawyer fields; blocks stripped (below) |
| `wordpress-seo` (Yoast) | SEO |
| `gravityforms` | 8 forms, 3,929 entries |
| `redirection` | Holds the redirect map — **kept**, decided 2026-08-10 |
| *(add)* Kadence theme + `bw-ai-schema-pro` | New stack; schema if the add-on lands |

Remove: `tinymce-advanced`, `akismet`, `antispam-bee`, `block-bad-queries`,
`header-footer`, `bw-dev`, `loftloader`, `mcp-adapter`, `molongui-authorship`,
`open-external-links-in-a-new-window`, `really-simple-ssl`,
`metronet-profile-picture`, `wp-cerber`, `wp-mail-smtp`, `wps-hide-login`.

**Four of those need a decision rather than a straight delete:**

- **`redirection` — KEPT** (decided 2026-08-10). It stays in the stack and holds
  the redirect map the project owes the client.
- **`wp-mail-smtp`** — this is how the site delivers mail. Removing it can
  silently break Gravity Forms notifications, which for a law firm means lost
  client enquiries. Confirm the delivery path before dropping it.
- **`metronet-profile-picture` — cross-checked, safe to remove.** All 13 lawyers
  already have their own featured image, and Darren's is the *same attachment*
  the plugin holds. No lawyer loses a photo. What does go is **author avatars on
  blog bylines** — replaced by binding author users to lawyer records, which the
  schema work needs anyway. Full table in `lawyers-cpt-migration.md`.
- **`really-simple-ssl` / `wp-cerber` / `block-bad-queries`** — SSL and security
  hardening on Flywheel. Flywheel handles SSL at the platform level, so these are
  very likely redundant here, but that is worth confirming rather than assuming.

### `bw-dev` ("League Law Custom Features") — nothing to rebuild

Audited in full. It is a 2019 scaffold that ended up carrying almost nothing:

| File | Contents | Verdict |
|---|---|---|
| `includes/cpt.php` | Registers `law_fact` (1 entry) | Dropping law facts anyway |
| `includes/acf.php` | Points ACF Local JSON at `bw-dev/backup/acf` | **Directory is empty** — no field groups on disk, they live in the DB |
| `includes/sidebar.php` | Entirely commented out | Dead |
| `includes/scripts.php` | Enqueues `assets/css/main.css` | That file is **37 bytes** — a sourcemap comment, no styling |
| `includes/debug.php` | `dump()` / `console_dump()` helpers | Dev leftovers |
| `includes/widget.php`, `shortcodes/index.php` | Empty stubs | Dead |

**Answer: nothing needs rebuilding. Safe to delete.** One consequence to handle:
removing it removes the ACF Local JSON save/load filter. That is desirable — but
create `acf-json/` in the Kadence child theme so field groups sync to disk in the
new location, matching how other WordPress projects here are laid out.

*(Aside: it defines unprefixed globals — `register_cpt()`, `dump()` — which is a
collision risk across the fleet. Another reason it goes rather than gets kept.)*

### ACF: keep the lawyer post type, strip the blocks

18 field groups. **Keep 1, decide on 1, remove 16.**

| Group | Attached to | Action |
|---|---|---|
| **Lawyers Settings** | `post_type == staff` | **KEEP** — these are the per-lawyer fields |
| **Lawyer Settings** | options page `acf-options-lawyer-settings` | **Decide.** Holds exactly one field, `lawyer_header_image`. Recommend dropping; the new design supplies its own header |
| Website / Page / Post Settings | options + page/post | Remove |
| 13 × `Block: …` groups | ACF blocks | Remove — new UI is Kadence patterns |

**Three block groups are already dead and need no placeholder** — `Post Grid`,
`Two Column Section`, and `Local Bar` have **zero** instances anywhere.

**Actual ACF block usage** across published and draft content:

| Block | Items | Block | Items |
|---|---:|---|---:|
| Icon Text | **44** | Service | 2 |
| Form | **9** | Lawyer Grid | 2 |
| Button | **7** | Hero | 1 |
| | | Image & Slider | 1 |

So the rebuild is really **Icon Text, Form, and Button** — the rest are one or two
instances each. Icon Text at 44 uses is the single most valuable pattern to get
right early.

**How to strip them — do not just delete the markup.** An ACF block stores its
field values as JSON *inside the block comment* in `post_content`:

```
<!-- wp:acf/iconstext {"name":"acf/iconstext","data":{"heading":"…","text":"…"}} /-->
```

Deleting the comment deletes the copy with it, which fails the text-in-equals-text-out
check. **Extract the `data` values into a visible placeholder block** carrying the
original block name and its text, e.g. a heading/paragraph pair prefixed
`[REBUILD — Icon Text]`. That way nothing is lost, every rebuild site is findable
with one search, and the page still reads sensibly in the editor meanwhile.

### Dropping law facts

`law_fact` has 1 entry. Remove the CPT with `bw-dev`; delete or migrate the single
post deliberately rather than orphaning it.

### Molongui → AI Schema Pro

Molongui Authorship comes out and `bw-ai-schema-pro` takes over author and Person
schema. **Two guest authors exist and need a decision at some point** (deferred,
not blocking) — see `.memory/guest-authors.md`.

## Patterns — the core build decision

**The rule: if a section appears more than once, it is a pattern. If a page needs
a layout no pattern covers, ask whether an existing pattern plus changed
variables gets there before building anything new.**

The goal is that building page 40 is as fast as building page 4, and that the
client's office can create a new page afterwards without us. That means a small
library of well-made patterns, not a large library of near-duplicates.

### Mechanics that must be right

These are the two things the Brentwood build proved matter, and both are easy to
get silently wrong:

1. **Insert unsynced, and unlocked.** Patterns are unsynced so each insertion is
   independent — editing one page never changes another. They are also unlocked
   so an inserted pattern is a genuine starting point rather than a guard-railed
   template. If the client later wants guard rails, that is one flag, not a
   rebuild.

2. **Declare every block type used inside a pattern as editable.** WordPress
   restricts non-declared blocks inside patterns to content-only editing, which
   makes images, buttons, tables, and spacers inert. The failure is invisible
   until someone tries to swap a photo and cannot. Keep the declaration in **one
   shared list** consumed by both the PHP and JS halves, so the two cannot drift.

   **Done-test for any pattern: insert it, and confirm an image inside it shows a
   Replace button and a button inside it can be re-linked.** A pattern that fails
   this is not finished.

3. **Prefer a toggle over a second pattern.** Photo-left and photo-right are the
   same section facing the other way: that is a control, not two library entries.
   Same for width variants. Every avoided near-duplicate is one less thing to
   maintain and one less choice the client has to make.

4. **Build patterns from real page markup.** Extracting attributes from actual
   pages produces valid theme markup with spacing that matches the rest of the
   site, rather than hand-written markup that looks right in isolation.

### Naming

One project-specific pattern category so the library is obvious in the inserter
and cannot collide with theme-shipped patterns. Patterns are named for what they
look like, not what they were first used for — "Dark panel + wide photo", not
"Homepage intro".

## Phases

Nothing here starts before the **Where do we build** question in
`.logs/handoff.md` is answered.

### Phase 0 — Foundations

**Order matters here — see §Stack cleanup for why the theme swap is not first.**

- [x] **Snapshot the database** — `.snapshots/leaguelaw-pre-theme-swap-*.sql.gz`,
      taken 2026-08-10, 126 tables verified. Re-snapshot before each destructive
      pass, not just once.
- [ ] Capture Core Web Vitals + full page/URL inventory from production as the
      **before** baseline — the proposal promises before/after numbers
- [ ] **Export the `redirection` rules** before that plugin is removed
- [ ] Install Kadence + child theme; create `acf-json/` in the child theme
- [ ] **Re-register `staff` in the child theme** (same key, same `/lawyers/` slug)
      and decide about `testimonials` — Total currently owns both
- [ ] **Verify all 13 lawyers still list in wp-admin**, then switch the active
      theme off `total-child-theme` and remove Total
- [ ] Remove the plugin set per §Stack cleanup, handling the four flagged ones
      deliberately; install `bw-ai-schema-pro`
- [ ] Strip the 16 ACF field groups; extract ACF block content into
      `[REBUILD — …]` placeholders per §Stack cleanup
- [ ] Run the targeted strip pass per §Content conversion, with the
      text-in-equals-text-out check
- [ ] Confirm the team roster with the client (additions, departures, and the
      "IS IT YOU?" placeholder entry)
- [ ] Audit the 47 pages and 424 posts (272 published + 152 drafts, **all
      retained**): what migrates, what merges, what retires with a redirect

### Phase 1 — Design modernization + mockups  ← **client gate**

- [ ] **Reference examples shortlist, sent to the client to rank — do this
      first.** The client asked for it explicitly and it settles direction before
      anything is designed, which protects the single feedback round that was
      sold. Criteria and notes: [`design-direction.md`](design-direction.md).
- [ ] Everything below is built against `design-direction.md`, which is **client
      direction and therefore a requirement** — contact at top, practice areas as
      immediate clickable zones, no hero imagery, league-of-teams framing.
- [ ] Establish the design system in Kadence: type scale, spacing, colour
      (keeping the orange), buttons, and the modernized versions of existing
      section treatments
- [ ] Mock up the **home page plus a set of key pages** — likely a practice-area
      page, a lawyer bio, the blog index, and contact
- [ ] All photography clearly labelled as placeholder, cropped to final aspect
      ratios so November's real photos drop in without re-layout
- [ ] **Gate: client sign-off.** One comprehensive round of feedback is what was
      sold. Build does not start before sign-off.

### Phase 2 — Pattern library
- [ ] Build the pattern set the approved mockups imply, per §Patterns
- [ ] Declare the editable-block list; verify each pattern against the done-test
- [ ] A test page rendering every pattern in one place, for review and training

### Phase 3 — Build and migrate
- [ ] Templates: pages, posts, lawyer bios, testimonials, location pages
- [ ] Migrate 427 posts with categories, dates, and featured images intact
- [ ] All six location pages, properly structured
- [ ] Contact and intake forms rebuilt on current tooling
- [ ] Base-scope schema, **including the corrected firm address** and the real
      practice areas
- [ ] Redirect map for every changed URL

### Phase 3b — AI schema  *(only if the client accepts the add-on)*
- [ ] Install and configure `bw-ai-schema-pro`; map `staff` as the team post type
- [ ] Firm entity, locations, services, FAQ and content-type schema
- [ ] Untangle author attribution (5 author IDs, 9 accounts, Molongui layer,
      2 `guest_author` entries) so each post resolves to a real person
- [ ] Team credentials and expertise via the plugin's survey workflow
      (`docs/SPEC-team-survey.md`), client reviews and finalizes before launch
- [ ] Validate all structured data

### Phase 4 — Photography swap  *(November, client-dependent)*
- [ ] Replace every placeholder with the new office and staff photography
- [ ] Update lawyer bios against the confirmed roster

### Phase 5 — Launch
- [ ] Performance pass; capture the **after** Core Web Vitals
- [ ] Security cleanup: retire the legacy plugin stack
- [ ] Launch checklist: redirects resolve, schema validates, forms deliver,
      sitemap clean
- [ ] Launch to Flywheel, training session, 30 days support

## Gates

1. **Database snapshot exists** before the strip pass runs. Non-negotiable.
2. **All 13 lawyers verified visible in wp-admin** under the new child theme
   before the Total theme is removed. Total registers `staff`; removing it first
   orphans them.
3. **Client ranks the reference examples** before mockups are designed. Their own
   request, and it settles direction cheaply.
4. **Client sign-off on mockups** before any build work (Phase 1 → 2). Sold as
   one comprehensive feedback round.
5. **Rian confirms the client accepted the AI add-on** before Phase 3b starts.
6. **Client confirms the roster** before lawyer bios are built, so bios and
   November photography match.

## Risks

- **November is both the launch target and the photography month.** The photos
  are the last dependency, so everything else must be finished and waiting.
  Placeholders cropped to final ratios are what protects the date.
- **Scope drift from modernization into redesign.** The client bought their site
  brought current. Check mockups against that framing before sending.
- **427 posts and their URLs are the firm's most valuable asset.** A missed
  redirect is a lost ranking. The redirect map is a deliverable.
- **Author attribution is tangled** (5 author IDs, 9 accounts, a Molongui layer).
  If the AI add-on lands, this untangling is on the critical path and should be
  scoped properly rather than absorbed.
