The quickest way to make a WordPress site hard to manage is to put everything into pages. Properties become pages with a table pasted in, team members become pages with a photo and a paragraph, and six months later nobody can filter, sort or reuse anything.
A content model fixes that. You decide which things the business publishes, what information each one has and how they relate. WordPress then gives editors a proper form for each. This guide uses a real-estate example, the kind of listing structure property developers such as Ace Ventures need, but the same thinking applies to yachts, courses, events or products.
Post type, taxonomy or field?Link to this section
Ask three questions about each piece of information:
| Question | If yes, use… | Example |
|---|---|---|
| Is it a thing with its own page? | Custom post type | A property, a project, a team member |
| Is it a group you filter or browse by? | Taxonomy | Location, property type, amenities |
| Is it a detail of one thing? | Meta field | Price, bedrooms, area, RERA number |
Two common mistakes:
- Using meta fields for things people filter by. Filtering on meta values (
meta_query) is slow on large sites becausewp_postmetaisn't indexed for value searches. If visitors browse "Villas in Nagole", location and type should be taxonomies. - Using taxonomies for unique values. A price or a phone number is not a category. Each value would create a term used once.
Register the post typeLink to this section
Register content types in a plugin, not the theme, so the content doesn't "disappear" when the design changes:
add_action( 'init', function () {
register_post_type( 'property', array(
'labels' => array(
'name' => __( 'Properties', 'acme' ),
'singular_name' => __( 'Property', 'acme' ),
'add_new_item' => __( 'Add new property', 'acme' ),
),
'public' => true,
'has_archive' => 'properties',
'rewrite' => array( 'slug' => 'properties', 'with_front' => false ),
'menu_icon' => 'dashicons-building',
'supports' => array( 'title', 'editor', 'excerpt', 'thumbnail', 'revisions' ),
'show_in_rest' => true, // block editor + REST API
'template' => array(
array( 'core/paragraph', array( 'placeholder' => 'Describe the property…' ) ),
array( 'core/gallery' ),
),
) );
} );A few arguments do a lot of work:
show_in_rest => trueenables the block editor and exposes the type at/wp-json/wp/v2/property, which you need for a headless frontend.templatepre-fills new posts with the right blocks, so every listing starts with the same structure.rewritewithwith_front => falsekeeps URLs clean if the blog has a/blog/prefix.
After registering or changing rewrite slugs, flush the rewrite rules once, either by saving Settings → Permalinks or with flush_rewrite_rules() on plugin activation. Never flush on every request.
Register taxonomiesLink to this section
add_action( 'init', function () {
register_taxonomy( 'location', 'property', array(
'label' => __( 'Locations', 'acme' ),
'hierarchical' => true, // city > neighbourhood
'rewrite' => array( 'slug' => 'properties/location' ),
'show_in_rest' => true,
'show_admin_column' => true,
) );
register_taxonomy( 'property_type', 'property', array(
'label' => __( 'Property types', 'acme' ),
'hierarchical' => true,
'show_in_rest' => true,
'show_admin_column' => true,
) );
} );show_admin_column adds a sortable column to the list screen, a small touch editors notice immediately.
Register meta fields properlyLink to this section
register_post_meta() documents each field's type, makes it available to the REST API and the block editor, and lets you set an authorization rule:
add_action( 'init', function () {
$fields = array(
'price_inr' => 'integer',
'bedrooms' => 'integer',
'area_sqft' => 'integer',
'rera_id' => 'string',
);
foreach ( $fields as $key => $type ) {
register_post_meta( 'property', $key, array(
'type' => $type,
'single' => true,
'show_in_rest' => true,
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
}
} );You can build the editing UI with a field plugin such as ACF or Meta Box, with a small block-editor sidebar panel, or with a classic meta box. Whatever the UI, registering the meta keeps the data model explicit and visible to the REST API.
Before relying on a format such as a registration number, write the validation pattern and test it against real examples in the regex tester. A field that accepts anything soon contains everything.
Querying efficientlyLink to this section
$query = new WP_Query( array(
'post_type' => 'property',
'posts_per_page' => 12,
'tax_query' => array(
array( 'taxonomy' => 'location', 'field' => 'slug', 'terms' => 'nagole' ),
),
'orderby' => 'date',
'no_found_rows' => true, // skip the total count when you don't paginate
) );- Filter by taxonomies first; they're indexed.
- Use
no_found_rows => truefor widgets and sliders that don't need pagination. - If you must sort by a numeric meta value such as price, set
'meta_key' => 'price_inr'and'orderby' => 'meta_value_num', and keep the result set small.
Relating content: properties and projectsLink to this section
Real content models have relationships: properties belong to a project, team members work in an office, courses have instructors. Two patterns cover most cases.
One-to-many: store a single post ID. Each property stores the ID of its parent project. A single integer is easy to query and to expose in the REST API:
register_post_meta( 'property', 'project_id', array(
'type' => 'integer',
'single' => true,
'show_in_rest' => true,
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
// All properties in one project.
$properties = new WP_Query( array(
'post_type' => 'property',
'meta_key' => 'project_id',
'meta_value' => $project_id,
'posts_per_page' => 50,
'no_found_rows' => true,
) );An exact match on one meta key is fine for a few thousand posts. If the relationship drives public filtering on a large site, mirror it as a taxonomy (one term per project) and filter with tax_query instead.
Many-to-many: use a taxonomy. Amenities, features and tags are shared across many posts, which is exactly what taxonomies are for.
Avoid storing lists of IDs as one serialized value. Relationship fields in some field plugins do this by default, and serialized arrays can't be queried efficiently. If you need "which properties reference this agent?", a serialized list forces slow LIKE searches.
Common mistakes and how to fix themLink to this section
- New URLs return 404. The rewrite rules weren't flushed. Save Settings → Permalinks once.
- The post type opens in the classic editor.
show_in_restis missing, oreditorisn't insupports. - Meta values don't save from the block editor. The meta isn't registered with
show_in_rest, or the key starts with an underscore. Keys beginning with_are protected, so they need anauth_callbackto be editable through the REST API. - The archive shows a page instead of listings. A page uses the same slug as the archive (
/properties/). Pick one: use the archive, or sethas_archivetofalseand build the page yourself. - The type can't be registered. Post type keys are limited to 20 characters and must avoid reserved names such as
post,page,attachment,action,orderandtheme. - Renaming a post type later. Changing the key doesn't move existing content. You have to update the
post_typecolumn of existing rows and add redirects for the old URLs, so choose names carefully at the start.
Templates for the new typeLink to this section
Block themes pick up single-property.html and archive-property.html from the templates/ folder. Classic themes use single-property.php and archive-property.php. In either, show meta fields in a consistent fact panel such as price, bedrooms and area, so visitors can compare listings at a glance.
Structured data for listingsLink to this section
Listings with clear facts are good candidates for structured data. Depending on the content, that might be Product, Event, Course or RealEstateListing. Generate a starting block with the schema generator, make sure every value matches what's visible on the page, and test it with Google's Rich Results Test. Read the API output of a listing in the JSON formatter to confirm the fields you need are exposed.
Content model checklistLink to this section
- ☐ Each content type has its own post type with clear labels and an icon.
- ☐ Browse/filter dimensions are taxonomies; unique details are meta fields.
- ☐ Types, taxonomies and meta are registered in a plugin, with
show_in_rest. - ☐ New posts start from a block
template. - ☐ Rewrite rules flushed once after changes.
- ☐ Queries filter by taxonomy and skip counts when they don't paginate.
- ☐ Editors were shown the new screens. Five minutes of training saves weeks of messy data.
The same approach powers listing-heavy sites like yacht brokerages, where a site such as Royal Yachts presents boats for sale and charter, each with consistent specifications. ## Frequently asked questions
Should I use ACF or register_post_meta()?Link to this section
Use both for different jobs. A field plugin such as ACF gives editors a good interface quickly. Registering the meta with register_post_meta() documents the type, controls who can edit it and exposes it to the REST API in a predictable shape. ACF can also expose its fields in the REST API when you enable that option per field group.
How many custom post types is too many?Link to this section
The number rarely matters. Overlap does. If two types share most of their fields and templates, they're probably one type with a taxonomy that tells them apart.
Do custom post types help or hurt SEO?Link to this section
They help when each type has a clear purpose: clean URLs, consistent templates and structured data. Problems come from thin archives, such as a taxonomy term with one post, being indexed. Give important archives an introduction and keep near-empty term archives out of the index.
Will my content disappear if I switch themes?Link to this section
Not if the types are registered in a plugin. Content stays in the database either way, but if the registration lives in the theme, the posts become unreachable until something registers the type again.
If you're planning a site with structured content, tell me what you publish and I'll suggest a model. The plugin development guide covers where this code should live.
Test yourself
3 quick questions on this lesson.
Finished this lesson?
Mark it complete to track your progress through the learning paths.