Page Block Skill
Build and push section-based pages using the Marketers Delight Page Block system (v7.0.0). Page Blocks are reusable HTML/CSS/JS/PHP sections stored in a custom database table (wp_md_page_blocks), embeddable via Gutenberg block picker, shortcode, or positioned on hooks site-wide.
What Is a Page Block?
A Page Block is a reusable content unit stored in wp_md_page_blocks with its own HTML, CSS, JS, and display conditions. It can be used in three ways:
- Gutenberg block — picked from the library in the Block Editor via
marketers-delight/page-block
- Shortcode —
[page_block id="123"] or [page_block slug="hero-section"]
- Positioned — hooked to a theme action (e.g.,
md_hook_before_header) and conditionally displayed site-wide
Database Schema
| Column |
Type |
Default |
Purpose |
id |
bigint |
AUTO_INCREMENT |
Primary key |
title |
varchar(255) |
'' |
Display name |
slug |
varchar(200) |
'' |
Unique identifier |
status |
varchar(20) |
'publish' |
publish/draft/trash |
content |
longtext |
— |
HTML (or HTML + PHP) |
css |
longtext |
— |
Block CSS |
js |
longtext |
— |
Block JavaScript |
js_location |
varchar(10) |
'footer' |
footer or inline |
output |
varchar(10) |
'inline' |
inline or file |
php_exec |
tinyint(1) |
0 |
Execute PHP in content |
format |
tinyint(1) |
0 |
Apply wpautop() |
position |
varchar(100) |
'' |
Theme hook name (empty = no auto-placement) |
priority |
int(11) |
10 |
Hook priority |
conditions |
longtext |
NULL |
JSON display conditions |
author |
bigint(20) |
0 |
Creator user ID |
created_at |
datetime |
CURRENT_TIMESTAMP |
Creation time |
updated_at |
datetime |
CURRENT_TIMESTAMP |
Last update time |
Gutenberg Block Reference Format
When embedded via the Block Editor, the block comment references by ID:
<!-- wp:marketers-delight/page-block {"blockId":1} /-->
<!-- wp:marketers-delight/page-block {"blockId":50} /-->
Key details:
- Self-closing block: ends with
/--> (no inner blocks)
blockId references wp_md_page_blocks.id
- All content/CSS/JS lives in the database, not in the block comment
- The block has
save: function() { return null; } — all rendering is server-side
Inline Page Block Format
For pages where content/CSS/JS should live directly in the post content (not in the database), use the inline-page-block block type:
<!-- wp:marketers-delight/inline-page-block {"content":"<section>...</section>","css":".class{css:value;}","js":"function (){}"} /-->
Key details:
- Block name:
marketers-delight/inline-page-block (NOT page-block)
- Self-closing block: ends with
/--> (no inner blocks)
- All content/CSS/JS is JSON-encoded in the block comment attributes
content: HTML string (JSON-escaped)
css: CSS string (JSON-escaped)
js: JavaScript string (JSON-escaped, optional)
- No
blockId — everything is inline in the post content
- Ideal for programmatic/templated pages (e.g., local SEO city pages) where each page needs unique content
- Can be mixed with database-backed
page-block references on the same page
When to use inline vs database:
- Database (
page-block with blockId): Shared sections reused across many pages (edit once, update everywhere)
- Inline (
inline-page-block): Unique per-page content, programmatic generation, templated pages
Python Pattern for Inline Page Blocks
import json
def inline_page_block(content, css, js=""):
"""Build an inline-page-block comment"""
obj = {"content": content, "css": css}
if js:
obj["js"] = js
return '<!-- wp:marketers-delight/inline-page-block ' + json.dumps(obj, ensure_ascii=False) + ' /-->'
# Build a page with multiple inline blocks
blocks = [
inline_page_block('<section class="hero">...</section>', '.hero { background: #000; }', '(function(){})();'),
inline_page_block('<section class="services">...</section>', '.services { padding: 4rem 0; }'),
]
page_content = "\n\n".join(blocks)
New blocks should use the database blockId reference format for shared content, and inline-page-block for per-page unique content.
How It Renders
CSS Delivery
- On
template_redirect, positioned blocks are queried and their CSS collected
- During
the_content, Gutenberg page blocks render and add their CSS
- All CSS is combined and output in
<head> as <style id="md-page-blocks-css">
- File output mode: generates
/wp-content/uploads/md-page-blocks/page-blocks-v{version}.css
HTML Rendering
- If
php_exec is true AND MD_ALLOW_PHP_SNIPPETS is defined truthy in wp-config.php AND the stored php_checksum matches md5($content) at render time, PHP is executed via temp file include. Otherwise <?php … ?> blocks are stripped (HTML between tags is preserved, which is misleading — see "PHP execution is double-gated" below).
- Shortcodes are processed via
do_shortcode()
- If
format is true, wpautop() is applied
- HTML is minified (whitespace collapsed, comments stripped,
<pre>/<code>/<script>/<style> preserved)
JS Delivery
- Inline JS collected and output in
wp_footer as <script id="md-page-blocks-js">
- JS with
js_location = 'inline' outputs immediately after the block's HTML
- File output mode: generates external minified JS file
Positioned Blocks
Blocks with a position set are automatically hooked to that theme action:
- On
template_redirect, all positioned blocks are queried
- Display conditions are evaluated (post types, post IDs, page types)
- Matching blocks register on their position hook at the specified priority
- CSS is collected early for
<head> output
REST API
Full CRUD at /wp-json/md/v1/page-blocks. Requires authentication.
| Method |
Endpoint |
Permission |
Purpose |
| GET |
/page-blocks |
edit_posts |
List blocks (paginated, filterable) |
| POST |
/page-blocks |
manage_options |
Create block |
| GET |
/page-blocks/{id} |
edit_posts |
Get single block |
| PUT/PATCH |
/page-blocks/{id} |
manage_options |
Update block |
| DELETE |
/page-blocks/{id} |
manage_options |
Trash (or ?force=true to delete) |
| GET |
/page-blocks/{id}/render |
edit_posts |
Render block HTML preview |
Query Parameters (GET /page-blocks)
| Param |
Type |
Default |
Options |
status |
string |
'publish' |
publish, draft, trash, '' (all) |
search |
string |
'' |
Search title and slug |
position |
string |
'' |
Filter by position hook |
orderby |
string |
'updated_at' |
id, title, slug, updated_at, created_at |
order |
string |
'DESC' |
ASC, DESC |
per_page |
int |
20 |
1–100 |
page |
int |
1 |
Page number |
Create/Update Fields
| Field |
Type |
Description |
title |
string |
Display name |
slug |
string |
Unique slug (auto-generated from title if omitted) |
status |
string |
publish, draft, trash |
content |
string |
HTML content |
css |
string |
CSS code |
js |
string |
JavaScript code |
js_location |
string |
'footer' or 'inline' |
output |
string |
'inline' or 'file' |
php_exec |
boolean |
Execute PHP in content |
format |
boolean |
Apply wpautop() formatting |
position |
string |
Theme hook name (empty = shortcode/block only) |
priority |
integer |
Hook priority (default 10) |
conditions |
object/null |
Display conditions JSON |
Python Pattern for Managing Page Blocks
import base64
import requests
WP_URL = "https://yoursite.com"
USERNAME = "your-username"
APP_PASSWORD = "..." # from .env
credentials = f"{USERNAME}:{APP_PASSWORD}"
auth_header = base64.b64encode(credentials.encode()).decode()
headers = {
"Authorization": f"Basic {auth_header}",
"Content-Type": "application/json",
}
# Create a new page block
response = requests.post(
f"{WP_URL}/wp-json/md/v1/page-blocks",
json={
"title": "Hero Section",
"content": '<section class="hero block-double-tb"><div class="inner">...</div></section>',
"css": ".hero { background: var(--color-bg); }",
"js": "(function() { /* interactions */ })();",
"status": "publish",
},
headers=headers,
timeout=30,
)
block = response.json()
block_id = block["id"] # Use this in Gutenberg: {"blockId": block_id}
# Update an existing page block
requests.put(
f"{WP_URL}/wp-json/md/v1/page-blocks/{block_id}",
json={"css": ".hero { background: linear-gradient(...); }"},
headers=headers,
)
# Create a positioned block (shows site-wide)
requests.post(
f"{WP_URL}/wp-json/md/v1/page-blocks",
json={
"title": "Announcement Bar",
"content": '<div class="announce block-half-tb"><div class="inner">...</div></div>',
"css": ".announce { background: var(--color-primary); color: #fff; }",
"position": "md_hook_before_header",
"priority": 5,
"conditions": {
"page_types": ["front_page", "blog"],
},
"status": "publish",
},
headers=headers,
)
# List all page blocks
blocks = requests.get(
f"{WP_URL}/wp-json/md/v1/page-blocks",
params={"per_page": 50},
headers=headers,
).json()
Embedding in a WordPress Page
After creating page blocks via the REST API, reference them in page content:
# Push page content with page block references
page_content = "\n\n".join([
'<!-- wp:marketers-delight/page-block {"blockId":1} /-->',
'<!-- wp:marketers-delight/page-block {"blockId":2} /-->',
'<!-- wp:marketers-delight/page-block {"blockId":50} /-->',
])
requests.post(
f"{WP_URL}/wp-json/wp/v2/pages/{PAGE_ID}",
json={"content": page_content},
headers=headers,
)
Display Conditions (Positioned Blocks)
Conditions are stored as JSON. All conditions must pass (AND logic).
{
"post_types": ["post", "page"],
"post_ids": [7172, 1234],
"page_types": ["front_page", "blog", "singular", "archive", "search", "404"]
}
- Empty conditions = show everywhere
post_types — only show on these post types
post_ids — only show on these specific posts/pages
page_types — only show on these WordPress conditionals
Available Position Hooks
| Hook |
Location |
md_hook_before_html |
Very top of page |
md_hook_before_header |
Before header |
md_hook_header_top |
Header top |
md_hook_header_bottom |
Header bottom |
md_hook_after_header |
After header |
md_hook_before_content_box |
Before content box |
md_hook_content_box_top |
Content box top |
md_hook_content_box_bottom |
Content box bottom |
md_hook_before_content |
Before main content |
md_hook_content_top |
Content top (inside) |
md_hook_before_the_content |
Before the content |
md_hook_content |
After post content |
md_hook_content_bottom |
Content bottom (inside) |
md_hook_after_content |
After main content |
md_hook_before_sidebar |
Before sidebar |
md_hook_after_sidebar |
After sidebar |
md_hook_before_footer |
Before footer |
md_hook_footer_top |
Footer top |
md_hook_footer_bottom |
Footer bottom |
md_hook_after_footer |
After footer |
md_hook_before_footer_copy |
Before footer copyright |
md_hook_after_footer_copy |
After footer copyright |
Section Architecture
Each Page Block represents one visual section. The recommended structure:
<section class="section-name block-double-tb" id="section-id" aria-labelledby="heading-id">
<div class="inner">
<!-- Section content using utility classes from style.css -->
</div>
</section>
Spacing Rules (CRITICAL — NO EXCEPTIONS)
ALL spacing must come from style.css utility classes applied in HTML. Never write padding, margin, or gap in section CSS. Zero exceptions.
Section CSS is only for: gradients, backgrounds, colors, borders, shadows, border-radius, transitions, transforms, display, flex-direction, font-family, font-weight, letter-spacing, line-height, and component-specific decoration. Never font-size.
Available Spacing Classes
Padding (block-*):
| Class |
Value |
Variants |
block-half |
1.0625rem |
-tb, -lr, -top, -bot |
block-single |
2.125rem |
-tb, -lr, -top, -bot |
block-mid |
3.1875rem |
-tb, -lr, -top, -bot |
block-double |
4.25rem |
-tb, -lr, -top, -bot |
Margin (mt-*, mb-*):
| Class |
Value |
mt-none / mb-none |
0 !important |
mt-small / mb-small |
0.375rem |
mt-half / mb-half |
1.0625rem |
mt-single / mb-single |
2.125rem |
mt-mid / mb-mid |
3.1875rem |
mt-double / mb-double |
4.25rem |
mr-half / mr-single / mr-double |
Right margin |
ml-small |
Left margin |
Gap (gap-*):
| Class |
Value |
gap-none |
0 |
gap-half |
1.0625rem |
gap-single |
2.125rem |
gap-mid |
3.1875rem |
gap-double |
4.25rem |
Grid (grid-*):
| Class |
Columns |
Responsive |
grid-2 |
2 |
1 col at <=640px |
grid-3 |
3 |
2 at <=768px, 1 at <=640px |
grid-4 |
4 |
3 at <=992px, 2 at <=768px, 1 at <=640px |
columns-55-45, columns-60-40, etc. |
Asymmetric |
1 col at <=768px |
Example: Correct Spacing Usage
<!-- Spacing is ALL in HTML via utility classes -->
<section class="gt-hero block-double-tb">
<div class="inner block-half-lr">
<h2 class="mb-half">Title</h2>
<p class="mb-single">Description</p>
<div class="grid-3 gap-single">
<div class="block-single">Card with padding</div>
</div>
<div class="gt-actions gap-single mt-single">
<a class="gt-button block-half" href="#">CTA</a>
</div>
</div>
</section>
/* CSS has ZERO padding/margin/gap — only decoration */
.gt-hero { background: linear-gradient(...); overflow: hidden; }
.gt-button { background: var(--color-accent); border-radius: var(--radius-m); }
Typography Rules (CRITICAL — NO FONT SIZES IN CSS)
NEVER set font-size in section CSS files. The theme handles all font sizing through semantic HTML elements (h1–h6, p, span, code) and utility classes (small, caps, etc.). Section CSS should not contain any font-size declarations.
What CSS CAN set for typography:
font-family — only when overriding to var(--font-head), var(--font-mono), or var(--font-serif)
font-weight — for emphasis (700, 800, 600, 500)
letter-spacing — for tight headings (-0.03em, -0.02em)
line-height — only via variables: var(--lh-tight), var(--lh-base), var(--lh-relaxed)
color — text color
What CSS must NEVER set:
font-size — in any form (px, rem, em, clamp, var)
Font Families (CSS is OK)
| Variable |
Value |
Usage |
var(--font-head) |
GTReallySans |
Headings, titles, badges, buttons, stat numbers |
var(--font-body) |
InterVar/Inter |
Body text (default, rarely needs explicit declaration) |
var(--font-serif) |
TiemposText |
Subtitles, quotes, editorial accents |
var(--font-mono) |
SF Mono |
Code blocks, technical labels |
CSS Conventions
- Define section-scoped CSS variables at the section class level
- Use
color-mix() for theme-aware colors that work in light/dark mode
- Use existing CSS variables from
globals.css (--color-primary, --color-text, etc.)
- No
@import in CSS — the theme already loads globals
- No
<link> or <script> in HTML — CSS goes in the css field, JS in js field
- Prefix component classes with
gt- (e.g., gt-book-hero__title, gt-services__card)
- Prefer CSS-only animations over JS-driven scroll animations
- Include
@media (prefers-reduced-motion: reduce) to disable animations
- Dark mode overrides via
[data-theme="dark"] selector
JS Conventions (Minimize JS)
- Prefer CSS-only solutions:
@keyframes for entrance animations, :hover/:focus-visible for interactions
- Only use JS when CSS cannot achieve the effect
- When JS is needed: wrap in IIFE, early-return if section not found, respect
prefers-reduced-motion
- Use event delegation on section wrapper
Admin Bar Integration
The MD Tools admin bar menu provides quick access:
- Compile All / CSS / JS — recompile theme assets
- + New Page Block — direct link to create a new page block
- Active Blocks (n) — shows all page blocks rendering on the current page (both positioned and Gutenberg-embedded), each linking to its edit screen
Workflow: Creating Page Blocks
Via Admin UI
- Go to MD Settings → Page Blocks → Add New
- Enter title, HTML content, CSS, and JS
- Optionally set position hook and display conditions for site-wide placement
- Save as publish or draft
Via REST API
POST /wp-json/md/v1/page-blocks with content/css/js fields
- Use the returned
id in Gutenberg: <!-- wp:marketers-delight/page-block {"blockId":ID} /-->
- Or set
position for automatic hook-based placement
Via Shortcode
[page_block id="123"]
[page_block slug="hero-section"]
Section Checklist
Important Notes
- No document shell: Never include
<html>, <head>, <body> in content
- No
<style> or <script> tags: CSS goes in css field, JS goes in js field
- PHP execution is double-gated (since dropin v2.0+):
php_exec=1 alone is not enough. The runtime checks md_page_blocks_execute_php() which requires:
define('MD_ALLOW_PHP_SNIPPETS', true) in wp-config.php — site-level opt-in constant. Without it, PHP tags are silently stripped at render time.
- The stored
php_checksum (md5 of content at save time) must match the current content's md5. If they diverge — e.g., the content was mutated directly in the DB, or a save raced the checksum write — the runtime falls back to stripping <?php … ?> tags.
- Inline Gutenberg blocks (
marketers-delight/inline-page-block) cannot run PHP — there's no save-time checksum to verify. Only database-stored page blocks ({"blockId":N} references) can execute PHP.
- Failure symptom: HTML between PHP tags renders once (control flow gone, plain HTML kept), all
<?php echo … ?> outputs are empty, loops appear to iterate exactly once. If you see this, check (a) the constant in wp-config.php, (b) re-save the block via REST/admin to refresh php_checksum.
- Sites can override the gate via the
md_page_blocks_can_execute_php filter (receives $gate_default, $content, $checksum) for stricter or looser policies.
- Shortcodes work:
do_shortcode() is always called on content — prefer shortcodes over php_exec when the dynamic logic already exists as a shortcode, since they don't depend on the MD_ALLOW_PHP_SNIPPETS gate.
- Minification is automatic: HTML, CSS, and JS are minified on render
- Slugs are unique: auto-generated from title if not provided, with
-2, -3 suffixes for duplicates
- Soft delete: DELETE without
?force=true trashes; with force=true permanently deletes
- Asset versioning: every insert/update/delete bumps an internal version counter for cache busting
1---2name: page-block3description: Build and push section-based pages using the Marketers Delight Page Block (`marketers-delight/page-block`) Gutenberg block with HTML, CSS, and optional JS in block attributes. Use when creating or updating WordPress pages that use Page Blocks and when generating or pushing Page Block content through the REST API.4---56# Page Block Skill78Build and push section-based pages using the Marketers Delight **Page Block** system (v7.0.0). Page Blocks are reusable HTML/CSS/JS/PHP sections stored in a custom database table (`wp_md_page_blocks`), embeddable via Gutenberg block picker, shortcode, or positioned on hooks site-wide.910---1112## What Is a Page Block?1314A Page Block is a **reusable content unit** stored in `wp_md_page_blocks` with its own HTML, CSS, JS, and display conditions. It can be used in three ways:15161. **Gutenberg block** — picked from the library in the Block Editor via `marketers-delight/page-block`172. **Shortcode** — `[page_block id="123"]` or `[page_block slug="hero-section"]`183. **Positioned** — hooked to a theme action (e.g., `md_hook_before_header`) and conditionally displayed site-wide1920### Database Schema2122| Column | Type | Default | Purpose |23|--------|------|---------|---------|24| `id` | bigint | AUTO_INCREMENT | Primary key |25| `title` | varchar(255) | `''` | Display name |26| `slug` | varchar(200) | `''` | Unique identifier |27| `status` | varchar(20) | `'publish'` | publish/draft/trash |28| `content` | longtext | — | HTML (or HTML + PHP) |29| `css` | longtext | — | Block CSS |30| `js` | longtext | — | Block JavaScript |31| `js_location` | varchar(10) | `'footer'` | footer or inline |32| `output` | varchar(10) | `'inline'` | inline or file |33| `php_exec` | tinyint(1) | `0` | Execute PHP in content |34| `format` | tinyint(1) | `0` | Apply wpautop() |35| `position` | varchar(100) | `''` | Theme hook name (empty = no auto-placement) |36| `priority` | int(11) | `10` | Hook priority |37| `conditions` | longtext | `NULL` | JSON display conditions |38| `author` | bigint(20) | `0` | Creator user ID |39| `created_at` | datetime | CURRENT_TIMESTAMP | Creation time |40| `updated_at` | datetime | CURRENT_TIMESTAMP | Last update time |4142### Gutenberg Block Reference Format4344When embedded via the Block Editor, the block comment references by ID:4546```47<!-- wp:marketers-delight/page-block {"blockId":1} /-->4849<!-- wp:marketers-delight/page-block {"blockId":50} /-->50```5152Key details:53- **Self-closing block**: ends with `/-->` (no inner blocks)54- `blockId` references `wp_md_page_blocks.id`55- All content/CSS/JS lives in the database, not in the block comment56- The block has `save: function() { return null; }` — all rendering is server-side5758### Inline Page Block Format5960For pages where content/CSS/JS should live directly in the post content (not in the database), use the **inline-page-block** block type:6162```63<!-- wp:marketers-delight/inline-page-block {"content":"<section>...</section>","css":".class{css:value;}","js":"function (){}"} /-->64```6566Key details:67- **Block name**: `marketers-delight/inline-page-block` (NOT `page-block`)68- **Self-closing block**: ends with `/-->` (no inner blocks)69- All content/CSS/JS is JSON-encoded in the block comment attributes70- `content`: HTML string (JSON-escaped)71- `css`: CSS string (JSON-escaped)72- `js`: JavaScript string (JSON-escaped, optional)73- No `blockId` — everything is inline in the post content74- Ideal for programmatic/templated pages (e.g., local SEO city pages) where each page needs unique content75- Can be mixed with database-backed `page-block` references on the same page7677**When to use inline vs database:**78- **Database (`page-block` with `blockId`)**: Shared sections reused across many pages (edit once, update everywhere)79- **Inline (`inline-page-block`)**: Unique per-page content, programmatic generation, templated pages8081### Python Pattern for Inline Page Blocks8283```python84import json8586def inline_page_block(content, css, js=""):87 """Build an inline-page-block comment"""88 obj = {"content": content, "css": css}89 if js:90 obj["js"] = js91 return '<!-- wp:marketers-delight/inline-page-block ' + json.dumps(obj, ensure_ascii=False) + ' /-->'9293# Build a page with multiple inline blocks94blocks = [95 inline_page_block('<section class="hero">...</section>', '.hero { background: #000; }', '(function(){})();'),96 inline_page_block('<section class="services">...</section>', '.services { padding: 4rem 0; }'),97]98page_content = "\n\n".join(blocks)99```100101New blocks should use the database `blockId` reference format for shared content, and `inline-page-block` for per-page unique content.102103---104105## How It Renders106107### CSS Delivery1081. On `template_redirect`, positioned blocks are queried and their CSS collected1092. During `the_content`, Gutenberg page blocks render and add their CSS1103. All CSS is combined and output in `<head>` as `<style id="md-page-blocks-css">`1114. File output mode: generates `/wp-content/uploads/md-page-blocks/page-blocks-v{version}.css`112113### HTML Rendering1141. If `php_exec` is true **AND** `MD_ALLOW_PHP_SNIPPETS` is defined truthy in `wp-config.php` **AND** the stored `php_checksum` matches `md5($content)` at render time, PHP is executed via temp file include. Otherwise `<?php … ?>` blocks are stripped (HTML between tags is preserved, which is misleading — see "PHP execution is double-gated" below).1152. Shortcodes are processed via `do_shortcode()`1163. If `format` is true, `wpautop()` is applied1174. HTML is minified (whitespace collapsed, comments stripped, `<pre>`/`<code>`/`<script>`/`<style>` preserved)118119### JS Delivery1201. Inline JS collected and output in `wp_footer` as `<script id="md-page-blocks-js">`1212. JS with `js_location = 'inline'` outputs immediately after the block's HTML1223. File output mode: generates external minified JS file123124### Positioned Blocks125Blocks with a `position` set are automatically hooked to that theme action:1261. On `template_redirect`, all positioned blocks are queried1272. Display conditions are evaluated (post types, post IDs, page types)1283. Matching blocks register on their position hook at the specified priority1294. CSS is collected early for `<head>` output130131---132133## REST API134135Full CRUD at `/wp-json/md/v1/page-blocks`. Requires authentication.136137| Method | Endpoint | Permission | Purpose |138|--------|----------|------------|---------|139| GET | `/page-blocks` | `edit_posts` | List blocks (paginated, filterable) |140| POST | `/page-blocks` | `manage_options` | Create block |141| GET | `/page-blocks/{id}` | `edit_posts` | Get single block |142| PUT/PATCH | `/page-blocks/{id}` | `manage_options` | Update block |143| DELETE | `/page-blocks/{id}` | `manage_options` | Trash (or `?force=true` to delete) |144| GET | `/page-blocks/{id}/render` | `edit_posts` | Render block HTML preview |145146### Query Parameters (GET /page-blocks)147148| Param | Type | Default | Options |149|-------|------|---------|---------|150| `status` | string | `'publish'` | publish, draft, trash, `''` (all) |151| `search` | string | `''` | Search title and slug |152| `position` | string | `''` | Filter by position hook |153| `orderby` | string | `'updated_at'` | id, title, slug, updated_at, created_at |154| `order` | string | `'DESC'` | ASC, DESC |155| `per_page` | int | `20` | 1–100 |156| `page` | int | `1` | Page number |157158### Create/Update Fields159160| Field | Type | Description |161|-------|------|-------------|162| `title` | string | Display name |163| `slug` | string | Unique slug (auto-generated from title if omitted) |164| `status` | string | publish, draft, trash |165| `content` | string | HTML content |166| `css` | string | CSS code |167| `js` | string | JavaScript code |168| `js_location` | string | `'footer'` or `'inline'` |169| `output` | string | `'inline'` or `'file'` |170| `php_exec` | boolean | Execute PHP in content |171| `format` | boolean | Apply wpautop() formatting |172| `position` | string | Theme hook name (empty = shortcode/block only) |173| `priority` | integer | Hook priority (default 10) |174| `conditions` | object/null | Display conditions JSON |175176### Python Pattern for Managing Page Blocks177178```python179import base64180import requests181182WP_URL = "https://yoursite.com"183USERNAME = "your-username"184APP_PASSWORD = "..." # from .env185186credentials = f"{USERNAME}:{APP_PASSWORD}"187auth_header = base64.b64encode(credentials.encode()).decode()188headers = {189 "Authorization": f"Basic {auth_header}",190 "Content-Type": "application/json",191}192193# Create a new page block194response = requests.post(195 f"{WP_URL}/wp-json/md/v1/page-blocks",196 json={197 "title": "Hero Section",198 "content": '<section class="hero block-double-tb"><div class="inner">...</div></section>',199 "css": ".hero { background: var(--color-bg); }",200 "js": "(function() { /* interactions */ })();",201 "status": "publish",202 },203 headers=headers,204 timeout=30,205)206block = response.json()207block_id = block["id"] # Use this in Gutenberg: {"blockId": block_id}208209# Update an existing page block210requests.put(211 f"{WP_URL}/wp-json/md/v1/page-blocks/{block_id}",212 json={"css": ".hero { background: linear-gradient(...); }"},213 headers=headers,214)215216# Create a positioned block (shows site-wide)217requests.post(218 f"{WP_URL}/wp-json/md/v1/page-blocks",219 json={220 "title": "Announcement Bar",221 "content": '<div class="announce block-half-tb"><div class="inner">...</div></div>',222 "css": ".announce { background: var(--color-primary); color: #fff; }",223 "position": "md_hook_before_header",224 "priority": 5,225 "conditions": {226 "page_types": ["front_page", "blog"],227 },228 "status": "publish",229 },230 headers=headers,231)232233# List all page blocks234blocks = requests.get(235 f"{WP_URL}/wp-json/md/v1/page-blocks",236 params={"per_page": 50},237 headers=headers,238).json()239```240241### Embedding in a WordPress Page242243After creating page blocks via the REST API, reference them in page content:244245```python246# Push page content with page block references247page_content = "\n\n".join([248 '<!-- wp:marketers-delight/page-block {"blockId":1} /-->',249 '<!-- wp:marketers-delight/page-block {"blockId":2} /-->',250 '<!-- wp:marketers-delight/page-block {"blockId":50} /-->',251])252253requests.post(254 f"{WP_URL}/wp-json/wp/v2/pages/{PAGE_ID}",255 json={"content": page_content},256 headers=headers,257)258```259260---261262## Display Conditions (Positioned Blocks)263264Conditions are stored as JSON. All conditions must pass (AND logic).265266```json267{268 "post_types": ["post", "page"],269 "post_ids": [7172, 1234],270 "page_types": ["front_page", "blog", "singular", "archive", "search", "404"]271}272```273274- Empty conditions = show everywhere275- `post_types` — only show on these post types276- `post_ids` — only show on these specific posts/pages277- `page_types` — only show on these WordPress conditionals278279---280281## Available Position Hooks282283| Hook | Location |284|------|----------|285| `md_hook_before_html` | Very top of page |286| `md_hook_before_header` | Before header |287| `md_hook_header_top` | Header top |288| `md_hook_header_bottom` | Header bottom |289| `md_hook_after_header` | After header |290| `md_hook_before_content_box` | Before content box |291| `md_hook_content_box_top` | Content box top |292| `md_hook_content_box_bottom` | Content box bottom |293| `md_hook_before_content` | Before main content |294| `md_hook_content_top` | Content top (inside) |295| `md_hook_before_the_content` | Before the content |296| `md_hook_content` | After post content |297| `md_hook_content_bottom` | Content bottom (inside) |298| `md_hook_after_content` | After main content |299| `md_hook_before_sidebar` | Before sidebar |300| `md_hook_after_sidebar` | After sidebar |301| `md_hook_before_footer` | Before footer |302| `md_hook_footer_top` | Footer top |303| `md_hook_footer_bottom` | Footer bottom |304| `md_hook_after_footer` | After footer |305| `md_hook_before_footer_copy` | Before footer copyright |306| `md_hook_after_footer_copy` | After footer copyright |307308---309310## Section Architecture311312Each Page Block represents one visual section. The recommended structure:313314```html315<section class="section-name block-double-tb" id="section-id" aria-labelledby="heading-id">316 <div class="inner">317 <!-- Section content using utility classes from style.css -->318 </div>319</section>320```321322### Spacing Rules (CRITICAL — NO EXCEPTIONS)323324**ALL spacing must come from `style.css` utility classes applied in HTML.** Never write `padding`, `margin`, or `gap` in section CSS. Zero exceptions.325326Section CSS is **only** for: gradients, backgrounds, colors, borders, shadows, border-radius, transitions, transforms, display, flex-direction, font-family, font-weight, letter-spacing, line-height, and component-specific decoration. **Never `font-size`.**327328#### Available Spacing Classes329330**Padding (`block-*`):**331332| Class | Value | Variants |333|-------|-------|----------|334| `block-half` | `1.0625rem` | `-tb`, `-lr`, `-top`, `-bot` |335| `block-single` | `2.125rem` | `-tb`, `-lr`, `-top`, `-bot` |336| `block-mid` | `3.1875rem` | `-tb`, `-lr`, `-top`, `-bot` |337| `block-double` | `4.25rem` | `-tb`, `-lr`, `-top`, `-bot` |338339**Margin (`mt-*`, `mb-*`):**340341| Class | Value |342|-------|-------|343| `mt-none` / `mb-none` | `0 !important` |344| `mt-small` / `mb-small` | `0.375rem` |345| `mt-half` / `mb-half` | `1.0625rem` |346| `mt-single` / `mb-single` | `2.125rem` |347| `mt-mid` / `mb-mid` | `3.1875rem` |348| `mt-double` / `mb-double` | `4.25rem` |349| `mr-half` / `mr-single` / `mr-double` | Right margin |350| `ml-small` | Left margin |351352**Gap (`gap-*`):**353354| Class | Value |355|-------|-------|356| `gap-none` | `0` |357| `gap-half` | `1.0625rem` |358| `gap-single` | `2.125rem` |359| `gap-mid` | `3.1875rem` |360| `gap-double` | `4.25rem` |361362**Grid (`grid-*`):**363364| Class | Columns | Responsive |365|-------|---------|------------|366| `grid-2` | 2 | 1 col at <=640px |367| `grid-3` | 3 | 2 at <=768px, 1 at <=640px |368| `grid-4` | 4 | 3 at <=992px, 2 at <=768px, 1 at <=640px |369| `columns-55-45`, `columns-60-40`, etc. | Asymmetric | 1 col at <=768px |370371#### Example: Correct Spacing Usage372373```html374<!-- Spacing is ALL in HTML via utility classes -->375<section class="gt-hero block-double-tb">376 <div class="inner block-half-lr">377 <h2 class="mb-half">Title</h2>378 <p class="mb-single">Description</p>379 <div class="grid-3 gap-single">380 <div class="block-single">Card with padding</div>381 </div>382 <div class="gt-actions gap-single mt-single">383 <a class="gt-button block-half" href="#">CTA</a>384 </div>385 </div>386</section>387```388389```css390/* CSS has ZERO padding/margin/gap — only decoration */391.gt-hero { background: linear-gradient(...); overflow: hidden; }392.gt-button { background: var(--color-accent); border-radius: var(--radius-m); }393```394395### Typography Rules (CRITICAL — NO FONT SIZES IN CSS)396397**NEVER set `font-size` in section CSS files.** The theme handles all font sizing through semantic HTML elements (`h1`–`h6`, `p`, `span`, `code`) and utility classes (`small`, `caps`, etc.). Section CSS should not contain any `font-size` declarations.398399**What CSS CAN set for typography:**400- `font-family` — only when overriding to `var(--font-head)`, `var(--font-mono)`, or `var(--font-serif)`401- `font-weight` — for emphasis (700, 800, 600, 500)402- `letter-spacing` — for tight headings (`-0.03em`, `-0.02em`)403- `line-height` — only via variables: `var(--lh-tight)`, `var(--lh-base)`, `var(--lh-relaxed)`404- `color` — text color405406**What CSS must NEVER set:**407- `font-size` — in any form (px, rem, em, clamp, var)408409#### Font Families (CSS is OK)410411| Variable | Value | Usage |412|----------|-------|-------|413| `var(--font-head)` | GTReallySans | Headings, titles, badges, buttons, stat numbers |414| `var(--font-body)` | InterVar/Inter | Body text (default, rarely needs explicit declaration) |415| `var(--font-serif)` | TiemposText | Subtitles, quotes, editorial accents |416| `var(--font-mono)` | SF Mono | Code blocks, technical labels |417418### CSS Conventions419- Define section-scoped CSS variables at the section class level420- Use `color-mix()` for theme-aware colors that work in light/dark mode421- Use existing CSS variables from `globals.css` (`--color-primary`, `--color-text`, etc.)422- **No `@import` in CSS** — the theme already loads globals423- **No `<link>` or `<script>` in HTML** — CSS goes in the `css` field, JS in `js` field424- Prefix component classes with `gt-` (e.g., `gt-book-hero__title`, `gt-services__card`)425- Prefer CSS-only animations over JS-driven scroll animations426- Include `@media (prefers-reduced-motion: reduce)` to disable animations427- Dark mode overrides via `[data-theme="dark"]` selector428429### JS Conventions (Minimize JS)430- Prefer CSS-only solutions: `@keyframes` for entrance animations, `:hover`/`:focus-visible` for interactions431- Only use JS when CSS cannot achieve the effect432- When JS is needed: wrap in IIFE, early-return if section not found, respect `prefers-reduced-motion`433- Use event delegation on section wrapper434435---436437## Admin Bar Integration438439The **MD Tools** admin bar menu provides quick access:440- **Compile All / CSS / JS** — recompile theme assets441- **+ New Page Block** — direct link to create a new page block442- **Active Blocks (n)** — shows all page blocks rendering on the current page (both positioned and Gutenberg-embedded), each linking to its edit screen443444---445446## Workflow: Creating Page Blocks447448### Via Admin UI4491. Go to **MD Settings → Page Blocks → Add New**4502. Enter title, HTML content, CSS, and JS4513. Optionally set position hook and display conditions for site-wide placement4524. Save as publish or draft453454### Via REST API4551. `POST /wp-json/md/v1/page-blocks` with content/css/js fields4562. Use the returned `id` in Gutenberg: `<!-- wp:marketers-delight/page-block {"blockId":ID} /-->`4573. Or set `position` for automatic hook-based placement458459### Via Shortcode460```461[page_block id="123"]462[page_block slug="hero-section"]463```464465### Section Checklist466- [ ] HTML uses semantic elements (`<section>`, `<header>`, `<nav>`)467- [ ] Spacing uses utility classes (`block-double-tb`, `mb-single`, etc.)468- [ ] CSS uses CSS variables, not hardcoded values469- [ ] CSS is scoped to section class (no global selectors)470- [ ] Dark mode works via `color-mix()` with theme variables471- [ ] Responsive breakpoints match the project system (640/768/992/1366)472- [ ] Accessibility: `aria-labelledby`, `aria-hidden="true"` on decorative elements473- [ ] JS wrapped in IIFE with early-return guard474- [ ] `content-visibility: auto` on below-fold sections for performance475476---477478## Important Notes479480- **No document shell**: Never include `<html>`, `<head>`, `<body>` in content481- **No `<style>` or `<script>` tags**: CSS goes in `css` field, JS goes in `js` field482- **PHP execution is double-gated** (since dropin v2.0+): `php_exec=1` alone is **not enough**. The runtime checks `md_page_blocks_execute_php()` which requires:483 1. `define('MD_ALLOW_PHP_SNIPPETS', true)` in `wp-config.php` — site-level opt-in constant. Without it, PHP tags are silently stripped at render time.484 2. The stored `php_checksum` (md5 of content at save time) must match the current content's md5. If they diverge — e.g., the content was mutated directly in the DB, or a save raced the checksum write — the runtime falls back to stripping `<?php … ?>` tags.485 - Inline Gutenberg blocks (`marketers-delight/inline-page-block`) **cannot** run PHP — there's no save-time checksum to verify. Only database-stored page blocks (`{"blockId":N}` references) can execute PHP.486 - **Failure symptom**: HTML between PHP tags renders once (control flow gone, plain HTML kept), all `<?php echo … ?>` outputs are empty, loops appear to iterate exactly once. If you see this, check (a) the constant in `wp-config.php`, (b) re-save the block via REST/admin to refresh `php_checksum`.487 - Sites can override the gate via the `md_page_blocks_can_execute_php` filter (receives `$gate_default, $content, $checksum`) for stricter or looser policies.488- **Shortcodes work**: `do_shortcode()` is always called on content — prefer shortcodes over `php_exec` when the dynamic logic already exists as a shortcode, since they don't depend on the `MD_ALLOW_PHP_SNIPPETS` gate.489- **Minification is automatic**: HTML, CSS, and JS are minified on render490- **Slugs are unique**: auto-generated from title if not provided, with `-2`, `-3` suffixes for duplicates491- **Soft delete**: DELETE without `?force=true` trashes; with `force=true` permanently deletes492- **Asset versioning**: every insert/update/delete bumps an internal version counter for cache busting