-
Notifications
You must be signed in to change notification settings - Fork 41
example my wordpress cpt section
Every custom post type your plugin registers shows up in the site window automatically, grouped into a folder named after your plugin. This page covers what you get for free, and the three hooks for the cases where the defaults are wrong.
defined( 'ABSPATH' ) || exit;Register a post type the normal way and it becomes a browsable section:
add_action(
'init',
static function () {
register_post_type(
'acme_recipe',
array(
'labels' => array( 'name' => __( 'Recipes', 'acme' ) ),
'public' => true,
'show_ui' => true,
'show_in_rest' => true,
'menu_icon' => 'dashicons-food',
'supports' => array( 'title', 'editor', 'thumbnail' ),
)
);
}
);The section appears inside an Acme folder at the root of the site
window, labelled from your plugin's Plugin Name header. Its tiles
show each recipe's featured image, and drag-to-trash, the lock badge,
and the live item counter all work the way they do for Posts.
Three details are worth knowing:
-
show_ui => trueis the gate. A type registered withshow_ui => falseis treated as internal bookkeeping and never appears. This is how OpenStation keeps its own private types out. -
The current user must hold the type's
edit_postscapability. Users who can't edit recipes never see the folder. -
menu_iconis reused as the section icon — a Dashicons class, an image URL, or a base64 data URI all work. Types without one fall back todashicons-admin-post.
A type registered with show_in_rest => false has no wp/v2
collection, so OpenStation serves it from
desktop-mode/v1/post-type/<slug> instead. That route is read and
trash only — no create, no update — and requires the type's
edit_posts capability in every context, so it is never publicly
readable.
If your type holds something that should have no REST endpoint at all, opt out:
add_filter(
'openstation_my_wordpress_post_type_rest_enabled',
static function ( $enabled, $post_type ) {
return 'acme_licence_key' === $post_type ? false : $enabled;
},
10,
2
);The section then disappears from the window rather than rendering a folder that can't open.
add_filter(
'openstation_my_wordpress_post_types',
static function ( $slugs ) {
return array_values( array_diff( $slugs, array( 'acme_import_log' ) ) );
}
);The capability check has already run by the time this filter fires, so everything still in the array is editable by the current user.
Attribution follows whichever file called register_post_type(), so a
suite split across several plugins lands in several folders. Point them
at one:
add_filter(
'openstation_my_wordpress_post_type_group',
static function ( $group, $post_type ) {
$ours = array( 'acme_recipe', 'acme_menu', 'acme_ingredient' );
if ( ! in_array( $post_type, $ours, true ) ) {
return $group;
}
return array(
'id' => 'plugin:acme-suite',
'label' => __( 'Acme Suite', 'acme' ),
'icon' => 'dashicons-food',
'order' => 15,
);
},
10,
2
);Returning null instead pulls a type out of its folder entirely and
renders it loose at the root, next to Posts and Pages.
Lower order values sort first; the built-in resolver uses 20 for
plugins and 30 for themes, so 15 puts your folder ahead of both.
Featured images replace the section icon by default. For a type where the featured image carries no meaning at a glance, keep the uniform icon grid:
add_filter(
'openstation_my_wordpress_post_type_entity',
static function ( $entity, $post_type ) {
if ( 'acme_import_log' === $post_type->name ) {
$entity['thumbnails'] = false;
}
return $entity;
},
10,
2
);If you'd rather define the section yourself — a custom label, a
different REST collection, your own render kind — add it through
openstation_my_wordpress_entities with a post_type that matches.
The automatic pass skips any type a section already covers, so there's
no duplicate folder:
add_filter(
'openstation_my_wordpress_entities',
static function ( $entities ) {
$entities[] = array(
'id' => 'acme-recipes',
'label' => __( 'Recipes', 'acme' ),
'icon' => 'dashicons-food',
'restPath' => 'acme/v1/recipes',
'kind' => 'post',
'post_type' => 'acme_recipe',
'thumbnails' => true,
'group' => 'plugin:acme-suite',
'groupLabel' => __( 'Acme Suite', 'acme' ),
'groupIcon' => 'dashicons-food',
'groupOrder' => 15,
);
return $entities;
}
);This wiki is generated from the docs/ directory — edits made here are overwritten by the next sync.
To change a page, open a pull request against docs/.
Guides
- Development guide
- Releasing openstation
- Agents security model
- API Index
- Architecture
- Bridge protocol — wiring overview
- <os-*> component reference
- Native Desktop Host — Experimental
- Desktop themes
- Dock customization — two registries, one mental model
- The event-driven framework
- Files on the Desktop
- Folder sharing
- Getting Started
- Hooks Reference
- Icons
- JavaScript Reference
- The Living Tree — algorithm definition
- Mio
- Native Windows & Framework Interop
- Plugin compatibility layer
- Progressive Web App (PWA)
- Station Home
- Using openstation from your own plugin
Migration notes
- Migration: built-in activity channels move to the os/ namespace
- Migration: window, wallpaper and widget bundles load on demand
- Migration — the navigation model
- Migration: a native window's tabs move to the window chrome
All examples
- AI Agents — extend and invoke from a plugin
- wp.os.ai.ask() — programmatic AI Copilot
- Tune the AI model config
- Custom arrange-menu action
- Open a child window its owner can't cover
- Style a specific admin page inside the iframe
- Code Blue — register your plugin's log file
- Open a file in the Code editor (deep-link from any window)
- Connect to a window — title-bar button + iframe pub/sub
- Content changes — live-refresh every window listing your type
- Custom window chrome (Experimental)
- Register a custom unfocused-window effect
- Example: render a data table
- Real file storage — react to uploads, gate policy, share from PHP
- React to a window being set free onto the real desktop
- Cross-window devtools — instrumentation primitives
- Add a dock item with a badge
- Decorate the dock without forking the renderer
- Replace the dock rail entirely
- Retune the Drafts widget's AI writing assistant
- Gate OpenStation by role
- Iframe-initiated window opens
- Build a feed reader without the bookkeeping
- Inject data into openStationConfig
- Render a list without losing clicks — renderKeyedList()
- Example: layout primitives (body → panel → row → col)
- Use <os-*> components from a plugin that ships as a zip
- Restyle and drive Mio
- Add an action that works on a whole selection
- WP Explorer — custom post types and their folder
- Add an action button to a WP Explorer preview pane
- Example: native Posts window
- Example: native window with tabs
- Native windows
- Customize note → post conversion
- Send a notification
- OAuth relay — connect to an external service
- OS-file drop
- <os-flyout> — window-scoped sliding card
- Plugins window — extras
- Track who's around — wp.os.presence
- Example: progress bar
- PWA install — surface your own button
- React to window events
- Example: extend the Trash
- Register a slash-command
- Register a desktop theme from a plugin
- Register a game
- Example: register a desktop icon (Jorvy)
- Register a wallpaper
- Register a widget
- Related entities — extend the title bar's "Related" menu
- The native-window render ctx
- Programmatic folder sharing
- Share state across multi-bundle plugins — wp.os.createSharedStore()
- Example: loading spinner
- Add an opt-in card to Station Home
- Accept drops on your desktop icon
- Give a tile two icons, one per state
- Add a row to a window's ⋯ menu
- Example: window activity & the status ring
- Window controls
- Subscribe to window lifecycle events
- Window links — relate windows and restyle the ties (Experimental)
- Window loading state — spinner overlay & ready signal
- Show a banner at the top of a window
- Pulse a window's icon — Window.requestAttention()
- Register a custom window reveal
- Window slots
- Window themes
- Native window with bundle-bound config