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 — the parser strips it.
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/
└── 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/
styles/bootstrap_black/ ships a single header.html plus a stylesheet, 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. |
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.
#
# 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
| 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 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.
<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.
<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>
<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.
<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. |
AND | Joins the previous test with &&. |
OR | Joins the previous test with ||. |
ISSET | Wraps the named variable in isset(). |
EMPTY | Wraps the named variable in empty(). |
Comparisons accept both symbols and words:
| Operator | Word form |
|---|---|
== | eq |
!= | neq |
< | lt |
> | gt |
<= | lte |
>= | gte |
<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>.
<IF NAME="current_smt == users">
...
<ELSEIF NAME="current_smt == team">
...
</IF>
<UNLESS NAME="no_results">
<p>{lang.RESULTS}</p>
</UNLESS>
<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.
(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.
<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.
<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 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.
<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. |
{DEFAULT_PATH_ADMIN} | URL of the default admin theme. |
{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
| Variable | Contents |
|---|---|
{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. |
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. |
{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.
<!-- 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 -->
| 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.
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 = 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/.
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}:
<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:
<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:
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.
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.
| 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 remote catalog, 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/. Styles listed in the public catalog install with one click:
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.
Troubleshooting
| Symptom | Cause |
|---|---|
No Template ! | The template does not exist in the style, its parent, or default. Check the filename and the depend_on chain. |
| 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 blank page after editing a condition | A comparison value containing a digit compiled to invalid PHP. Quote-free values must be pure text or pure numbers. |
| JavaScript breaks after compilation | Braces in the script looked like template tags. Wrap the block in <IGNORE>. |
| Scheduled cleanup stopped | {run_queue} was removed from footer.html. |
Reference
Tags
| Tag | Compiles 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
| 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, Masmak. |
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. |