7. Understanding the Content Model
Up to now we've been working inside the theme. This lesson shifts to the MU plugin, where the content model lives. No code changes to the theme. This is a read-only exploration.
Data should persist regardless of which theme is active, so CPTs, taxonomies, meta fields, and relationships belong in the plugin layer, not the theme.
Learning Outcomes
- Know how CPTs, taxonomies, and post meta are defined in the MU plugin.
- Understand the
ModuleInterfacepattern and how modules auto-register. - Be able to add a new meta field and confirm it appears in the REST API.
- Understand why
show_in_restis the key that unlocks everything downstream.
The module system
The 10up-plugin uses the same ModuleInterface pattern introduced in Lesson 6. The plugin's bootstrap flow works like this:
plugin.phpcreatesnew PluginCore()and calls$plugin_core->setup()PluginCore::init()callsModuleInitialization::instance()->init_classes( TENUP_PLUGIN_INC )ModuleInitializationscans all PHP classes insrc/- For each class implementing
ModuleInterface, it callscan_register()-- if true, callsregister()
Drop a new PHP class in src/ that implements this interface and it will be auto-discovered -- no manual registration needed.
Custom post types
CPTs extend AbstractPostType from the framework. Here's the Movie post type:
namespace TenUpPlugin\PostTypes;
use TenupFramework\PostTypes\AbstractPostType;
class Movie extends AbstractPostType {
const POST_TYPE = 'tenup-movie';
const SINGULAR_LABEL = 'Movie';
const PLURAL_LABEL = 'Movies';
public function get_name() {
return self::POST_TYPE;
}
public function get_editor_supports() {
$options = parent::get_editor_supports();
return array_merge( $options, [ 'custom-fields' ] );
}
public function get_options() {
$options = parent::get_options();
return array_merge( $options, [
'rewrite' => [ 'slug' => 'movies' ],
] );
}
public function get_supported_taxonomies() {
return [ 'tenup-genre' ];
}
}
Key things to note:
custom-fieldssupport is required for post meta to work with the block editor and the Block Bindings API. Without it, the REST API won't expose meta fields for this post type.- Constants (
POST_TYPE,SINGULAR_LABEL) keep the slug and labels in one place. Other classes referenceMovie::POST_TYPEinstead of hardcoding strings. - The
Personpost type follows the same pattern withtenup-personand apeoplerewrite slug.
Taxonomies
Taxonomies extend AbstractTaxonomy:
namespace TenUpPlugin\Taxonomies;
use TenupFramework\Taxonomies\AbstractTaxonomy;
use TenUpPlugin\PostTypes\Movie;
class Genre extends AbstractTaxonomy {
const TAXONOMY_NAME = 'tenup-genre';
const SINGULAR_LABEL = 'Genre';
const PLURAL_LABEL = 'Genres';
public function get_post_types() {
return [ Movie::POST_TYPE ];
}
}
The get_post_types() method associates the taxonomy with one or more post types. The framework handles register_taxonomy() and all the label generation.
Post meta
Post meta fields extend AbstractPostMeta. The abstract class handles register_post_meta() and provides a consistent interface for defining field type, default value, REST schema, and allowed values.
Here's a simple string field:
class MoviePlot extends AbstractPostMeta {
const META_KEY = 'tenup_movie_plot';
protected $type = 'string';
public function get_post_types(): array {
return [ Movie::POST_TYPE ];
}
}
And a complex object field:MovieRuntime stores hours and minutes as a structured object:
class MovieRuntime extends AbstractPostMeta {
const META_KEY = 'tenup_movie_runtime';
protected $type = 'object';
protected $default_value = [
'hours' => '0',
'minutes' => '0',
];
public function get_schema(): array {
$schema = parent::get_schema();
$schema['schema']['properties'] = [
'hours' => [
'type' => 'string',
'description' => __( 'Hours', 'tenup-block-theme' ),
],
'minutes' => [
'type' => 'string',
'description' => __( 'Minutes', 'tenup-block-theme' ),
],
];
return $schema;
}
public function get_post_types(): array {
return [ Movie::POST_TYPE ];
}
}
The abstract class automatically includes show_in_rest with the schema, this is what makes the field visible to the REST API, the block editor, and block bindings.
show_in_rest is the single most important flag for any meta field. Without it, the field is invisible to the editor, block bindings, and JavaScript. If a field doesn't appear where you expect, check show_in_rest first.
All 14 meta fields
The plugin defines 14 meta fields across two post types:
Movie (8 fields): tenup_movie_imdb_id, tenup_movie_mpa_rating, tenup_movie_plot, tenup_movie_release_year, tenup_movie_runtime (object), tenup_movie_viewer_rating, tenup_movie_viewer_rating_count, tenup_movie_trailer_id
Person (6 fields): tenup_person_biography, tenup_person_birthplace, tenup_person_born, tenup_person_deathplace, tenup_person_died, tenup_person_imdb_id
Relationships
The plugin uses Content Connect for bidirectional many-to-many relationships between post types.
class Relationships implements ModuleInterface {
public static $relationships;
public function __construct() {
self::$relationships = [
'movie_person' => [
'from' => [
'cpt' => Movie::POST_TYPE,
'name' => __( 'Related People', 'tenup-block-theme' ),
],
'to' => [
'cpt' => Person::POST_TYPE,
'name' => __( 'Related Movies', 'tenup-block-theme' ),
],
],
];
}
public function register() {
add_action( 'tenup-content-connect-init', [ get_called_class(), 'define_relationships' ] );
}
}
This creates a bidirectional relationship between Movies and People. From the admin, editors can relate a Movie to its cast members, and those relationships are queryable from both sides. The theme's Block Bindings (covered in Lesson 10) use Content Connect to display linked names.
Tasks
-
Trace the initialization flow. Start at
plugin.php, follow toPluginCore.php, and see howModuleInitialization::instance()->init_classes()auto-discovers classes. -
Read a CPT definition. Open
PostTypes/Movie.php. Note the slug, rewrite, supported taxonomies, andcustom-fieldssupport. -
Read the abstract meta class. Open
AbstractPostMeta.php. See howregister_post_meta()is called withshow_in_rest,single,type, and optionaldefault/enum. -
Read a simple field (
MoviePlot.php) and a complex one (MovieRuntime.php, an object type with a properties schema). -
Read
Relationships.php. See how Content Connect defines bidirectional many-to-many relationships. -
Verify data in the REST API. Visit
/wp-json/wp/v2/tenup-movie/{id}and confirm meta fields appear in the response.
Ship it checkpoint
- Can explain where CPTs, meta, and relationships are defined
- Can see meta fields in the REST API response
- Understands why
show_in_restmatters
Takeaways
- The content model belongs in the MU plugin. Data outlives design.
- All modules implement
ModuleInterfacewithcan_register(),register(), andload_order(). show_in_restis the single most important flag. Without it, the field is invisible to the editor, bindings, and JS.- Complex meta (like
MovieRuntime) uses theobjecttype with apropertiesschema. - Content Connect provides bidirectional many-to-many relationships between post types.