Hooks and templates (full)
Full hook list and template notes — for deep customization beyond Hooks and filters.
Overview
The hook system lets themes and plugins extend the CMS without editing core files. Actions run side effects; filters transform a value and must return the new value.
Implementation: App\Services\HookService, global helpers in app/Helpers/HookHelper.php (loaded from HookServiceProvider).
Registering hooks (where and when)
| Location | File | When it runs |
|---|---|---|
| Active theme | themes/{folder}/functions.php |
Loaded during ThemeServiceProvider::boot() (parent theme’s functions.php first, then child). |
| Parent theme | themes/{parent}/functions.php |
Before the active theme’s functions.php. |
| Plugin | plugins/{slug}/index.php (or path returned by Plugin::getIndexPath()) |
Each active plugin is require_once in PluginServiceProvider::boot(), before plugins.loaded. |
Order of bootstrap (simplified):
- Service providers register;
HookHelperdefinesadd_action,add_filter,do_action,apply_filters, etc. HookServiceProviderregisters default filter/action callbacks (e.g. empty placeholders for sometheme.*hooks).- Active plugins are loaded →
plugin.loading/plugin.loadedper plugin →plugins.loaded. - Theme view namespaces and
functions.php(parent then child) →theme.loaded.
Use add_action / add_filter from any of the files above. Use do_action / apply_filters only in core or in your own code; themes/plugins should normally only subscribe.
Actions vs filters
Action
add_action('hook_name', function ($arg1, $arg2) {
// no return; use for echo, logging, registering extra state
}, 10);
do_action('hook_name', $arg1, $arg2);
Filter
add_filter('hook_name', function ($value, $maybeExtra) {
return $modifiedValue; // must return
}, 10);
$value = apply_filters('hook_name', $value, $maybeExtra);
Priority
Lower number runs earlier. Default is 10.
Removing hooks
remove_action('hook_name', $callable, 10);
remove_filter('hook_name', $callable, 10);
// Omitting $callable + priority can remove all callbacks for that hook (see HookService).
Checking
has_action('hook_name');
has_filter('hook_name');
Template hierarchy (how paths work)
TemplateHierarchy builds a list of relative view names (dots, no theme:: prefix), e.g. pages.home, pages.blog, single.
Resolution:
apply_filters('template_hierarchy', $templates, $theme)can alter the list.- For each candidate, the core checks
View::exists("themes.{folder}.views.{candidate}")— i.e. file
themes/{active_theme}/views/{path with dots as slashes}.blade.php. - Child theme is tried first; then parent theme if the theme is a child.
- The chosen view name is passed through
apply_filters('template_include', $templatePath, $template, $theme)(or$parentTheme).
Namespace in Blade: use the theme:: prefix, e.g. theme::pages.home → same file as above.
The lists below match app/Services/TemplateHierarchy.php (current core).
Home
pages.home
pages.index
index
Blog / archive
pages.blog
pages.archive
archive
index
Category archive ($categorySlug)
pages.category-{slug}
pages.category
pages.archive
archive
index
Tag archive ($tagSlug)
pages.tag-{slug}
pages.tag
pages.archive
archive
index
Single post ($postSlug, optional $postType default post)
pages.{postType}-{slug}
pages.{postType}
pages.single
single
index
CMS page ($pageSlug)
pages.page-{slug}
pages.page
page
index
Search
pages.search
search
index
404
pages.404
404
index
Static catch-all page (FrontendController::staticPage)
The initial list is built in the controller, then filtered:
- Filter:
static_page_templates— receives(array $templates, string $pageSlug).
Hooks reference (core as of current codebase)
The following are dispatched from core unless noted.
Plugins
| Hook | Type | Arguments | Emitted from |
|---|---|---|---|
plugin.loading |
Action | $plugin (App\Models\Plugin) |
PluginServiceProvider before require |
plugin.loaded |
Action | $plugin |
After successful require_once |
plugins.loaded |
Action | $plugins (collection of active plugins) |
End of plugin boot |
Theme bootstrap
| Hook | Type | Arguments | Emitted from |
|---|---|---|---|
theme.view_path_base |
Filter | $path (string), $theme |
ThemeServiceProvider — filesystem path to active theme views |
theme.parent_view_path_base |
Filter | $path, $parentTheme |
Parent theme views path |
theme.assets_path_base |
Filter | $path, $theme |
Theme assets folder (source for publish) |
theme.public_assets_path |
Filter | $path, $theme |
Public assets destination |
theme.loaded |
Action | $theme |
After namespaces registered |
Theme model (App\Models\Theme)
| Hook | Type | Arguments |
|---|---|---|
theme.view_path |
Filter | Dot view path string, $theme, $view name |
theme.asset_path |
Filter | Full asset URL string, $theme, $path |
theme.parent_view_path |
Filter | Parent view name for parent_theme::, $parentTheme, $view |
Frontend (App\Http\Controllers\FrontendController)
| Hook | Type | Arguments |
|---|---|---|
before_home_page |
Action | $posts, $categories, $tags |
home_page_data |
Filter | $data (array) |
blog_posts_query |
Filter | $query (Eloquent builder), Request $request |
blog_posts_per_page |
Filter | int (default 9) |
before_blog_page |
Action | $posts, $categories, $tags, Request $request |
blog_page_data |
Filter | $data (array), Request $request |
post_found |
Action | $post |
related_posts_query |
Filter | $query (builder), $post |
related_posts_count |
Filter | int (default 3) |
before_single_post |
Action | $post, $relatedPosts |
single_post_data |
Filter | $data (array) |
page_found |
Action | $page |
before_page |
Action | $page |
page_data |
Filter | $data (array) |
before_static_page |
Action | $pageSlug (string) |
static_page_data |
Filter | $data (array), $pageSlug (string) |
static_page_templates |
Filter | array $templates, $pageSlug |
Preview routes (previewPost / previewPage) reuse before_single_post, single_post_data, page_found, before_page, page_data as appropriate.
Template resolution
| Hook | Type | Arguments |
|---|---|---|
template_hierarchy |
Filter | array $templates, $theme |
template_include |
Filter | Resolved view name string, $template key, $theme or $parentTheme |
Blade / theme layouts (used in bundled themes)
| Hook | Type | Notes |
|---|---|---|
post.content |
Filter | (string $html, $post) — wrap or sanitize post body in templates. |
post.meta |
Filter | (string $html, $post) — extra HTML in post meta area. |
theme.styles |
Action | Used in some layouts; callbacks should echo HTML. do_action itself returns nothing — output only if callbacks print. |
theme.scripts |
Action | Same pattern as theme.styles. |
Registered in HookServiceProvider but not used in core views yet
These filters have default pass-through callbacks; nothing in core calls apply_filters('theme.content', …) today:
theme.content
Theme lifecycle (ThemeController)
Dispatched when an admin activates or deletes a theme (requires manage themes permission).
| Hook | Type | Arguments | When |
|---|---|---|---|
theme.before_activate |
Action | Theme $theme |
Before other themes are deactivated and this one is saved as active. |
theme.after_activate |
Action | Theme $theme |
After activation; use fresh() state (is_active === true). |
theme.before_delete |
Action | Theme $theme |
Before filesystem folder removal and DB delete. Only runs for non-active themes. |
theme.after_delete |
Action | array $snapshot |
After the theme row is removed. Contains at least id, name, slug, folder_name (for logging or cache invalidation). |
HookServiceProvider still registers empty default callbacks for these hooks so early subscribers can attach; core now also fires them from ThemeController::activate and ThemeController::destroy.
Child themes
If theme.json / theme settings define a parent, view paths and functions.php load order are: parent first, then child. Template search in TemplateHierarchy::locateTemplate checks the child theme’s views first, then the parent’s.
Usage examples
Extra CSS on home (echo in action)
add_action('before_home_page', function () {
echo '<style>.home-banner { padding: 2rem 0; }</style>';
});
Posts per page
add_filter('blog_posts_per_page', fn () => 12);
Extra data for blog page
add_filter('blog_page_data', function (array $data, $request) {
$data['site_notice'] = 'Welcome to our blog.';
return $data;
}, 10, 2);
Related posts by same category
Post uses a single category relationship (not categories):
add_filter('related_posts_query', function ($query, $post) {
if (! $post->category_id) {
return $query;
}
return $query->where('category_id', $post->category_id);
}, 10, 2);
Adjust template hierarchy
add_filter('template_hierarchy', function (array $templates, $theme) {
array_unshift($templates, 'pages.custom-home');
return $templates;
}, 10, 2);
See also
docs/theme-development-guide.md— broader theme authoring.app/Services/HookService.php— hook storage and execution.app/Helpers/HookHelper.php— global API.