11 Commits

Author SHA1 Message Date
230476e8e5 Claude redid the router 2026-06-30 01:35:55 -07:00
d5871ed8e7 minor updates 2026-06-16 19:00:30 -07:00
ad6e07c76d fixed css and changed timezone 2026-05-04 11:30:14 -07:00
06310d9eb9 Updated readme and composer 2026-05-03 13:27:30 -07:00
f679d0b20e safety fix 2026-02-07 17:26:26 -08:00
9feccf9eaa tabs and draft working 2026-01-26 21:55:22 -08:00
14ec6b7e7a fixed matomo to be in the master twig template. 2026-01-22 15:40:46 -08:00
42a828a778 edit page template 2026-01-12 19:00:03 -08:00
81c239f043 added matomo 2026-01-09 12:41:55 -08:00
0d91b61bd0 control panel head changes 2025-12-22 18:32:35 -08:00
fd9a71c4d6 added page to the front end 2025-12-16 05:35:56 -08:00
19 changed files with 614 additions and 128 deletions

View File

@@ -6,13 +6,20 @@ NovaconiumPHP is a high-performance PHP framework designed with inspiration from
Pronounced: Noh-vah-koh-nee-um
Packagist: https://packagist.org/packages/4lt/novaconium
Master Repo: https://git.4lt.ca/4lt/novaconium
* Packagist: https://packagist.org/packages/4lt/novaconium
* Master Repo: https://git.4lt.ca/4lt/novaconium
## Getting Started
Novaconium is heavly influenced by docker, but you can use composer outside of docker.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Install-Composer-On-Debian.md).
Novaconium is designed to be developed primarily using Docker. Instead of relying on tools installed directly on your system, common development tasks are executed inside containers:
* Composer runs inside a Docker container rather than on your host machine
* Apache / PHP are served from containers instead of your local environment
* Sass compilation is also handled within a container
As long as you have Docker installed, you can use the full development environment without installing additional dependencies on your system.
You can [learn more about how novaconium works with composer](https://git.4lt.ca/4lt/novaconium/src/branch/master/docs/Composer.md).
```bash
PROJECTNAME=novaproject;
@@ -24,9 +31,9 @@ docker run --rm --interactive --tty --volume ./novaconium/:/app composer:latest
cp -R novaconium/vendor/4lt/novaconium/skeleton/. .;
# Edit .env
# pwgen -cnsB1v 12 root password
# pwgen -cnsB1v 12 mysql user password (need in both config and env)
# pwgen -cnsB1v 64 framework key (need in config)
# pwgen -cnsB1v 12 # root password
# pwgen -cnsB1v 12 # mysql user password (need in both config and env)
# pwgen -cnsB1v 64 # framework key (need in config)
# Edit novaconium/App/config.php
docker compose up -d

View File

@@ -8,7 +8,7 @@ Update novaconium with composer in docker: ```docker run --rm --interactive --tt
## Install Composer natively on Debian
Assuming you have nala installed:
Assuming you have nala installed (otherwise use apt-get):
```bash
sudo nala install curl php-cli php-mbstring git unzip

101
docs/Router.md Normal file
View File

@@ -0,0 +1,101 @@
# Router
`Novaconium\Router` resolves the current HTTP request to a controller file, based on a route table defined in PHP config files. It supports exact-match routes and simple parameterized routes (e.g. `/users/{id}`).
## How it works
On construction, the router runs through this pipeline:
1. **Load routes** — merges framework-level routes with app-level routes.
2. **Prepare path** — normalizes `REQUEST_URI` into a clean path string.
3. **Prepare query** — parses the query string into an array.
4. **Determine request type**`get` or `post`.
5. **Find controller** — matches the path against the route table.
6. **Resolve controller file** — turns the matched controller string into an actual file path, falling back to a 404 controller if needed.
## Properties
| Property | Type | Description |
|---|---|---|
| `$routes` | array | Combined route table (framework + app routes), keyed by path pattern. |
| `$query` | array | Parsed query string parameters. |
| `$path` | string | Normalized request path. |
| `$controller` | string | Resolved controller identifier (e.g. `Users/show` or `NOVACONIUM/Errors`). |
| `$controllerPath` | string | Absolute file path to the controller. |
| `$parameters` | array | Named parameters extracted from a parameterized route match. |
| `$requestType` | string | `'get'` or `'post'`. |
## Route table format
Routes are defined as an associative array, keyed by URL path. Each entry maps HTTP methods to a controller string:
```php
$routes = [
'/' => [
'get' => 'Home/index',
],
'/users/{id}' => [
'get' => 'Users/show',
],
'/login' => [
'get' => 'Auth/loginForm',
'post' => 'Auth/login',
],
];
```
Controller strings starting with `NOVACONIUM` are resolved against the framework's controller directory (e.g. `NOVACONIUM/Errors``FRAMEWORKPATH/controllers/Errors.php`); all other controller strings are resolved against the app's controller directory (`BASEPATH/App/controllers/`).
## Matching behavior
### 1. Exact match
The router first checks for an exact match between `$this->path` and a key in `$this->routes`. If found, and the current request type has a handler defined, that controller is returned immediately.
### 2. Parameterized match
If no exact match is found, the router scans the route table for patterns containing `{`. For each candidate:
- The **static prefix** of the pattern (everything before the first `{`) must prefix-match the current path.
- The pattern and the path must have the **same number of `/`-separated segments**.
The first route pattern satisfying both conditions is treated as the match — the router does not keep searching for a "better" match after this point, even if the matched route turns out to have no handler for the current request method (see [Known quirks](#known-quirks) below).
Once a candidate route is selected, the router walks its segments looking for the first `{param}` segment, extracts the corresponding path segment into `$this->parameters[paramName]`, and returns immediately with the controller for the current request type.
### 3. No match
If neither an exact nor a parameterized match is found, `$this->controller` is set to `'404'`, and `setRouteFile()` will fail to find a corresponding controller file, triggering the 404 fallback described below.
## Controller file resolution (`setRouteFile`)
Given the resolved `$this->controller` string:
1. If it starts with `NOVACONIUM`, look in `FRAMEWORKPATH/controllers/`.
2. Otherwise, look in `BASEPATH/App/controllers/`.
3. If the file exists, use it.
4. If not, fall back to `BASEPATH/App/controllers/404.php` if it exists.
5. Otherwise, fall back to the framework's built-in `FRAMEWORKPATH/controllers/404.php`.
## Known quirks
These behaviors exist in the current implementation and are preserved intentionally for backward compatibility — they're documented here so they aren't mistaken for bugs during future changes.
- **Only the first `{param}` in a route pattern is captured.** If a route pattern has multiple parameters (e.g. `/users/{id}/posts/{postId}`), only `id` is extracted into `$this->parameters`; `postId` is never processed, because the matching loop returns as soon as it finds the first parameter segment.
- **The first prefix+segment-count match wins, even without a method handler.** If a parameterized route's static prefix and segment count match the request, but that route has no handler defined for the current request type (e.g. the route only defines `get` but the request is `post`), the router still commits to that route and returns `null` as the controller (which then falls through to the 404 controller). It does **not** continue searching for another route that might have a valid handler.
- **No support for trailing wildcard or optional segments.** Segment counts must match exactly between the pattern and the path; there's no support for `*` or optional `{param?}` style segments.
## Debugging
Call `$router->debug()` to dump the resolved path, controller path, extracted parameters, and full route table as an HTML table, then halt execution via `die()`. Intended for development use only.
## Example
```php
$router = new \Novaconium\Router();
include $router->controllerPath;
// Inside the controller, access route parameters via:
$router->parameters['id'];
```

View File

@@ -1,12 +1,10 @@
# Sample Docker Compose
services:
corxn:
image: 4lights/corxn:6.0.0
image: 4lights/corxn:8.5.3
ports:
- "8000:80"
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./novaconium:/data
- ./data/logs:/var/log/apache2 # Optional Logs
- "./logs:/data/logs"
@@ -29,8 +27,6 @@ services:
MYSQL_USER: novaconium
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
- ./data/db:/var/lib/mysql
networks:
- internal
@@ -49,9 +45,6 @@ services:
- PMA_USER=root
- PMA_PASSWORD=${MYSQL_ROOT_PASSWORD}
- UPLOAD_LIMIT=200M
volumes:
- "/etc/timezone:/etc/timezone:ro"
- "/etc/localtime:/etc/localtime:ro"
networks:
proxy:

View File

@@ -10,6 +10,8 @@ $config = [
'base_url' => 'http://localhost:8000',
'secure_key' => '', //64 alphanumeric characters
'logfile' => '/logs/novaconium.log',
'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE'
'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE',
'matomo_url' => 'matomo.4lt.ca',
'matomo_id' => '0',
'fonts' => 'https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap'
];

View File

@@ -0,0 +1,33 @@
<?php
$slug = $router->parameters['slug'];
$query=<<<EOSQL
SELECT
id,
title,
heading,
description,
keywords,
author,
body,
created,
updated
FROM pages
WHERE slug = '$slug'
EOSQL;
//$db->debugGetRow($query);
$data = $db->getRow($query);
if(!$data) {
http_response_code('404');
header("Content-Type: text/html");
view('404');
exit;
}
$data = array_merge($data, [
'menuActive' => 'blog',
'pageid' => 'page',
'permissionsGroup' => $session->get('group')
]);
view('page', $data);

View File

@@ -8,5 +8,8 @@ $routes = [
],
'/humans.txt' => [
'get' => 'humans'
],
'/page/{slug}' => [
'get' => 'page'
]
];

View File

@@ -0,0 +1,11 @@
{% extends '@novaconium/master.html.twig' %}
{% block content %}
<h1>{{ title }}</h1>
<div class="page-meta">
{% if updated is not empty %}
<span class="page-updated">Last Updated: {{ updated | date("M\\. jS Y \\a\\t g:ia") }}</span>
{% endif %}
</div>
{{ body | raw}}
{% endblock %}

View File

@@ -4,7 +4,7 @@
// ini_set('display_errors', 1);
// Define the base path where the website is running from
define('BASEPATH', dirname(__DIR__, 1));
define('BASEPATH', dirname(__DIR__));
// Load Composer's autoload file to handle class autoloading
require BASEPATH . '/vendor/autoload.php';

View File

@@ -0,0 +1,185 @@
// /js/tabs.js
// Tab switching
function switchTab(tabId, button) {
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => content.classList.remove('active'));
const buttons = document.querySelectorAll('.tab-button');
buttons.forEach(b => b.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
button.classList.add('active');
if (tabId === 'content6') {
setTimeout(initTags, 0);
}
}
// Tags Init (with custom dropdown autocomplete)
let tagsListeners = [];
function initTags() {
const tagsInput = document.getElementById('tags-input');
const tagsField = document.getElementById('tags');
const hiddenTags = document.getElementById('tags_json');
const dropdown = document.getElementById('tags-dropdown');
if (!tagsInput || !tagsField || !hiddenTags || !dropdown) {
console.warn('Tags elements missing');
return;
}
// Remove old listeners
tagsListeners.forEach(ls => ls());
tagsListeners = [];
let tags = [];
let existingTags = [];
try {
tags = JSON.parse(tagsInput.dataset.tags || '[]');
existingTags = JSON.parse(tagsInput.dataset.existingTags || '[]');
} catch (e) {
console.warn('JSON error:', e);
}
let selectedIndex = -1;
function renderTags() {
tagsInput.innerHTML = '';
tags.forEach((tag, index) => {
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = `${tag} <button type="button" class="tag-remove">&times;</button>`;
chip.querySelector('.tag-remove').onclick = () => {
tags.splice(index, 1);
renderTags();
hiddenTags.value = JSON.stringify(tags);
};
tagsInput.appendChild(chip);
});
tagsInput.appendChild(tagsField);
tagsInput.appendChild(dropdown);
hiddenTags.value = JSON.stringify(tags);
}
function updateDropdown() {
const value = tagsField.value.toLowerCase().trim();
dropdown.innerHTML = '';
dropdown.setAttribute('aria-expanded', value ? 'true' : 'false');
selectedIndex = -1;
if (!value) return;
const matches = existingTags
.filter(tag => tag.toLowerCase().startsWith(value) && !tags.includes(tag.toLowerCase()))
.slice(0, 10);
matches.forEach((tag, index) => {
const li = document.createElement('li');
li.textContent = tag;
li.setAttribute('role', 'option');
li.onclick = () => selectTag(tag);
li.onmouseover = () => { selectedIndex = index; updateHighlight(); };
dropdown.appendChild(li);
});
if (matches.length) dropdown.parentElement.classList.add('has-dropdown');
else dropdown.parentElement.classList.remove('has-dropdown');
}
function updateHighlight() {
dropdown.querySelectorAll('li').forEach((li, index) => {
li.classList.toggle('selected', index === selectedIndex);
});
}
function selectTag(tag) {
addTag(tag, true);
tagsField.value = '';
dropdown.setAttribute('aria-expanded', 'false');
}
function addTag(inputValue, refocus = false) {
const tag = inputValue.trim().toLowerCase().replace(/[^\w-]/g, '');
if (tag && tag.length > 0 && tag.length <= 50 && !tags.includes(tag)) {
tags.push(tag);
renderTags();
if (refocus) tagsField.focus();
}
tagsField.value = '';
updateDropdown();
}
const keydownListener = (e) => {
if (dropdown.getAttribute('aria-expanded') === 'true') {
if (e.key === 'ArrowDown') {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, dropdown.querySelectorAll('li').length - 1);
updateHighlight();
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
updateHighlight();
dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.scrollIntoView({ block: 'nearest' });
} else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIndex >= 0) {
e.preventDefault();
const selected = dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.textContent;
if (selected) selectTag(selected);
} else if (e.key === 'Escape') {
e.preventDefault();
tagsField.blur();
}
}
if (!dropdown.getAttribute('aria-expanded') === 'true' || selectedIndex < 0) {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
addTag(tagsField.value, true);
} else if (e.key === 'Tab') {
e.preventDefault();
if (tagsField.value.trim()) addTag(tagsField.value, true);
}
}
};
tagsField.addEventListener('keydown', keydownListener);
tagsListeners.push(() => tagsField.removeEventListener('keydown', keydownListener));
const inputListener = () => updateDropdown();
tagsField.addEventListener('input', inputListener);
tagsListeners.push(() => tagsField.removeEventListener('input', inputListener));
const blurListener = () => {
setTimeout(() => dropdown.setAttribute('aria-expanded', 'false'), 150);
if (tagsField.value.trim()) addTag(tagsField.value);
};
tagsField.addEventListener('blur', blurListener);
tagsListeners.push(() => tagsField.removeEventListener('blur', blurListener));
renderTags();
}
// Init on load and observe tab changes
document.addEventListener('DOMContentLoaded', () => {
const tagsTab = document.getElementById('content6');
if (tagsTab?.classList.contains('active')) {
initTags();
}
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target;
if (target.id === 'content6' && target.classList.contains('active')) {
initTags();
}
}
});
});
observer.observe(document.body, { subtree: true, attributes: true });
});
// Export switchTab for external use if needed
window.switchTab = switchTab;

View File

@@ -1,5 +1,6 @@
<?php
namespace Novaconium;
class Router {
public $routes = [];
public $query = [];
@@ -18,29 +19,30 @@ class Router {
$this->controllerPath = $this->setRouteFile();
}
/**
* Merge framework routes with app-defined routes (app routes can override framework routes).
*/
private function loadRoutes() {
require_once( \FRAMEWORKPATH . '/config/routes.php');
// Check if Path exists
require_once(\FRAMEWORKPATH . '/config/routes.php');
if (file_exists(\BASEPATH . '/App/routes.php')) {
require_once( \BASEPATH . '/App/routes.php');
}
$routes = array_merge((array)$routes, (array)$framework_routes);
return $routes;
require_once(\BASEPATH . '/App/routes.php');
}
return array_merge((array)$routes, (array)$framework_routes);
}
/**
* Normalize the request path: strip query string, trailing slash, and anything after "&".
*/
private function preparePath() {
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
//homepage
if ($path === '/') {
return $path;
}
// remove empty directory path
$path = rtrim($path, '/'); // remove trailing slash
//remove anything after and including ampersand
$path = rtrim($path, '/');
$path = preg_replace('/&.+$/', '', $path);
return $path;
@@ -49,82 +51,146 @@ class Router {
private function prepareQuery() {
$parsedUri = parse_url($_SERVER['REQUEST_URI']);
$queryArray = [];
if (isset($parsedUri['query'])) {
parse_str($parsedUri['query'], $queryArray);
}
return $queryArray;
}
private function getRequestType() {
// is the requewst a get or post?
if (empty($_POST)) {
return 'get';
} else {
return 'post';
}
return empty($_POST) ? 'get' : 'post';
}
/**
* Resolve the current path to a controller string.
* First tries an exact match, then falls back to matching parameterized
* routes like "/users/{id}".
*/
private function findController() {
// one to one match
if (array_key_exists($this->path, $this->routes)) {
if (!empty($this->routes[$this->path][$this->requestType])) {
return $this->routes[$this->path][$this->requestType];
}
$exactMatch = $this->findExactMatch();
if ($exactMatch !== null) {
return $exactMatch;
}
foreach ($this->routes as $key => $value) {
// Check if key contains a curly bracket, if not continue. We already checked above.
if (!strpos($key, '{')) continue;
// Remove everything after the curly bracket, from key
$keyPath = substr($key, 0, strpos($key, '{'));
//see if keyPath matches the first characters of $this->path, only the first characters have to match
if (strpos($this->path, $keyPath) === 0) {
// We have a potential match. Now check if the parameter count is equal
$keyParams = explode('/', $key);
$pathParams = explode('/', $this->path);
$keyParamCount = count($keyParams);
$pathParamCount = count($pathParams);
if ($keyParamCount === $pathParamCount) {
for ($i=0; $i < $pathParamCount; $i++) {
if (strpos($keyParams[$i], '{') !== false) {
$keyParams[$i] = substr($keyParams[$i], 1, -1);
$this->parameters[$keyParams[$i]] = $pathParams[$i];
return $this->routes[$key][$this->requestType];
}
}
}
}
$paramMatch = $this->findParameterizedMatch();
if ($paramMatch !== null) {
return $paramMatch;
}
return '404';
}
// checks if the file exists, sets file path
private function setRouteFile() {
private function findExactMatch() {
if (array_key_exists($this->path, $this->routes)
&& !empty($this->routes[$this->path][$this->requestType])
) {
return $this->routes[$this->path][$this->requestType];
}
return null;
}
/**
* Loop over parameterized routes (those containing "{") looking for the
* first one whose static prefix matches the path AND whose segment count
* matches. As soon as such a route is found, control is handed off to
* extractControllerFromMatch(), which returns immediately (even with a
* `null` controller) -- this mirrors the original code's behavior of
* stopping the search on the first prefix+segment-count match, rather
* than continuing on to try other candidate routes.
*/
private function findParameterizedMatch() {
foreach ($this->routes as $routePattern => $methods) {
// Only parameterized routes (containing "{") are relevant here;
// plain routes were already checked in findExactMatch().
if (strpos($routePattern, '{') === false) {
continue;
}
if (!$this->routeMatchesPath($routePattern)) {
continue;
}
if (!$this->segmentCountsMatch($routePattern)) {
continue;
}
// Original behavior: once we find a route with a matching prefix
// and segment count, we commit to it -- even if it turns out
// there's no handler for the current request method.
return $this->extractControllerFromMatch($routePattern, $methods);
}
return null;
}
/**
* Quick check: does the static portion of the route pattern (everything
* before the first "{") prefix-match the current path?
*/
private function routeMatchesPath($routePattern) {
$staticPrefix = substr($routePattern, 0, strpos($routePattern, '{'));
return strpos($this->path, $staticPrefix) === 0;
}
private function segmentCountsMatch($routePattern) {
$patternSegments = explode('/', $routePattern);
$pathSegments = explode('/', $this->path);
return count($patternSegments) === count($pathSegments);
}
/**
* Extracts named parameters into $this->parameters and returns the
* controller for the current request type (or null if that method
* isn't defined for this route).
*
* Matches the original quirk: only the FIRST "{param}" segment found
* gets added to $this->parameters, and the method returns right away --
* any additional "{param}" segments later in the pattern are never
* processed. This is preserved as-is rather than "fixed", since
* behavior should stay unchanged.
*/
private function extractControllerFromMatch($routePattern, $methods) {
$patternSegments = explode('/', $routePattern);
$pathSegments = explode('/', $this->path);
foreach ($patternSegments as $i => $segment) {
if (strpos($segment, '{') !== false) {
$paramName = substr($segment, 1, -1); // strip { }
$this->parameters[$paramName] = $pathSegments[$i];
return $methods[$this->requestType] ?? null;
}
}
return null;
}
/**
* Resolve the controller string to an actual file path, falling back to a 404 controller.
*/
private function setRouteFile() {
if (str_starts_with($this->controller, 'NOVACONIUM')) {
$trimmed = substr($this->controller, strlen('NOVACONIUM/'));
$cp = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
$controllerPath = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php';
} else {
$cp = \BASEPATH . '/App/controllers/' . $this->controller . '.php';
$controllerPath = \BASEPATH . '/App/controllers/' . $this->controller . '.php';
}
if (file_exists($cp)) {
return $cp;
} else {
//Check if 404 exits
if (file_exists( \BASEPATH . '/App/controllers/404.php')) {
if (file_exists($controllerPath)) {
return $controllerPath;
}
if (file_exists(\BASEPATH . '/App/controllers/404.php')) {
return \BASEPATH . '/App/controllers/404.php';
} else {
}
return \FRAMEWORKPATH . '/controllers/404.php';
}
}
}
public function debug() {
echo '<div id="router-debug-container" class="debug">';
@@ -137,6 +203,4 @@ class Router {
die();
}
}

View File

@@ -1,5 +1,16 @@
<?php
use Novaconium\Logger;
use Novaconium\Session;
use Novaconium\MessageHandler;
use Novaconium\Database;
use Novaconium\Post;
use Novaconium\Redirect;
use Novaconium\Router;
$db = null;
$post = null;
// --- Load Config ---
if (file_exists(\BASEPATH . '/App/config.php')) {
require_once \BASEPATH . '/App/config.php';
@@ -11,43 +22,38 @@ require_once \FRAMEWORKPATH . '/src/functions.php';
require_once \FRAMEWORKPATH . '/src/twig.php';
// --- Logging ---
use Novaconium\Logger;
$log = new Logger(\BASEPATH . $config['logfile'], $config['loglevel']);
// --- Twig Data Array ---
$data = [];
$data['fonts'] = $config['fonts'] ?? [];
$data['matomo_url'] = $config['matomo_url'] ?? '';
$data['matomo_id'] = $config['matomo_id'] ?? '0';
// --- Session ---
use Novaconium\Session;
$session = new Session();
$data['token'] = $session->get('token');
$data['username'] = $session->get('username');
// --- Messages ---
use Novaconium\MessageHandler;
$messages = new MessageHandler($session->flash('messages'));
foreach (['error', 'notice'] as $key) {
$data[$key] = $messages->showMessages($key);
}
// --- Database ---
use Novaconium\Database;
if (!empty($config['database']['host'])) {
$db = new Database($config['database']);
}
// --- POST Wrapper ---
use Novaconium\Post;
if (!empty($_POST)) {
$post = new Post($_POST);
}
// --- Redirect Handler ---
use Novaconium\Redirect;
$redirect = new Redirect();
// --- Router ---
use Novaconium\Router;
$router = new Router();
require_once $router->controllerPath;

View File

@@ -2,7 +2,7 @@
<html class="no-js" lang="en">
<head>
{% include ['@override/head.html.twig', '@novaconium/head.html.twig'] %}
{% include ['@novaconium/cp/head.html.twig'] %}
</head>
<body id="{{ pageid | default('pageid') }}" class="{{ pageclass | default('pageclass') }}" >
@@ -28,12 +28,12 @@
<div class="copyright">&copy; {{ 'now' | date('Y') }} Novaconium</div>
</footer>
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% if editor == 'ace' %}
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}
{% if debug is not empty %}
<div id="debug">
<h2>Debugging Information</h2>

60
twig/cp/head.html.twig Normal file
View File

@@ -0,0 +1,60 @@
{# =============================================================================
<HEAD>
=============================================================================
#}
<meta charset="utf-8">
<title>{{ title | default('Welcome To Novaconium') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{# SEO & METADATA #}
<meta name="generator" content="Novaconium" />
<meta name="description" content="{{ description | default('No description given') }}">
<meta name="keywords" content="{{ keywords | default('website') }}">
<meta name="author" content="{{ author | default('anonymous') }}">
{# DARK MODE & THEME HINTS #}
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#000000">
{# OPEN GRAPH (OG) FOR SOCIAL SHARING #}
<meta property="og:title" content="{{ title | default('Welcome') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ app_url | default(request.scheme ~ '://' ~ request.host) }}">
<meta property="og:image" content="{{ og_image | default('/icon.png') }}">
{# PWA & FAVICONS #}
<link rel="manifest" href="site.webmanifest">
<link rel="apple-touch-icon" href="/icon.png">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{# GOOGLE FONTS (CDN VIA PRECONNECT) #}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="{{ fonts | default('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&family=Roboto+Mono&family=Source+Code+Pro&family=Lato&family=Poppins&family=Material+Icons&family=Material+Icons+Outlined&family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap') }}"
rel="stylesheet"
>
{% if editor == 'ace' %}
<!-- ACE Editor -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/mode-html.min.js"></script> {# HTML syntax #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/theme-tomorrow_night.min.js"></script> {# Dark theme #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ext-language_tools.min.js"></script> {# Autocomplete #}
<!-- END ACE Editor -->
{% endif %}
{# highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/ir-black.min.css">
<script src="/js/tabs.js"></script>
{# STYLESHEET #}
<link rel="stylesheet" href="/css/novaconium.css">

View File

@@ -1,11 +1,5 @@
<!--
What goes very last on the page.
right before the /body
like javascript
or analytics
such as javascript
-->
{% include '@novaconium/javascript/page-edit.html.twig' %}
{% if editor == 'ace' %}
{% include '@novaconium/javascript/ace.html.twig' %}
{% endif %}

View File

@@ -39,22 +39,7 @@
>
{# STYLESHEET #}
{% if pageclass == "novaconium" %}
<link rel="stylesheet" href="/css/novaconium.css">
{% else %}
<link rel="stylesheet" href="/css/main.css">
{% endif %}
{% if editor == 'ace' %}
<!-- ACE Editor -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/mode-html.min.js"></script> {# HTML syntax #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/theme-tomorrow_night.min.js"></script> {# Dark theme #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.35.2/ext-language_tools.min.js"></script> {# Autocomplete #}
<!-- END ACE Editor -->
{% endif %}
<link rel="stylesheet" href="/css/novaconium.css">
{# highlight.js #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>

View File

@@ -60,4 +60,32 @@
{% endif %}
{% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %}
{% if matomo_id > 0 %}
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
{% if is_404 %}
_paq.push([
'setDocumentTitle',
'404 / Not Found - ' + document.location.pathname
]);
{% endif %}
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u = "//{{ matomo_url|trim('/') }}/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', {{ matomo_id }}]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</body></html>

View File

@@ -1,3 +1,4 @@
{% set is_404 = status_code is defined and status_code == 404 %}
{% extends '@novaconium/master.html.twig' %}
{% block content %}

View File

@@ -14,3 +14,16 @@
<div id="body-editor" class="ace-editor"></div> {# Ace mounts here #}
</div>
</div>
<div class="form-group">
<label for="draft">
<input
type="checkbox"
id="draft"
name="draft"
value="1"
{% if rows.draft|default(false) %}checked{% endif %}
>
Save as draft
</label>
</div>