Template hierarchy
Every front-end controller used to name exactly one view. Rendering a listing
meant item.php, and that was the whole conversation — a theme that wanted a
different layout for one category had nowhere to put it.
Controllers now name an ordered list, most specific first, and core renders the first one a theme actually ships.
What core offers
Section titled “What core offers”| Page | Candidates, in order |
|---|---|
| Listing | item-{categoryId}.php, item.php |
| Search / category results | search-{category}.php, search.php |
| Static page | page-{slug}.php, the picked template, page.php |
| Everything else | one candidate, the name it has always had |
{category} on a results page is the token from the URL — the slug for
/jobs, the id when the search was made by id. It is offered only when the
search is filtered to exactly one category.
Ship item-12.php and listings in category 12 render through it. Ship nothing
and every page resolves exactly as before.
Locating a view yourself
Section titled “Locating a view yourself”osc_locate_template(array $candidates, string $context = ''): stringReturns the view name of the first candidate any theme in the stack can
render — not a filesystem path, because rendering goes through
osc_current_web_theme_path(), which also points the theme’s own asset URLs at
whichever theme answered.
When nothing matches, the last candidate comes back. A caller that names one view therefore behaves exactly as it did before there was a list.
Two orderings, and which one wins
Section titled “Two orderings, and which one wins”Views are resolved against a stack: your theme, its parent when it declares one, then the theme core keeps as a last resort.
Candidate order applies within one theme, and theme order comes first. If
your theme ships item.php and a fallback theme happens to ship item-12.php,
your generic view wins. Otherwise adding a candidate could hand one of your pages
to a theme the site is not running.
osc_theme_template_paths() returns that stack as absolute directory paths.
Adding a view without a core patch
Section titled “Adding a view without a core patch”The candidate list passes through the template_candidates filter, with the
route’s context slug as the second argument:
osc_add_filter('template_candidates', 'mytheme_templates', 5, 2);
function mytheme_templates(array $candidates, string $context): array{ if ($context === 'item' && osc_item_is_premium()) { array_unshift($candidates, 'item-premium.php'); }
return $candidates;}Context slugs are the route names: home, search, item, item-post,
item-edit, item-contact, item-send-friend, page, contact, custom,
user-custom, 404, and one per account view (user-profile,
user-dashboard, user-items, user-alerts, user-login, user-register,
user-recover, user-forgot_password, user-change_email,
user-change_username, user-change_password, user-delete_account,
user-public-profile).
A filter that returns anything other than an array is ignored, and candidates
that are absolute or contain .. are dropped — neither can blank a page or
reach outside the theme directories.
Editing reuses the publishing form
Section titled “Editing reuses the publishing form”Core asks for item-edit.php first and falls back to item-post.php, because the
two carry the same fields — ItemForm hands both the same list, and the view can
tell them apart with osc_is_edit_page().
So a theme ships one publishing form and gets editing for free. Ship
item-edit.php as well and yours still wins for that route.
ItemForm supplies everything that differs between the two, so the view does not
have to work out which one it is:
| Call | What it gives you |
|---|---|
ItemForm::route_hidden() |
page, action, and on edit the listing’s id and secret |
ItemForm::location_record() |
the listing when editing, the seller’s own address when publishing |
ItemForm::selected_country() |
the country the region list should be built from |
ItemForm::selected_region() |
the region the city list should be built from |
ItemForm::plugin_item_fields() |
the plugin fields for whichever form this is |
The two selected_* calls prefer the posted value, which matters without
JavaScript: change country, submit, and the re-rendered form offers that
country’s regions rather than the previous one’s.
osc_show_item_comments() renders the comment thread and its form inside your
listing page — see The comment block below.
The title and description inputs pick their own locale — the visitor’s when
publishing, and when editing the one the listing’s text actually came from, so an
edit updates the translation it is showing instead of copying it into a second
one. osc_item_content_locale() reports that locale and
ItemForm::locale_field_id() the id these fields carry, which is not their name.
Core loads the uploader and the location combobox from the head on both routes,
so you no longer enqueue osc-uploader or osc-ui-common before the form.
The comment block
Section titled “The comment block”Comments are on by default, so a theme that draws no comment block ships a feature nobody can reach. One call renders the whole thing — the count, the thread, the form:
<?php osc_show_item_comments(); ?>It prints nothing when comments are switched off, so it needs no guard.
The field names belong to core’s add_comment action, not to your theme. That is
the point: you restyle it, you do not rebuild it.
Restyling it
Section titled “Restyling it”The block carries its own class names and styles itself with :where() rules, so
any rule of yours wins without !important and without raising specificity.
| Class | What it wraps |
|---|---|
oe-comments |
The whole block |
oe-comments-head · oe-comments-count |
The heading and its count |
oe-comments-list |
The thread |
oe-comment |
One comment |
oe-comment-head · oe-comment-author · oe-comment-date |
Its byline |
oe-comment-title · oe-comment-body |
Its text |
oe-comment-actions · oe-comment-delete |
Its actions |
oe-comments-empty |
Shown when there are none yet |
oe-comments-gate |
Shown when a visitor must sign in first |
oe-comment-form · oe-field · oe-comment-optional · oe-actions |
The form |
| Hook | When |
|---|---|
item_comments_before |
Before the block |
comment_form |
Inside the form, after the built-in fields |
item_comments_after |
After the block |