diff --git a/components/search/SearchItemNoun.vue b/components/search/SearchItemNoun.vue
new file mode 100644
index 000000000..7247c5e9b
--- /dev/null
+++ b/components/search/SearchItemNoun.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/search.vue b/pages/search.vue
index 3dad62b96..a4e8d141b 100644
--- a/pages/search.vue
+++ b/pages/search.vue
@@ -33,6 +33,7 @@ const searchInput = useTemplateRef('searchInput');
diff --git a/server/api/search.get.ts b/server/api/search.get.ts
index 54a18b1e7..136e4d693 100644
--- a/server/api/search.get.ts
+++ b/server/api/search.get.ts
@@ -7,9 +7,11 @@ import type { MatchInfo, SearchResult } from 'minisearch';
import type { Config } from '~/locale/config.ts';
import { getPosts, type PostMetadata } from '~/server/blog.ts';
+import { getNounEntries } from '~/server/express/nouns.ts';
import { loadSuml, loadSumlFromBase } from '~/server/loader.ts';
import { rootDir } from '~/server/paths.ts';
import { parsePronouns } from '~/src/buildPronoun.ts';
+import { genders, gendersWithNumerus } from '~/src/classes.ts';
import { clearLinkedText } from '~/src/helpers.ts';
import parseMarkdown from '~/src/parseMarkdown.ts';
import { Translator } from '~/src/translator.ts';
@@ -136,6 +138,62 @@ class SearchIndexPronoun extends SearchIndex {
+ TYPE = 'noun' as const;
+
+ constructor() {
+ super(['url', 'title', 'content']);
+ }
+
+ async getDocuments(config: Config): Promise {
+ if (!config.nouns.enabled) {
+ return [];
+ }
+
+ const base = encodeURIComponent(config.nouns.route);
+
+ const db = useDatabase();
+ const nouns = await getNounEntries(db, () => false);
+ return nouns.map((noun, id): SearchDocumentNoun => {
+ const firstWords = genders
+ .filter((gender) => noun[gender])
+ .map((gender) => noun[gender].split('|')[0]);
+ return {
+ id,
+ type: this.TYPE,
+ url: `/${base}?filter=${firstWords[0]}`,
+ title: firstWords.join(' – '),
+ content: gendersWithNumerus
+ .filter((genderWithNumerus) => noun[genderWithNumerus])
+ .map((genderWithNumerus) => noun[genderWithNumerus].replaceAll('|', ', '))
+ .join(' – '),
+ };
+ });
+ }
+
+ override transform(result: SearchResult): SearchResultNoun {
+ const document = this.documents[result.id];
+ const termsByField = getTermsByField(result.match);
+ return {
+ id: document.id,
+ type: document.type,
+ url: document.url,
+ title: highlightMatches(document.title, termsByField.title),
+ content: highlightMatches(document.content, termsByField.content, true),
+ };
+ }
+}
+
interface SearchDocumentBlog extends PostMetadata {
id: number;
type: SearchIndexBlog['TYPE'];
@@ -201,6 +259,7 @@ export default defineEventHandler(async (event) => {
const indices = Object.fromEntries(
[
new SearchIndexPronoun(),
+ new SearchIndexNoun(),
new SearchIndexBlog(),
].map((index) => [index.TYPE, index]),
);
diff --git a/server/express/nouns.ts b/server/express/nouns.ts
index 834890cb5..698c24d4e 100644
--- a/server/express/nouns.ts
+++ b/server/express/nouns.ts
@@ -99,7 +99,7 @@ const selectFragment = (sourcesMap: Record, keyAndFragment: s
const router = Router();
-const getNounEntries = defineCachedFunction(async (db: Database, isGranted: Request['isGranted']) => {
+export const getNounEntries = defineCachedFunction(async (db: Database, isGranted: Request['isGranted']) => {
return await addVersions(db, isGranted, await db.all(SQL`
SELECT n.*, u.username AS author FROM nouns n
LEFT JOIN users u ON n.author_id = u.id