Update AGENT.md to reflect current project structure and philosophy Add comprehensive architecture documentation covering: - Directory layout - Stable contracts - Request flow - Module dependencies - Page vs list detection - Deployment models - Demo fallback Remove outdated plugin system documentation Add new content system documentation Add configuration documentation Add context API documentation Add hooks and plugins documentation Add templates documentation Add rendering documentation Add development environment documentation
4.7 KiB
Hooks & Plugins
Hook System
Defined in app/hooks.php. Two constructs:
Hook Enum
enum Hook: string {
case CONTEXT_READY = 'context_ready';
case PROCESS_CONTENT = 'process_content';
case TEMPLATE_VARS = 'template_vars';
}
Hooks Class
Hooks::add(Hook $hook, callable $callback): void
Hooks::apply(Hook $hook, mixed $value, mixed ...$args): mixed
apply chains all registered callbacks — each receives the return value of the previous. Callbacks must return the modified value or the chain breaks.
The Three Hooks
Hook::CONTEXT_READY
When: After Context creation, before routing.
Signature: function(Context $ctx, array $config): Context
Use: Set context values, process config, modify request state.
Must return: $ctx
Hook::PROCESS_CONTENT
When: During content loading — files, metadata, dates.
Signature: function(mixed $data, string $dirOrType, string $extraContext = ''): mixed
Must return: Modified $data
Called in these contexts:
$data type |
$dirOrType |
$extraContext |
Purpose |
|---|---|---|---|
array of ['path','name','ext'] |
directory path | "" |
Filter content files (e.g., by language) |
| array (metadata) | directory path | "metadata" |
Transform metadata (e.g., merge language sections) |
| string (date) | "date_format" |
"" |
Format date string |
Warning: The second parameter is overloaded — it is a directory path in the first two cases but a type identifier string in the third. Plugins must distinguish these cases carefully. Use $extraContext === 'metadata' to detect metadata processing, and $dirOrType === 'date_format' to detect date formatting. In all other cases, $dirOrType is a directory path and $data is the file list array.
Hook::TEMPLATE_VARS
When: Before rendering any template (page or list).
Signature: function(array $vars, Context $ctx): array
Must return: Modified $vars array
This is the primary extension point for adding custom template variables.
Plugin Manager
Defined in app/plugins.php. Singleton via getPluginManager().
Loading
getPluginManager()->loadGlobalPlugins(array $config) // Called during createContext()
getPluginManager()->loadPagePlugins(?array $metadata) // Called before rendering
Global plugins: Loaded from config [plugins] enabled = "name1,name2".
Page plugins: Loaded from metadata plugins = "name1,name2".
Resolution Order
For each plugin name, checks:
custom/plugins/{scope}/{name}.phpapp/plugins/{scope}/{name}.php
Custom wins. Each plugin is loaded once (require_once).
Introspection
getPluginManager()->getLoadedPlugins(): array // Names of all loaded plugins
getPluginManager()->isLoaded(string $name): bool
getPluginManager()->getPluginInfo(string $name): ?array // ['path','scope','loaded_at']
Writing a Plugin
Create custom/plugins/global/{name}.php:
<?php
Hooks::add(Hook::TEMPLATE_VARS, function(array $vars, Context $ctx): array {
if (isset($vars['content'])) {
$words = str_word_count(strip_tags($vars['content']));
$vars['readingTime'] = max(1, round($words / 200));
}
return $vars;
});
Enable in custom/config.ini:
[plugins]
enabled = "languages,your-plugin-name"
Plugin Rules
- Always return the modified value from hook callbacks
- Prefix helper functions to avoid collisions (
myPlugin_helper()) - Read config via the
$configparameter inCONTEXT_READY, not by reading files - Check variable existence before using (
isset(),??) - No inter-plugin communication — plugins are independent
- Execution order follows registration order (load order), no priority system
Accessing Config from Plugins
Hooks::add(Hook::CONTEXT_READY, function(Context $ctx, array $config): Context {
$myVal = $config['my_plugin']['key'] ?? 'default';
$ctx->set('my_key', $myVal);
return $ctx;
});
Built-in Plugin: languages.php
The language plugin registers all three hooks:
- CONTEXT_READY: Extracts language from URL prefix, sets
currentLang/defaultLang/langPrefix/translations/availableLangson context - PROCESS_CONTENT: Filters content files by language variant (
name.lang.ext), merges language metadata sections, formats dates with translated month names - TEMPLATE_VARS: Exposes language variables and
$languageUrlsto templates
Language file naming: name.lang.ext (e.g., index.no.md). A 2-letter code before the extension triggers language filtering.
Translation files: custom/languages/{lang}.ini merged over app/default/languages/{lang}.ini. Supports INI sections flattened to dot notation ([section] key = val → section.key).