Gateway
Second attempt at building a structured data plugin for WordPress with Laravel model integration.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/caseyjmilne/gateway/archive/refs/heads/claude%2Fwordpress-datatable-block-pl580v.zipReadme
Gateway
A WordPress plugin for custom Gutenberg blocks, starting with a Data Table block: a sortable, searchable grid of posts for any registered post type, powered by DataTables.
Requirements
- WordPress 6.3+
- PHP 8.2+ (raised from 7.4 by the vendored
illuminate/databasepackage -- see "Laravel Models (Illuminate/Eloquent)" below) - Node.js 18+ (build tooling only -- not required at runtime)
Getting started
npm install
npm run build
Compiled assets are committed to the repo (blocks/*/build/), so the plugin
works immediately after activation even without running a build. Run
npm run build after changing anything under blocks/*/src/.
Available scripts:
| Script | Purpose |
|---|---|
npm run build |
Clean + production build of every block. |
npm run start |
Watch mode for development. |
npm run clean |
Remove all blocks/*/build output. |
npm run format |
Format block source with wp-scripts format. |
npm run lint:js |
Lint block JS. |
npm run lint:css |
Lint block styles. |
Architecture
gateway.php Plugin bootstrap: constants + boots Block_Loader, Column_Registry, Columns_REST_Controller, Facet_Query
includes/
class-block-loader.php Scans /blocks and register_block_type()'s every block found
class-column-registry.php Discovers columns (core fields + meta/ACF) for a post type; renders cell values
class-columns-rest-controller.php GET /gateway/v1/columns/<post_type> (the block editor's column/facet picker)
class-facet-query.php Applies validated facets to a WP_Query; distinct values for select/checkboxes facets
blocks/
shared/ Cross-block JS, NOT a block itself (no block.json -- webpack's glob skips it)
datatable.js Shared DataTables init/destroy helpers (jQuery + datatables.net-dt)
dom.js Pure DOM helpers (getColumnIndexByKey) -- no jQuery/DataTables dependency
wait-for-datatable.js "Find the sibling table, wait for a DataTable, hide a native widget" (jQuery only)
use-live-datatable-sync.js Editor-only: keeps a live preview wired to the (possibly-replaced) sibling DataTable instance
use-available-columns.js Fetches the field list for a post type (shared by both blocks below)
datatable/
block.json Block metadata, attributes, providesContext, asset + render wiring
render.php PHP render callback: renders the Facets position (a core/group Row, or whatever a site owner put there) then header/body/footer, in that order
src/
index.js Editor registration (editorScript)
edit.js Editor UI: InspectorControls + one self-healing InnerBlocks skeleton
save.js Persists InnerBlocks content only -- render.php doesn't use it
view.js Front-end entry (viewScript): finds tables, inits DataTables
style.scss Shared styles (front end + editor)
controls/
post-type-control.js Reusable "Post Type" SelectControl
limit-control.js "Limit" numeric field
page-size-control.js "Page Size" numeric field
columns-panel.js Renders the two below for the selected columns
facets-panel.js Renders the two below for the selected facets
available-columns-list.js Click-to-toggle field selection (shared by columns + facets)
column-config-table.js Drag-to-reorder + click-to-toggle-sortable table
facet-config-table.js Drag-to-reorder table; "Default" button opens a modal with compare/value
facet-compare-options.js Facet comparison operator list (=, !=, >, ...)
hooks/
use-reconcile-field-list.js Drops selections no longer valid for the current post type
use-required-inner-blocks.js React hook: name-based self-heal for the 4 required children (see below)
build/ Compiled output (generated, do not hand-edit)
datatable-header/
block.json Block metadata, parent restricting it to gateway/datatable
render.php PHP render callback: echoes $content as-is (see "Header/body/footer" below)
src/
index.js Editor registration
edit.js Editor UI: an InnerBlocks area restricted to gateway/datatable-page-size + gateway/datatable-search
save.js Persists InnerBlocks content -- this block's own wrapper + classes
view.js No front-end behavior -- exists only so style.scss gets built
style.scss Layout for the page-size/search children (flex row, spread to opposite ends)
build/ Compiled output (generated, do not hand-edit)
datatable-body/
block.json Block metadata, attributes (mirrored context, SSR-preview fallback only), usesContext
render.php PHP render callback: the actual <table> -- headings + rows -- moved here from datatable/render.php
src/
index.js Editor registration
edit.js Editor UI: mirrors context into attributes, then <ServerSideRender> + DataTables init
save.js Persists nothing (leaf, dynamic block)
hooks/
use-datatable-init.js React hook: (re)inits DataTables against an async-rendered container
build/ Compiled output (generated, do not hand-edit)
datatable-footer/
block.json Block metadata, parent restricting it to gateway/datatable
render.php PHP render callback: echoes $content as-is (see "Header/body/footer" below)
src/
index.js Editor registration
edit.js Editor UI: an InnerBlocks area restricted to gateway/pagination + gateway/datatable-results
save.js Persists InnerBlocks content -- this block's own wrapper + classes
view.js No front-end behavior -- exists only so style.scss gets built
style.scss Layout for the pagination/results children
build/ Compiled output (generated, do not hand-edit)
facet/
block.json Block metadata, ancestor + usesContext restricting it to gateway/datatable
render.php PHP render callback: the input/select/checkboxes control
src/
index.js Editor registration
edit.js Editor UI: pick a facet + a UI type, with validity warnings
view.js Front-end entry: hooks the control into the sibling DataTable instance
style.scss Facet control styles
controls/
facet-key-control.js "Facet" select, from the parent's configured facets (context)
ui-type-control.js "UI Type" select: input / select / checkboxes
compare-control.js "Compare" select (Contains/Equals), Input UI type only
build/ Compiled output (generated, do not hand-edit)
datatable-page-size/
block.json Block metadata, parent restricting it to gateway/datatable-header
render.php PHP render callback: an empty, disabled <select> skeleton
src/
index.js Editor registration
edit.js Editor UI: a *live* preview -- see "A live editor preview" below
view.js Front-end entry: finds+waits for the table, hands off to attach-page-size.js
attach-page-size.js Shared option-populating/wiring logic, used by both edit.js and view.js
style.scss Page Size control styles
build/ Compiled output (generated, do not hand-edit)
datatable-search/
block.json Block metadata, parent restricting it to gateway/datatable-header
render.php PHP render callback: a disabled search <input> skeleton
src/
index.js Editor registration
edit.js Editor UI: a static preview (no settings to configure)
view.js Front-end entry: drives the sibling DataTable's search() API
style.scss Search control styles
build/ Compiled output (generated, do not hand-edit)
pagination/
block.json Block metadata, parent restricting it to gateway/datatable-footer
render.php PHP render callback: an empty Prev/Next + page-number skeleton
src/
index.js Editor registration
edit.js Editor UI: a *live* preview -- see "Pagination and Results" below
view.js Front-end entry: finds+waits for the table, hands off to attach-pagination.js
attach-pagination.js Shared button-building/wiring logic, used by both edit.js and view.js
style.scss Pagination control styles
build/ Compiled output (generated, do not hand-edit)
datatable-results/
block.json Block metadata, parent restricting it to gateway/datatable-footer
render.php PHP render callback: an empty "Showing X to Y of Z entries" skeleton
src/
index.js Editor registration
edit.js Editor UI: a *live* preview -- see "A live editor preview" below
view.js Front-end entry: finds+waits for the table, hands off to attach-results.js
attach-results.js Shared info-text-building logic, used by both edit.js and view.js
style.scss Results text styles
build/ Compiled output (generated, do not hand-edit)
Block discovery / autoloading
Block_Loader::register_blocks() (hooked on init) globs every directory
under /blocks and calls register_block_type() on each one that has a
block.json. Nothing in PHP needs to change to add a new block -- drop a new
blocks/<slug>/ directory in place (with its own block.json, src/, and
optional render.php) and it's picked up automatically. webpack.config.js
does the equivalent for the build: it globs blocks/*/block.json and wires
up index.js/view.js entries per block automatically, compiling each into
that block's own build/ directory.
A dedicated "Gateway" block category, for the four top-level blocks
Block_Loader::register_category() (on block_categories_all, alongside
register_blocks()'s own init hook) registers one new inserter
category, gateway/"Gateway" -- prepended ahead of core's own categories
(Text/Media/Design/etc.), rather than appended after them, so a plugin
whose main purpose IS these blocks gets surfaced first, not buried below
groupings most of this plugin's own users have no reason to browse.
Only the four block.json files a site owner actually starts a layout
with set their own category to "gateway": gateway/data-cards,
gateway/data-display, gateway/datatable, gateway/single-record --
recognizable as exactly the four with neither a parent nor an
ancestor restriction, the only ones ever offered from the top-level
inserter to begin with. Every other block this plugin ships (every
datatable-*/data-cards-* child, gateway/card-field-text/-number/
-image, gateway/related-items, gateway/facet/gateway/card-facet,
gateway/pagination) deliberately keeps its existing "widgets"
category untouched -- none of them are ever reachable from that
top-level list regardless of which category they claim (a parent
-restricted block only ever appears nested inside its one named parent;
an ancestor-restricted one only once already inside a matching
ancestor), so grouping them under "Gateway" too would just be dead
weight in a category no one browses looking for them.
Verified with a new standalone PHP smoke test (register_category()
returns core's own categories untouched plus exactly one new,
well-formed entry, prepended ahead of them) plus a second pass reading
every real blocks/*/block.json on disk and confirming, block by block,
that a top-level block (no parent/ancestor) is in "gateway" and a
child block (either restriction) is not -- alongside the full existing
regression suite. No build step needed: category is plain block.json
metadata register_block_type() reads directly, never something
webpack/@wordpress/scripts compiles into a block's own build/
output.
Dynamic blocks, server-rendered
Every block in this plugin has no meaningful client-side save() markup
(each returns null, or -- for the three InnerBlocks wrappers -- persists
only the child delimiter comments). All markup -- on both the front end and
in the block editor -- comes from each block's own render.php via
block.json's "render" field:
- Front end: WordPress calls
render.phpwhen the block is rendered in a post/page -- normal recursive block rendering, context and all. - Editor: the one block whose editor preview needs to show real,
server-computed content --
gateway/datatable-body, the actual<table>-- renders<ServerSideRender block="gateway/datatable-body" />, which calls that samerender.phpthrough theblock-rendererREST endpoint. This guarantees the editor preview is never out of sync with what actually ships to the front end. (gateway/datatableitself doesn't use<ServerSideRender>at all any more -- see "Header/body/footer" below for why that moved.)