novaris-dev / framework
Novaris Framework: The core foundation of the Novaris Content Management System (CMS), providing a robust and flexible architecture designed to power dynamic content-driven websites
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.9
- intervention/image: ^3
- league/commonmark: ^2.10.1
- league/config: ^1.2
- symfony/http-foundation: ^6.0
- symfony/var-dumper: ^6.0
- symfony/yaml: ^6.0
- vlucas/phpdotenv: ^5.6
Requires (Dev)
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-develop
- v1.1.9
- v1.1.8
- v1.1.7
- v1.1.6
- v1.1.5
- v1.1.4
- v1.1.3
- v1.1.2
- v1.1.1
- v1.1.0
- v1.0.9
- v1.0.8
- v1.0.7
- v1.0.6
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-release/1.1.9
- dev-fix/svgz-encoding
- dev-release/1.1.8
- dev-fix/media-svg-headers
- dev-release/1.1.7
- dev-fix/cache-export-hardening
- dev-release/1.1.6
- dev-ci/theme-check
- dev-fix/image-names-search-embeds
- dev-release/1.1.5
- dev-feature/post-formats
- dev-release/1.1.4
- dev-fix/export-feeds-empty-terms-upscale
- dev-release/1.1.3
- dev-feature/embeds
- dev-release/1.2.0
- dev-release/1.1.0
- dev-feature/social-tag
- dev-fix/user-path-urls
- dev-release/1.0.8
- dev-fix/serve-user-media
- dev-release/1.0.7
- dev-feat/user-path
- dev-release/1.0.6
- dev-fix/site-config-export-watch
- dev-copilot/repository-file-organization-overview
This package is auto-updated.
Last update: 2026-11-10 18:32:10 UTC
README
Novaris is a lightweight PHP framework designed to provide the core foundation for building flat-file, content-driven websites and applications.
It provides the underlying architecture for the Novaris Content Management System (CMS), including content management, routing, caching, configuration, templates, service providers, dependency injection, and other tools needed to build dynamic websites without requiring a traditional database.
Requirements
- PHP 8.1 or higher
- Composer
PHP 8.2 or newer is recommended for development.
Installation
Install Novaris using Composer:
composer require novaris-dev/framework
Content folder
A site's content and media live in its user/ folder, in user/content and user/media.
To use another folder for a while, such as a clone of theme-test-data when testing a theme, add NOVARIS_USER_PATH to the site's .env:
NOVARIS_USER_PATH=../theme-test-data
The folder needs content and media folders, like user/. A relative path is relative to the site root. Remove the line to go back to the site's own user/ folder.
Content links to images as /user/media/..., and Novaris rewrites those links to wherever the folder is:
- A folder inside the site, such as
NOVARIS_USER_PATH="theme-test-data": images link to/theme-test-data/media/..., so the web server serves them directly. Nothing else to set up. - A folder outside the site, such as
../theme-test-data: images keep their/user/media/...links and Novaris serves them, which works with servers that send missing files toindex.php. PHP's built-in server doesn't, so usevendor/bin/novaris servewith it.
Resized images are saved next to the originals, so they are written into that folder.
Serving media safely
An SVG can contain a script, and if someone opens the file directly, the script runs as part of your site. The same goes for HTML and XML files. When Novaris serves a media file itself, it sends headers that stop this: X-Content-Type-Options: nosniff on every file and, on SVG, HTML and XML files, a Content-Security-Policy with sandbox. SVGs still show, inline and in <img> tags. Only their scripts are blocked.
Most of the time the web server serves /user/media directly, without Novaris, so add the same headers there. With nginx, in the site's server block:
location ~* ^/user/media/.+\.svgz$ { add_header Content-Encoding "gzip" always; add_header X-Content-Type-Options "nosniff" always; add_header Content-Security-Policy "default-src 'none'; img-src data:; style-src 'unsafe-inline'; sandbox" always; } location ~* ^/user/media/.+\.(svg|html?|xhtml|xml)$ { add_header X-Content-Type-Options "nosniff" always; add_header Content-Security-Policy "default-src 'none'; img-src data:; style-src 'unsafe-inline'; sandbox" always; }
The .svgz block comes first, because nginx uses the first matching one. It tells the browser that the file is gzipped, which it needs to show it.
nginx drops add_header lines from the server block inside a location that has its own, so repeat any you rely on, such as Strict-Transport-Security, in it.
With Apache and mod_headers, in the site's .htaccess (the last two lines let browsers show gzipped .svgz files):
<If "%{REQUEST_URI} =~ m#^/user/media/.+\.(svgz?|html?|xhtml|xml)$#i"> Header always set X-Content-Type-Options "nosniff" Header always set Content-Security-Policy "default-src 'none'; img-src data:; style-src 'unsafe-inline'; sandbox" </If> AddType image/svg+xml .svgz AddEncoding gzip .svgz
If NOVARIS_USER_PATH points at a folder inside the site, use its media path, such as /theme-test-data/media/, instead of /user/media/. Static exports copy media to user/media, so set the same headers on the static host if it lets you.
Search
Novaris can build a search index that themes search in the browser, so search works the same on live sites and static exports. Turn it on in config/app.php, under supports:
'supports' => [ 'search' => true, // or [ 'length' => 5000 ] ],
A theme can turn it on in its own config/app.php, and a site can turn it off with 'search' => false.
With search on:
/search.jsonlists every public post and page: itstitle,url, contenttype,date,terms(such as categories), a shortexcerpt, and itstextas plain text, without Markdown or HTML. Hidden entries (visibility: hidden), private content types and private_folders are left out, and so are taxonomies such as categories.lengthsets the most characters of text per entry (default 5000)./searchis a page for the results. It uses the theme'ssearchtemplate, or thepagetemplate if there isn't one, andis_search()is true on it.novaris exportwrites both, assearch.jsonandsearch/index.html, with URLs for the exported site.
A content type can be left out, or a taxonomy added, with its search option in config/content.php:
'post' => [ 'path' => '_posts', 'search' => false ],
Themes show the search box and results. These functions in Novaris\Theme\Site help:
| Function | Returns |
|---|---|
search_enabled() |
Whether search is on |
search_url( $query = '' ) |
The search page URL, such as /search?q=gallery |
search_index_url() |
The URL of search.json, for the theme's script to load |
search_query() |
The ?q= query on live sites, for filling in the search box. Static exports have no request, so scripts should read the query from the address |
Embeds
With embeds on, a link on its own line in Markdown becomes an embed:
Some text. https://www.youtube.com/watch?v=dQw4w9WgXcQ https://vimeo.com/76979871 /user/media/clip.mp4
- YouTube (
youtube.com/watch,youtu.be,/shorts/,/live/,/embed/, with at=start time) and Vimeo (including unlisted and channel links) become videos. - Video and audio files (
.mp4,.webm,.ogv,.mov,.mp3,.m4a,.ogg,.wav,.flac,.opus) get a<video>or<audio>player.
Nothing is fetched from the internet while building the page, so embeds work the same on live sites and static exports. Links inside a sentence, links with their own text ([My video](…)), and links to anything else stay links.
Turn embeds on in config/app.php, under supports:
'supports' => [ 'embeds' => true, // or [ 'click_to_load' => true, 'privacy' => true ] ],
click_to_load(defaulttrue): YouTube and Vimeo show a play button, with the video's thumbnail for YouTube, and the theme's script swaps in the real player when it's clicked (.embed__play[data-embed-src]). The provider's scripts only load for people who play the video, and without the script the play button is a link to the video. Set it tofalseto put the player in the page straight away, as a lazy-loaded iframe, with no script needed.privacy(defaulttrue): YouTube players use youtube-nocookie.com and Vimeo players usednt=1.
Embeds render as <figure class="embed embed--youtube"> (or --vimeo, --video, --audio), with the player inside .embed__frame. Themes style them, usually as a 16:9 box.
Feeds and sitemaps
Content types with 'feed' => true in config/content.php get an RSS feed (such as /blog/feed) and an Atom feed (/blog/feed/atom). With 'sitemap' => true in config/app.php, /sitemap lists a sitemap for each public content type, such as /sitemap/post and /sitemap/page.
The framework renders them with its own templates in resources/views (feed/rss.php, feed/atom.php, sitemap.php and sitemap-index.php). A theme can replace any of them with a template of the same name, or with feed.php for both feed formats.
novaris export writes them as .xml files, such as blog/feed.xml and sitemap/post.xml, so static hosts serve them as XML, and points links to them at those files.
Post formats
A post format lets a theme show some posts differently, as in ClassicPress: a quote post can show just the quote, a link post can link straight to the page it's about, and so on. A post sets its format in its front matter:
format: quote
The formats are aside, audio, chat, gallery, image, link, quote, status and video. A theme lists the ones it supports in its config/app.php:
'supports' => [ 'post-formats' => [ 'aside', 'link', 'quote' ], // or true for all ],
A post whose format the theme doesn't support, or that has none, is standard.
In templates:
$entry->format()returns the format, and$entry->hasFormat( 'quote' )checks it.post_class( [], $entry )adds aformat-quoteclass.- Single posts use a
single-post-quoteorsingle-format-quotetemplate if the theme has one. - For lists,
$engine->each( 'entry', $entries, 'entry', fn( $entry ) => $entry->formatHierarchy( 'collection' ) )usesentry/collection-quote.phpfor quote posts andentry/collection.phpfor the rest.
Themes
A site picks its theme in config/app.php:
'theme' => 'amicable', // installs from novaris-dev/amicable 'theme' => 'someone/their-theme', // installs from any GitHub user or organization
A missing theme is downloaded from the repository's latest GitHub release into themes/{slug}.
theme.json
Every theme has a theme.json at its root:
{
"$schema": "https://raw.githubusercontent.com/novaris-dev/framework/develop/schemas/theme.schema.json",
"name": "Amicable",
"slug": "amicable",
"version": "0.2.0",
"repository": "novaris-dev/amicable"
}
| Field | Required | Description |
|---|---|---|
name |
Yes | Theme name shown to people. |
slug |
Yes | Folder name. Must match the GitHub repository name. |
version |
Yes | Version without a leading v, such as 1.0.0. |
repository |
For updates | GitHub repository updates come from, as owner/slug. |
parent |
No | Parent theme, as slug or owner/slug. |
Theme features, such as featured images and custom headers, are configured in config/app.php under supports, not in theme.json.
The $schema line is optional. It gives editors such as VS Code autocomplete and warnings for missing or misspelled fields. This file is not the WordPress theme.json format.
To be installable, a theme's GitHub release needs an attached zip named {slug}.{version}.zip, such as amicable.0.2.0.zip, containing the theme folder and its theme.json.