{"id":379263,"date":"2026-10-08T10:02:19","date_gmt":"2026-10-08T10:02:19","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/custom-headless-data\/"},"modified":"2026-10-08T10:09:23","modified_gmt":"2026-10-08T10:09:23","slug":"lukdev-custom-data-tables","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/lukdev-custom-data-tables\/","author":23574744,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"0.1.0","stable_tag":"0.1.0","tested":"7.1.3","requires":"6.2","requires_php":"7.4","requires_plugins":null,"header_name":"LUKDEV Custom Data Tables","header_author":"LUKDEV","header_description":"Define custom data structures (fields) and manage rows of data exposed through the REST API.","assets_banners_color":"171717","last_updated":"2026-10-08 10:09:23","external_support_url":"","external_repository_url":"","donate_link":"https:\/\/www.lukaszdabik.pl\/","header_plugin_uri":"https:\/\/lukdev-custom-data-tables.lukaszdabik.pl\/","header_author_uri":"https:\/\/www.lukaszdabik.pl\/","rating":0,"author_block_rating":0,"active_installs":0,"downloads":83,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"0.1.0":{"tag":"0.1.0","author":"lukdev","date":"2026-10-08 10:09:23","revision":3734221}},"upgrade_notice":{"0.1.0":"<p>First release.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3734310,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3734310,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256},"icon.svg":{"filename":"icon.svg","revision":3734310,"resolution":false,"location":"assets","locale":false}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3734310,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3734310,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["0.1.0"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3734195,"resolution":"1","location":"assets","locale":"","width":1816,"height":1560},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3734195,"resolution":"2","location":"assets","locale":"","width":1816,"height":1560},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3734195,"resolution":"3","location":"assets","locale":"","width":1816,"height":1560},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3734195,"resolution":"4","location":"assets","locale":"","width":1816,"height":1560}},"screenshots":{"1":"List of data structures.","2":"Structure builder with field settings.","3":"Rows editor."}},"plugin_section":[],"plugin_tags":[2010,3524,141196,2244,23853],"plugin_category":[],"plugin_contributors":[284970],"plugin_business_model":[],"class_list":["post-379263","plugin","type-plugin","status-publish","hentry","plugin_tags-custom-fields","plugin_tags-data","plugin_tags-headless","plugin_tags-repeater","plugin_tags-rest-api","plugin_contributors-lukdev","plugin_committers-lukdev"],"banners":{"banner":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/banner-772x250.png?rev=3734310","banner_2x":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/banner-1544x500.png?rev=3734310","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/icon.svg?rev=3734310","icon":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/icon.svg?rev=3734310","icon_2x":false,"generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/screenshot-1.png?rev=3734195","caption":"List of data structures."},{"src":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/screenshot-2.png?rev=3734195","caption":"Structure builder with field settings."},{"src":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/screenshot-3.png?rev=3734195","caption":"Rows editor."},{"src":"https:\/\/ps.w.org\/lukdev-custom-data-tables\/assets\/screenshot-4.png?rev=3734195","caption":""}],"raw_content":"<!--section=description-->\n<p>A WordPress plugin for defining custom data structures (sets of fields) and managing rows of data for them in the admin. The data is exposed through a public, read-only REST API, so a headless front end can use it.<\/p>\n\n<p>Think of it as a small, standalone alternative to ACF options pages and repeaters. Each structure is a list of rows, stored in its own database tables instead of post meta.<\/p>\n\n<p>Author: <a href=\"https:\/\/www.lukaszdabik.pl\/\">LUKDEV<\/a>. Text domain: <code>lukdev-custom-data-tables<\/code>.<\/p>\n\n<h3>Requirements<\/h3>\n\n<ul>\n<li>WordPress 6.2+ (queries use the <code>%i<\/code> identifier placeholder of <code>$wpdb-&gt;prepare()<\/code>)<\/li>\n<li>PHP 7.4+<\/li>\n<\/ul>\n\n<h3>Usage<\/h3>\n\n<p>Go to <strong>LUKDEV Custom Data Tables<\/strong> in the admin menu. You need the <code>manage_options<\/code> capability.<\/p>\n\n<ol>\n<li><strong>Create a structure.<\/strong> Give it a name and, optionally, a slug (the name is used when the slug is empty). Then add fields. A field's key is generated from its label, for example <code>Hero title<\/code> becomes <code>hero_title<\/code>. Keys must be unique within a structure.<\/li>\n<li><strong>Add rows.<\/strong> After you save the structure, the rows editor opens. Rows are saved one at a time, can be moved up or down, and can be deleted.<\/li>\n<li><strong>Fetch the data<\/strong> from the REST API (see below).<\/li>\n<\/ol>\n\n<p>Deleting a structure also deletes all its rows.<\/p>\n\n<h3>Field types<\/h3>\n\n<p>Each type is listed with its settings, the value stored in the database and the value returned by the API.<\/p>\n\n<ul>\n<li><code>text<\/code>: no settings. Stored: string. API: string.<\/li>\n<li><code>textarea<\/code>: no settings. Stored: string. API: string.<\/li>\n<li><code>wysiwyg<\/code>: no settings. Stored: HTML (<code>wp_kses_post<\/code>). API: string.<\/li>\n<li><code>number<\/code>: no settings. Stored: numeric string. API: string.<\/li>\n<li><code>email<\/code>: no settings. Stored: string. API: string.<\/li>\n<li><code>url<\/code>: no settings. Stored: string. API: string.<\/li>\n<li><code>boolean<\/code>: no settings. Stored: <code>\"1\"<\/code> \/ <code>\"0\"<\/code>. API: <code>true<\/code> \/ <code>false<\/code>.<\/li>\n<li><code>select<\/code>: settings: options, default. Stored: option value. API: string.<\/li>\n<li><code>radio<\/code>: settings: options, default. Stored: option value. API: string.<\/li>\n<li><code>checkbox<\/code>: settings: options, defaults. Stored: list of option values. API: array of strings.<\/li>\n<li><code>image<\/code>: no settings. Stored: attachment ID. API: image object or <code>null<\/code>.<\/li>\n<li><code>color_picker<\/code>: settings: default, opacity, return format (<code>string<\/code> \/ <code>array<\/code>). Stored: <code>#rrggbb<\/code> or <code>rgba(r,g,b,a)<\/code>. API: string, or <code>{ red, green, blue, alpha }<\/code>.<\/li>\n<li><code>date_picker<\/code>: settings: display format, return format, first day of week. Stored: <code>Ymd<\/code>. API: date in the return format.<\/li>\n<li><code>post_object<\/code>: settings: post types, multiple. Stored: post ID or list of IDs. API: post object, <code>null<\/code>, or array of posts.<\/li>\n<li><code>relationship<\/code>: settings: post types, filters (search \/ post type \/ taxonomy), max. Stored: list of post IDs. API: array of posts.<\/li>\n<li><code>link<\/code>: no settings. Stored: <code>{ page, url, title, target }<\/code>. API: <code>{ title, url, target }<\/code> or <code>null<\/code>.<\/li>\n<li><code>repeater<\/code>: settings: sub-fields. Stored: list of items. API: array of objects.<\/li>\n<\/ul>\n\n<p>Notes:<\/p>\n\n<ul>\n<li><strong>Options:<\/strong> an option without a value uses its label as the value, and the other way round.<\/li>\n<li><strong>Repeaters<\/strong> can't be nested. A repeater added as a sub-field is saved as <code>text<\/code>.<\/li>\n<li><strong>Post object and relationship fields<\/strong> accept any public post type (except attachments) when no post types are selected. The API returns published posts only. Drafts and private posts picked in the admin are left out.<\/li>\n<li><strong>Link fields<\/strong> point to either a page or a custom URL. The page wins if both are set. The page's title is used when the link text is empty.<\/li>\n<li><strong>Date picker fields<\/strong> use PHP date formats. The default for both display and return is <code>d\/m\/Y<\/code>.<\/li>\n<li><strong>Changing a field's type or settings keeps saved data where possible.<\/strong> For example, a single post object becomes a list when <em>multiple<\/em> is turned on, and a select value becomes a one-item checkbox list. Fields saved before field types existed are treated as <code>textarea<\/code>.<\/li>\n<\/ul>\n\n<h3>REST API<\/h3>\n\n<p>Both endpoints are public (no authentication) and read-only.<\/p>\n\n<h3><code>GET \/wp-json\/ldcdt\/v1\/data<\/code><\/h3>\n\n<p>Lists all structures.<\/p>\n\n<pre><code>[\n    {\n        \"name\": \"Team\",\n        \"slug\": \"team\",\n        \"fields\": [\n            { \"key\": \"name\", \"label\": \"Name\", \"type\": \"text\" },\n            { \"key\": \"photo\", \"label\": \"Photo\", \"type\": \"image\" }\n        ],\n        \"url\": \"https:\/\/example.com\/wp-json\/ldcdt\/v1\/data\/team\"\n    }\n]\n<\/code><\/pre>\n\n<h3><code>GET \/wp-json\/ldcdt\/v1\/data\/{slug}<\/code><\/h3>\n\n<p>Returns one structure with its rows, in the order set in the admin. Each item has an <code>id<\/code> plus one property per field key. Returns <code>404<\/code> (<code>ldcdt_not_found<\/code>) for an unknown slug.<\/p>\n\n<pre><code>{\n  \"name\": \"Team\",\n  \"slug\": \"team\",\n  \"fields\": [ ... ],\n  \"items\": [\n    {\n      \"id\": 1,\n      \"name\": \"Jane Doe\",\n      \"photo\": {\n        \"id\": 42,\n        \"url\": \"https:\/\/example.com\/app\/uploads\/jane.jpg\",\n        \"alt\": \"\",\n        \"width\": 1200,\n        \"height\": 800,\n        \"sizes\": {\n          \"thumbnail\": { \"url\": \"...\", \"width\": 150, \"height\": 150 }\n        }\n      }\n    }\n  ]\n}\n<\/code><\/pre>\n\n<p>Shapes of the other object values:<\/p>\n\n<ul>\n<li><strong>Post<\/strong> (<code>post_object<\/code>, <code>relationship<\/code>): <code>{ id, type, slug, title, url, date }<\/code>, where <code>date<\/code> is an ISO 8601 GMT date.<\/li>\n<li><strong>Link:<\/strong> <code>{ title, url, target }<\/code>, where <code>target<\/code> is <code>\"\"<\/code> or <code>\"_blank\"<\/code>.<\/li>\n<\/ul>\n\n<h3>Project structure<\/h3>\n\n<pre><code>lukdev-custom-data-tables.php           Plugin header, constants, bootstrap\nincludes\/\n  install.php                  Table names, creation and upgrades\n  fields.php                   Field types, sanitization and API formatting\n  data.php                     Reading structures and rows\n  rest-api.php                 REST routes (ldcdt\/v1)\nadmin\/\n  admin.php                    Menu, assets, page router, notices\n  handlers.php                 admin-post.php form handlers, post search AJAX\n  partials\/                    List, structure builder and rows editor screens\n  css\/, js\/                    Admin assets\n  vendor\/select2\/              Select2 4.1.0 (MIT)&lt;h3&gt;Extending&lt;\/h3&gt;\n<\/code><\/pre>\n\n<p>- To add a field type, register it in <code>ldcdt_field_types()<\/code>. Then handle its settings in <code>ldcdt_sanitize_fields()<\/code>, its value in <code>ldcdt_sanitize_value()<\/code> and <code>ldcdt_format_value()<\/code>, and its inputs in the admin partials and <code>admin\/js\/admin.js<\/code>.\n- Everything uses the <code>ldcdt<\/code> prefix: functions (<code>ldcdt_<\/code>), constants (<code>LDCDT_<\/code>), tables, options, the REST namespace, CSS classes and JS. Sites upgraded from Custom Headless Data (old <code>chd<\/code> \/ <code>chdata<\/code> prefixes) have their tables and option renamed automatically. Admin form actions are <code>ldcdt_save_structure<\/code>, <code>ldcdt_delete_structure<\/code>, <code>ldcdt_save_row<\/code>, <code>ldcdt_delete_row<\/code> and <code>ldcdt_move_row<\/code>. The post search AJAX action is <code>ldcdt_search_posts<\/code>.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Copy the <code>lukdev-custom-data-tables<\/code> directory to <code>wp-content\/plugins\/<\/code> (<code>web\/app\/plugins\/<\/code> on Bedrock).<\/li>\n<li>Activate <strong>LUKDEV Custom Data Tables<\/strong> in <em>Plugins<\/em>.<\/li>\n<\/ol>\n\n<p>On activation the plugin creates two tables. If the plugin is already active and the stored <code>ldcdt_db_version<\/code> option differs from <code>LDCDT_DB_VERSION<\/code>, it creates or updates them on <code>plugins_loaded<\/code>.<\/p>\n\n<ul>\n<li><code>{prefix}ldcdt_structures<\/code>: <code>id<\/code>, <code>name<\/code>, <code>slug<\/code> (unique), <code>fields<\/code> (JSON field definitions), <code>created_at<\/code><\/li>\n<li><code>{prefix}ldcdt_rows<\/code>: <code>id<\/code>, <code>structure_id<\/code>, <code>data<\/code> (JSON values keyed by field key), <code>sort_order<\/code><\/li>\n<\/ul>\n\n<!--section=faq-->\n<dl>\n<dt id=\"is%20the%20rest%20api%20protected%3F\"><h3>Is the REST API protected?<\/h3><\/dt>\n<dd><p>No. Both endpoints are public and read-only, so don't store private data in a structure. Editing data in the admin requires the <code>manage_options<\/code> capability.<\/p><\/dd>\n<dt id=\"where%20is%20the%20data%20stored%3F\"><h3>Where is the data stored?<\/h3><\/dt>\n<dd><p>In two custom tables, <code>{prefix}ldcdt_structures<\/code> and <code>{prefix}ldcdt_rows<\/code>, not in post meta or options.<\/p><\/dd>\n<dt id=\"can%20i%20nest%20repeaters%3F\"><h3>Can I nest repeaters?<\/h3><\/dt>\n<dd><p>No. A repeater added as a sub-field of another repeater is saved as a <code>text<\/code> field.<\/p><\/dd>\n<dt id=\"why%20doesn%27t%20a%20post%20i%20picked%20show%20up%20in%20the%20api%3F\"><h3>Why doesn't a post I picked show up in the API?<\/h3><\/dt>\n<dd><p>The API returns published posts only. Drafts and private posts picked in a post object or relationship field are left out.<\/p><\/dd>\n<dt id=\"what%20happens%20to%20saved%20data%20when%20i%20change%20a%20field%27s%20type%3F\"><h3>What happens to saved data when I change a field's type?<\/h3><\/dt>\n<dd><p>The plugin keeps the data where possible. For example, a select value becomes a one-item checkbox list, and a single post object becomes a list when <em>multiple<\/em> is turned on.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h3>0.1.0<\/h3>\n\n<ul>\n<li>First release.<\/li>\n<\/ul>","raw_excerpt":"Define custom data structures (fields) and manage rows of data exposed through the REST API.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/379263","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=379263"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/lukdev"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=379263"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=379263"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=379263"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=379263"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=379263"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=379263"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}