29 Commits

Author SHA1 Message Date
Jürgen Mummert 0c18b268ae Fix composer namespace references 2026-03-09 10:50:17 +01:00
Jürgen Mummert 188de3d03f Rename vendor and namespace to mummert 2026-03-09 10:10:13 +01:00
Jürgen Mummert d874fe4274 Add resilient CDN loader for Meilisearch browser client 2026-02-24 12:58:34 +01:00
Jürgen Mummert c790a1c312 Fix browser import for Meilisearch frontend client 2026-02-24 12:52:47 +01:00
Jürgen Mummert 7e757bbb6a Fix: remove search-active body class on reset button click 2026-01-15 09:21:41 +01:00
Jürgen Mummert 9f86a5240d add files uuid 2026-01-12 11:28:22 +01:00
Jürgen Mummert 59b261c333 add files uuid 2026-01-12 11:20:02 +01:00
Jürgen Mummert 0912738528 add files uuid 2026-01-12 11:19:09 +01:00
Jürgen Mummert 8b2c6e6b92 add files uuid 2026-01-12 11:18:02 +01:00
Jürgen Mummert ca1305c9c6 add files uuid 2026-01-12 11:11:35 +01:00
Jürgen Mummert e6e3e9339a add files uuid 2026-01-12 11:07:08 +01:00
Jürgen Mummert b7a5e95c7d add files uuid 2026-01-12 10:59:36 +01:00
Jürgen Mummert bc35527f3f add files uuid 2026-01-12 10:39:09 +01:00
Jürgen Mummert e04dfb2bd4 add files uuid 2026-01-12 10:32:45 +01:00
Jürgen Mummert 6d8d0938f1 add files uuid 2026-01-12 10:26:02 +01:00
Jürgen Mummert 8f9c9cea72 add files uuid 2026-01-12 10:23:08 +01:00
Jürgen Mummert ad532e7b4c add files uuid 2026-01-12 10:19:48 +01:00
Jürgen Mummert 2257178cb6 add files uuid 2026-01-12 10:05:46 +01:00
Jürgen Mummert 2f8eddda36 add files uuid 2026-01-12 10:01:53 +01:00
Jürgen Mummert 5026f615f2 add files uuid 2026-01-12 09:54:45 +01:00
Jürgen Mummert f402e6546a add files uuid 2026-01-12 09:41:54 +01:00
Jürgen Mummert 579f58b614 add files uuid 2026-01-12 09:35:46 +01:00
Jürgen Mummert 17188537bc Debug File Indexing 2026-01-11 19:31:31 +01:00
Jürgen Mummert 3427f6b60b Tika Title encoding 2026-01-11 18:51:38 +01:00
Jürgen Mummert 6e41df002e Tika Title encoding 2026-01-11 18:49:15 +01:00
Jürgen Mummert 838f574574 Tika Title encoding 2026-01-11 18:41:16 +01:00
Jürgen Mummert 8549e4e9da Tika Title encoding 2026-01-11 18:35:00 +01:00
Jürgen Mummert 29f7920cb5 Tika Title encoding 2026-01-11 18:29:25 +01:00
Jürgen Mummert 0c637c2f92 Fix file indexing in Contao 5.6 (inject DBAL connection, add debug logs) 2026-01-11 18:19:40 +01:00
17 changed files with 367 additions and 139 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
{
"name": "mummert-media/contao-meilisearch-bundle",
"name": "mummert/contao-meilisearch-bundle",
"description": "Contao Meilisearch integration bundle",
"type": "contao-bundle",
"license": "MIT",
@@ -12,10 +12,10 @@
},
"autoload": {
"psr-4": {
"MummertMedia\\ContaoMeilisearchBundle\\": "src/"
"Mummert\\ContaoMeilisearchBundle\\": "src/"
}
},
"extra": {
"contao-manager-plugin": "MummertMedia\\ContaoMeilisearchBundle\\ContaoManager\\Plugin"
"contao-manager-plugin": "Mummert\\ContaoMeilisearchBundle\\ContaoManager\\Plugin"
}
}
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\Command;
namespace Mummert\ContaoMeilisearchBundle\Command;
use Contao\CoreBundle\Framework\ContaoFramework;
use Doctrine\DBAL\Connection;
+7 -2
View File
@@ -1,9 +1,10 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\Command;
namespace Mummert\ContaoMeilisearchBundle\Command;
use Contao\CoreBundle\Framework\ContaoFramework;
use Contao\Database;
use Contao\System;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -102,7 +103,11 @@ class MeilisearchFilesParseCommand extends Command
continue;
}
$absolutePath = TL_ROOT . '/' . $normalized;
$root = defined('TL_ROOT')
? TL_ROOT
: System::getContainer()->getParameter('kernel.project_dir') . '/public';
$absolutePath = $root . '/' . $normalized;
if (!is_file($absolutePath)) {
$this->log('File missing, skip', [
+2 -2
View File
@@ -1,8 +1,8 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\Command;
namespace Mummert\ContaoMeilisearchBundle\Command;
use MummertMedia\ContaoMeilisearchBundle\Service\MeilisearchIndexService;
use Mummert\ContaoMeilisearchBundle\Service\MeilisearchIndexService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
+2 -2
View File
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\ContaoManager;
namespace Mummert\ContaoMeilisearchBundle\ContaoManager;
use Contao\CalendarBundle\ContaoCalendarBundle;
use Contao\CoreBundle\ContaoCoreBundle;
@@ -8,7 +8,7 @@ use Contao\ManagerPlugin\Bundle\BundlePluginInterface;
use Contao\ManagerPlugin\Bundle\Config\BundleConfig;
use Contao\ManagerPlugin\Bundle\Parser\ParserInterface;
use Contao\NewsBundle\ContaoNewsBundle;
use MummertMedia\ContaoMeilisearchBundle\ContaoMeilisearchBundle;
use Mummert\ContaoMeilisearchBundle\ContaoMeilisearchBundle;
class Plugin implements BundlePluginInterface
{
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle;
namespace Mummert\ContaoMeilisearchBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\Controller\FrontendModule;
namespace Mummert\ContaoMeilisearchBundle\Controller\FrontendModule;
use Contao\Config;
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\DependencyInjection;
namespace Mummert\ContaoMeilisearchBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+16 -107
View File
@@ -1,20 +1,20 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\EventListener;
namespace Mummert\ContaoMeilisearchBundle\EventListener;
use Contao\Config;
use Contao\System;
use Mummert\ContaoMeilisearchBundle\Service\MeilisearchFileHelper;
class IndexPageListener
{
public function __construct()
{
public function __construct(
private readonly MeilisearchFileHelper $fileHelper,
) {
}
private function debug(string $message, array $context = []): void
{
// Debug bewusst immer aktiv (bis du es wieder entfernst)
// Kontext kurz halten, damit Logs nicht explodieren
$ctx = $context ? ' | ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : '';
error_log('[ContaoMeilisearch][IndexPageListener] ' . $message . $ctx);
}
@@ -76,8 +76,6 @@ class IndexPageListener
$parsed['page']['keywords'] ?? null,
];
$this->debug('Meta: keyword sources', ['sources' => $keywordSources]);
$keywords = [];
foreach ($keywordSources as $src) {
if (!is_string($src) || trim($src) === '') {
@@ -92,32 +90,17 @@ class IndexPageListener
$set['keywords'] = implode(' ', array_unique($keywords));
}
$this->debug('Meta: keywords result', [
'keywords' => $set['keywords'] ?? null,
]);
// IMAGEPATH (UUID)
$searchImage = $parsed['page']['searchimage'] ?? null;
$this->debug('Meta: searchimage candidate', ['searchimage' => $searchImage]);
if (!empty($searchImage)) {
$set['imagepath'] = trim((string) $searchImage);
// IMAGEPATH
if (!empty($parsed['page']['searchimage'] ?? null)) {
$set['imagepath'] = trim((string) $parsed['page']['searchimage']);
}
// STARTDATE
$startDate =
$parsed['event']['startDate']
?? $parsed['news']['startDate']
?? null;
$this->debug('Meta: startDate candidate', ['startDate' => $startDate]);
if (is_numeric($startDate) && (int) $startDate > 0) {
$set['startDate'] = (int) $startDate;
if (is_numeric($parsed['event']['startDate'] ?? null)) {
$set['startDate'] = (int) $parsed['event']['startDate'];
}
// CHECKSUM
try {
$checksumSeed = (string) ($data['checksum'] ?? '');
$checksumSeed .= '|' . ($set['keywords'] ?? '');
$checksumSeed .= '|' . ($set['priority'] ?? '');
@@ -125,44 +108,23 @@ class IndexPageListener
$checksumSeed .= '|' . ($set['startDate'] ?? '');
$set['checksum'] = md5($checksumSeed);
$this->debug('Checksum generated', [
'seed_preview' => substr($checksumSeed, 0, 120) . (strlen($checksumSeed) > 120 ? '…' : ''),
'checksum' => $set['checksum'],
]);
} catch (\Throwable $e) {
$this->debug('Failed to generate checksum', [
'error' => $e->getMessage(),
'class' => $e::class,
]);
}
}
}
/*
* =====================
* DATEI-ERKENNUNG + UPSERT
* DATEI-ERKENNUNG (NUR ERKENNUNG!)
* =====================
*/
if ((int) ($data['protected'] ?? 0) !== 0) {
$this->debug('Abort: protected page', ['protected' => $data['protected'] ?? null]);
return;
}
$indexFiles = (bool) Config::get('meilisearch_index_files');
$this->debug('File indexing setting', [
'meilisearch_index_files' => $indexFiles,
]);
if (!$indexFiles) {
$this->debug('Abort: file indexing disabled');
if (!Config::get('meilisearch_index_files')) {
return;
}
$links = $this->findAllLinks($content);
$this->debug('Links found', ['count' => count($links)]);
$fileLinks = [];
foreach ($links as $link) {
@@ -174,64 +136,15 @@ class IndexPageListener
$this->debug('Indexable file links found', [
'count' => count($fileLinks),
'types' => array_count_values(array_column($fileLinks, 'type')),
]);
if ($fileLinks) {
$db = System::getContainer()->get('database_connection');
$time = time();
foreach ($fileLinks as $file) {
$url = strtok($file['url'], '#');
$path = parse_url($url, PHP_URL_PATH);
$abs = $path ? TL_ROOT . '/' . ltrim($path, '/') : null;
$mtime = ($abs && is_file($abs)) ? filemtime($abs) : 0;
$checksum = md5($url . '|' . $mtime);
$existing = $db->fetchAssociative(
'SELECT id, checksum FROM tl_search_files WHERE url = ?',
[$url]
$this->fileHelper->collect(
$file['url'],
$file['type'],
(int) ($data['pid'] ?? 0)
);
if ($existing) {
$db->update(
'tl_search_files',
[
'tstamp' => $time,
'last_seen' => $time,
'page_id' => (int) ($data['pid'] ?? 0),
'file_mtime' => $mtime,
'checksum' => $checksum,
],
['id' => $existing['id']]
);
$this->debug('File updated', [
'url' => $url,
'checksum' => $checksum,
]);
} else {
$db->insert(
'tl_search_files',
[
'tstamp' => $time,
'last_seen' => $time,
'type' => $file['type'],
'url' => $url,
'title' => $file['linkText'] ?? basename($url),
'page_id' => (int) ($data['pid'] ?? 0),
'file_mtime' => $mtime,
'checksum' => $checksum,
]
);
$this->debug('File inserted', [
'url' => $url,
'checksum' => $checksum,
]);
}
}
}
@@ -281,11 +194,7 @@ class IndexPageListener
private function detectIndexableFileType(string $url): ?string
{
$url = strtok($url, '#');
$parts = parse_url($url);
if (!$parts) {
return null;
}
if (!empty($parts['path'])) {
$ext = strtolower(pathinfo($parts['path'], PATHINFO_EXTENSION));
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\EventListener;
namespace Mummert\ContaoMeilisearchBundle\EventListener;
use Contao\CalendarEventsModel;
use Contao\Config;
+4 -4
View File
@@ -2,24 +2,24 @@ services:
# Alias MUSS vorhanden sein (richtig platziert)
Psr\Container\ContainerInterface: '@service_container'
MummertMedia\ContaoMeilisearchBundle\:
Mummert\ContaoMeilisearchBundle\:
resource: '../../{Command,EventListener,Service}'
autowire: true
autoconfigure: true
MummertMedia\ContaoMeilisearchBundle\EventListener\MeilisearchPageMarkerListener:
Mummert\ContaoMeilisearchBundle\EventListener\MeilisearchPageMarkerListener:
autowire: true
autoconfigure: false
tags:
- { name: contao.hook, hook: outputFrontendTemplate, method: onOutputFrontendTemplate }
MummertMedia\ContaoMeilisearchBundle\EventListener\IndexPageListener:
Mummert\ContaoMeilisearchBundle\EventListener\IndexPageListener:
autowire: true
autoconfigure: false
tags:
- { name: contao.hook, hook: indexPage, method: onIndexPage }
MummertMedia\ContaoMeilisearchBundle\Controller\FrontendModule\MeilisearchSearchController:
Mummert\ContaoMeilisearchBundle\Controller\FrontendModule\MeilisearchSearchController:
autowire: true
autoconfigure: false
tags:
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
use MummertMedia\ContaoMeilisearchBundle\EventListener\MeilisearchPageMarkerListener;
use Mummert\ContaoMeilisearchBundle\EventListener\MeilisearchPageMarkerListener;
$GLOBALS['TL_HOOKS']['outputFrontendTemplate'][] = [
+6 -1
View File
@@ -12,7 +12,8 @@ $GLOBALS['TL_DCA']['tl_search_files'] = [
'url' => 'unique',
'type' => 'index',
'checksum' => 'index',
'last_seen' => 'index', // ⬅️ NEU (für Cleanup-Performance)
'uuid' => 'index',
'last_seen' => 'index',
],
],
],
@@ -63,6 +64,10 @@ $GLOBALS['TL_DCA']['tl_search_files'] = [
'sql' => "mediumtext NULL",
],
'uuid' => [
'sql' => "binary(16) NULL",
],
/*
* md5(url + filemtime)
* → erkennt Änderungen zuverlässig
@@ -60,10 +60,44 @@ Contao 5 Frontend Module Template
</div>
</div>
<script type="module">
import MeiliSearch from 'https://cdn.jsdelivr.net/npm/meilisearch@latest/dist/bundles/meilisearch.esm.js';
<script>
(function () {
const CDN_URLS = [
'https://cdn.jsdelivr.net/npm/meilisearch@0.39.0/dist/bundles/meilisearch.umd.min.js',
'https://unpkg.com/meilisearch@0.39.0/dist/bundles/meilisearch.umd.min.js'
];
document.addEventListener('DOMContentLoaded', () => {
function loadClient(urls, onDone) {
if (typeof MeiliSearch !== 'undefined') {
onDone(true, null);
return;
}
if (!urls.length) {
onDone(false, 'Alle CDN-Quellen fehlgeschlagen (mögliche CSP-Blockierung von script-src).');
return;
}
const url = urls.shift();
const script = document.createElement('script');
script.src = url;
script.async = true;
script.crossOrigin = 'anonymous';
script.onload = () => {
if (typeof MeiliSearch !== 'undefined') {
onDone(true, null);
} else {
loadClient(urls, onDone);
}
};
script.onerror = () => loadClient(urls, onDone);
document.head.appendChild(script);
}
function initSearch() {
const wrapper = document.querySelector('.meilisearch-search');
if (!wrapper) return;
@@ -96,6 +130,10 @@ Contao 5 Frontend Module Template
input.value = '';
results.innerHTML = '';
clear.classList.add('is-hidden');
// ✅ WICHTIG: Suchmodus verlassen
document.body.classList.remove('search-active');
input.focus();
});
@@ -217,7 +255,19 @@ Contao 5 Frontend Module Template
results.appendChild(node);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
loadClient([...CDN_URLS], (ok, reason) => {
if (!ok) {
console.error('[Meilisearch] Browser client konnte nicht geladen werden. ' + reason);
return;
}
initSearch();
});
});
})();
</script>
{% endblock %}
<!-- indexer::continue -->
+259
View File
@@ -0,0 +1,259 @@
<?php
namespace Mummert\ContaoMeilisearchBundle\Service;
use Contao\FilesModel;
use Contao\StringUtil;
use Contao\System;
use Doctrine\DBAL\Connection;
class MeilisearchFileHelper
{
public function __construct(
private readonly Connection $connection,
) {
}
/**
* Zentrale Datei-Verarbeitung
*/
public function collect(string $url, string $type, int $pageId): void
{
$this->log('collect() start', [
'url' => $url,
'type' => $type,
'pageId' => $pageId,
]);
// -------------------------------------------------
// 1. URL normalisieren
// -------------------------------------------------
$cleanUrl = strtok($url, '#');
$parts = parse_url($cleanUrl);
if (!$parts) {
$this->log('Invalid URL, skip');
return;
}
// -------------------------------------------------
// 2. Externe Datei? → skip
// -------------------------------------------------
if (!empty($parts['host'])) {
$currentRequest = System::getContainer()
->get('request_stack')
->getCurrentRequest();
$pageHost = $currentRequest
? parse_url($currentRequest->getSchemeAndHttpHost(), PHP_URL_HOST)
: null;
if ($pageHost && $parts['host'] !== $pageHost) {
$this->log('External file detected, skip', [
'host' => $parts['host'],
]);
return;
}
}
// -------------------------------------------------
// 3. Pfad-Kandidaten sammeln (ohne Annahmen!)
// -------------------------------------------------
$query = [];
if (!empty($parts['query'])) {
parse_str($parts['query'], $query);
}
$pathCandidates = [];
// direkter Pfad
if (!empty($parts['path'])) {
$pathCandidates[] = $parts['path'];
}
// Download-Parameter
foreach (['file', 'f', 'p'] as $param) {
if (!empty($query[$param])) {
$pathCandidates[] = $query[$param];
}
}
// normalisieren
$pathCandidates = array_values(array_unique(array_filter(array_map(
static function ($candidate) {
$candidate = rawurldecode(html_entity_decode((string) $candidate, ENT_QUOTES));
return ltrim($candidate, '/') ?: null;
},
$pathCandidates
))));
$this->log('Path candidates (normalized)', [
'candidates' => $pathCandidates,
]);
// -------------------------------------------------
// 4. FilesModel (DBAFS) auflösen → UUID
// -------------------------------------------------
$fileModel = null;
foreach ($pathCandidates as $candidate) {
// 1) direkt
$model = FilesModel::findByPath($candidate);
if ($model && $model->uuid) {
$fileModel = $model;
$this->log('Resolved via FilesModel (direct)', [
'candidate' => $candidate,
'path' => $model->path,
]);
break;
}
// 2) fallback: files/ davor
if (!str_starts_with($candidate, 'files/')) {
$model = FilesModel::findByPath('files/' . $candidate);
if ($model && $model->uuid) {
$fileModel = $model;
$this->log('Resolved via FilesModel (files/ prefix)', [
'candidate' => $candidate,
'path' => $model->path,
]);
break;
}
}
}
if (!$fileModel) {
$this->log('No Contao file model found, skip', [
'candidates' => $pathCandidates,
]);
return;
}
$normalizedPath = (string) $fileModel->path;
$uuidBin = $fileModel->uuid;
$uuid = StringUtil::binToUuid($uuidBin);
$canonicalUrl = '/' . ltrim($normalizedPath, '/');
$this->log('UUID resolved', [
'path' => $canonicalUrl,
'uuid' => $uuid,
]);
// -------------------------------------------------
// 5. Datei im Filesystem prüfen
// -------------------------------------------------
$projectDir = System::getContainer()->getParameter('kernel.project_dir');
$abs = $projectDir . '/public/' . $normalizedPath;
if (!is_file($abs)) {
$this->log('Resolved model but file missing on filesystem, skip', [
'path' => $normalizedPath,
'abs' => $abs,
]);
return;
}
// -------------------------------------------------
// 6. Redaktionellen Titel aus tl_files.meta
// -------------------------------------------------
$title = null;
$meta = StringUtil::deserialize($fileModel->meta, true);
// 1) bevorzugte Sprache (falls vorhanden)
$lang = $GLOBALS['TL_LANGUAGE'] ?? null;
if ($lang && !empty($meta[$lang]['title'])) {
$title = trim((string) $meta[$lang]['title']);
}
// 2) Fallback: erste verfügbare Sprache
if ($title === null && is_array($meta)) {
foreach ($meta as $langKey => $langMeta) {
if (!empty($langMeta['title'])) {
$title = trim((string) $langMeta['title']);
break;
}
}
}
if ($title) {
$this->log('Title resolved from tl_files', [
'title' => $title,
]);
}
// -------------------------------------------------
// 7. Datei-Infos
// -------------------------------------------------
$mtime = filemtime($abs) ?: 0;
$checksum = md5($normalizedPath . '|' . $mtime);
$now = time();
// -------------------------------------------------
// 8. Upsert über UUID
// -------------------------------------------------
$existing = $this->connection->fetchAssociative(
'SELECT id FROM tl_search_files WHERE uuid = ?',
[$uuidBin]
);
if ($existing) {
$data = [
'tstamp' => $now,
'last_seen' => $now,
'type' => $type,
'url' => $canonicalUrl,
'page_id' => $pageId,
'file_mtime' => $mtime,
'checksum' => $checksum,
];
if ($title !== null) {
$data['title'] = $title;
}
$this->connection->update(
'tl_search_files',
$data,
['id' => $existing['id']]
);
$this->log('File updated by UUID', [
'uuid' => $uuid,
]);
} else {
$this->connection->insert(
'tl_search_files',
[
'tstamp' => $now,
'last_seen' => $now,
'type' => $type,
'url' => $canonicalUrl,
'title' => $title ?? basename($normalizedPath),
'page_id' => $pageId,
'file_mtime' => $mtime,
'checksum' => $checksum,
'uuid' => $uuidBin,
]
);
$this->log('File inserted by UUID', [
'uuid' => $uuid,
]);
}
$this->log('collect() end');
}
// -------------------------------------------------
// Logging
// -------------------------------------------------
private function log(string $message, array $context = []): void
{
$ctx = $context
? ' | ' . json_encode($context, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
: '';
error_log('[ContaoMeilisearch][MeilisearchFileHelper] ' . $message . $ctx);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\Service;
namespace Mummert\ContaoMeilisearchBundle\Service;
use Contao\Config;
use Contao\CoreBundle\Framework\ContaoFramework;
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
namespace MummertMedia\ContaoMeilisearchBundle\Service;
namespace Mummert\ContaoMeilisearchBundle\Service;
use Contao\Config;
use Contao\CoreBundle\Framework\ContaoFramework;