From redaxo-search-it
Provides REDAXO modules for Search It addon: search forms, result outputs, pagination, media/PDF/file search, category filters, autocomplete. Use when building or editing search modules in REDAXO.
How this skill is triggered — by the user, by Claude, or both
Slash command
/redaxo-search-it:search-it-modulesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Search It does not ship ready-made modules. You create them as REDAXO modules (Input + Output) and adapt them to the project.
Search It does not ship ready-made modules. You create them as REDAXO modules (Input + Output) and adapt them to the project.
<fieldset>
<legend>Search form</legend>
<label>Result page article ID</label>
REX_LINK[id=1 widget=1]
</fieldset>
<form class="search_it-form" action="<?= rex_getUrl(REX_LINK_ID[1]) ?>" method="get">
<input type="text" name="search" value="<?= rex_escape(rex_request('search', 'string', '')) ?>" placeholder="Search...">
<button type="submit">Search</button>
</form>
The class="search_it-form" is required if you use the autocomplete/suggest feature. REX_LINK_ID[1] points to the article where the result module sits.
For multilingual projects with Sprog, replace the placeholder:
placeholder="<?= rex_escape(sprogdown('search_placeholder', 'Suchen...')) ?>"
No input needed (or optionally a heading via REX_VALUE[1]).
<?php
use FriendsOfRedaxo\SearchIt\SearchIt;
$searchTerm = rex_request('search', 'string', '');
if ($searchTerm === '') {
return;
}
$search = new SearchIt();
$result = $search->search($searchTerm);
if ($result['count'] > 0) {
echo '<p>' . $result['count'] . ' results for "' . rex_escape($searchTerm) . '"</p>';
echo '<ul class="search-results">';
foreach ($result['hits'] as $hit) {
if ($hit['type'] === 'article') {
$article = rex_article::get($hit['fid'], $hit['clang']);
if ($article) {
echo '<li class="search-results__item">';
echo '<a href="' . rex_getUrl($hit['fid'], $hit['clang']) . '">';
echo rex_escape($article->getName());
echo '</a>';
echo '<p>' . $hit['highlightedtext'] . '</p>';
echo '</li>';
}
}
}
echo '</ul>';
} else {
echo '<p>No results for "' . rex_escape($searchTerm) . '".</p>';
}
Index additional DB columns in the backend (Settings > Additional sources) for richer results:
<?php
use FriendsOfRedaxo\SearchIt\SearchIt;
$searchTerm = rex_request('search', 'string', '');
if ($searchTerm === '') {
return;
}
$search = new SearchIt();
$result = $search->search($searchTerm);
if ($result['count'] === 0) {
echo '<p>No results for "' . rex_escape($searchTerm) . '".</p>';
return;
}
echo '<p>' . $result['count'] . ' results for "' . rex_escape($searchTerm) . '"</p>';
foreach ($result['hits'] as $hit) {
$url = '';
$title = '';
$teaser = $hit['highlightedtext'];
switch ($hit['type']) {
case 'article':
$article = rex_article::get($hit['fid'], $hit['clang']);
if (!$article) continue 2;
$url = rex_getUrl($hit['fid'], $hit['clang']);
$title = $article->getName();
break;
case 'db_column':
if ($hit['table'] === rex::getTable('article')) {
$article = rex_article::get($hit['fid'], $hit['clang']);
if (!$article) continue 2;
$url = rex_getUrl($hit['fid'], $hit['clang']);
$title = $article->getName();
} else {
continue 2; // skip non-article DB hits or handle custom tables
}
break;
case 'file':
$url = rex_url::media($hit['filename']);
$title = $hit['filename'];
break;
default:
continue 2;
}
echo '<div class="search-results__item">';
echo '<h3><a href="' . $url . '">' . rex_escape($title) . '</a></h3>';
echo '<p>' . $teaser . '</p>';
echo '</div>';
}
<?php
use FriendsOfRedaxo\SearchIt\SearchIt;
$searchTerm = rex_request('search', 'string', '');
if ($searchTerm === '') {
return;
}
$page = rex_request('page', 'int', 1);
$perPage = 10;
$search = new SearchIt();
$search->setLimit(($page - 1) * $perPage, $perPage);
$result = $search->search($searchTerm);
$totalPages = (int) ceil($result['count'] / $perPage);
// ... render results ...
// Pagination links
if ($totalPages > 1) {
$currentUrl = rex_getUrl(rex_article::getCurrentId(), rex_clang::getCurrentId());
echo '<nav class="pagination">';
for ($i = 1; $i <= $totalPages; $i++) {
$activeClass = ($i === $page) ? ' class="active"' : '';
echo '<a href="' . $currentUrl . '?search=' . urlencode($searchTerm) . '&page=' . $i . '"' . $activeClass . '>' . $i . '</a> ';
}
echo '</nav>';
}
First configure additional sources in the backend: add rex_media.title, rex_media.filename, rex_media.med_description as indexed columns.
<?php
use FriendsOfRedaxo\SearchIt\SearchIt;
$searchTerm = rex_request('search', 'string', '');
if ($searchTerm === '') {
return;
}
$search = new SearchIt();
$search->searchInDbColumn(rex::getTable('media'), 'title');
$result = $search->search($searchTerm);
if ($result['count'] === 0) {
echo '<p>No media found.</p>';
return;
}
echo '<div class="media-results">';
foreach ($result['hits'] as $hit) {
if ($hit['type'] !== 'db_column' && $hit['type'] !== 'file') {
continue;
}
$filename = $hit['filename'] ?? $hit['values']['filename'] ?? '';
if ($filename === '') {
continue;
}
$media = rex_media::get($filename);
if (!$media) {
continue;
}
echo '<div class="media-results__item">';
if ($media->isImage()) {
echo '<img src="' . rex_url::media($filename) . '" alt="' . rex_escape($media->getTitle()) . '">';
}
echo '<p>' . rex_escape($media->getTitle()) . '</p>';
echo '</div>';
}
echo '</div>';
<fieldset>
<legend>Search settings</legend>
<label>Results per page</label>
<input type="text" name="REX_INPUT_VALUE[1]" value="REX_VALUE[1]" placeholder="10">
</fieldset>
<?php
use FriendsOfRedaxo\SearchIt\SearchIt;
$searchTerm = rex_request('search', 'string', '');
$categoryId = rex_request('category', 'int', 0);
$page = rex_request('page', 'int', 1);
$perPage = (int) ('REX_VALUE[1]' ?: 10);
// Search form with category filter
$categories = rex_category::getRootCategories(true);
?>
<form class="search_it-form" action="<?= rex_getUrl(rex_article::getCurrentId()) ?>" method="get">
<input type="text" name="search" value="<?= rex_escape($searchTerm) ?>" placeholder="Search...">
<select name="category">
<option value="0">All categories</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat->getId() ?>"<?= $cat->getId() === $categoryId ? ' selected' : '' ?>>
<?= rex_escape($cat->getName()) ?>
</option>
<?php endforeach ?>
</select>
<button type="submit">Search</button>
</form>
<?php
if ($searchTerm === '') {
return;
}
$search = new SearchIt();
$search->setLimit(($page - 1) * $perPage, $perPage);
if ($categoryId > 0) {
$search->searchInCategoryTree($categoryId);
}
$result = $search->search($searchTerm);
if ($result['count'] === 0) {
echo '<p>No results.</p>';
// Similarity suggestion
if (!empty($result['simwordsnewsearch'])) {
echo '<p>Did you mean: <a href="?search=' . urlencode($result['simwordsnewsearch']) . '">'
. rex_escape($result['simwordsnewsearch']) . '</a>?</p>';
}
return;
}
echo '<p>' . $result['count'] . ' results</p>';
foreach ($result['hits'] as $hit) {
switch ($hit['type']) {
case 'article':
case 'db_column':
$fid = (int) $hit['fid'];
$article = rex_article::get($fid, $hit['clang']);
if (!$article) continue 2;
$url = rex_getUrl($fid, $hit['clang']);
$title = $article->getName();
break;
case 'file':
$url = rex_url::media($hit['filename']);
$title = $hit['filename'];
if (in_array($hit['fileext'], ['pdf', 'doc', 'docx'])) {
$title .= ' (Download)';
}
break;
default:
continue 2;
}
echo '<div class="search-results__item">';
echo '<h3><a href="' . $url . '">' . rex_escape($title) . '</a></h3>';
echo '<p>' . $hit['highlightedtext'] . '</p>';
echo '</div>';
}
// Pagination
$totalPages = (int) ceil($result['count'] / $perPage);
if ($totalPages > 1) {
$baseUrl = rex_getUrl(rex_article::getCurrentId(), rex_clang::getCurrentId());
$params = '?search=' . urlencode($searchTerm);
if ($categoryId > 0) {
$params .= '&category=' . $categoryId;
}
echo '<nav class="pagination">';
for ($i = 1; $i <= $totalPages; $i++) {
$activeClass = ($i === $page) ? ' class="active"' : '';
echo '<a href="' . $baseUrl . $params . '&page=' . $i . '"' . $activeClass . '>' . $i . '</a> ';
}
echo '</nav>';
}
When using the URL addon (v2.0+), enable URL indexing in the backend settings. URL addon hits have type === 'url':
case 'url':
// Reconstruct URL from URL addon profile
$urlData = rex_sql::factory()
->setTable(rex::getTable('url_generator_url'))
->setWhere('md5' , $hit['fid'])
->select();
if ($urlData->getRows() > 0) {
$url = $urlData->getValue('url');
$title = $urlData->getValue('seo_title') ?: $urlData->getValue('url');
}
break;
Enable in backend settings (Settings > Suggest). The search form needs class="search_it-form" and input name="search". Then include the generated JS before </body> in your template:
<?php
if (rex_addon::get('search_it')->isAvailable()) {
echo rex_addon::get('search_it')->getProperty('suggest_js', '');
}
?>
REX_VALUE[id=1] as article ID directly instead of REX_LINK_ID[1] – use the link widget so editors pick the target article visually.return; or wrapping in if when search term is empty – the module output runs on every page load, not just when searching.$hit['highlightedtext'] through rex_escape() – the highlighted text contains intentional HTML (<mark> tags). Escaping it strips the highlighting. Only escape user-provided values like article names.rex_getUrl() – breaks when URL structure changes.$result['count'] but forgetting to call setLimit() – you get all results on every page.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub friendsofredaxo/claude-marketplace --plugin redaxo-search-it