Native Oxygen Builder Page Creation

Template Cloning + Surgical Content Replacement
Playbook v1.0 | Created Apr 10, 2026 | Source: Sewer Camera Inspection (Page 1313) | Author: Robert Dove + Nexus AI

Overview

This playbook documents how to create new service pages for callbrightside.com using native Oxygen Builder elements instead of raw HTML code blocks. Pages built this way are fully editable in the Oxygen UI, support drag-and-drop, and inherit responsive breakpoints automatically.

The Approach: Template Cloning

Instead of rebuilding every element from scratch, we clone an existing working page's JSON structure, replace selectors and content, then insert the modified JSON directly into the WordPress database. This takes ~15 minutes per page vs. 2-3 hours of manual Oxygen UI work.

Extract Source
Page JSON
Deep Copy +
Replace IDs
Swap Content
by ct_id
Minify +
Insert SQL
Verify in
Oxygen UI

Why Native vs. Code Block?

FeatureNative OxygenCode Block (raw HTML)
Editable in UIYesNo
Drag and dropYesNo
Responsive built-inYesManual CSS
Reusable componentsYesNo
Team maintainableYesDev only
Build speed (with playbook)~15 min~30 min

Page Architecture

Component Tree (Page 1313 - Sewer Camera)

root (id: 0, depth: 0) ├── ct_div_block ct_id: 2 "specials notice" - Dynamic month/promo JS ├── ct_section ct_id: 4 "service intro" - Hero + intro content │ ├── ct_div_block ct_id: 5 "intro grid" - 2-column layout │ │ ├── ct_div_block image column │ │ │ └── ct_image ct_id: 6 hero image (attachment_id) │ │ └── ct_div_block content column │ │ ├── ct_headline ct_id: 8 H1 - page title │ │ ├── ct_text_block ct_id: 9 intro paragraph │ │ └── ct_reusable CTA widget (view_id: 52) │ └── oxy-shape-divider wavy SVG separator ├── ct_section ct_id: 11 "service content" - Main body + sidebar │ ├── ct_div_block primary content column │ │ ├── ct_headline ct_id: 13 H2 - "What Is..." │ │ ├── oxy_rich_text ct_id: 14 answer paragraph │ │ ├── ct_headline ct_id: 15 H2 - "When Do You Need..." │ │ ├── oxy_rich_text ct_id: 16 answer paragraph │ │ ├── ct_headline ct_id: 17 H2 - "What Happens During..." │ │ ├── oxy_rich_text ct_id: 18 answer paragraph │ │ ├── ct_headline ct_id: 39 H2 - "Why Choose..." │ │ └── oxy_rich_text ct_id: 40 answer paragraph │ └── ct_div_block sidebar │ └── ct_reusable service areas (view_id: 264) ├── ct_reusable guarantees section (view_id: 46) ├── ct_reusable services grid (view_id: 36) ├── ct_reusable members/reviews (view_id: 47) └── ct_modal ct_id: 48 exit-intent popup ├── ct_div_block backdrop overlay ├── ct_div_block modal content │ ├── ct_headline ct_id: 51 modal heading │ ├── ct_code_block ct_id: 52 modal body text │ ├── ct_link_button CTA button │ └── ct_image x3 feature icons (BBB, flat rate, family) └── ct_link_button close X button

Database Structure

TableKeyValuePurpose
ukueq_postmetact_builder_jsonFull JSON blob (~14K chars)Entire Oxygen page structure
ukueq_postmetact_other_template10Header/footer template reference
ukueq_postmetact_builder_shortcodes(empty)Shortcode placeholder
ukueq_postspost_content(empty)Oxygen stores structure in meta, not content

Prerequisites

1
Source Template Page
A working Oxygen page to clone from. Current master template: Page 137 (Sewer Repair). Any service page with the standard layout works. The source page must have the full component tree (specials, hero, content sections, reusables, modal).
2
Target Page Created in WordPress
Create a new page in WordPress as a draft. Note the page ID (visible in the URL: post.php?post=XXXX). Set the title, slug, and permalink. For sewer camera, this was Page ID 1313 at /sewer-camera-inspection/.
3
phpMyAdmin or MySQL CLI Access
Need direct database access to extract source JSON and insert modified JSON. Hostinger phpMyAdmin is at the hosting panel. Table prefix is ukueq_.
4
Python 3 + json module
Build scripts use Python for JSON parsing, deep copy, recursive traversal. Standard library only (json, copy, re). Run on VM or local machine.
5
Content Ready
Before starting: H1 title, intro paragraph, 3-4 H2 sections with rich text content, hero image (uploaded to WordPress media library with attachment ID), modal heading + text, specials notice text.

Step 1: Extract Source Page JSON

1A

Export from phpMyAdmin

Run this SQL query to get the Oxygen JSON from the source page:

SQL Query
SELECT meta_value
FROM ukueq_postmeta
WHERE post_id = 137
AND meta_key = 'ct_builder_json';

Copy the entire meta_value result. This is the raw Oxygen JSON (will be heavily escaped if exported via SQL dump).

1B

Parse the JSON

If copied directly from phpMyAdmin results tab, it should be valid JSON. If from a SQL dump file, you need to unescape SQL backslash encoding first:

Python - parse_oxy_json.py
import json, re

# If from SQL dump, unescape backslashes
raw = open('source_dump.sql').read()
# Extract the JSON value between quotes
match = re.search(r"'(\{.*?\})'", raw, re.DOTALL)
if match:
    escaped = match.group(1)
    # Unescape SQL-style escaping
    unescaped = escaped.replace("\\'", "'").replace('\\"', '"').replace('\\\\', '\\')
    data = json.loads(unescaped)

    # Save clean version
    with open('source_page_oxy.json', 'w') as f:
        json.dump(data, f, indent=2)
    print(f"Parsed OK. Root children: {len(data.get('children', []))}")
1C

Verify the Source JSON

Confirm the structure is intact before proceeding:

Verification Checks
# Must have root with children
assert data['name'] == 'root'
assert len(data['children']) >= 5  # sections + reusables + modal

# Must have selectors with source page ID
json_str = json.dumps(data)
count = json_str.count(f'-137')  # Source page ID
print(f"Found {count} selectors with page ID 137")
SQL Escaping Pitfall: phpMyAdmin SQL dumps add multiple escaping layers (MySQL + PHP). If json.loads() fails, try the character-by-character unescape in decode_oxy.py. Three independent decode strategies exist because this step fails most often.

Step 2: Clone + Replace Selectors

2A

Deep Copy the Structure

Python
import copy

source_id = 137   # Page we're cloning FROM
target_id = 1313  # Page we're building FOR

# Deep copy to avoid mutating original
page = copy.deepcopy(data)
2B

Recursive Selector Replacement

Every Oxygen element has a selector field that includes the page ID (e.g., div_block-2-137). These MUST be updated to the new page ID or styles will collide.

Python - replace_selectors()
def replace_selectors(obj, old_id, new_id):
    """Recursively replace page ID in all selector strings."""
    if isinstance(obj, dict):
        for key, val in obj.items():
            if key == 'selector' and isinstance(val, str):
                obj[key] = val.replace(f'-{old_id}', f'-{new_id}')
            elif key == 'activeselector' and isinstance(val, str):
                obj[key] = val.replace(f'-{old_id}', f'-{new_id}')
            else:
                replace_selectors(val, old_id, new_id)
    elif isinstance(obj, list):
        for item in obj:
            replace_selectors(item, old_id, new_id)

replace_selectors(page, source_id, target_id)
2C

Verify Replacement

Python
json_str = json.dumps(page)
old_count = json_str.count(f'-{source_id}')
new_count = json_str.count(f'-{target_id}')

print(f"Old selectors remaining: {old_count}")  # MUST be 0
print(f"New selectors created: {new_count}")     # Should be 30+

assert old_count == 0, "STOP: Old selectors still present!"
Critical: If old_count is not 0, some selectors were missed. Check for selectors that use the page ID in a different format (underscores, no hyphen, etc). Never proceed with old selectors remaining.

Step 3: Customize Content

3A

Find Elements by ct_id

Every element in the Oxygen tree has a unique ct_id. Use this helper to locate any element:

Python - find_by_ct_id()
def find_by_ct_id(obj, target_id):
    """Find an element in the Oxygen tree by its ct_id."""
    if isinstance(obj, dict):
        opts = obj.get('options', {})
        if opts.get('ct_id') == target_id:
            return obj
        for child in obj.get('children', []):
            result = find_by_ct_id(child, target_id)
            if result:
                return result
    return None
3B

Update Content Elements

Replace text, images, and links for the new service. Target elements by ct_id:

Python - Content Updates
# H1 - Page Title
el = find_by_ct_id(page, 8)
el['options']['ct_content'] = 'Sewer Camera Inspection in Kansas City<br>'

# Intro paragraph
el = find_by_ct_id(page, 9)
el['options']['ct_content'] = 'See inside your pipes before you spend a dollar...'

# H2 sections (repeat for each)
el = find_by_ct_id(page, 13)
el['options']['ct_content'] = 'What Is a Sewer Camera Inspection?'

# Rich text (HTML content)
el = find_by_ct_id(page, 14)
el['options']['ct_content'] = '''<p>A sewer camera inspection uses a...</p>
<p>Our plumber inserts a flexible rod...</p>'''
3C

Update Hero Image

The image element references a WordPress attachment by ID, URL, and dimensions:

Python - Image Update
# Hero image (ct_id: 6)
el = find_by_ct_id(page, 6)
opts = el['options']['original']
opts['attachment_id'] = 1202           # WordPress media ID
opts['attachment_url'] = 'https://www.callbrightside.com/wp-content/uploads/2024/sewer-camera-hero.jpg'
opts['attachment_size'] = 'image-640'
opts['attachment_height'] = 427
opts['attachment_width'] = 640
opts['alt'] = 'Bright Side Plumbing technician performing sewer camera inspection'
Image must exist in WordPress media library FIRST. Upload the image, note the attachment ID from the URL (post=XXXX in media editor), then reference it here. Wrong attachment_id = broken image.
3D

Update Modal / Exit Intent

Python
# Modal heading (ct_id: 51)
el = find_by_ct_id(page, 51)
el['options']['ct_content'] = 'Free Sewer Camera Inspection'

# Modal body text (ct_id: 52 - code block)
el = find_by_ct_id(page, 52)
el['options']['original']['code-php'] = '''<p style="color:#555;font-size:15px;">
Get a free HD camera inspection with any sewer repair estimate.
See exactly what is happening inside your pipes.
</p>'''
3E

Update Specials Notice JavaScript

The dynamic monthly special (ct_id: 3) uses JavaScript to display the current month:

Python
# Specials notice (ct_id: 3 - code block)
el = find_by_ct_id(page, 3)
el['options']['original']['code-js'] = '''
var months = ["JANUARY","FEBRUARY","MARCH","APRIL","MAY","JUNE",
  "JULY","AUGUST","SEPTEMBER","OCTOBER","NOVEMBER","DECEMBER"];
var currentMonth = months[new Date().getMonth()];
document.getElementById("special-notice").innerHTML =
  "<strong>" + currentMonth + " SPECIAL:</strong> " +
  "Free sewer camera inspection with any repair estimate";
'''

Content Map Reference (Page 1313)

ct_idElement TypeContent
3ct_code_blockSpecials notice (JS)
6ct_imageHero image
8ct_headlineH1 - Page title
9ct_text_blockIntro paragraph
13ct_headlineH2 - "What Is..."
14oxy_rich_textAnswer paragraph
15ct_headlineH2 - "When Do You Need..."
16oxy_rich_textAnswer paragraph
17ct_headlineH2 - "What Happens..."
18oxy_rich_textAnswer paragraph
39ct_headlineH2 - "Why Choose..."
40oxy_rich_textAnswer paragraph
51ct_headlineModal heading
52ct_code_blockModal body text

Step 4: Generate SQL + Insert

4A

Generate Two JSON Versions

Python
# Pretty version (for human review)
with open('sewer_camera_inspection_oxy.json', 'w') as f:
    json.dump(page, f, indent=2)

# Compact version (for database)
compact = json.dumps(page, separators=(',', ':'))
with open('sewer_camera_inspection_oxy_compact.json', 'w') as f:
    f.write(compact)

print(f"Pretty: {os.path.getsize('sewer_camera_inspection_oxy.json')} bytes")
print(f"Compact: {len(compact)} chars")
4B

Generate SQL Insert Script

Python
target_id = 1313

# Escape single quotes for SQL
sql_safe = compact.replace("'", "\\'")

sql = f"""-- Sewer Camera Inspection Page (ID: {target_id})
-- Generated by build_camera_page.py

INSERT INTO ukueq_postmeta (post_id, meta_key, meta_value)
VALUES ({target_id}, 'ct_builder_json', '{sql_safe}');

INSERT INTO ukueq_postmeta (post_id, meta_key, meta_value)
VALUES ({target_id}, 'ct_other_template', '10');

INSERT INTO ukueq_postmeta (post_id, meta_key, meta_value)
VALUES ({target_id}, 'ct_builder_shortcodes', '');
"""

with open('sewer_camera_inspection_insert.sql', 'w') as f:
    f.write(sql)
4C

Execute in phpMyAdmin

1. Open phpMyAdmin for the callbrightside.com database

2. Go to SQL tab

3. Check if meta already exists (to avoid duplicates):

SELECT meta_id FROM ukueq_postmeta
WHERE post_id = 1313 AND meta_key = 'ct_builder_json';

4. If exists, UPDATE instead of INSERT:

UPDATE ukueq_postmeta
SET meta_value = '[COMPACT JSON]'
WHERE post_id = 1313 AND meta_key = 'ct_builder_json';

5. If not exists, run the INSERT statements from the SQL file

UPDATE vs INSERT: If the page was previously opened in Oxygen (even once), the ct_builder_json row already exists. Running INSERT will create a DUPLICATE row and break the page. Always check first. Use UPDATE if the row exists.

Step 5: Verify

5A

Open in Oxygen Editor

Navigate to: wp-admin/post.php?post=1313&action=edit

Click "Edit with Oxygen". The builder should load all elements in the visual editor. If you see a blank canvas, the JSON was malformed or not inserted correctly.

5B

Verify Component Tree

In Oxygen's structure panel (left sidebar), confirm:

  • Specials notice div at top
  • Service intro section with image + content grid
  • Content section with 4 H2/text pairs
  • Sidebar with service areas reusable
  • Guarantees reusable component
  • Services grid reusable component
  • Members/reviews reusable component
  • Exit-intent modal at bottom
5C

Verify Content

  • H1 shows correct service title
  • Hero image loads (not broken)
  • All 4 H2 sections have correct headings
  • Rich text paragraphs render properly
  • Phone number links are correct: (913) 963-1029
  • Modal popup shows correct offer text
  • Specials notice shows current month
5D

Verify Responsive

Use Oxygen's responsive preview to check tablet and mobile views. Since we preserved all media query settings from the source page, responsive behavior should work identically.

5E

Publish + Cache Purge

1. Save in Oxygen editor

2. Publish the page (change from draft to published)

3. Purge Cloudflare cache for the URL

4. Purge WP Rocket / LiteSpeed cache

5. Visit the live URL in incognito to verify

JSON Structure Reference

Root Node

{
  "id": 0,
  "name": "root",
  "depth": 0,
  "children": [...]   // All page elements
}

Element Node

{
  "id": 2,
  "name": "ct_div_block",        // Element type
  "options": {
    "ct_id": 2,                   // Component tree ID (unique per page)
    "ct_parent": 0,               // Parent ct_id (0 = root)
    "selector": "div_block-2-1313", // CSS selector (includes page ID)
    "nicename": "specials notice",  // Label in Oxygen UI
    "classes": ["special-notice"],  // CSS classes
    "activeselector": "special-notice",
    "ct_depth": 1,                // Depth in tree
    "ct_content": "",             // Text/HTML content
    "original": {                 // Styling properties
      "background-color": "#fff",
      "padding-top": "20",
      "padding-top-unit": "px"
    },
    "media": {                    // Responsive overrides
      "phone-portrait": {
        "original": { "font-size": "14" }
      }
    }
  },
  "children": [...],             // Child elements
  "depth": 1
}

Image Element

{
  "name": "ct_image",
  "options": {
    "original": {
      "attachment_id": 1202,
      "attachment_url": "https://...",
      "attachment_size": "image-640",
      "attachment_height": 427,
      "attachment_width": 640,
      "alt": "Description text",
      "src": "https://...",
      "object-fit": "cover"
    }
  }
}

Code Block Element

{
  "name": "ct_code_block",
  "options": {
    "original": {
      "code-php": "<div id='container'></div>",
      "code-js": "document.getElementById('container').innerHTML = 'Hello';"
    }
  }
}

Reusable Component

{
  "name": "ct_reusable",
  "options": {
    "view_id": 52,                    // WordPress post/view ID to embed
    "selector": "reusable-10-1313"    // Selector with page ID
  }
}

Oxygen Element Types

TypePurposeContent FieldNotes
ct_sectionMajor page sectionNone (container)Use for top-level blocks
ct_div_blockGeneric container/wrapperNone (container)Grids, columns, wrappers
ct_headlineHeading (H1-H6)ct_contentSet tag in original.tag
ct_text_blockPlain text paragraphct_contentNo HTML formatting
oxy_rich_textRich HTML textct_contentSupports <p>, <ul>, <a>, etc.
ct_imageImageoriginal.*Uses attachment_id + URL
ct_code_blockPHP/JS codeoriginal.code-php/jsExecutes on page load
ct_link_buttonCTA buttonct_content + urlStyled link element
ct_modalModal/popupChildren elementsHas trigger settings
ct_reusableEmbed another postview_idShared components
oxy-shape-dividerSVG section dividerSVG settingsWavy/angled borders

Shared Reusable Components

These components are shared across all service pages. Edit once, updates everywhere.

view_idComponentUsed For
52CTA Button WidgetCall-to-action in hero section
46Guarantees SectionTrust badges, warranty info
36Services GridList of all plumbing services
264Service AreasSidebar city/area list
47Members / ReviewsTeam + review cards
Key advantage: When you update a reusable component (like adding a new service area), it automatically updates on ALL pages that reference it. No per-page edits needed.

Pitfalls and Gotchas

!
SQL Escaping Layers
phpMyAdmin SQL dumps add multiple escaping layers. A quote becomes \' which becomes \\' in the dump. If json.loads() fails on the extracted text, try progressively unescaping: first \\\\\\, then \\'', then \\"". Three decode scripts exist for this reason.
!
Duplicate postmeta Rows
If the page was ever opened in Oxygen, ct_builder_json already exists. Running INSERT creates a duplicate. Always check with SELECT first. Use UPDATE if the row exists. Duplicates cause Oxygen to load the wrong version or break entirely.
!
Selector Collision
If ANY selectors still reference the old page ID, CSS from the source page will bleed into the new page (and vice versa). The verification step (0 old selectors) is non-negotiable. One missed selector = unpredictable styling.
!
Broken Image References
attachment_id must match an existing WordPress media library item. If you use the source page's attachment_id, it will show the source page's image (which might be fine for shared images, but wrong for hero images). Always upload new images first and get the correct ID.
!
Cache After Insert
WordPress, WP Rocket, LiteSpeed, and Cloudflare all cache aggressively. After inserting/updating the JSON, you MUST purge all cache layers. Visiting the page without purging will show stale or broken content.
!
Rich Text HTML Entities
Content in ct_content for rich text elements uses HTML. Special characters must be properly encoded. Ampersands as &amp;, quotes as &quot;. Malformed HTML in ct_content can break the entire Oxygen editor for that page.

New Page Checklist

Use this checklist every time you create a new service page:

Pre-Build

Build

Insert

Verify

SEO

Pages to Build Using This Playbook

PageSource TemplateStatusPriority
/sewer-camera-inspection/Page 137DONE (ID: 1313)P1
/sewer-line-replacement/Page 1313 or 137NextP1
/emergency-sewer-repair/Page 1313 or 137PlannedP1
/trenchless-sewer-repair/Page 1313 or 137PlannedP1
/sewer-line-cleaning/Page 1313 or 137PlannedP2
/sewer-line-repair/Page 1313 or 137PlannedP2
Now that Page 1313 is built, it becomes the new preferred source template (it already has camera-specific patterns that are closer to the other sewer sub-pages than the generic Page 137 template).