Styles Development
A Kleeja style is a folder of HTML templates and assets that controls how the front end looks. Styles live under styles/, and the administrator picks the active one from the admin panel.
Templates are plain HTML with a small tag language on top. Kleeja compiles each template to PHP once, caches the result, and serves the cached file on later requests. You never write PHP in a template, because the compiler strips it.
includes/style.php, where kleeja_style finds and caches templates and KleejaTemplateCompiler compiles them. The style manager lives in includes/adm/m_styles.php. Kleeja 4 ships one front-end style, bootstrap, and the old default style is gone. See Upgrading a style from Kleeja 3.How it works
includes/common.php builds the style paths from two settings: style holds the active style folder, and style_depend_on holds its parent when the style inherits from another one. When style is empty, Kleeja uses bootstrap.
When a page calls $tpl->display('login'), the engine resolves login.html against the active style, falls back to the parent, compiles the file into cache/tpl_login.php, and includes it. The compiled file is reused until the cache is cleared.
Every global PHP variable is a template variable. display() copies $GLOBALS into the engine before rendering, so $config, $lang, $userinfo and everything a page defines are readable as {config.sitename}, {lang.LOGIN} and so on.
Folder structure
A style is a folder under styles/ whose name uses lowercase letters, digits, dots and underscores, with at least three characters.
styles/
└── my_style/
├── info.txt # required — style metadata
├── header.html # required — opens the page, loads CSS
├── footer.html # required — closes the page
├── index_body.html # required — the upload form
├── up_boxes.html # required — result blocks after an upload
├── info.html # required — information messages
├── err.html # required — error messages
├── login.html # ... one file per page, see the table below
├── screenshot.png # optional — thumbnail in the admin panel
├── index.html # optional — blocks directory listing
├── css/
├── js/
└── images/
bootstrap_black ships a header.html, a stylesheet and its info.txt, and inherits everything else from bootstrap.Templates
| Template | Rendered by | Page |
|---|---|---|
header.html | Saaheader() | Opening markup for every page. |
footer.html | Saafooter() | Closing markup for every page. |
index_body.html | index.php | The upload form on the home page. |
up_boxes.html | get_up_tpl_box() | Link blocks shown after a successful upload. |
info.html | kleeja_info() | Information messages. Also replaces the home page when uploads are off. |
err.html | kleeja_err() | Error messages. |
download.html | do.php | The download landing page for a file. |
login.html | ucp.php | The login form. |
register.html | ucp.php | The registration form. |
profile.html | ucp.php | The user profile form. |
fileuser.html | ucp.php | The user's file manager. |
get_pass.html | ucp.php | Password recovery. |
guide.html | go.php | The guide page. |
rules.html | go.php | The rules page. |
report.html | go.php | The report-a-file form. |
call.html | go.php | The contact form. |
stats.html | go.php | Site statistics. |
Emails use includes/mail_template.html, which is not part of a style.
No Template !. A style without depend_on must ship every template above, because Kleeja 4 has no default style to fall back to.The info.txt file
Every style carries an info.txt that the admin panel reads. The parser ignores blank lines and lines starting with #, splits the rest on the first =, and treats a colon in the key as a sub-key.
#
# This is a configuration file of the style.
#
#Style name
name = My Style
#Style desc
desc:en = A clean, responsive style for Kleeja
desc:ar = ستايل بسيط ومتجاوب لكليجا
#Style copyright
copyright = 2026 Your Name
#Version of style
version = 1.0
#Min. required version of kleeja
kleeja_version = 4.0.0
#name of the style required by this style
depend_on = bootstrap
#plugins required to install this style
#plugins_required = test, test2
| Key | Purpose |
|---|---|
name | Display name in the style list. |
desc:<lang> | Description per language code. Falls back to desc:en. |
copyright | Copyright line shown with the style. |
version | Style version. The store uses it to detect updates. |
kleeja_version | Minimum Kleeja version. Activation fails on anything older. |
depend_on | Parent style folder. Missing templates resolve there. |
plugins_required | Comma-separated plugin folder names that must be installed and enabled. |
info.txt drives the checks that run when an administrator activates the style. A missing parent folder, a Kleeja version below kleeja_version, or an uninstalled required plugin blocks activation with a clear error.Template syntax
The compiler turns a handful of pseudo-tags into PHP. Everything else passes through untouched.
When a template has a mistake, such as an <IF> that is never closed or an <ELSE> in the wrong place, Kleeja stops with a Template error page that names the template and the line.
Variables
Wrap a variable name in single braces. A dot walks into an array.
<title>{title} - {config.sitename}</title>
<p>{lang.WELCOME}, {username}</p>
<link rel="stylesheet" href="{STYLE_PATH}css/stylesheet.css" />
Names may contain letters, digits, underscores and dots, and nothing else. {lang.X} and {olang.X} are special: when the key does not exist, the engine prints the tag itself instead of an empty string, which makes missing translations easy to spot.
Filters
A variable prints as it is. Kleeja stores text HTML-encoded, so most values are safe in HTML already. Add a filter after a | when a value goes somewhere else:
| Filter | Output |
|---|---|
{name|e} | HTML-encoded exactly once, even when the stored text was encoded twice. |
{name|url} | Encoded for a URL with rawurlencode(). |
{name|js} | A JSON value, quotes included, safe inside a <script> block. |
{name|int} | An integer. |
<script>
var STYLE_LANG = { copy: {lang.COPY|js}, copied: {lang.COPIED|js} };
</script>
<a href="https://example.com/share?title={title|url}">{title|e}</a>
Loops
<LOOP> iterates an array. Inside the loop, double braces read the current row, {%key%} prints the current key and {%value%} prints the current value.
<ul class="menu">
<LOOP NAME="top_menu">
<IF LOOP="show">
<li><a href="{{url}}">{{title}}</a></li>
</IF>
</LOOP>
</ul>
For a flat array of strings, {%value%} is all you need:
<IF NAME="ERRORS">
<ul class="alert">
<LOOP NAME="ERRORS">
<li>{%value%}</li>
</LOOP>
</ul>
</IF>
Loops can be nested. Inside the inner loop, {{field}} reads the inner row, and the outer row comes back when the inner loop closes. <LOOP NAME> always names a global variable, never a field of the current row, so prepare the inner array in PHP.
Conditions
<IF> takes attributes rather than a free-form expression. Use NAME for a global variable and LOOP for a field of the current loop row.
<IF NAME="user_is">
<p>{lang.WELCOME} {username}</p>
<ELSE>
<a href="{action_login}">{lang.LOGIN}</a>
</IF>
<IF NAME="config.safe_code">
<img src="{captcha_file_path}" alt="{lang.REFRESH_CAPTCHA}" />
</IF>
| Attribute | Meaning |
|---|---|
NAME | A global variable, optionally with a comparison. |
LOOP | A field of the current loop row, optionally with a comparison. Use either NAME or LOOP, not both. |
ISSET | Wraps the named variable in isset(). |
EMPTY | Wraps the named variable in empty(). |
AND | One more comparison, joined with &&. |
OR | One more comparison, joined with ||. |
The tests of NAME or LOOP, ISSET and EMPTY are joined with &&, then AND and OR are added in that order. Attribute values can use double or single quotes.
Comparisons accept both symbols and words:
| Operator | Word form |
|---|---|
== | eq |
!= | neq |
< | lt |
> | gt |
<= | lte |
>= | gte |
The value on the right is a number, a word, a 'quoted text' for values with spaces, or another variable as {name} or {{field}}. A word is compared as text, digits included, so <IF NAME="style == bootstrap4"> works as expected.
<IF NAME="go_current == index" ISSET="extras.header">
<div class="extras_header">{extras.header}</div>
</IF>
<ELSEIF> extends a chain and <UNLESS> inverts a test. Close all three with </IF>, </UNLESS> or the generic </END>.
<IF NAME="current_smt == users">
...
<ELSEIF NAME="current_smt == team">
...
</IF>
<UNLESS NAME="no_results">
<p>{lang.RESULTS}</p>
</UNLESS>
$x or a function call, compiles to false and raises a warning with the template name and line. Compute the value in PHP and test a variable instead.Inline conditions
A parenthesised ternary is shorthand for a short <IF>. The condition takes no quotes or commas.
(page_stats?<div class="footer_stats">{page_stats}</div>:)
<a href="{{url}}" class="nav-link(go_current=={{name}}? active:)">{{title}}</a>
Leave either branch empty to render nothing on that side.
Browser detection
<IS_BROWSER> renders a block only for the browsers you name. Use != to invert it, and a comma-separated list to match several.
<IS_BROWSER!="mobile">
<img src="{user_avatar}" alt="{username}" />
</IS_BROWSER>
<IS_BROWSER="safari,firefox">
<link rel="stylesheet" href="{THIS_STYLE_PATH}css/fixes.css" />
</IS_BROWSER>
Recognised names are chrome, edge, firefox, safari, opera, ie, konqueror, mozilla, webkit and mobile. Append a version number to any name except mobile to target it, as in ie11 or firefox115.
Row helpers
Inside a loop, <ODD> and <EVEN> test a numeric field of the current row, and <RAND> alternates between two strings on every call. <ODD> and <EVEN> need their closing tags, or they print as text.
<LOOP NAME="files">
<tr class="<RAND="row_a","row_b">">
<td>{{name}}</td>
</tr>
</LOOP>
Including a template
<INCLUDE> renders another template in place. Add PATH to pull it from a directory outside the active style.
<INCLUDE NAME="sidebar">
Escaping
The compiler strips PHP from templates, so <?php ?>, <? ?>, <% %> and <script language="php"> blocks never survive compilation, and a stray <? prints as text. Wrap markup in <IGNORE> to hide it from the compiler entirely, which is useful for JavaScript or CSS that would otherwise look like a template tag.
<IGNORE>
<script>const tpl = { name: "{not a variable}" };</script>
</IGNORE>
Template variables
Paths
| Variable | Points to |
|---|---|
{STYLE_PATH} | URL of the parent style when depend_on is set, otherwise the active style. Use it for inherited assets. |
{THIS_STYLE_PATH} | URL of the active style folder. Use it for your own overrides. |
{STYLE_PATH_ADMIN} | URL of the admin theme, admin/Damask/. |
{DEFAULT_PATH_ADMIN} | URL of the default admin theme, also admin/Damask/. |
{STYLE_PATH} resolves to the parent. Loading {STYLE_PATH}css/stylesheet.css then {THIS_STYLE_PATH}css/stylesheet.css gives you the parent's stylesheet followed by your overrides, which is exactly what bootstrap_black does.Page and user
| Variable | Contents |
|---|---|
{title} | Page title. |
{dir} | Text direction, rtl or ltr, from the active language. Also {lang.DIR}. |
{lang.LANG_SMALL_NAME} | Language code for the lang attribute, like en-us. |
{charset} | Character set, always utf-8. |
{username} | The visitor's name, or the guest label. |
{user_is} | True when the visitor is logged in. |
{user_avatar} | Gravatar URL, falling back to the style's images/user_avater.png. |
{userinfo.*} | The logged-in user's row. |
{action_login}, {H_FORM_KEYS_LOGIN} | The action and the form key of a login form in the header. |
{config.*} | Any setting from the admin panel, such as {config.sitename} or {config.siteurl}. |
{lang.*} | Language strings from lang/<code>/. |
{olang.*} | Language strings added by plugins. |
{text} | The message body in info.html and err.html. |
Menus and blocks
| Variable | Contents |
|---|---|
{top_menu} | Loop of the top navigation: {{name}}, {{title}}, {{url}}, {{show}}. |
{side_menu} | Loop of the user menu, with the same fields plus {{icon}}, a Font Awesome name without the fa- prefix. |
{go_current} | Name of the current page, for marking the active menu item. |
{extras.header} | Extra HTML injected above the content. |
{extras.footer} | Extra HTML injected below the content. |
{EXTRA_CODE_META} | Extra tags for the <head>, where plugins add meta tags. |
{page_stats} | Generation time and query count, when enabled. |
{admin_page} | Link to the admin panel, set only for users who can open it. |
{run_queue} | The queue pixel. Keep it in footer.html. |
{googleanalytics} | Analytics snippet, when configured. |
{go_back_browser} | Localised "go back" label. |
{is_embedded} | True when the page is opened with ?embedded=1, for styles that offer an embeddable upload page. |
footer.html must keep {run_queue}. It triggers Kleeja's scheduled tasks, including old-file cleanup, and dropping it silently stops them.Upload result blocks
up_boxes.html is not a normal template. Kleeja splits it into named blocks and does a plain {var} replacement, with no conditions, loops or language strings. Blocks are delimited by HTML comments.
<!-- BEGIN image -->
<div class="up-box">
<div class="up-box-label">{b_title}</div>
<input type="text" readonly value="{b_url_link}" aria-label="{b_title}">
</div>
<!-- END image -->
| Block | Shown for |
|---|---|
image_thumb | An uploaded image with a thumbnail. |
image | An uploaded image, direct and BBCode links. |
file | A non-image upload. |
del_file_code | The deletion code for the upload. |
Available placeholders are {b_title}, {b_bbc_title}, {b_url_link}, {b_img_link}, {b_code_link}, plus {siteurl} and {sitename}. All four blocks must exist, because the uploader requests them by name. A child style without its own up_boxes.html uses the parent's.
Style inheritance
Set depend_on in info.txt to build on an existing style. Activating the child writes both style and style_depend_on to the settings, and the engine then resolves every missing template against the parent.
name = Bootstrap Black
version = 2.0
kleeja_version = 4.0.0
depend_on = bootstrap
Template resolution runs in this order:
The active style
styles/<style>/<template>.html.
The parent style
When style_depend_on is set, the same filename under the parent folder.
depend_on is not followed. Without depend_on, the engine still looks in styles/default/, which Kleeja 4 no longer ships. Build on bootstrap, or ship every template yourself.Inside a child style, load the parent's assets through {STYLE_PATH} and your own through {THIS_STYLE_PATH}:
<IF NAME="lang.DIR==rtl">
<link rel="stylesheet" href="{config.siteurl}includes/static_shared_files/bootstrap/css/bootstrap.rtl.min.css">
<ELSE>
<link rel="stylesheet" href="{config.siteurl}includes/static_shared_files/bootstrap/css/bootstrap.min.css">
</IF>
<link rel="stylesheet" href="{config.siteurl}includes/static_shared_files/fontawesome/css/all.min.css">
<link rel="stylesheet" href="{STYLE_PATH}css/stylesheet.css">
<link rel="stylesheet" href="{THIS_STYLE_PATH}css/stylesheet.css">
A child cannot add a stylesheet without its own header.html, so it copies the parent's header and adds one <link>. When the parent's header.html changes in a new Kleeja release, copy it again and reapply your changes.
Assets and direction
Keep stylesheets in css/, scripts in js/ and graphics in images/, and reference them through a path variable so the style keeps working after it is renamed or inherited.
Kleeja ships Bootstrap 5.3 with its RTL build, and Font Awesome, in includes/static_shared_files/. The admin theme and the bootstrap style load them from there, and your style can too, so visitors download them once.
Kleeja ships Arabic and English, so a style must handle both directions. The {dir} variable carries rtl or ltr, which lets you set the document direction and load the matching Bootstrap build:
<html lang="{lang.LANG_SMALL_NAME}" dir="{dir}">
...
<IF NAME="lang.DIR==rtl">
<link rel="stylesheet" href="{config.siteurl}includes/static_shared_files/bootstrap/css/bootstrap.rtl.min.css">
<ELSE>
<link rel="stylesheet" href="{config.siteurl}includes/static_shared_files/bootstrap/css/bootstrap.min.css">
</IF>
Light and dark modes
The bootstrap style has a light, dark and automatic color mode. js/color-mode.js sets data-bs-theme on the <html> element before the page is painted, and remembers the visitor's choice in the browser. A child style inherits the switch. Give your own colors through Bootstrap's CSS variables, or with a [data-bs-theme="dark"] rule, so they work in both modes.
Scripts
The bootstrap style's js/script.js reads its strings from a STYLE_LANG object that footer.html defines. A child that replaces footer.html keeps that object and the <script> tags, or copy buttons and upload checks stop working.
Add a screenshot.png to the style folder for a thumbnail in the style list. Without it, the admin panel shows a generic icon.
Development workflow
Compiled templates are cached in cache/, so edits to an .html file are invisible until the cache is cleared. Turn caching off while you work:
define('DEV_STAGE', true);
DEV_STAGE compiles templates again on every request, reports all PHP errors and adds a Debug Info link to the footer for administrators. To keep caching off without the rest of the debug output, use STOP_TPL_CACHE instead.
Reading the compiled output
Each template compiles to cache/tpl_<name>.php, with the name lowercased and every character outside a-z0-9-_ replaced by a hyphen. A template that a plugin renders from its own folder gets an MD5 hash of that folder after its name. Opening the compiled file is the quickest way to see what a tag turned into when a template misbehaves.
Managing styles
Administrators work with styles in Admin → Styles. The page lists the styles present in styles/ and, on the store tab, the styles available in the Kleeja store.
| Action | Effect |
|---|---|
| Select | Writes style and style_depend_on, then clears the cache. Runs the parent, version and required-plugin checks first. |
| Upload | Extracts an uploaded .zip into styles/. |
| Download | Fetches and extracts a style from the store, with automatic rollback on failure. Founder accounts only. |
| Delete folder | Removes the style folder. The active style cannot be deleted. |
Activation fails with a specific error in three cases: the folder named by depend_on is missing, kleeja_version is newer than the running Kleeja, or a plugin listed in plugins_required is missing or disabled.
Packaging and distribution
Ship a style as a .zip containing one top-level folder, the same shape as the folder in styles/.
zip -r my_style.zip my_style -x "*.git*"
Administrators upload the archive in Admin → Styles, and Kleeja extracts it directly into styles/ without renaming it, so the top-level folder must carry the style's name. A style downloaded from the store is renamed to its catalog name, so the archive that GitHub builds for a release, with a folder like my_style-1.0/, works there.
Styles listed in the public catalog install with one click. Kleeja 4 reads kleeja-4-catalog.json, where each style has an entry like this one:
{
"type": "style",
"name": "my_style",
"icon": "https://raw.githubusercontent.com/you/my_style/main/screenshot.png",
"title": { "en": "My Style", "ar": "ستايلي" },
"description": { "en": "A clean, responsive style", "ar": "ستايل بسيط ومتجاوب" },
"developer": "Your Name",
"website": "https://github.com/you/my_style",
"kleeja_version": { "min": "4.0.0", "max": "4.9.9" },
"depend_on": "bootstrap",
"plugins_required": "",
"file": {
"version": "1.0",
"url": "https://github.com/you/my_style/archive/1.0.zip"
}
}
The admin theme
The admin panel uses its own theme, fixed to admin/Damask/ and built on Bootstrap 5.3, whose templates are the files beginning with admin_. The engine routes any template name with that prefix to the admin theme instead of the active style.
Admin templates are not part of a front-end style. To replace them, ship a plugin that rewrites the admin path variables from the end_common hook.
Upgrading a style from Kleeja 3
Kleeja 4 replaced the template compiler and the bundled styles. Check these points when you port a style:
- Inheritance. The
defaultstyle is gone. Setdepend_on = bootstrap, or ship every template. - Version. Set
kleeja_version = 4.0.0ininfo.txtand in your catalog entry. - Bootstrap. The
bootstrapstyle moved to Bootstrap 5.3 and loads it fromincludes/static_shared_files/. Templates copied from the old Bootstrap 4 style need new markup. - Conditions. Remove empty
AND=""attributes, which now stop the page with a template error.ISSETandEMPTYcombine withNAMEwithout them. - PHP in templates. Conditions that ran PHP code are now
false. Move that logic into a plugin or a page script. - Browser names.
flockis gone, andedgeis new. - Templates that grew features. Compare your
header.htmlandfooter.htmlwith the ones instyles/bootstrap/, which now carry the color mode switch and theSTYLE_LANGstrings.
Troubleshooting
| Symptom | Cause |
|---|---|
No Template ! | The template does not exist in the style or its parent. Check the filename and depend_on. |
Template error | A block is not closed, or a tag is in the wrong place. The message names the template and the line. |
| Edits do nothing | The compiled template is cached. Enable DEV_STAGE or clear the cache. |
A {variable} prints literally | For {lang.*} and {olang.*} the key is missing. For anything else the global does not exist on that page. |
| A condition is always false | It contains PHP code, which templates cannot run. The warning names the template and the line. |
Text shows &quot; | The stored text was encoded twice. Print it with the |e filter. |
| JavaScript breaks after compilation | Braces in the script looked like template tags. Wrap the block in <IGNORE>, and print values with the |js filter. |
| Scheduled cleanup stopped | {run_queue} was removed from footer.html. |
Reference
Tags
| Tag | Compiles to |
|---|---|
{var}, {a.b} | Print a global variable. |
{var|e}, |url, |js, |int | Print a variable through a filter. |
{{field}} | Print a field of the current loop row. |
{%key%}, {%value%} | Print the current loop key or value. |
<LOOP NAME="x"> … </LOOP> | foreach over an array. |
<IF NAME="x"> … </IF> | Conditional block. |
<ELSEIF …>, <ELSE> | Condition chain. |
<UNLESS …> … </UNLESS> | Inverted condition. |
(cond?a:b) | Inline condition. |
<IS_BROWSER="x"> … </IS_BROWSER> | Browser test, != to invert. |
<ODD="field"> … </ODD> | Render on odd values. |
<EVEN="field"> … </EVEN> | Render on even values. |
<RAND="a","b"> | Alternate between two strings. |
<INCLUDE NAME="tpl"> | Render another template. |
<IGNORE> … </IGNORE> | Hide a block from the compiler. |
</END> | Generic closing tag. |
Engine API
| Method | Signature |
|---|---|
display() | display(string $template_name, string $style_path = ''): string |
assign() | assign(string $var, mixed $to): void |
template_exists() | template_exists(string $template_name, string $style_path = ''): string|false |
kleeja_style_info() | kleeja_style_info(string $style_name): array|false — includes/functions_display.php |
get_up_tpl_box() | get_up_tpl_box(string $box_name, array $extra = []): string — includes/functions_display.php |
is_browser() | is_browser(string $b): bool — includes/functions_display.php |
Settings and constants
| Name | Meaning |
|---|---|
config.style | Folder name of the active style. |
config.style_depend_on | Folder name of the parent style, or empty. |
ACP_STYLE_NAME | The admin theme folder, Damask. |
DEV_STAGE | Disables template caching and enables debug output. |
STOP_TPL_CACHE | Disables template caching only. |
PATH | Absolute filesystem path to the Kleeja root, with a trailing slash. |