diff --git a/.gitignore b/.gitignore
index e4401d9..7491cbe 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ content/.user.ini
content/php.ini
custom/assets/petition-map-data.json
custom/data/petition-map-cache.json
+custom/data/publiser/
+content/publiser/.htpasswd
diff --git a/Containerfile b/Containerfile
index 60fe44c..dfc0b20 100644
--- a/Containerfile
+++ b/Containerfile
@@ -3,6 +3,13 @@ FROM php:8.4.14-apache
# Enable Apache modules and custom config as root during build
RUN a2enmod rewrite headers
+# GD extension - used by /publiser to resize/recompress uploaded images
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends libjpeg62-turbo-dev libpng-dev libwebp-dev \
+ && docker-php-ext-configure gd --with-jpeg --with-webp \
+ && docker-php-ext-install gd \
+ && rm -rf /var/lib/apt/lists/*
+
COPY apache.conf /etc/apache2/conf-available/custom.conf
RUN a2enconf custom
diff --git a/compose.yaml b/compose.yaml
index 694c31f..b8e0017 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -10,4 +10,4 @@ services:
ports:
- "4040:80"
command: >
- bash -c "chown -R www-data:www-data /var/www/custom/data /var/www/custom/assets && apache2-foreground"
+ bash -c "chown -R www-data:www-data /var/www/custom/data /var/www/custom/assets /var/www/html/nyheter /var/www/html/artikler && apache2-foreground"
diff --git a/content/.htaccess b/content/.htaccess
index 379c049..632ef79 100644
--- a/content/.htaccess
+++ b/content/.htaccess
@@ -2,8 +2,8 @@ DirectorySlash Off
# Block direct access to content source files
- # Allow only the entry point
-
+ # Allow only the entry point, and the standalone /publiser admin app
+
Require all denied
@@ -30,6 +30,13 @@ DirectorySlash Off
RewriteCond %{REQUEST_URI} ^/app/
RewriteRule ^(.*)$ /index.php [L,QSA]
+ # Standalone /publiser admin app - bypass the CMS entirely, dispatch
+ # everything to its own entry point (static assets like .css/.js are
+ # excluded so Apache serves those files directly)
+ RewriteCond %{REQUEST_URI} ^/publiser
+ RewriteCond %{REQUEST_URI} !\.(css|js|png|jpe?g|webp|gif|svg)$
+ RewriteRule ^publiser(/.*)?$ publiser/index.php [L,QSA]
+
# Don't rewrite if file exists
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php [L,QSA]
diff --git a/content/.htaccess.base b/content/.htaccess.base
index ffeacd3..042b87a 100644
--- a/content/.htaccess.base
+++ b/content/.htaccess.base
@@ -2,8 +2,8 @@ DirectorySlash Off
# Block direct access to content source files
- # Allow only the entry point
-
+ # Allow only the entry point, and the standalone /publiser admin app
+
Require all denied
@@ -30,6 +30,13 @@ DirectorySlash Off
RewriteCond %{REQUEST_URI} ^/app/
RewriteRule ^(.*)$ /index.php [L,QSA]
+ # Standalone /publiser admin app - bypass the CMS entirely, dispatch
+ # everything to its own entry point (static assets like .css/.js are
+ # excluded so Apache serves those files directly)
+ RewriteCond %{REQUEST_URI} ^/publiser
+ RewriteCond %{REQUEST_URI} !\.(css|js|png|jpe?g|webp|gif|svg)$
+ RewriteRule ^publiser(/.*)?$ publiser/index.php [L,QSA]
+
# Don't rewrite if file exists
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /index.php [L,QSA]
diff --git a/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/10-spleis-into.md b/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/10-spleis-into.md
index 82252c6..0cee1a5 100644
--- a/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/10-spleis-into.md
+++ b/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/10-spleis-into.md
@@ -11,5 +11,3 @@ Uten førerkort blir hverdagen veldig vanskelig: matbutikk, legeavtaler og famil
Denne rettssaken er en unik mulighet til å utfordre regelverket og skape reell endring. Ditt bidrag går direkte til rettssaken – en kamp for rettferdighet, medisinsk frihet og et verdig liv for alle pasienter. Bidra nå, og vær med på å endre regelverket!
Gi ditt bidrag i dag – hver krone teller! Les Glenn Dahl [sin historie på NRK.no](https://www.nrk.no/norge/nektes-a-kjore-bil-fordi-han-bruker-medisinsk-cannabis-1.17210314).
-
-
diff --git a/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/20-spleis-embed.html b/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/20-spleis-embed.html
new file mode 100644
index 0000000..092bbfd
--- /dev/null
+++ b/content/nyheter/2026-03-10-SPLEIS-forerkort-tingretten/20-spleis-embed.html
@@ -0,0 +1 @@
+
diff --git a/content/publiser/.htaccess b/content/publiser/.htaccess
new file mode 100644
index 0000000..693842f
--- /dev/null
+++ b/content/publiser/.htaccess
@@ -0,0 +1,15 @@
+DirectorySlash On
+
+# Auth is checked in PHP (index.php), not via Apache AuthUserFile: relative
+# AuthUserFile paths resolve against ServerRoot, not this directory, and
+# that differs per environment (podman dev container vs cPanel). Some SAPIs
+# (LSAPI/CGI/FastCGI, used on cPanel) strip the Authorization header from
+# PHP by default, so forward it explicitly via an internal env var.
+
+ RewriteEngine On
+ RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+
+
+
+ Require all denied
+
diff --git a/content/publiser/index.php b/content/publiser/index.php
new file mode 100644
index 0000000..410f1c5
--- /dev/null
+++ b/content/publiser/index.php
@@ -0,0 +1,726 @@
+ $v) {
+ $url .= '&' . urlencode($k) . '=' . urlencode((string)$v);
+ }
+ return $url;
+}
+
+function publiserPublicUrl(string $section, string $folder): string {
+ return '/' . rawurlencode($section) . '/' . rawurlencode($folder) . '/';
+}
+
+function publiserImageUrl(string $location, string $section, string $folder, string $filename): string {
+ if ($location === 'live') {
+ return '/' . rawurlencode($section) . '/' . rawurlencode($folder) . '/' . rawurlencode($filename);
+ }
+ return publiserBaseUrl() . '?action=image&location=' . urlencode($location)
+ . '§ion=' . urlencode($section) . '&folder=' . urlencode($folder) . '&file=' . urlencode($filename);
+}
+
+function publiserCheckCsrf(): void {
+ global $csrf;
+ $token = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
+ if (!hash_equals($csrf, $token)) {
+ http_response_code(403);
+ die('Ugyldig forespørsel (utløpt økt). Last siden på nytt og prøv igjen.');
+ }
+}
+
+function publiserRedirect(string $url): never {
+ header("Location: $url");
+ exit;
+}
+
+function publiserItemDir(string $location, string $section, string $folder): string {
+ return match ($location) {
+ 'live' => publiserLiveDir($section, $folder),
+ 'draft' => publiserDraftDir($section, $folder),
+ 'trash' => publiserTrashDir($section, $folder),
+ default => throw new RuntimeException('Ugyldig plassering'),
+ };
+}
+
+// --- minimal Markdown -> HTML for populating the WYSIWYG editor on load ----
+// (mirrors the bounded tag set publiser.js can serialize back to Markdown)
+
+function publiserEscapeInline(string $text): string {
+ return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+}
+
+function publiserInlineMdToHtml(string $text, callable $imageUrlResolver): string {
+ $text = publiserEscapeInline($text);
+
+ $text = preg_replace_callback('/!\[(.*?)\]\((.*?)\)/', function ($m) use ($imageUrlResolver) {
+ $alt = $m[1];
+ $file = $m[2];
+ $realFile = html_entity_decode($file, ENT_QUOTES, 'UTF-8');
+ $url = htmlspecialchars($imageUrlResolver($realFile), ENT_QUOTES, 'UTF-8');
+ return '
';
+ }, $text) ?? $text;
+
+ $text = preg_replace_callback('/\[(.*?)\]\((.*?)\)/', function ($m) {
+ return '' . $m[1] . '';
+ }, $text) ?? $text;
+
+ $text = preg_replace('/\*\*(.+?)\*\*/', '$1', $text) ?? $text;
+ $text = preg_replace('/(?$1', $text) ?? $text;
+
+ return $text;
+}
+
+function publiserBlockMdToHtml(string $md, callable $imageUrlResolver): string {
+ $md = str_replace("\r\n", "\n", trim($md));
+ if ($md === '') return '
';
+ $blocks = preg_split('/\n{2,}/', $md) ?: [];
+ $html = [];
+
+ foreach ($blocks as $block) {
+ $block = trim($block);
+ if ($block === '') continue;
+ $lines = explode("\n", $block);
+
+ if (preg_match('/^<[a-zA-Z][a-zA-Z0-9-]*(\s[^>]*)?>/', $lines[0])) {
+ // Raw HTML embedded in markdown (e.g. an