| 1 | /**
|
|---|
| 2 | * Admin: advanced SEO settings for the primary site.
|
|---|
| 3 | *
|
|---|
| 4 | * GET /admin/seo -> form with all SEO/social fields for the main site
|
|---|
| 5 | * POST /admin/seo -> save (god-only)
|
|---|
| 6 | *
|
|---|
| 7 | * These fields are already consumed by the <head> (shell.ejs) and the JSON-LD/
|
|---|
| 8 | * OpenGraph tags, but were previously not editable anywhere. The basic
|
|---|
| 9 | * fields (title/bio/robots) remain in Appearance; this is the advanced layer:
|
|---|
| 10 | * title template, canonical, social share image, verification metas,
|
|---|
| 11 | * publisher/JSON-LD and OpenGraph locale.
|
|---|
| 12 | *
|
|---|
| 13 | * Operates on the PRIMARY site (solo = the only site; hub = the company site).
|
|---|
| 14 | */
|
|---|
| 15 |
|
|---|
| 16 | import express from 'express';
|
|---|
| 17 | import db from '../config/database.js';
|
|---|
| 18 | import { renderPage } from '../middleware/render.js';
|
|---|
| 19 | import { requireGod } from '../middleware/auth.js';
|
|---|
| 20 | import { getPrimarySite } from '../middleware/site.js';
|
|---|
| 21 |
|
|---|
| 22 | const router = express.Router();
|
|---|
| 23 |
|
|---|
| 24 | function trimOrNull(v, max) {
|
|---|
| 25 | const s = (v == null ? '' : String(v)).trim();
|
|---|
| 26 | return s ? s.slice(0, max) : null;
|
|---|
| 27 | }
|
|---|
| 28 |
|
|---|
| 29 | // ==================== FORM ====================
|
|---|
| 30 | router.get('/', requireGod, (req, res) => {
|
|---|
| 31 | const primary = getPrimarySite();
|
|---|
| 32 | if (!primary) {
|
|---|
| 33 | return res.redirect('/admin/sites/new?error=' + encodeURIComponent('Maak eerst een site aan'));
|
|---|
| 34 | }
|
|---|
| 35 | const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(primary.id);
|
|---|
| 36 |
|
|---|
| 37 | renderPage(req, res, 'pages/admin-seo', {
|
|---|
| 38 | pageTitleKey: 'admin.t_seo',
|
|---|
| 39 | bodyClass: 'on-admin',
|
|---|
| 40 | site,
|
|---|
| 41 | success: req.query.success || null,
|
|---|
| 42 | error: req.query.error || null,
|
|---|
| 43 | });
|
|---|
| 44 | });
|
|---|
| 45 |
|
|---|
| 46 | // ==================== SAVE ====================
|
|---|
| 47 | router.post('/', requireGod, (req, res) => {
|
|---|
| 48 | const primary = getPrimarySite();
|
|---|
| 49 | if (!primary) return res.redirect('/admin/seo?error=' + encodeURIComponent('Geen site gevonden'));
|
|---|
| 50 |
|
|---|
| 51 | const f = req.body;
|
|---|
| 52 | const schemaType = f.schema_type === 'Organization' ? 'Organization' : 'Person';
|
|---|
| 53 |
|
|---|
| 54 | db.prepare(`
|
|---|
| 55 | UPDATE sites SET
|
|---|
| 56 | robots_index = ?,
|
|---|
| 57 | title_template = ?,
|
|---|
| 58 | canonical = ?,
|
|---|
| 59 | default_description = ?,
|
|---|
| 60 | og_image_default = ?,
|
|---|
| 61 | og_theme = ?,
|
|---|
| 62 | og_locale = ?,
|
|---|
| 63 | author = ?,
|
|---|
| 64 | twitter = ?,
|
|---|
| 65 | facebook_app_id = ?,
|
|---|
| 66 | google_verification = ?,
|
|---|
| 67 | bing_verification = ?,
|
|---|
| 68 | pinterest_verification = ?,
|
|---|
| 69 | yandex_verification = ?,
|
|---|
| 70 | schema_type = ?,
|
|---|
| 71 | publisher_name = ?,
|
|---|
| 72 | publisher_url = ?,
|
|---|
| 73 | publisher_logo = ?,
|
|---|
| 74 | updated_at = CURRENT_TIMESTAMP
|
|---|
| 75 | WHERE id = ?
|
|---|
| 76 | `).run(
|
|---|
| 77 | f.robots_index ? 1 : 0,
|
|---|
| 78 | (f.title_template || '{title} — {site}').slice(0, 200),
|
|---|
| 79 | trimOrNull(f.canonical, 200),
|
|---|
| 80 | trimOrNull(f.default_description, 500),
|
|---|
| 81 | trimOrNull(f.og_image_default, 500),
|
|---|
| 82 | (f.og_theme === 'light' || f.og_theme === 'dark') ? f.og_theme : null, // null = auto (follow site theme)
|
|---|
| 83 | trimOrNull(f.og_locale, 32),
|
|---|
| 84 | trimOrNull(f.author, 120),
|
|---|
| 85 | trimOrNull(f.twitter, 64),
|
|---|
| 86 | trimOrNull(f.facebook_app_id, 64),
|
|---|
| 87 | trimOrNull(f.google_verification, 200),
|
|---|
| 88 | trimOrNull(f.bing_verification, 200),
|
|---|
| 89 | trimOrNull(f.pinterest_verification, 200),
|
|---|
| 90 | trimOrNull(f.yandex_verification, 200),
|
|---|
| 91 | schemaType,
|
|---|
| 92 | trimOrNull(f.publisher_name, 200),
|
|---|
| 93 | trimOrNull(f.publisher_url, 200),
|
|---|
| 94 | trimOrNull(f.publisher_logo, 500),
|
|---|
| 95 | primary.id,
|
|---|
| 96 | );
|
|---|
| 97 |
|
|---|
| 98 | res.redirect('/admin/seo?success=' + encodeURIComponent('SEO-instellingen opgeslagen'));
|
|---|
| 99 | });
|
|---|
| 100 |
|
|---|
| 101 | export default router;
|
|---|