| 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 | import { isMbid } from '../services/ap-core.js';
|
|---|
| 22 | import MusicBrainz from '../services/MusicBrainzService.js';
|
|---|
| 23 |
|
|---|
| 24 | const router = express.Router();
|
|---|
| 25 |
|
|---|
| 26 | function trimOrNull(v, max) {
|
|---|
| 27 | const s = (v == null ? '' : String(v)).trim();
|
|---|
| 28 | return s ? s.slice(0, max) : null;
|
|---|
| 29 | }
|
|---|
| 30 |
|
|---|
| 31 | // ==================== FORM ====================
|
|---|
| 32 | router.get('/', requireGod, (req, res) => {
|
|---|
| 33 | const primary = getPrimarySite();
|
|---|
| 34 | if (!primary) {
|
|---|
| 35 | return res.redirect('/admin/sites/new?error=' + encodeURIComponent('Maak eerst een site aan'));
|
|---|
| 36 | }
|
|---|
| 37 | const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(primary.id);
|
|---|
| 38 |
|
|---|
| 39 | renderPage(req, res, 'pages/admin-seo', {
|
|---|
| 40 | pageTitleKey: 'admin.t_seo',
|
|---|
| 41 | pageJs: 'admin-seo',
|
|---|
| 42 | bodyClass: 'on-admin',
|
|---|
| 43 | site,
|
|---|
| 44 | success: req.query.success || null,
|
|---|
| 45 | error: req.query.error || null,
|
|---|
| 46 | });
|
|---|
| 47 | });
|
|---|
| 48 |
|
|---|
| 49 | /**
|
|---|
| 50 | * "Zoek jezelf op" -- kandidaten uit MusicBrainz (shaer-mbz).
|
|---|
| 51 | *
|
|---|
| 52 | * De zoekopdracht draait HIER en niet in de browser: MusicBrainz staat een
|
|---|
| 53 | * verzoek per seconde toe per APPLICATIE, en dat is alleen af te dwingen als
|
|---|
| 54 | * alles langs een plek gaat. Bovendien eisen ze een User-Agent met contact, en
|
|---|
| 55 | * die kan een browser niet zetten.
|
|---|
| 56 | *
|
|---|
| 57 | * Wij kiezen NIET. Ook niet als er precies een treffer is: een verkeerd geraden
|
|---|
| 58 | * MBID zet jouw naam onder andermans werk.
|
|---|
| 59 | */
|
|---|
| 60 | router.get('/api/musicbrainz', requireGod, async (req, res) => {
|
|---|
| 61 | const site = getPrimarySite(req);
|
|---|
| 62 | const q = String(req.query.q || (site && (site.publisher_name || site.title)) || '').trim();
|
|---|
| 63 | if (!q) return res.json({ ok: true, q: '', kandidaten: [] });
|
|---|
| 64 | // Wie zijn id al kent plakt het hier. Een zoekopdracht op een UUID levert bij
|
|---|
| 65 | // MusicBrainz niets op, dus zonder deze tak geeft plakken juist het slechtste
|
|---|
| 66 | // resultaat.
|
|---|
| 67 | if (isMbid(q)) {
|
|---|
| 68 | const een = await MusicBrainz.haalArtiest(q);
|
|---|
| 69 | return res.json({ ok: true, q, kandidaten: een ? [een] : [] });
|
|---|
| 70 | }
|
|---|
| 71 | res.json({ ok: true, q, kandidaten: await MusicBrainz.zoekArtiesten(q) });
|
|---|
| 72 | });
|
|---|
| 73 |
|
|---|
| 74 | /**
|
|---|
| 75 | * De terug-weg: noemt de MusicBrainz-pagina ons domein? (shaer-mbz)
|
|---|
| 76 | *
|
|---|
| 77 | * Een koppeling van onze kant is een bewering -- iedereen kan een id typen.
|
|---|
| 78 | * Pas als de artiestenpagina TERUGWIJST is het een paar. Wij zetten die
|
|---|
| 79 | * verwijzing niet zelf: dat kan niet via hun API en hoort ook niet, de artiest
|
|---|
| 80 | * doet dat op musicbrainz.org onder "social networking".
|
|---|
| 81 | */
|
|---|
| 82 | router.get('/api/musicbrainz/terugweg', requireGod, async (req, res) => {
|
|---|
| 83 | const mbid = String(req.query.mbid || '').trim().toLowerCase();
|
|---|
| 84 | const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
|
|---|
| 85 | if (!isMbid(mbid) || !base) return res.json({ ok: true, verified: false, urls: [] });
|
|---|
| 86 | res.json({ ok: true, ...(await MusicBrainz.controleerTerugweg(mbid, base)) });
|
|---|
| 87 | });
|
|---|
| 88 |
|
|---|
| 89 | // ==================== SAVE ====================
|
|---|
| 90 | router.post('/', requireGod, (req, res) => {
|
|---|
| 91 | const primary = getPrimarySite();
|
|---|
| 92 | if (!primary) return res.redirect('/admin/seo?error=' + encodeURIComponent('Geen site gevonden'));
|
|---|
| 93 |
|
|---|
| 94 | const f = req.body;
|
|---|
| 95 | const schemaType = f.schema_type === 'Organization' ? 'Organization' : 'Person';
|
|---|
| 96 |
|
|---|
| 97 | db.prepare(`
|
|---|
| 98 | UPDATE sites SET
|
|---|
| 99 | robots_index = ?,
|
|---|
| 100 | title_template = ?,
|
|---|
| 101 | canonical = ?,
|
|---|
| 102 | default_description = ?,
|
|---|
| 103 | og_image_default = ?,
|
|---|
| 104 | og_theme = ?,
|
|---|
| 105 | og_locale = ?,
|
|---|
| 106 | author = ?,
|
|---|
| 107 | twitter = ?,
|
|---|
| 108 | facebook_app_id = ?,
|
|---|
| 109 | google_verification = ?,
|
|---|
| 110 | bing_verification = ?,
|
|---|
| 111 | pinterest_verification = ?,
|
|---|
| 112 | yandex_verification = ?,
|
|---|
| 113 | schema_type = ?,
|
|---|
| 114 | publisher_name = ?,
|
|---|
| 115 | publisher_url = ?,
|
|---|
| 116 | publisher_logo = ?,
|
|---|
| 117 | mb_artist_id = ?, mb_artist_name = ?,
|
|---|
| 118 | updated_at = CURRENT_TIMESTAMP
|
|---|
| 119 | WHERE id = ?
|
|---|
| 120 | // De MusicBrainz-koppeling (shaer-mbz). Alleen een echte MBID komt de kolom
|
|---|
| 121 | // in: zonder deze zeef sluipt er een URL of een handle in het veld dat naar
|
|---|
| 122 | // buiten gaat, en het gaat naar TWEE uitgangen -- de JSON-LD en de actor.
|
|---|
| 123 | // Leeg is een geldige keuze; dat is ontkoppelen.
|
|---|
| 124 | const mbRuw = String(f.mb_artist_id || '').trim().toLowerCase();
|
|---|
| 125 | const mbArtistId = isMbid(mbRuw) ? mbRuw : null;
|
|---|
| 126 |
|
|---|
| 127 | `).run(
|
|---|
| 128 | f.robots_index ? 1 : 0,
|
|---|
| 129 | (f.title_template || '{title} — {site}').slice(0, 200),
|
|---|
| 130 | trimOrNull(f.canonical, 200),
|
|---|
| 131 | trimOrNull(f.default_description, 500),
|
|---|
| 132 | trimOrNull(f.og_image_default, 500),
|
|---|
| 133 | (f.og_theme === 'light' || f.og_theme === 'dark') ? f.og_theme : null, // null = auto (follow site theme)
|
|---|
| 134 | trimOrNull(f.og_locale, 32),
|
|---|
| 135 | trimOrNull(f.author, 120),
|
|---|
| 136 | trimOrNull(f.twitter, 64),
|
|---|
| 137 | trimOrNull(f.facebook_app_id, 64),
|
|---|
| 138 | trimOrNull(f.google_verification, 200),
|
|---|
| 139 | trimOrNull(f.bing_verification, 200),
|
|---|
| 140 | trimOrNull(f.pinterest_verification, 200),
|
|---|
| 141 | trimOrNull(f.yandex_verification, 200),
|
|---|
| 142 | schemaType,
|
|---|
| 143 | trimOrNull(f.publisher_name, 200),
|
|---|
| 144 | trimOrNull(f.publisher_url, 200),
|
|---|
| 145 | trimOrNull(f.publisher_logo, 500),
|
|---|
| 146 | mbArtistId,
|
|---|
| 147 | mbArtistId ? (String(f.mb_artist_name || '').trim().slice(0, 200) || null) : null,
|
|---|
| 148 | primary.id,
|
|---|
| 149 | );
|
|---|
| 150 |
|
|---|
| 151 | res.redirect('/admin/seo?success=' + encodeURIComponent('SEO-instellingen opgeslagen'));
|
|---|
| 152 | });
|
|---|
| 153 |
|
|---|
| 154 | export default router;
|
|---|