Developer docs
Start
Build with AI
Themes
Apps
- App development
- App development best practices
- Hooks and filters
- The App API
- Sending email from apps
- Admin bar for apps and themes
- Public routes for apps
- Admin screens, routes and UI components
- Dashboard widgets
- Data tables
- AJAX actions
- Media and uploads for apps
- App permissions
- Content types, blocks and widgets
- Content types, taxonomies and custom fields
- Search
- When an app fails: fault isolation and recovery
API and AI agents
Security and contributing
Theme reference: variables and helpers
A quick-lookup page for theme authors. For the concepts, start with Theme development.
Variables every template gets
| Variable | Contents |
|---|---|
$base | URL prefix of the install: '' at a domain root, /blog in a subfolder |
$site_title, $tagline | From the Customizer (Site identity) |
$logo_url, $favicon_url | Image URLs, or empty |
$footer_text | Footer text from the Customizer, or empty |
$primary_menu, $footer_menu | Menu trees for the primary and footer locations, ready for menu_html() |
$seo | This page's title, description and so on, for showing (a breadcrumb, a share link). Core prints the tags itself |
$current_url | The request path |
$custom_css | The site's custom CSS (already printed by bh_head()) |
$is_home | true on the front page |
$is_preview | true when an editor is previewing a draft |
Per template
| Template | Variables |
|---|---|
index.php | $posts (rows for this page), $meta (pagination) |
single.php | $post, $terms (its categories and tags), $comments (approved), $comments_count, $comments_open |
page.php | $post |
archive.php | $posts, $meta, $archive_type (category, tag or author), $term (for categories and tags), $author (for authors), $title |
search.php | $posts, $meta, $query |
Note: On
single.phpandpage.php,$pageholds the same row as$post, not a page number. Read pagination from$meta['page'].
When the front page is set to a static page, core renders page.php for it with $is_home = true.
$meta (pagination)
['page' => 2, 'per_page' => 10, 'total' => 57, 'last_page' => 6]
A post row ($post, items of $posts)
| Field | Notes |
|---|---|
id, uuid, slug, type | type is post or page |
title, excerpt | Plain text; escape when printing |
content | Finished HTML on single and page templates. Print as is |
content_format | blocks, markdown or html: how it was written, not how it arrives |
status | published, or draft/pending in a preview |
published_at, created_at, updated_at | UTC, Y-m-d H:i:s. Show with bh_date() |
author_id, author_name, author_avatar_url | The author |
featured_url, featured_alt, featured_width, featured_height, featured_sizes | Featured image; use bh_post_image() |
comment_status | open or closed |
view_count | Views counted by core |
A term row ($terms, $term)
id, name, slug, description, parent_id, count, and on $terms also taxonomy_slug (category or tag). Link a term with:
<?php $prefix = $t['taxonomy_slug'] === 'tag' ? '/tag/' : '/category/'; ?>
<a href="<?= htmlspecialchars(link_to($prefix . $t['slug'])) ?>"><?= htmlspecialchars($t['name']) ?></a>
A comment row ($comments)
id, parent_id (empty for a top-level comment), author_name, author_url, content (plain text: escape it), created_at (UTC).
Page structure
| Helper | Use |
|---|---|
bh_head() | Echo inside <head>. Required |
bh_footer() | Echo before </body>. Required |
bh_body_class($extra) | Classes for <body>: is-home or is-inner, the theme slug, preview state, plus $extra |
bh_ajax_client() | Load Basehim.ajax on the public site. Call before bh_head() |
bh_is_preview() | true inside the Customizer's preview frame |
Links and assets
| Helper | Returns |
|---|---|
link_to($path) | $path with the install's base path, e.g. link_to('/search') |
\App\Core\Helpers::postUrl($post) | A post's or page's path under the current permalink setting (prefix with $base) |
theme_asset($file, $version = true) | URL of a file in this theme's assets/, with ?v= |
icon($name, $class) | A Heroicon as inline SVG, e.g. icon('magnifying-glass', 'w-4 h-4') |
Menus
| Helper | Use |
|---|---|
menu_html($items, $opts) | Nested <ul> with dropdowns to three levels. $opts: class (default bh-menu; keep bh-menu in it for the built-in styles), aria, depth |
menu_at($location) | Items for any location declared in theme.json |
menu_has_children($items) | Whether any item has children |
menu_assets() | Dropdown CSS and behaviour; echo once in <head> |
<?= menu_html($primary_menu, ['class' => 'bh-menu site-nav', 'aria' => 'Primary']) ?>
<?= menu_html(menu_at('utility')) ?>
Widgets
| Helper | Use |
|---|---|
widget_area($key) | Rendered widgets of an area, or '' |
has_widget_area($key) | Whether the area has any widgets |
<?php if (has_widget_area('sidebar')): ?>
<aside><?= widget_area('sidebar') ?></aside>
<?php endif; ?>
Comments
| Helper | Use |
|---|---|
bh_comment_form($post, $args) | Core's comment form: CSRF token, honeypot, replies, no page reload. Returns the "closed" text when comments are off |
bh_comment_count($post) | Approved comments on a post |
Mark a count so the form updates it after a new comment:
<span data-bh-comment-count="<?= (int) $post['id'] ?>"><?= (int) $comments_count ?></span>
Any element with data-bh-reply data-id="…" data-name="…" turns the form into a reply to that comment. Style hooks and options (class, button_class, label_submit, styles => false…) are listed in Customizer, menus and widgets.
Authors
| Helper | Returns |
|---|---|
bh_author_box($post, $args) | The author box (avatar, name, bio, link). Empty when Settings → Reading → Author box on posts is off |
bh_author($post) | Public profile: display_name, bio, slug, url, avatar_url, post_count |
bh_author_url($post) | Path to the author's archive, or '' when archives are off or the author has no posts |
bh_avatar($userOrPost, $size) | An avatar <img>, or initials |
Images
| Helper | Returns |
|---|---|
bh_post_image($post, $size, $attrs) | Responsive <img> of the featured image (srcset, width, height, lazy loading), or '' |
bh_image_url($post, $size) | One size's URL; falls back to the original |
bh_image_sizes($post) | All sizes, smallest first: name => [url, width, height] |
Sizes: thumbnail (square), medium, large, full (original).
Dates and times
Stored times are UTC. These show them in the site's timezone and formats:
| Helper | Example output |
|---|---|
bh_date($t, $format = null) | October 10, 2026 |
bh_time($t, $format = null) | 1:32 pm |
bh_datetime($t, $format = null) | October 10, 2026 1:32 pm |
bh_time_ago($t, $maxDays = 7) | 5 minutes ago |
bh_time_tag($t, $format = null, $relative = false) | <time datetime="2026-10-10T13:32:00+05:00">…</time> |
bh_iso8601($t) | 2026-10-10T13:32:00+05:00 |
bh_local_time($t) | DateTimeImmutable in the site timezone |
bh_to_utc($local) | A time typed in the site timezone, as UTC to store |
bh_now() | Now, UTC, for storing |
bh_timezone() | e.g. Asia/Karachi |
Settings and options
| Helper | Returns |
|---|---|
bh_theme_option($key, $default) | A Customizer option of this theme, preview-aware |
bh_setting($group, $key, $default) | A site setting, e.g. bh_setting('reading', 'posts_per_page', 10) |
SEO
| Helper | Use |
|---|---|
bh_seo() | This page's SEO head: set(), get(), addJsonLd(), disable(), handOff() |
bh_seo_value($key) | Read one value back, e.g. bh_seo_value('title') |
AI agents (WebMCP)
| Helper | Use |
|---|---|
bh_webmcp_form($name, $description, $opts) | Attributes that describe a form to in-browser agents |
bh_webmcp_param($description) | Describes one form field |
Both return '' while WebMCP is off, so they are safe to leave in. See AI discovery.
Leave a comment