Kleeja Logo
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 — the parser strips it.

This page documents the style system in Kleeja 3.x. The template engine lives in includes/style.php and the style manager in includes/adm/m_styles.php.

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 a page calls $tpl->display('login'), the engine resolves login.html against the active style, falls back to the parent or to default, 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/
    └── images/
A style that inherits from another needs only the templates it changes. styles/bootstrap_black/ ships a single header.html plus a stylesheet, 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.
A missing template is fatal. When the engine cannot resolve a name in the active style, its parent, or default, it stops the request with No Template !.

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 = 3.2.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 parser rewrites a handful of pseudo-tags into PHP. Everything else passes through untouched.

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

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>
Nested loops share one row variable. An inner <LOOP> overwrites the outer row, so {{field}} after the inner loop closes no longer refers to the outer one. Flatten the data in PHP instead of nesting.

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.
ANDJoins the previous test with &&.
ORJoins the previous test with ||.
ISSETWraps the named variable in isset().
EMPTYWraps the named variable in empty().

Comparisons accept both symbols and words:

OperatorWord form
==eq
!=neq
<lt
>gt
<=lte
>=gte
admin/Masmak/admin_header.html
<IF NAME="go_to==start" AND="" ISSET="go_menu_html">
    <nav>{go_menu_html}</nav>
</IF>

Attributes always compile in the order NAME, LOOP, AND, OR, ISSET, EMPTY. Pass an empty AND="" to place the && operator before an ISSET or EMPTY test, as above.

<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>
A comparison value that contains a digit is emitted unquoted, so <IF NAME="style == bootstrap4"> compiles to invalid PHP. Compare against pure text or pure numbers, and move anything else into a variable you set in PHP.

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}}" (go_current=={{name}}?class="current":)>{{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="ie,opera">
    <link rel="stylesheet" href="{THIS_STYLE_PATH}css/legacy.css" />
</IS_BROWSER>

Recognised names are ie, firefox, safari, chrome, flock, opera, konqueror, mozilla, webkit and mobile. Append a version number to ie or firefox to target it, as in ie6.

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.

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 parser strips PHP from templates, so <?php ?>, <? ?>, <% %> and <script language="php"> blocks never survive compilation. Wrap markup in <IGNORE> to hide it from the parser entirely — 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.
{DEFAULT_PATH_ADMIN}URL of the default admin theme.
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 — exactly what bootstrap_black does.

Page and user

VariableContents
{title}Page title.
{dir}Text direction, rtl or ltr, from the active language.
{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 default avatar.
{userinfo.*}The logged-in user's row.
{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.
{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, shown to administrators.
{run_queue}The queue pixel. Keep it in footer.html.
{googleanalytics}Analytics snippet, when configured.
{go_back_browser}Localised "go back" label.
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 or loops. Blocks are delimited by HTML comments.

styles/my_style/up_boxes.html
<!-- BEGIN image -->
    <table class="up_box_input">
        <tr>
            <td class="btitle">{b_title}</td>
            <td><textarea readonly rows="1">{b_url_link}</textarea></td>
        </tr>
    </table>
<!-- 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.

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 = 1.0
kleeja_version = 2.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 default style

When the style has no parent and is not default itself, the same filename under styles/default/.

The fallbacks are mutually exclusive. A style that declares depend_on falls back only to its parent, never to default. Make sure the parent is complete, or ship the missing templates 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
<link href="{STYLE_PATH}css/bootstrap.min.css" rel="stylesheet">
<link href="{STYLE_PATH}css/stylesheet.css" rel="stylesheet">
<link href="{THIS_STYLE_PATH}css/stylesheet.css" rel="stylesheet">

Assets and direction

Keep stylesheets in css/ and graphics in images/, and reference them through a path variable so the style keeps working after it is renamed or inherited.

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 a direction-specific stylesheet:

styles/my_style/header.html
<html dir="{dir}">
...
<IF NAME="lang.DIR==ltr">
    <link rel="stylesheet" href="{STYLE_PATH}css/ltr.css" />
</IF>

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 disables template caching, reports all PHP errors and adds a debug link to the footer. 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 → Repair → Clear 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. Opening that 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 remote catalog.

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 remote catalog, 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/. Styles listed in the public catalog install with one click:

The Kleeja store catalog.

The admin theme

The admin panel uses its own theme, fixed to admin/Masmak/, 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 — the bundled zaki_admin_theme does exactly that from the end_common hook.

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

Troubleshooting

SymptomCause
No Template !The template does not exist in the style, its parent, or default. Check the filename and the depend_on chain.
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 blank page after editing a conditionA comparison value containing a digit compiled to invalid PHP. Quote-free values must be pure text or pure numbers.
JavaScript breaks after compilationBraces in the script looked like template tags. Wrap the block in <IGNORE>.
Scheduled cleanup stopped{run_queue} was removed from footer.html.

Reference

Tags

TagCompiles to
{var}, {a.b}Print a global variable.
{{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 parser.
</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|falseincludes/functions_display.php
get_up_tpl_box()get_up_tpl_box(string $box_name, array $extra = []): stringincludes/functions_display.php
is_browser()is_browser(string $b): boolincludes/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, Masmak.
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.