WP Manifestindependent plugin directory
manifest / content / simple-post-types

Etherlabz Simple Post Types

Register custom post types via a tidy admin UI — auto-exposed at /wp-json/wp/v2/{slug} for headless front-ends. Sibling to Etherlabz Headless Bridge.

★ 1stars
17release downloads
v0.1.3latest · 5mo ago

Install

Download simple-post-types-0.1.3.zip From the v0.1.3 release on GitHub, May 7, 2026

Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/etherlabz/simple-post-types/releases/download/v0.1.3/simple-post-types-0.1.3.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Etherlabz Simple Post Types

A focused admin UI for registering custom post types, custom fields, and per-method API auth in WordPress. No code, no menu-icon guessing, no register_post_type boilerplate. New types appear at /wp-json/wp/v2/{slug} the moment you save — ready for your headless front-end to consume, with optional per-method bearer-token auth so you can publish public submission forms while keeping the underlying data private.


Why this plugin

Every WordPress build needs at least one custom post type. The standard answer is register_post_type() in a theme functions.php — fast for a developer, opaque for everyone else. Plugins like CPT UI work but pile features on the screen and don't help with the modern needs that headless and submission-driven sites have:

  • Custom fields that show up in the editor and on the REST API in one go
  • Per-method API auth so you can have a public POST endpoint for contact-form submissions while keeping read auth-gated
  • Method-disable for routes you simply don't want exposed (returns 405)
  • Activity log so you can see who deleted what (and whether they purged data)
  • Delete-with-data confirmation that's clear about what will and won't be erased

Simple Post Types is the focused 90% answer to all of that. One screen, one tab for activity history, zero of the legacy options that no one fills in correctly. It pairs with the sibling Etherlabz Headless Bridge plugin so any CPT you create here is automatically decorated with featured_image_url and seo fields on the REST API.

No Composer dependencies. No npm. No build step. Drop it in wp-content/plugins/ and activate.


Features at a glance

Capability Out of the box
One-screen UI for creating, editing, and deleting custom post types ✅
Slug, singular/plural labels, menu icon (curated dashicon picker + custom upload), menu position ✅
Editor "supports" pickers (title, editor, thumbnail, excerpt, author, comments, revisions, custom-fields, page-attributes) ✅
All 12 register_post_type flags exposed (Visibility / Behavior groups) ✅
Taxonomy attachment (existing taxonomy slugs) ✅
Auto-exposed at /wp-json/wp/v2/{rest_base} with full REST schema ✅
Custom fields per CPT — text, textarea, number, email, url, date, select, checkbox, image, color ✅
Custom fields auto-render in the editor metabox + auto-expose on /wp/v2/{cpt} meta ✅
Per-method API auth — public / auth / disabled per HTTP method (GET, POST, PUT, PATCH, DELETE) ✅
Quick-preset chips: All public / Read public, writes auth / Submission only / Read-only / Write public, reads auth / All auth ✅
Encrypted API key storage (AES-256-CBC keyed off AUTH_KEY/AUTH_SALT) ✅
Custom delete-confirm modal with explicit "Keep posts in DB" vs "Permanently delete data" choice ✅
Activity log — recent deletions with post count + data-purge flag + user, capped at 100 entries ✅
Tabs (Post types / Activity log) with sessionStorage + URL-hash persistence ✅
Reserved core slugs (post, page, wp_block, …) blocked at validation ✅
Strict slug regex (^[a-z][a-z0-9_-]{0,19}$) enforced on save ✅
Permalink rewrite rules flushed automatically on save / delete ✅
Pairs with Headless Bridge — every CPT gets featured_image_url + seo for free ✅
Admin UI uses Etherlabz "Warm Sunset" brand palette ✅
Translation-ready (simple-post-types text domain) ✅

Requirements

Minimum Recommended
PHP 7.4 8.2+
WordPress 6.0 latest
OpenSSL extension required (encrypted API keys) enabled by default in modern PHP
Sibling plugin (optional) — Headless Bridge for headless front-ends

No Composer, no npm, no build step at runtime. The dev toolchain is php -l, node --check, phpunit, rsync, and zip.


Installation

From a release ZIP

  1. Download the latest ZIP from the Releases page.
  2. WP Admin → Plugins → Add New → Upload Plugin → pick the ZIP.
  3. Activate Etherlabz Simple Post Types.
  4. Click Post Types in the admin sidebar → Add post type.

From source

cd wp-content/plugins
git clone https://github.com/EtherLabZ/simple-post-types.git

Then activate as above. Nothing to install — there are no runtime dependencies.


Setup

After activation, the Post Types menu item appears in your admin sidebar (orange dashicons-screenoptions icon).

Creating a post type

Click Add post type, fill in:

Field Notes
Slug 1–20 lowercase letters/digits/dashes/underscores. Must start with a letter. Reserved core slugs are blocked. Read-only after first save (renames silently orphan data).
Singular / plural label What you'll see in the admin UI ("Case Study", "Case Studies"). Defaults derived from the slug.
REST base Path under /wp-json/wp/v2/. Defaults to the slug.
Permalink slug Path on the public site. Defaults to the slug.
Menu icon Three modes — None (WP default) / Dashicon (curated catalog of ~120 icons with live filter) / Custom upload (via wp.media library).
Menu position 0–100. WP positions: 5=Posts, 10=Media, 20=Pages, 25=Comments, 60=Appearance.
Taxonomies Existing taxonomy slugs (e.g. category, post_tag) to attach. Comma-separated.
Editor support Tick which editor features (title, editor, thumbnail, excerpt, etc.) the type should expose.
Visibility flags Public, Show admin UI, Show in admin menu, Show in admin bar, Available in nav menus, Show in REST API, Exclude from front-end search.
Behavior flags Enabled (register on init), Has archive, Hierarchical (page-like), Includable in WP exports, Has query_var.
Custom fields See below.
API authentication See below.

Click Create post type. Rewrite rules flush automatically — /wp-json/wp/v2/{slug} is live the moment you save.

Custom fields

Each CPT can define typed metadata fields. They:

  1. Appear in the editor as a metabox (one input per field, with the right widget type)
  2. Auto-register as REST-exposed post meta — visible under meta on /wp-json/wp/v2/{cpt} responses

Ten supported types:

Type UI
text <input type="text">
textarea 4-row <textarea>
number <input type="number">
email / url / date matching native inputs
select <select> populated from the field's options[]
checkbox single boolean
image preview + Choose / Clear buttons (uses wp.media)
color native color picker

Each field carries: key (snake-case slug), label, type, description (help text), default value, required flag, options (for select).

Reserved keys (id, post_title, post_content, etc.) are blocked at sanitize time.

API authentication

Each CPT can lock its REST endpoints behind a bearer token, with per-method granularity. The matrix has five rows (one per HTTP method) and three states each:

State Behavior
public Anyone can call this method without a token
auth Caller must send Authorization: Bearer <api_key>
disabled WordPress returns 405 Method Not Allowed

HEAD aliases to GET. OPTIONS is always public so CORS preflights aren't accidentally locked down.

Six preset chips above the matrix stamp common bundles in one click:

Preset What it stamps
All public Every method public (default for new CPTs)
Read public, writes auth GET public, POST/PUT/PATCH/DELETE auth
Submission only (public POST) GET auth, POST public, writes disabled — perfect for contact forms
Read-only API (only GET enabled) GET public, all writes disabled
Write public, reads auth GET auth, all writes public
All auth Every method auth

Hand-tune any row afterwards — click any of the three state pills to flip that method's state.

The API key field auto-shows when any method is set to auth and auto-hides when none are. Click Generate to create a 64-hex-char key from CSPRNG; copy it once via the Copy button (the key is encrypted at rest and never re-shown).

Activity log

The Activity log tab tracks the most recent 100 deletions:

Field Notes
When Relative timestamp (just now, 3 hr ago, 2 days ago) with full ISO-8601 on hover
Post type Plural label + slug code
Posts Number of posts that existed at deletion time
Action kept in DB (green tag) or data purged (red tag)
By WP user_login of whoever clicked Delete

A Clear log button empties the log. Activity is local to the site — never sent anywhere.

Going headless?

Pair this with Etherlabz Headless Bridge to get featured-image URLs, normalized SEO with static-host canonical rewrite, per-post stylesheet manifests, configurable CORS, headless-mode hardening, and rebuild webhooks for Astro / Next.js / SvelteKit. Headless Bridge picks up every CPT registered with show_in_rest: true automatically.


REST surface

The plugin doesn't add its own REST routes. Every CPT you create is registered with show_in_rest: true (unless you uncheck the flag), so the standard core endpoints work:

# List all case-studies
curl 'https://your-site.com/wp-json/wp/v2/case-studies'

# Get one
curl 'https://your-site.com/wp-json/wp/v2/case-studies?slug=apollo'

# Schema
curl 'https://your-site.com/wp-json/wp/v2/case-studies/schema'

When you also have Etherlabz Headless Bridge installed, every response gets the same featured_image_url + seo fields the standard /wp/v2/posts endpoint does:

{
  "id": 1234,
  "slug": "apollo",
  "title": { "rendered": "Apollo case study" },
  "featured_image_url": {
    "thumbnail": "https://...", "medium": "https://...", "large": "https://...", "full": "https://..."
  },
  "seo": {
    "title":       "Apollo case study — Etherlabz",
    "description": "…",
    "canonical":   "https://etherlabz.co/case-studies/apollo/",
    "og_image":    "https://...",
    "og_type":     "article",
    "provider":    "yoast"
  },
  "meta": {
    "client_name":  "Apollo",
    "industry":     "Aerospace",
    "hero_image":   "https://etherlabz.co/...",
    "is_featured":  true
  }
}

The meta object is populated automatically from the custom fields you defined.

API auth examples

# CPT with "Submission only" preset
# GET requires auth, POST is public, PUT/PATCH/DELETE return 405

# Public POST — anyone can submit
curl -X POST 'https://your-site/wp-json/wp/v2/contact-forms' \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello","status":"publish","meta":{"email":"a@b.com"}}'
# → 201 Created

# Auth-gated GET
curl 'https://your-site/wp-json/wp/v2/contact-forms'
# → 401 simple_post_types_api_key_required

curl 'https://your-site/wp-json/wp/v2/contact-forms' \
  -H 'Authorization: Bearer <your-key>'
# → 200 OK with submissions

# Disabled PUT — even with auth
curl -X PUT 'https://your-site/wp-json/wp/v2/contact-forms/123' \
  -H 'Authorization: Bearer <your-key>' \
  -d '{"title":"x"}'
# → 405 Method Not Allowed

Architecture

A small, single-purpose codebase. Same shape as the sibling Etherlabz plugins so anyone moving between them feels at home.

  • Namespaced PHP (Etherlabz\Simple_Post_Types) with declare(strict_types=1).
  • PSR-4 autoloading via a custom 30-line loader — zero Composer runtime deps.
  • Singleton bootstrap (Main) wires every module exactly once.
  • Registrable interface — every module has one register_hooks() entry point.
  • Strict input validation — reserved-name blocklist, slug regex, supports allow-list, field-type allow-list, scope allow-list.
  • Encrypted secrets at rest via Core/Encryption.php (AES-256-CBC keyed off AUTH_KEY/AUTH_SALT).
  • Per-method REST gate — rest_authentication_errors filter resolves the entry's api_methods map and returns 405 for disabled methods or 401 for unauth bearer requests.
  • Activity-log capping — FIFO at 100 entries, auto-pruned on every write.
  • Activation hook registers all CPTs once and flushes rewrite rules so the new permalinks work immediately.
simple-post-types/
├── simple-post-types.php           Plugin bootstrap + header
├── uninstall.php                   Clean removal of every spt_* option
├── inc/
│   ├── Autoloader.php              PSR-4 autoloader (no Composer)
│   ├── Main.php                    Singleton + module registry + activation
│   ├── Contracts/
│   │   ├── Registrable.php         interface { register_hooks(): void }
│   │   └── Singleton.php           shared singleton trait
│   ├── Core/
│   │   ├── Assets.php              Admin CSS/JS enqueue + i18n strings + dashicon catalog
│   │   └── Encryption.php          AES-256-CBC secret storage (api_key)
│   └── Modules/
│       ├── Post_Types.php          CPT registration + sanitize + reserved-slug guard +
│       │                           delete-with-data + activity log
│       ├── Custom_Fields.php       register_post_meta + metabox render + sanitize per type
│       ├── API_Auth.php            Per-method REST auth gate (public/auth/disabled)
│       ├── Ajax_Handler.php        Save / delete (with optional data purge) / count posts /
│       │                           generate API key / clear log — all nonced + cap-gated
│       └── Settings/
│           └── Admin_Screen.php    Top-level admin menu page
├── templates/admin-screen.php      Tabbed admin UI (Post types / Activity log)
├── assets/
│   ├── css/admin.css               Etherlabz "Warm Sunset" palette
│   ├── css/metabox.css             Editor metabox styling for custom fields
│   ├── js/admin.js                 Tab persistence + custom confirm modal +
│   │                               icon picker + method matrix + activity-log render
│   └── js/metabox.js               Image-field wp.media picker for the editor metabox
├── tests/
│   ├── bootstrap.php               WP function polyfills (no Brain\Monkey, no full WP test setup)
│   └── Unit/                       Post_Types, API_Auth, Deletion_Log
├── bin/
│   ├── build.sh                    Production ZIP builder (rsync + zip)
│   └── release.sh                  gh-CLI release publisher
├── .github/workflows/
│   ├── ci.yml                      Lint matrix + PHPUnit on every push/PR
│   └── release.yml                 Tag-push → ZIP → GitHub release
├── Makefile                        `make help` for every target
├── .distignore                     Files excluded from the production ZIP
└── RELEASING.md                    How to cut a new release

Reserved slugs

These are blocked at save time because they collide with WordPress core or break the admin in confusing ways:

post, page, attachment, revision, nav_menu_item, custom_css,
customize_changeset, oembed_cache, user_request, wp_block,
wp_template, wp_template_part, wp_global_styles, wp_navigation,
action, author, order, theme

If you legitimately need to extend one of these (e.g. post itself), do it in your theme/plugin code with add_post_type_support() — that's the right tool for that job, not a CPT registration form.


Development

make help            # list every available target
make lint            # php -l + node --check across the tree (no Composer/npm needed for lint)
make test            # composer install + phpunit (101 tests / 240 assertions)
make check           # lint + test (used in CI)
make build           # produces dist/simple-post-types-<version>.zip
make release         # build + create draft GitHub release (requires gh CLI)
make release-publish # build + publish a release immediately
make clean           # rm -rf dist/

CI runs on every push / PR via .github/workflows/ci.yml — PHP 7.4 / 8.2 / 8.3 × Node 20 lint matrix + PHPUnit + a build smoke test.

Releases are automated on tag-push via .github/workflows/release.yml. Push a vX.Y.Z tag and the Action lints, builds, and creates the GitHub release with the ZIP attached.

See RELEASING.md for the full release procedure.


Testing

101 tests / 240 assertions, all green. PHPUnit 9.6 with WP-function polyfills (no Brain\Monkey, no full WP test setup, no extra runtime deps).

Coverage:

  • Post_Types — slug validation (6 valid + 9 rejected including reserved-name + length cap), supports allowlist + dedupe, menu_icon (dashicon class / https URL / data:image/svg+xml / javascript: rejection / garbage rejection), sanitize_entry full shape (slug normalisation, label defaults, position clamping), boolean-flag regression (every flag against string "false"/"true"/native bool/missing keys), every flag key landing on saved entry, custom-fields sanitize (reserved-key blocklist, dedupe, unknown-type collapse, select options string AND array parsing, required-flag regression, label defaults), per-method API auth (Submission-only preset, key wipe when no method needs auth, legacy scope fallback, scope mirror derived from methods)
  • API_Auth — sanitize_scope allowlist + rejection, sanitize_key bounds + alphabet, generate_key entropy + uniqueness, full 9-cell scope×method matrix, route matching incl. products vs product lookalike rejection, sanitize_methods (defaults missing→public, valid passthrough, unknown→public, ignores TRACE/etc), methods_from_scope for all four scopes + unknown-fallback, resolve_methods (explicit wins over legacy, fallback works, default all-public), entry_requires_auth (true when any auth, false when none, legacy fallback)
  • Deletion_Log — record_deletion shape + user-login capture + data-purged flag + post-count clamping, log is newest-first, log caps at LOG_MAX_ENTRIES, get_log skips non-array rows in storage, returns empty for non-array option, clear_log empties the log, log-row shape completeness

Manual end-to-end verification on wp-env:

Step Expected
Activate plugin Menu appears, no PHP notices
Add CPT via UI → save New type appears at /wp-json/wp/v2/{slug} immediately
Reserved CPT slug (post, wp_block, …) Rejected with a clear error
Add custom field (text) → save → edit a post Metabox row appears in the editor
GET /wp-json/wp/v2/{cpt} Includes the meta value under meta.{key}
API auth: Submission-only preset → POST without auth Succeeds (201)
API auth: Submission-only preset → GET without auth 401
API auth: Submission-only preset → PUT with auth 405
Delete CPT with 0 posts Modal shows count=0; only Keep option; one-click delete
Delete CPT with N posts → Keep Posts remain in DB; CPT unregistered
Delete CPT with N posts → Purge All posts permanently deleted incl. meta + terms + comments
Activity log tab Shows the deletion with kept in DB or data purged tag
Tab state survives reload Stays on Activity log after refresh
Plugin uninstall Clears every spt_* option

Security model

  • All admin AJAX endpoints require manage_options + a verified nonce (spt_admin_nonce).
  • Strict input validation — slug regex ^[a-z][a-z0-9_-]{0,19}$, reserved-slug blocklist, supports allow-list, field-type allow-list, scope/state allow-lists.
  • Output escaping — admin templates pipe through esc_html / esc_attr / esc_url. JS rendering escapes every interpolated value through a single escapeHtml() helper.
  • Menu icon restricted to dashicons-* classes / http(s)://... URLs / data:image/svg+xml;base64,... URIs only; javascript: / ftp: / arbitrary strings dropped.
  • API key storage — sanitized to 32–96 chars in [A-Za-z0-9_-], then AES-256-CBC encrypted at rest. Never round-tripped to the browser; UI shows key_set: true|false.
  • API key validation — Authorization: Bearer <key> header read with case-insensitive header lookup; fallback to ?api_key= query var only if the value passes the same regex; constant-time compare via hash_equals().
  • Route matching is anchored — rest_base product does NOT match /wp/v2/products (boundary check after the prefix).
  • Method-disable returns HTTP 405, not 401, so callers see the correct semantic.
  • OPTIONS is always public — CORS preflights are never accidentally locked down.
  • Authenticated WP users (cookie session) are never blocked by API_Auth — this is purely an extra gate for anonymous REST clients.
  • Capability: registered with capability_type: post, so editor/author/contributor roles work as expected without custom mapping.
  • Data deletion — the purge path uses wp_delete_post($id, true) so post_meta, term_relationships, comments, and commentmeta are cleaned by core in batches of 200. Purge happens before unregistering the type so we never leak rows in wp_posts.
  • Uninstall removes every spt_* option.

The threat model assumes site admins are trusted. The plugin only renders to users with manage_options.


License

MIT. Use it, fork it, ship it.


Built with ♥ by Mizety at Etherlabz GitHub · Issues · Releases · Sibling: Headless Bridge

Releases

24 releases. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v0.1.3 latest
May 7, 2026 5mo ago
simple-post-types-0.1.3.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.2.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc11.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc10.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc9.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc8.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc7.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc6.zip
0
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc5.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc4.zip
0
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc3.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc2.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.1-rc1.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.0.zip
0
May 7, 2026 5mo ago
simple-post-types-0.1.0-rc9.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.0-rc8.zip
0
May 7, 2026 5mo ago
simple-post-types-0.1.0-rc7.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.0-rc6.zip
1
May 7, 2026 5mo ago
simple-post-types-0.1.0-rc5.zip
0
May 6, 2026 5mo ago
simple-post-types-0.1.0-rc4.zip
0
May 6, 2026 5mo ago
simple-post-types-0.1.0-rc3.zip
1
May 6, 2026 5mo ago
simple-post-types-0.1.0-rc2.zip
0
May 6, 2026 5mo ago
simple-post-types-0.1.0-rc1.zip
1