๐ Documentation
Klay is a modern content management system built to be flexible, lightweight and easy to use.
- ๐ฌ Klay lives in a single ~380KB php file. No complex system architecture cluttering up your code.
- ๐ Move quickly. Setup data types, eg. pages, blocks, whatever quickly and pull into your existing og new website in less than 5 minutes.
- ๐ Multilingual of course
- ๐ช๐ผ Built for flexible usage: Use headlessly for a JS framework site, build an API or inject CMS powers into an existing system. Power any site, small or large.
- ๐ชฝ No forced structure: Design your own item types, fields and reusable blocks. Organise your project folders any way you want.
- ๐ ๏ธ Built-in routing, controllers, database query builder, filters and events.
- ๐๏ธ File based SQLite database. No need for a separate database server.
- ๐ชด Dummy data seeding quickly allows you to instantly populate test content
- ๐ผ๏ธ Organized media library with drag-and-drop file uploads, tag-based organisation, dominant color calculation and auto-resizing
- โก Lightning quick and easy-to-use CMS panel.
- ๐ Batteries included: Page preview, content backups, output caching, dummy data seed generator, events, filters, plugins, webhooks and more.
- ๐ฒ Climate friendly. A lean, modern tech with less bloat makes the internet run faster and greener.
๐ฅ Motivation
Having worked with websites for many years, I've tried every CMS system under the sun. I've never found one that felt right. WordPress is flexible, but requires third party plugins in order to be more that a Blog system. The admin panel is slow, the database is a mess, you need plugins for stuff the system should do properly itself and the coding experience just feels outdated. Then there's something like Craft CMS, which is stronger in a lot of areas, but also is very opinionated (Twig), restricted and majorly bloated.
I never found the system I always wanted and for years thought of building one myself โ until one day, after being fed up with WordPress, I did exactly that. Not long after I had several client websites running on Klay, and have been able to develop sites quicker and with less hassle and nonsense.
๐ CMS Scope
Klay is by design a Headless CMS system and just that. Klay doesn't force you to do your frontend in one way or another, like many other CMS's do.
There's routing and controllers helping you along, but whether you want to render your frontend with PHP, Nuxt, Svelte, React or something else is entirely up to you.
- For a PHP rendered site you probably want to create some view files for each page and block, and some CSS and Javascript assets placed where you want them.
- For a JS rendered site (eg. Next/Nuxt/Svelte/etc) or maybe a Flutter native app, you probably just want to keep your CMS headless, with a couple of API endpoint routes, so you can fetch content.
- You could even use Klay to setup your own project management or CRM system, with no necessary frontend?
๐ก Concepts
It's all about items, fields and blocks.
For a website you might want something called 'pages'. For an app that could be 'screens'.
You can define any number of item types you want, for instance if you're building a company website, that might be 'pages', 'projects' and 'people'.
Items are by default multiple but can also be singular, for instance if you want something called 'Footer', with fields for the footer content.
Items are populated with Fields, for instance a number field for project year, a relation field, tying related projects together, and perhaps a blocks field, for showing reusable content blocks on pages.
Blocks can be thought of as stackable lego bricks for content. They are reusable and can easily be pieced together to form page content. Most any design for a website or app can be divied into reusable components, if you will, so this what Blocks in Klay represents. Any defined blocks can be selected in a block field.
- Item schemas: These define the types of items you want in your CMS, for instance you could have something called pages for a website, screens for an app or clients and jobs for a CRM/project management system.
- Block schemas: If youre using one or more block fields, youll want a definition of what kind of blocks the user can build with. For instance, you could create Page-hero and Text-image blocks for a website.
โก Quick-start
You can spend a an hour reading all of this documentation, or you can jump in, and try things out yourself.
- ๐ฆ Download and unzip our quick-start Klay project. There's also a minimal version.
- ๐ Open the project folder.
- ๐ฅ๏ธ Run php -S localhost:2147 -t ./ in your terminal to spin up a local server.
- ๐ Open http://localhost:2147 in your browser.
- ๐ Play around, test things and have fun!
For local development you can use the above PHP server, MAMP, Docker or whatever you prefer. You may have an outdated or no local PHP version. Run php -v to check. It's easy to install or update for Mac, Linux or Windows.
โ Requirements
All you need is a basic PHP server with SQLite and the standard GD graphics library. In other words, a totally normal, basic PHP server that you can find for cheap anywhere.
- PHP >= 7.1
- SQLite >= 3.38.0
- GD image processing lib (for resizing images)
- SQLite JSON functions Usually found in SQLite 3.38.0 or newer. Not necessary to run Klay, but needed to enable the Database Query builder JSON functionality.
To check your system run $klay->checkSystemRequirements(); which will output a report.
Almost any server running WordPress, Craft, Laravel, CodeIgniter, etc. will run Klay smoothly.
โ๏ธ Install
Installation is crazy simple, since Klay is one single file, always downloadable from here: Latest version.
- Put Klay.php wherever you want it
- Define the path to Klay: define('KLAY_PATH', 'wherever')
- Require the file: require KLAY_PATH.'Klay.php'
- Run klay: $klay = new Klay()
<?php define('KLAY_PATH', ''); require KLAY_PATH.'Klay.php'; $klay = new Klay([ /* config here */ ]);
..
Installation via Composer and CLI is on its way. If you're interested in this let us know.
๐๏ธ Configuration
Here you define how you want your Klay to run. Below you can see an example:
<?php define('KLAY_PATH', ''); require KLAY_PATH.'Klay.php'; KLAY_PATH.'functions.php'; $klay = new Klay([ 'base_url' => 'https://my-website.com', 'admin_url' => 'admin', 'item_schemas'=> require KLAY_PATH.'item_schemas.php', 'blocks'=> require KLAY_PATH.'blocks.php', 'routes'=> require KLAY_PATH.'routes.php', 'logins' => [ ['username'=>'myuser', 'password'=>'monkeyfunk', 'hint'=>'The password rhymes with honkytonk.'] ], ]);
..
<?php if(!defined('KLAY_PATH')) die('no-access'); $item_schemas = []; $item_schemas[] = [ 'name'=>'page', 'title'=>'Page', 'fields'=>[ ... ] ]; return $item_schemas;
..
<?php if(!defined('KLAY_PATH')) die('no-access'); $blocks = []; $blocks[] = [ 'name'=>'heroblock', 'title'=>'Hero block', 'fields'=>[ ... ] ]; return $blocks;
..
<?php if(!defined('KLAY_PATH')) die('no-access'); $routes = []; $routes['example'] = function($klay){ $klay->output(200, [ 'msg' => 'Hello world ๐ซก' ]); }; $routes['random-dog'] = function($klay){ $klay->output(200, [ 'random_dog' => randomDog(), 'time' => showTime() ]); }; return $routes;
..
<?php if(!defined('KLAY_PATH')) die('no-access'); function showTime() { return date('H:i:s'); } function randomDog(){ $dogs = ['๐', '๐ฉ', '๐ญ']; return $dogs[array_rand($dogs)]; }
..
In the above example, schemas, blocks, routes and functions are placed in separate files, which return their content and are then required into the configuration. With Klay there is no forced project folder structure, and you are free to have everything in one big file, or like above split into separate files.
Config params:
env | [string, default='.env'] | If you want to load config from an .env file for maximum security, this way you can specify the location of such a file. Default is .env inside the KLAY_PATH. |
base_url | [string, required] | The URL home of your app, with no trailing slash please. |
content_url | [string, required] | Full URL where your content is available. Typically same as base_url, but with /content appended. |
content_path | [string, required, default='content'] | Remember to change content_url as well. |
admin_url | [string, required] | This is the URL by which you access the admin panel. Will typically by 'admin' or 'cms', but can be anything you want. |
logins | [array, required] | Here you define authorized users. See authentication. |
show_admin | [boolean, default=false] | If true, the admin will be shown regardless of URL. |
login_disabled | [boolean, optional, default=false] | If this is set to true, there will be no login check. This is handy if you are embedding Klay on top of an existing system that already has the user authenticated. Remember to only allow authenticated users the access, however. |
multilingual | [boolean, optional, default=false] | If this is defined and set to false, multilingual functionality will be disabled. |
page_title | [string, optional] | The HTML title of the admin panel. |
admin_logo | [string, optional] | If you supply a SVG string here, the logo in the admin panel will be this svg. Useful if you want to brand the admin panel. |
brand_color | [string, optional] | If you supply a color here in a web-format, eg. #ff0000 or red, then this will be used in the admin panel. |
item_schemas | [array, optional] | Item type/schema definitions. Read more. |
blocks | [array, optional] | Block type/schema definitions. Read more. |
routes | [array, optional] | URL routes and their logic handlers. Read more. |
controllers | [array, optional] | Route controllers. Read more. |
filters | [array, optional] | Filters modifying data at certain points. Read more. |
events | [array, optional] | Events handlers responding to certain events. Read more. |
admin_css | [string, optional] | Custom CSS used in the admin panel. Mostly useful for custom field, block and rich-text previews. You can use file_get_contents() to load from separate .css file. |
admin_js | [string, optional] | Custom JavaScript used in the admin panel. Mostly useful for custom block and field previews. You can use file_get_contents() to load from separate .js file. |
output_db_warnings | [boolean] | |
mkdir_chmod | [chmod, default=0777] | Set the chmod for created folders, eg. for asset uploads, backups, etc. |
view_path | [string] | If you want Klay to render views |
html_cache_enabled | [boolean, default=true] | Enable/disable the HTML cache. Can also be controlled in the admin panel. |
asset_img_sizes | [array, default=[400]] | Sizes you want images to be copied and resized to. The original is always kept intact. A good value for this is something like [400,800,1200,2400]. |
admin_languages | [array, default=undefined] | If you want the admin panel in more languages than english, supply an array in the following format: [ 'fr'=>KLAY_PATH.'/admin-lang.fr.json', 'es'=>KLAY_PATH.'/admin-lang.es.json' ]. |
autorun | [boolean, default=true] | If set to false, Klay will boot up but not execute any routes or the admin panel before you manually run $klay->run(). This way you can do stuff before full execution. Normally not necessary, but if you want to load Klay from outside and fetch from its database, this is the way to do it. |
block_autopreviews | [boolean, default=false] | If set, and no custom block preview is defined, a block auto-preview will be generated based on fields. |
preview_enabled | [boolean, default=true] | If set to false, the item URL preview functionality will be disabled. |
homescreen_links | [array, default=NULL] | Want to show custom links on the home screen? Fill this array with any number of child arrays in this format: ['label'=>'My link', 'url'=>'https://website.com', 'target'=>'_blank', 'icon'=>'topic']. |
plugins | [array, default=NULL] | Load one or more plugins here. Array should contain a list of paths to plugin folders. See more details in the plugins section. |
max_upload_mb | [int] | If you want a cap on upload sizes you can define it here. |
max_upload_mb_img | [int] | Overrides max_upload_mb for image uploads. |
max_upload_mb_video | [int] | Overrides max_upload_mb for video uploads. |
display_errors | [boolean] | Shows/hides errors and warnings. In production should be set to false. If not defined here, or in .env file, will detect system config and follow that. |
log_errors | [boolean/string] | Logs errors and warnings to a file if a filepath is defined in string format. If set to false will be disabled. If not defined here, or in .env file, will detect system config and follow that. |
error_display_html | [string] | Here you can replace the system error html output shown when display_errors are disabled. |
The following values can be read into the above config from an .env file:
DISPLAY_ERRORS, LOG_ERRORS, BASE_URL, CONTENT_URL, ADMIN_URL, CONTENT_PATH, MAX_UPLOAD_MB, MAX_UPLOAD_MB_IMG, MAX_UPLOAD_MB_VIDEO, MKDIR_CHMOD
๐ Project structure
Klay is built for maximum flexibility. Have your files and folders organised the way you want them.
Some people like to have their item schemas, routes, controllers, etc. placed in separate files, for instance in an app folder, residing somewhere on the server, perhaps above the public webroot. You are totally free to do so and can simply include them directly into the Klay config object.
A typical setup could look like this:
public/index.php | App starting point, config, boot |
app/Klay.php | Klay itself |
app/routes.php | URL routing |
app/functions.php | Helper functions for this and that |
app/item_schemas.php | Item schemas and fields |
app/blocks.php | Blocks and fields |
content/* | This is where your content lives: The SQLite database, asset files, backups, cache items, etc. Path is controlled by the content_path config item. |
Klay.php <- The admin interface (one-file, concatenated)
- your site should load either Klay itself or something like that /content/files/ <- Here, uploads will be placed, mostly images /content/data/ <- Here, cms database content will be placed, json files, one big or many small. And post types, fields, blocks are also saved here
Want it some other way? All in one file or spread out in folders and files? Its completely up to you and controllable in your entry point index.php file.
One important detail is that your index.php should begin with declaration of KLAY_PATH like so:
..
..and that any other included php file begins with:
..
This is for security reasons, preventing direct script access to anything but index.php. And also, KLAY_PATH is used at various places to reference the path of the Klay.php file.
๐ Item schemas
Any content you create in Klay is an item with schema defining nature and its fields. When initialising Klay, you input a param called item_schema containing an array containing child arrays, each representing an item type with the following parameters:
- name (string) (required) the keyword of the type. Used to reference the item type
- title (string) The admin panel title
- singular (boolean) (default:false): If set to true, the admin panel will not allow multiple items, but instead just show the one. Useful for something like globally relevant data, for instance if you want a view showing global fields for a website.
- url_slug (string) The URL for the item type. Examples are (:base)/page/(:slug), (:base)/my-itemtype/(:slug), etc. If set, the system will know the URL of the item using its slug and be able to show links and a preview-panel in the admin panel. If you know the URL you want for an item, fill it in here.
- active (boolean) (default:true): Will disable the block is set to false.
- title_single (string): The admin panel title for single item
- title_plural (string): The admin panel title for multiple items
- icon (string): The icon shown in the admin panel. Define which Google font icon is used. For a star icon write simply 'star' as value.
- multilingual (boolean): If defined and set to false, the type will live across languages
- fields (Array containing child arrays): List of fields to be shown for the item. More about fields here.
Example:
[ [ 'name' => 'page', 'title' => 'Web page', 'title_plural' => 'Web pages', 'icon' => 'globe', 'fields' => [ ... ], ], [ 'name' => 'global_settings', 'singular' => TRUE, 'icon' => 'wrench', 'fields' => [ ... ], ], ]
..
๐ค Fields
Item schemas and blocks can have one or more fields of different types.
All fields have these common attributes:
- type [string] [required] The type of field, see possible options below.
- name [string] [required] the key to be used to retrieve the data
- label [string] The visible label above the field in the admin panel
- help [string] Shows a small info popup button alongside the label in the admin panel with this text.
- default [mixed] The default value when not set)
- group [string] If set, will group the field in a tab by this name. A Google font icon can be used as well, eg. 'icon(star)'
- show_if [string] Show the field conditionally. Can access field attributes using a $ prefix. Strings must be in backticks. Example: 'show_if' => '($text1 == `jo`)'
- hide_if [string] Like 'show_if', but opposite.
- cms_autopreview [boolean, default=false] If set to true, the field will be used in block auto-previews.
Field types
The type parameter can be either of the following:
- text A regular text field
- number Numbers
- min [number] A minimum value for the field
- max [number] A maximum value for the field
- step [number] A value increase, eg. 0.5 or 1
- asset An asset (file) from the asset library, for instance an image or a video.
- richtext A rich text field containing html representing headlines, text paragraphs, links, etc.
- relation A relation to another item.
- types [string] If you want only certain item schema types selectable write them in a comma-seperated string.
- geolocation A geoposition on a map (required mapbox access token defined in settings panel)
- color A colorpicker field.
- date A date on a date select input.
- range A value on a range selector. Same as 'number' but different UI.
- textarea A textarea. The middle ground between a 'text' field and a 'richtext' field. No html, but multiline.
- checkbox A boolean checkbox that can either be enabled or disabled.
- select A select dropdown from which you can select between defined options.
- options [required] An array, associative array or an object with key/value pairs.
- multiple [boolean] Control if more than one can be selected.
- repeater A list of multiple fields.
- min [number] Minimum amount of items
- max [number] Maximum amount of items
- fields [required] An array of sub fields.
- blocks A selection of one or more blocks.
- min [number] Minimum amount of blocks
- max [number] Maximum amount of blocks
- only [array] [default=undefined] If you want only certain blocks selectable in this field, define an array with their names in.
- not [array] [default=undefined] If you want certain blocks not selectable in this field, define an array with their names in.
- note A non-interactive field comprised of a visible note to the user.
- preview A non-interactive preview field. Useful if you want to just show something in the admin panel. The name of the field is matched to a corresponding Field preview.
๐งฑ Blocks
Blocks are reusable modules that can be selected in block fields. A typical use is having an item called 'page', which represents web pages, with a field of type 'block', where the user can compose content in reusable blocks.
Structure is an array containing child arrays, each containing:
name | [string] [required] | The reference key of the block. |
title | [string] | Override name if you want something else as the title of the block. |
fields | [array containing child arrays] | Most of the time you probably want fields in your block. Fields are the same as are used in item schemas. More about fields. |
๐ Routing
When initialising Klay in your PHP, supply a parameter called routes containing an array of keys and functions with ($klay) params.
The keys represent URL routes, and the functions are called when that URL is called.
$routes['/'] = function($klay){ ... // this is the home URL route $routes['route3/jeff'] = 'controller2'; $routes['route3/(:any)/jack'] = 'controller2'; // (:any), (:str) or (:num) can be used to pull parameteres in $routes['POST::/about'] = function($klay){ ... // optionally specify HTTP method $routes['GET/POST::/api/endpoint1'] = function($klay){ ... // optionally specify one of multiple HTTP methods $routes['404'] = function($klay){ ... // 404 page route $routes['api/(:any)'] = function($klay){ ... // Anything after 'api' is matched as long as there is only one slash $routes['api/*'] = function($klay){ ... // Anything after 'api' is matched, and not just somrthing single slashed.
..
- You can use $klay->render_view('page',['data'=>'xxx']) to render a view. The view should then be called page.php and be placed in the folder defined by the view_path parameter supplied to Klay. A path can also be supplied as the param of the render_view() function. The second param of the function is the data you wish to supply the view with.
TODO: Review this section
..
Will return the active route.
..
Checks if $str is the active route.
..
Get the full active route path.
..
Get all URL route params.
..
Get URL route param at specific slot, starting with the first at 1.
..
Shortcut function checking if route path matches criteria.
..
Shortcut function checking if route param matches criteria.
๐น๏ธ Controllers
When initialising Klay in your PHP, supply a parameter called controllers containing an array of keys and functions.
You can execute code directly in your route callback functions, as shown in the Routing section, but if you want to do the same thing more than once, then you should consider using a controller and invoking that from both routes:
'myController' => function($klay){ /* ... */ }
..
You can then in your routes point to your controller:
$routes['page/(:any)'] = 'myController'; $routes['page/(:any)'] = 'controller::myController'; // same as above, only optionally prefixed with 'controller::' for visual aid // and if you want to supply the controller with custom params you can also use these syntaxes: $routes['page/(:any)'] = [ 'myController', [ 'number'=>22 ] ]; $routes['page/(:any)'] = [ 'controller::myController', [ 'number'=>22 ] ]; $routes['page/(:any)'] = [ 'controller'=>'pageSingle', 'params'=>[ 'number'=>22 ] ];
..
Inside your controller, you probably want to access stuff like URL params. In the above example we are loading a page, and using the (:any) route param, we will probably want the content of that param. For that you could use $klay->get_rute_param(1) as defined in the Routing section.
..
Will get the active controller in action.
..
Checks if the current controller is the one you supply in the $str param.
๐๏ธ Database
Klay uses SQLite, which has the advantages of common relational databases, is performant while being file-based. This means it lives and runs right besides your files, which makes backups a snap, and means you don't need multiple servers.
You don't need to create your database or build the structure, Klay does this automatically as soon as you save something to the database.
Each item in the database has a few common fields which are type-agnostic, and then it has a JSON field called fields, which holds a JSON structure for any combination of fields you may have stored on the item.
Using SQLite, JSON fields can be queried, even when deeply nested, making SQLite a great combination relational/nonrelational database.
For manual database administration you may be familiar with PHPMyAdmin. If you need something like this, you can use the excellent Adminer library, which is a single file SQL editor. Download a version optimized for use with Klay here. For security reasons you should change the filename to something not easily guessed, eg. db9273.php, then call the file to open the editor, and remember to delete it directly afterwards โ or uncomment the die() comment at the top of the file.
Read more about interacting with the database here.
๐ Authentication
A lot of systems store admin users in a database and allow them to restore forgotten passwords via email. In order to make Klay as lean and simple as possible as well as to tighten security, instead we are defining admin users by code in the logins config param when initialising Klay:
$klay = new Klay([ ... 'logins' => [ ['username'=>'user1', 'name'=>'User 1', 'password'=>'rawjerry', 'hint'=>'The password rhymes with strawberry.' 'permissions'=>'anything', ], ['username'=>'user2', 'password'=>getenv('user2_pass'), 'permisions'=>'anything|!create:page' ], ['username'=>'user3', 'password'=>getenv('user3_pass'), 'permissions'=>'create:page|!delete:page' ] ], ]);
..
Security info: Consider storing your passwords in encrypted form inside a public-access protected .env file, for increased security. You can use Encrypt online to easily encrypt any string, so password are never accessible even if server file access is somehow obtained. By placing an .env file next to Klay.php, (or at a location referenced in your configuration 'env' parameter, all of its key/value pairs will be obtainable using the native PHP getenv() method.)
Users can be assigned individual permissions. The format is a string, with a | delimiter. Anything with a ! infront means permission is NOT granted. If anything is in the list, the user can do literally anything (except anything defined with a ! in front).
Permission options: create:{TYPE} and delete:{TYPE}
If an admin user forgots their password it's a simple matter of asking the webmaster/sysadmin to look it up for them and/or create a new one.
..
Checks if an admin user is logged in.
..
Same as getLoggedInUser() but strips out sensitive password/hint details.
..
Checks if the logged in user has permission. Input is a string, eg. 'create:{item_type}', 'delete:{item_type}', 'anything'.
๐๏ธ Database queries
Klay uses a SQLite database, because it's the perfect combination of convenience and performance.
Klay has a built-in query builder for easy database interaction:
$inserted = $klay->db('page') ->insert([ 'title'=>'Welcome' ]); // if success: $inserted = { uid:'xxx', title:'Welcome', ... created_at:'yyyy-mm-dd hh:mm:ss' } // if failure: $inserted = FALSE
..
$allPages = $klay->db('page')->all(); $publishedPages = $klay->db('page')->published()->all(); // returns an array of items: [ {...}, {...}, {...} ] $aboutPage = $klay->db('page')->slug('about')->one(); $contactPage = $klay->db('page')->uid('u3b4k5')->one(); // returns single item objects: {...}
..
$updated = $klay->db('page') ->where('slug','in','j2hh3g4,u3hy2gv,oi3h4b') ->update([ 'published'=>1 ]); // returns $affectedRowCount / false
..
$done = $klay->db('page') ->replace([ 'uid'=>'', 'title'=>'Welcome' ]); // returns $affectedRowCount / false
..
$inserted = $klay->db('page') ->upsert([ 'title'=>'New page' ]); // returns $affectedRowCount / false $updated = $klay->db('page') ->upsert([ 'uid'=>$inserted->uid, 'title'=>'New page with new title' ]); // returns $affectedRowCount / false
..
$done = $klay->db('page') ->uid('x8h2y5t') ->delete(); // returns $affectedRowCount / false
..
//groups,and,or //test //sql/params
..
Method | Params | Return | Notes |
---|---|---|---|
db() | $itemtype (eg. 'page') | $query instance | The database table to pull from. |
select() | $columns (string/array), eg. 'title,slug', ['title','slug'] or default * | $query instance | Which data columns to fetch. |
where() | ($column, $value) or ($column, $operator, $value) | $query instance | If used with 2 columns, the operator is assumed to be =. Operators list: =, !=, >, <, !>, !<, >=, <=, like, not like, in, not in |
limit() | (int) | $query instance | Set the limitt. |
offset() | (int) | $query instance | Set the offset. |
order() | (string) | $query instance | Sets the ordering. Examples: created_at DESC, title ASC, random, created_at DESC, title ASC, ORDER BY JSON_EXTRACT(fields, '$.date') DESC |
andWhere() | same as where() | $query instance | Like where() but with AND in front. |
orWhere() | same as where() | Like where() but with OR in front. | |
published() | - | $query instance | Shortcut for: where('published',1) |
unpublished() | - | $query instance | Shortcut for: group()->where('published','is',NULL)->orWhere('published','!=',1)->groupend() |
uid() | $uid | $query instance | Shortcut for: where('uid',$uid) |
tagged() | $tag (string) | $query instance | Adds where() criteras so only items with this tag are queried. |
language() | $language | $query instance | Shortcut for: where('language',$language), 'en' for english, 'fr' for french, etc. |
and() | - | $query instance | Will put an AND in your query. Klay will automatically assume AND between where pieces, so this is not needed, but some prefer to use it to let the code explain itself. |
or() | - | $query instance | Will put an OR in your query. |
group() | - | $query instance | Will put an ( in your query, starting a logic grouping. |
groupend() | - | $query instance | Will put an ) in your query, ending a logic grouping. |
sql() | $sql (string) | $query instance | $query instance |
params() | (array) | $query instance | If you want to write your own custom SQL string you can use this. You make have a complex string that cannot be build with the query builder, in that case this is your fallback. |
withRelated() | - | $query instance | You may have fields referencing assets or other items. With this method in your chain, those items will be fetched and nested into your output. |
raw() | - | $query instance | Klay automatically parses JSON fields and finds and appends asset metadata. If you want just the raw database output you can use this. |
one() | - | $item (object) | Will execute and fetch 1 item matching criterias, and will output the single item object. This is an action method, meaning it should be called at the end of your chain. |
all() | - | $items (array) | Will execute and fetch all items matching criterias. Will output an array of item objects. This is an action method, meaning it should be called at the end of your chain. |
insert() | (array/object) | $item (object) or false on failure | This is an action method, meaning it should be called at the end of your chain. |
update() | (array/object) | (int) $affectedRowCount or false on failure | This is an action method, meaning it should be called at the end of your chain. |
replace() | (array/object) | (int) $affectedRowCount or false on failure | If an existing item is found, it will be replaced entirely. If you only want to update certain parts of the item, use update() or upsert() instead. This is an action method, meaning it should be called at the end of your chain. |
upsert() | (array/object) | (int) $affectedRowCount or false on failure | Will perform an insert if no uid is found in the input array/object, or a update if one is found referencing an existing item. This is an action method, meaning it should be called at the end of your chain. |
delete() | - | (int) $affectedRowCount or false on failure | Will delete an item. Can be used in combination with preceding where() and limit(). This is an action method, meaning it should be called at the end of your chain. |
test() | - | HTML output | If you want to test-drive without actually performing your query you can use this to output the SQL and params built by your query. This is an action method, meaning it should be called at the end of your chain. Will output HTML to the view using the dd() function, ending all other subsequent code execution. |
If your SQLite version is below 3.38.0, or is built without JSON functions, then you can still use it, but JSON functions will be disabled. To check for this you can run $klay->checkSystemRequirements();.
Database security
Klay provides SQL Injection Protection, since database calls are built on the PDO interface using prepared statements. Notice, however, that protection does not cover raw SQL using sql(), meaning you will have to manually take care of escaping these, or use ? and params() โ this is especially important if you are using input variables such as coming from a HTTP request. Input can be escaped manually using $escaped_input = $klay->db_esc($user_input).
..
Returns the $input but escaped and ready for database query usage.
..
Will find references to other items or assets and load them into the object. input is single item or array of items.
..
Used to output entire database, for dev/testing. Useful, as we often have no phpmyadmin og adminer access on server. Will return an array of tables and rows. Use with $klay->output(200,$klay->db_inspect_all()) to output to the browser. Using a JSON browser plugin is highly reccomended.
๐๏ธ Assets
You can upload any type of file to the asset library.
If you upload an image, Klay will automatically clone web-friendly resized versions. The asset_img_sizes config array controls which sizes you want these in, apart from the original and an obligatory 400px version.
If you want custom fields for asset items, that is possible. Just create an item schema for asset, and it will be visible in the asset library.
๐๏ธ Filters
When initialising Klay in your PHP, supply a parameter called filters, which should be an array containing key => function pairs. The filter function has parameters ($klay, $data, $params) and should return $data, which you can modify before returning it. The contents of the $params object is different per filter type.
Filters available:
- item:save applied before saving an item. [params: $itemtype]
- item:generated applied before saving a dummy generated item using the seeder. [params: $itemtype]
๐ข Events
When initialising Klay in your PHP you can also supply a parameter called events, containing an array of keys and callback functions.
Functions have the following params: ($klay, $data). The content of the data param is an object which contains relevant data depending on the event type.
Events available:
- item:saved is called when an item was saved. [params: $itemtype, $item]
- item:deleted is called when an item was deleted. [params: $itemtype, $uid]
๐ฑ Seeding
Seeding is super useful when testing your site/app. In the admin panel under settings you will find a screen from which you can generate any number of dummy items. Even dummy images can be generated. And deleted. They are all flagged as dummy-generated and easy to delete from this screen without real content being affected.
Want to test how your site looks and performs with thousands of blog posts? This is the way to do it in seconds.
$itemtype = $klay->get_item_type('page'); $dummy_page = $klay->generate_dummy_item($itemtype); $saved = $this->db_save_item('page', $dummy_page);
..
The above example shows how you can generate an item and save it in code.
If you want to customize seeding for specific items you can apply the item:generated filter.
๐ Multilingual
Have your content in any number of languages you want.
In the admin panel under settings you can define th enumber of languages you want to use, and which is the primary language.
With more than one language active, content can be switched between languages. If you have eg. a 'page' type in a certain language, you can one-click duplicate it to any missing language version and then translate the text.
In order for Klay to be multilingual, you have to have multiple languages defined in the admin panel. Remember that individual item types can have multilingual disabled.
Items across languages are technically separate, but related items, tied together by their language_grouping attribute.
If you want to get language version of an item in another language, this would be the way to do it:
$page_fr = $klay->db('page') ->where('language_grouping', $page_en->language_grouping) ->language('fr') ->language('fr') ->one();
..
๐ฌ Admin panel language
The admin panel by default is in English, but if you want to add languages for admin users to select between you can add admin translation file(s). Below you'll find a number of translations.
- English (Already baked into Klay, so no need to add this. Just for reference as a good translation starting point.)
- Spanish
- French
- Danish
- German
- Portuguese
If you need another language you can run the english version through here for a good translated starting point. You can also write in and ask for any languages you might need, and we'll happily generate them and publish them here for use by the community. ๐๐ผ
โฎ๏ธ Backups
From the admin panel you will find the backup screen. Here you can see the current amount of data, you can create a new backup with/without asset files and you can see, download and delete previous backups.
Most CMS systems and hosting providers fail to make it easy for you to do backups periodically. Backups are vital, which is why they are easily made, zipped and downloaded directly from the admin panel.
๐ฆ Cache
Using Klay's cache you can save/retrieve any kind of data, which is stored in flatfile JSON on the server, making it super quick to load for smaller datasets.
You may have a complex combination of 4 database queries, with some extra filtering and sorting done afterwards. If you save the result of that in the cache and load from there subsequently you can serve the data a lot quicker with less processing needed.
Anything cached is identified by the $key parameter, which can be anything unique you think is fitting.
Combining events (on_item_saved, for instance) and the cache can work wonders.
$klay->cache_put($key, $data); // save data to the cache $klay->cache_get($key); // fetch data from the cache $klay->cache_wipe($key); // delete data from the cache
..
๐งพ HTML output cache
With URL's being used as identifiers, you can also cache the entire HTML response for a request. And serve it on following requests, saving database calls and processing. This will make an already fast system even faster.
$klay->html_cache_put($url, $html); // save to the cache $klay->html_cache_get($url); // fetch from the cache $klay->html_cache_wipe($url); // delete from the cache $klay->html_cache_wipe_from_slug($slug); // fetch based on slug, not full url $klay->html_cache_wipe_all(); // delete the entire html cache
..
Using the item:saved and item:deleted events you can easily run $klay->html_cache_put() or $klay->html_cache_wipe() to refresh the HTML output cache whenever data updates.
๐ Requests and responses
..
For easily retrieving GET og POST input data. Will also run the input through a sanitize filter for increased security. And supports multipart_data if the input has data placed in a 'multipart_data' key. And supports data set as $_FILES. If you want data from a other types of HTTP requests, eg. DELETE/PUT/PATCH you will have to retrieve that manually.
..
Will make Klay output JSON with a HTTP code
..
Will render a view. See more at Routes.
..
Same as render_view(), but will check the HTML output cache and render from that. Will save to cache if not already cached. See more at Output cache
..
Will output the 404 error page as defined in $routes.
..
'Drop-and-die': Will output any data you put into it to the view as well as relevant debug data.
..
Shortcut to output error for missing input params. Will output a HTTP-400 with data: status='error', error='missing-param' and missing-param=$paramName).
๐ ๏ธ Methods
Klay offers a a number of functions and definitions that you can use.
..
Will give you the base url of your app/website.
..
Will check and report if your system meets requirements. $forceOutput param triggers a dd() output of the report and can be set to false if you want the output in data instead.
..
Will output the system error page. If a config error_display_html is declared will use that.
Image functions
..
Creates a GD image ressource, format is jpg/png/webp.
..
Sharpens a GD image ressource.
..
Resizes a GD image ressource, params are ($image, $new_width).
..
Gets dominant color from a GD image ressource.
..
Will output an image, quality is 1-10, if no filepath, will stream to browser.
..
Returns an array of the image sizes defined for you site/app.
..
Will check if webp is supported by the users browser and return the alt format instead if not.
Ulitity functions
..
Returns a boolean from input such as true/'true'/1/'1'/false/'false'/0/'0'.
..
Will make a string URL slug-able. Lowercase letters and hypens.
..
Will retrieve a random item from an array.
..
Will merge two objects.
..
Will clamp the value inside the min/max.
..
Will generate a uid, consisting of alphanumeric characters.
..
Generate a dummy image for the item type. Input can be an item type name string, or an item type object. Will output a dummy item based on the item type fields, with lorem ipsum text, selected values, plausible relations and assets.
..
Will return the item type object from its string name.
..
Will check if the item type is singular.
..
If the item type has a URL scheme, will return the URL for the item.
..
Manually trigger a webhook URL with data.
..
Manually trigget the 'manual_publish' webhook, called when the user clicks the 'Publish' button in the admin panel.
..
Will retrieve a random asset. Used by the dummy item generator.
..
Will retrive a random asset uid using get_random_asset(). Used by the dummy item generator.
..
Will generate dummy text. Used by the dummy item seeder.
๐ช Admin customization
Apart from defining your item types, blocks and fields you also have a lot more control over functionality and representation in your admin panel.
- Config item admin_js can contain a string of JS code loaded into the admin panel.
- Config item admin_css can contain a string of CSS code loaded into the admin panel.
- Tip: Use php file_get_contents() if you want to load from separate files.
Admin JS
- Rich text modifiers are used to apply your own custom classes to HTML elements. In your frontend you may have button classes you want to be able to put on a tags, for instance. Use klay.addRichTextModifier() to add one or more of these.
- field_previews are used to render preview type field in the admin panel. This field is visual-only and can use data in other fields. You could show a graph preview of another field value, for instance.
- block_previews Here you define functions that output HTML for mini-previews of block fields. These previews can then be styled in the admin_css file.
- block_custom_btns When defined, these produce custom actions buttons visible for each block in block fields. They can be used to makes it easy for users to eg. change a value inside block without having to click into it.
Within your admin JS, you can use jQuery and Vue, since they are already loaded in for you.
You can also use these premade UI functions:
- Klay.msg(input)
string / { text: string, dimClose:bool, closeBtn:bool, selfDestructing:bool, okText:string } - Klay.prompt(input)
{ text:string, dimClose:bool, closeBtn:bool, selfDestructing:bool, okText:string,cancelText:string, onOkay:function(), onCancel:function() } - Klay.confirm(input)
{ text:string, dimClose:bool, closeBtn:bool, selfDestructing:bool, okText:string,cancelText:string, onOkay:function(), onCancel:function() } - Klay.spinner(input)
input (boolean/object) If input is a boolean it will toggle the spinner visibility. If input is an object, you can set 'active' true/false as well as 'blocking' true/false to have a click blocking backdrop and a 'msg' (string) of text shown under the spinner. - Klay.toast(input)
{ text, title, type:string (succes/info/warning/error), icon:string (info/etc - see icons under item schemas) closeButton:boolean, closeDuration:int (ms), position: string(top-right,top-left,bottom-right,bottom-left), preventDuplicates:bool, } - klay.addCustomLink({...})
Will add a link in the admin panel. Params:
- position (string) Controls where it is shown, eg. 'menubar'.
- label (string)
- url (string)
- target (string) Use '_blank' to open link in new window
- onClick (function) If you want to perform an action on click - klay.addCustomView({...})
Will add a custom view in the admin panel. Params:
- slug (string) The hash URL for the view, eg. 'myview'
- html (string/function) A string of HTML or a function returning a string og HTML
- enter (function) Called when view is ready
- exit (function) Called just before view is removed - klay.addRichTextModifier(input)
- Input (object/array) An object or an array of objects if you want to add several with one call. Item params:
- class (string) The class to add to the element
- label (string) The label seen in the admin editor
- tags (string) Comma-separated list of HTML tags the modifier can be used with
- group (string) When modifiers have the same group only one of them can be used at a time
- preview_css (string) Inline css for preview in the admin editor.
Admin CSS
Used for admin block previews. These are small previews for quick visual reference. You can style across blocks, target specific blocks or specific block preview classes you may have added in admin.js.
Example
$klay = new Klay([ ... 'admin_css' => file_get_contents(KLAY_PATH.'/admin.css'); 'admin_js' => file_get_contents(KLAY_PATH.'/admin.js'); ]);
..
// Enables toggling of CSS classes in specified HTML elements. When grouped, only one of the values can be selected, so they are never combined. klay.addRichTextModifier([ { class:'btn1', label:'Button: Red', tags:'a,h2,h4', group:'g434', preview_css:'background-color:red !important; color:white !important; padding: .5em 1.5em;' }, { class:'btn2', label:'Button: Green', tags:'a,h1,h3', group:'g434', preview_css:'background-color:green !important; color:white !important; padding: .5em 1.5em;' }, { class:'btn3', label:'Button: Blue', tags:'a', group:'g434', preview_css:'background-color:blue !important; color:white !important; padding: .5em 1.5em;' }, ]) // Dynamic classes for all blocks klay.block_previews.cls = (x)=>{ let cls = [] if( x.data['theme'] ) cls.push('theme-'+x.data['theme']) if( isTrue(x.data['flipped']) ) cls.push('is-flipped') return cls } // Dynamic css for all blocks klay.block_previews.css = (x)=>{ let css = {} return css } // Dynamic classes for 'myblock' blocks klay.block_previews.cls_myblock = (x)=>{ return ['class1','class2'] } // Dynamic css for 'myblock' blocks klay.block_previews.css_myblock = (x)=>{ return { color:'blue' } } // Dynamic HTML content before/after standard HTML for 'myblock' blocks klay.block_previews.before_myblock = (x)=>{ return `Myblock before` } klay.block_previews.after_myblock = (x)=>{ return `Myblock after` } // Custom HTML content for 'myblock' blocks klay.block_previews.myblock = (x)=>{ return `${x.asset_preview_or(x.data.img, '` } klay.block_custom_btns.myblock = [ { icon:'star', tooltip:'Myblock custom button', onClick(block){ console.log({ msg:'My block custom button clicked', block }) let layout = block.data.layout block.data.layout = (layout == 'a') = 'b' : 'a' klay.toast({ text:'Block layout changed' }) }, }, { icon:'bolt', tooltip:'Myblock custom button 2', onClick(block){ console.log({ msg:'My block custom button 2 clicked', block }) block.data.text = 'Generic lorem ipsum text' klay.toast({ text:'Block text changed' }) }, }, ]No image selected, click to select one...')}${(x.data.text ? x.data.text : 'No text defined.')}
..
/* styling for specific block type previews */ .blockpreview.type-myblock { background-color: black; color: white; } .blockpreview.type-myotherblock { background-color: white; color: black; } /* styling for all block previews */ .blockpreview { ... } /* styling for all block, with generic cross-block custom class added in admin.js */ .blockpreview.theme-light { ... }
..
๐งฉ Plugins
Klay is built to be extendable through plugins.
In your config, you can define an array of which plugins to load. Each item here refers to a folder containing the plugin files.
Plugins consist of a PHP and optionally CSS and JS files, all should be named the same as the plugin folder:
- myPlugin/myPlugin.php (required)
- myPlugin/myPlugin.js (optional)
- myPlugin/myPlugin.css (optional)
The PHP file must return an array, which can contain any of these config attributes, which will be activated when Klay loads:
routes,blocks,item_schemas,filters,events,controllers.
If you want the plugin to supply any functions as well, you can define these before returning the plugin array.
As for the plugin CSS and JS files, they must follow the conventions defined under admin customization.
In summation, plugins can:
- Add item schemas and blocks
- Add routes and controllers
- Setup filters and events
- Add buttons in the menu bar, using JS addCustomLink()
- Add custom views, using JS addCustomView()
- Trigger JS UI elements, such as alert messages, prompts, confirms, spinners and toasts
Let's say you want to build a plugin for sending emails? You could setup an item type called email with a text field called sent. Then you could create a function called createEmail([]) which adds a new item. And an api/send-unsent-emails route that takes all created emails with an empty sent field, sends them via eg. Mailgun SMTP CURL and updates each database item so the sent field now has the date/time. And this way you only send emails that have not been sent. ๐ Voila!
If you've built a plugin you want to share with the community do let us know.
๐ต PHP based frontend
Since Klay is a headless CMS, you have freedom to choose the frontend you prefer.
A simple setup would be using PHP and the same server to render the frontend. You can use Klay's built-in router to show your content fresh from the database using the database fetching methods. And you can use controllers, setup views and helper functions same as with most MVC systems such as Laravel, Codeigniter, etc. but without some forced structure.
This setup is old-school, tried and tested, simple and works great. Performance-wise your site will be very fast as well, since Klay is so light-weight and performant. And if you want an added speed-boost, you can use the built-in caching to cache database queries, or even the output cache to completely cache the output html, making your site essentially a static-site serving prerendered HTML. It doesn't get much faster than that.
๐ก JS based frontend
Since Klay is a headless CMS, you have freedom to choose the frontend you prefer.
If you want to build your frontend in JS then Klay is a perfect fit. You could be using Vue, React, Svelte, Nuxt, Next, Sveltekit, Astro or anything else.
The typical setup is that you have your Klay CMS on a PHP server with a webhook calling your frontend hosting, which could be anything from Vercel, Cloudflare, Netlify, Heroku, Digital Ocean or anything else you might be using.
When the webhook is called will probably want to configure your setup so that it triggers a new build where your JS frontend pulls in CMS content and builds with that.
Essentially it works the same as with any other headless CMS.
๐ฃ Other language frontends
If you want your frontend in another language, you sure can. Ruby, Python, etc. โ it's just a matter of connecting to Klay and pulling in your content.
โ๏ธ Updating Klay
You can always find the latest (and previous) versions of Klay on the download page.
Updating your version is a small matter of replacing your existing Klay.php file with the new version.
Breaking changes: Please check the release log to see if you need to update anything in your code. Any version released after yours could contain breaking changes.
Download via Composer and CLI is on its way. If you're interested in this let us know.