Development

Styles Development

Learn how to build your own styles for Kleeja.

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.

This page documents the style system in Kleeja 4. The template engine lives in 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/
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/
A style that inherits from another needs only the templates it changes. bootstrap_black ships a header.html, a stylesheet and its info.txt, and inherits everything else from bootstrap.

Templates

TemplateRendered byPage
header.htmlSaaheader()Opening markup for every page.
footer.htmlSaafooter()Closing markup for every page.
index_body.htmlindex.phpThe upload form on the home page.
up_boxes.htmlget_up_tpl_box()Link blocks shown after a successful upload.
info.htmlkleeja_info()Information messages. Also replaces the home page when uploads are off.
err.htmlkleeja_err()Error messages.
download.htmldo.phpThe download landing page for a file.
login.htmlucp.phpThe login form.
register.htmlucp.phpThe registration form.
profile.htmlucp.phpThe user profile form.
fileuser.htmlucp.phpThe user's file manager.
get_pass.htmlucp.phpPassword recovery.
guide.htmlgo.phpThe guide page.
rules.htmlgo.phpThe rules page.
report.htmlgo.phpThe report-a-file form.
call.htmlgo.phpThe contact form.
stats.htmlgo.phpSite statistics.

Emails use includes/mail_template.html, which is not part of a style.

A missing template is fatal. When the engine cannot resolve a name in the active style or its parent, it stops the request with 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.

styles/my_style/info.txt
#
# 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
KeyPurpose
nameDisplay name in the style list.
desc:<lang>Description per language code. Falls back to desc:en.
copyrightCopyright line shown with the style.
versionStyle version. The store uses it to detect updates.
kleeja_versionMinimum Kleeja version. Activation fails on anything older.
depend_onParent style folder. Missing templates resolve there.
plugins_requiredComma-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.

styles/my_style/header.html
<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:

FilterOutput
{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.
styles/my_style/footer.html
<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.

styles/my_style/header.html
<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:

styles/my_style/login.html
<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.

styles/my_style/index_body.html
<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>
AttributeMeaning
NAMEA global variable, optionally with a comparison.
LOOPA field of the current loop row, optionally with a comparison. Use either NAME or LOOP, not both.
ISSETWraps the named variable in isset().
EMPTYWraps the named variable in empty().
ANDOne more comparison, joined with &&.
OROne 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:

OperatorWord 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.

styles/my_style/header.html
<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>.

styles/my_style/profile.html
<IF NAME="current_smt == users">
    ...
<ELSEIF NAME="current_smt == team">
    ...
</IF>

<UNLESS NAME="no_results">
    <p>{lang.RESULTS}</p>
</UNLESS>
Templates cannot run PHP. A condition that contains PHP code, such as $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.

styles/my_style/footer.html
(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.

styles/my_style/header.html
<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.

styles/my_style/fileuser.html
<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.

styles/my_style/index_body.html
<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.

styles/my_style/footer.html
<IGNORE>
<script>const tpl = { name: "{not a variable}" };</script>
</IGNORE>

Template variables

Paths

VariablePoints 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/.
In a child style, {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

VariableContents
{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.
VariableContents
{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.

styles/my_style/up_boxes.html
<!-- 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 -->
BlockShown for
image_thumbAn uploaded image with a thumbnail.
imageAn uploaded image, direct and BBCode links.
fileA non-image upload.
del_file_codeThe 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.

Kleeja removes every line break and the indentation after it before it uses the blocks. Keep each tag on one line, or attributes on separate lines run together.

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.

styles/bootstrap_black/info.txt
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.

The parent is the only fallback, and it is one level deep: a parent's own 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}:

styles/bootstrap_black/header.html
<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:

styles/my_style/header.html
<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:

config.php
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.

On a live site, clear the cache from Admin → Maintenance → Delete Cache after uploading a changed template. Activating a style clears it automatically.

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.

ActionEffect
SelectWrites style and style_depend_on, then clears the cache. Runs the parent, version and required-plugin checks first.
UploadExtracts an uploaded .zip into styles/.
DownloadFetches and extracts a style from the store, with automatic rollback on failure. Founder accounts only.
Delete folderRemoves 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/.

Terminal
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:

kleeja-4-catalog.json
{
  "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 Kleeja store catalog.

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.

See the plugin documentation for the full hook and path-override example.

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 default style is gone. Set depend_on = bootstrap, or ship every template.
  • Version. Set kleeja_version = 4.0.0 in info.txt and in your catalog entry.
  • Bootstrap. The bootstrap style moved to Bootstrap 5.3 and loads it from includes/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. ISSET and EMPTY combine with NAME without them.
  • PHP in templates. Conditions that ran PHP code are now false. Move that logic into a plugin or a page script.
  • Browser names. flock is gone, and edge is new.
  • Templates that grew features. Compare your header.html and footer.html with the ones in styles/bootstrap/, which now carry the color mode switch and the STYLE_LANG strings.

Troubleshooting

SymptomCause
No Template !The template does not exist in the style or its parent. Check the filename and depend_on.
Template errorA block is not closed, or a tag is in the wrong place. The message names the template and the line.
Edits do nothingThe compiled template is cached. Enable DEV_STAGE or clear the cache.
A {variable} prints literallyFor {lang.*} and {olang.*} the key is missing. For anything else the global does not exist on that page.
A condition is always falseIt contains PHP code, which templates cannot run. The warning names the template and the line.
Text shows &amp;quot;The stored text was encoded twice. Print it with the |e filter.
JavaScript breaks after compilationBraces 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

TagCompiles to
{var}, {a.b}Print a global variable.
{var|e}, |url, |js, |intPrint 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

MethodSignature
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

NameMeaning
config.styleFolder name of the active style.
config.style_depend_onFolder name of the parent style, or empty.
ACP_STYLE_NAMEThe admin theme folder, Damask.
DEV_STAGEDisables template caching and enables debug output.
STOP_TPL_CACHEDisables template caching only.
PATHAbsolute filesystem path to the Kleeja root, with a trailing slash.