Repository navigation
SSI Guide
HTML.PHP speaks the Server Side Includes dialect your .shtml trees already
use β so migrations from Apache mod_include to nginx/PHP-FPM, containers,
or static-plus-PHP hosting keep working without touching the documents.
Note
Canonical spec: docs/SSI.md.
This page adds the operational perspective: policies, recipes, parity notes.
Inside renders β automatic. The SSI pass runs after the template
language, over the assembled document (ssi.enabled, on by default).
Standalone β any string (legacy pages, WordPress post content, option values):
use HtmlPhp\Ssi\DocumentInfo;
use HtmlPhp\RequestContext;
$html = $engine->processSsi($content, new DocumentInfo(
file: '/var/www/page.shtml', // anchors file= includes + LAST_MODIFIED
root: '/var/www', // containment root for virtual=
request: RequestContext::fromServer($_SERVER), // built ONCE at the edge
vars: ['role' => 'admin'], // render scope visible as $role in #if/#echo
));Important
The engine never reads $_SERVER. Hosts construct RequestContext once
per request; CLI/jobs use RequestContext::cli($cwd) or a frozen clock for
deterministic DATE_LOCAL in tests and static exports.
| Directive | Attributes | Notes |
|---|---|---|
#config |
timefmt sizefmt errmsg
|
strftime tokens (converted internally β no deprecated strftime()); state scoped to the render |
#set |
var value
|
$VAR/${VAR} expansion; reserved names refused; never touches $GLOBALS
|
#include |
file virtual stub wait
|
file= = relative to current document; virtual= = from doc root; stub falls back to a #block; wait accepted (sequential engine) |
#echo |
var encoding
|
built-ins β #set vars β scope vars; encoding=none|entity|url|base64; escaped by default |
#if #elif #else #endif
|
expr |
full grammar below; inactive branches drop includes & sets |
#exec |
cmd cgi
|
deny by default β see policies |
#fsize #flastmod
|
file/virtual (optional) |
default: current document; honour #config sizefmt/timefmt
|
#printenv |
β | dumps visible vars NAME=value (escaped, allow-listed); disable via ssi.printenv_enabled
|
#block #endblock
|
name |
fallback content for #include stub=; never renders in place |
| custom | via registry |
DirectiveRegistry::register(DirectiveInterface) β checked before built-ins |
Unknown directives stay in the output verbatim (they're ordinary comments to
us) and record ssi-unknown-directive. The v2-era <!--##name--> and
<!--@var=β¦--> forms were removed in v4.
DATE_LOCAL Β· DATE_GMT Β· LAST_MODIFIED Β· DOCUMENT_URI Β·
DOCUMENT_NAME Β· DOCUMENT_ROOT Β· QUERY_STRING Β· SERVER_NAME Β·
SERVER_SOFTWARE Β· SERVER_PROTOCOL Β· REQUEST_METHOD Β· REQUEST_URI Β·
SCRIPT_NAME Β· CONTENT_TYPE Β· REMOTE_ADDR Β· HTTP_USER_AGENT β plus
any HTTP_* header from the request context.
Extend/override via the htmlphp.ssi.builtins filter. ssi.echo_allow
restricts which names may be echoed at all.
Everything is a string (mod_include semantics):
<!--#if expr="$user = 'admin'" --> string equality (= or ==)
<!--#if expr="$count > 10" --> numeric-aware ordering
<!--#if expr="$uri = /^\/admin\//" --> regex match (also with !=)
<!--#if expr="-f /private/notes.txt" --> file tests: -f -d -e -s -x -r -w -F
<!--#if expr="!(-A /x)" --> -A/-U parse but are ALWAYS false
<!--#if expr="($a && $b) || ! $c" --> logic + grouping
<!--#if expr="$flag" --> bare truthiness ('' and '0' are false)Warning
-A/-U are Apache subrequest tests. A template engine that performs
network I/O is an SSRF vector, so v4 refuses them (false + diagnostic).
Use -f on a local file, or a custom directive.
Syntax/length errors never throw: ssi-expr-error diagnostic + false.
Cap: limits.max_expression_length (4 096).
ssi.exec_mode |
Behaviour |
|---|---|
deny (default)
|
refuse + ssi-exec-denied diagnostic (+ errmsg if set) |
allow-list |
cmd= binaries must realpath/basename-match ssi.exec_cmd_allow; cgi= must be doc-root-contained with an extension from ssi.exec_cgi_extensions
|
delegate |
your callable decides: fn(string $kind, array $info): bool|string β true run, false deny, string = literal output |
Execution hardening: no shell (proc_open argv + quote-aware tokenizer),
minimal CGI env, hard timeout (limits.exec_timeout β€ 60 s), output cap
(limits.max_exec_output_bytes), control-byte stripping. cgi runs
php -f spawned, or in-process with ssi.exec_cgi_mode: 'include'
(throws β diagnostics, never fatals).
Tip
Need a dynamic fragment? A custom directive is the professional move β same power, no shell, testable:
$engine->ssi()->directives()->register(new class implements HtmlPhp\Ssi\DirectiveInterface {
public function name(): string { return 'cartcount'; }
public function render(array $attributes, HtmlPhp\Ssi\SsiContext $context): string {
return (string) Cart::count(); // your code, your tests
}
});<!--#cartcount --> now works in any document.
Theme-overridable includes β remap resolved paths (how the WP bridge does child-theme overrides):
$engine->events()->on('htmlphp.ssi.include_path', function (string $path, string $label): string {
$theme = get_stylesheet_directory() . '/ssi' . basename($path);
return is_file($theme) ? $theme : $path;
});Graceful degradation with stubs β
<!--#block name="news-fallback"--><p>News temporarily unavailable.</p><!--#endblock-->
<!--#include virtual="/fragments/news.html" stub="news-fallback" -->Base64 data handoff to JS β
<script>const cfg = JSON.parse(atob('<!--#echo var="config_json" encoding="base64" -->'));</script>Feature flags from the edge β
<!--#if expr="$HTTP_X_BETA = '1'" --> β¦beta bannerβ¦ <!--#endif-->| Behaviour | Apache | HTML.PHP v4 |
|---|---|---|
| Evaluation point | server, per served document | engine, after template language, per rendered document |
#set scope |
request | render (never globals) |
| include recursion limits | none practical |
max_ssi_depth 16 Β· max_ssi_tags 256 Β· cycle stack Β· byte caps |
-A/-U subrequests |
supported | refused (SSRF-safe), always false |
XBitHack, .var content negotiation |
supported | out of scope |
#include wait |
parallel | accepted, no-op (sequential) |
Legacy ##name/@var
|
never existed | removed in v4 |
β‘οΈ Related: Security β Β§3 exec Β· Configuration β ssi group Β· Troubleshooting β SSI codes
HTML.PHP 5.0.0 Β· MIT Β© Mikhail Deynekin Β· Repository Β· Releases Β· Report a security issue
Wiki pages mirror docs/ β found a mistake? Edit the page or open an issue.
π Home Problem Domain Getting Started
Template Syntax Expressions & Math SSI Guide I18n Guide WordPress Integration CLI
Configuration API Cheatsheet Diagnostics & Troubleshooting
Architecture Security Model Testing & CI
πΊοΈ ROADMAP Migration 4.x β 5.0 Migration 3.x β 4.0 Contributing Repository β