Chat-With-AI / src /modules /search /routes.ts
NathMen12's picture
Upload 55 files
cc27fd7 verified
Raw
History Blame Contribute Delete
3.87 kB
// @ts-nocheck
import type { FastifyInstance } from 'fastify';
import { channelRepository } from '@core/storage/repositories/channels.js';
import { validateInput, searchSchema } from '@core/security/validation.js';
import { formatNumber, formatRelativeTime, getSortField } from '@core/utils/helpers.js';
export async function searchRoutes(app: FastifyInstance) {
// Home page - redirects to channels list
app.get('/', async (request, reply) => {
return reply.redirect('/channels');
});
// Search API endpoint (for HTMX live search)
app.get('/api/search', async (request, reply) => {
const validation = validateInput(searchSchema, request.query);
const params = validation.success ? validation.data : { q: '', sort: 'newest', page: 1, limit: 20 };
const tag = request.query.tag as string | undefined;
const search = params.q;
const sort = params.sort;
const page = params.page;
const limit = Math.min(params.limit, 50);
const offset = (page - 1) * limit;
const { field, order } = getSortField(sort);
const [channels, total] = await Promise.all([
channelRepository.findPublished({
tag,
search,
sortBy: field,
sortOrder: order,
limit,
offset,
}),
channelRepository.countPublished({ ...(tag && { tags: tag }) }),
]);
return reply.send({
channels: channels.map(c => ({
id: c.id,
name: c.name,
description: c.description,
iconUrl: c.iconUrl,
followerCount: c.followerCount,
formattedFollowers: formatNumber(c.followerCount),
voteCount: c.voteCount,
formattedVotes: formatNumber(c.voteCount),
tags: c.tags,
relativeTime: formatRelativeTime(c.createdAt),
ownerUsername: c.ownerUsername,
})),
pagination: {
page,
totalPages: Math.ceil(total / limit),
total,
hasNext: page * limit < total,
hasPrev: page > 1,
},
});
});
// Tag autocomplete
app.get('/api/tags', async (request, reply) => {
const query = (request.query.q as string || '').toLowerCase();
const channels = await channelRepository.findPublished({ limit: 1000 });
const tags = new Set<string>();
channels.forEach(c => {
c.tags.forEach(t => {
if (!query || t.includes(query)) {
tags.add(t);
}
});
});
return reply.send(Array.from(tags).sort().slice(0, 20));
});
// Sitemap
app.get('/sitemap.xml', async (request, reply) => {
const channels = await channelRepository.findPublished({ limit: 50000 });
const baseUrl = 'https://your-domain.com'; // TODO: from config
const urls = [
{ url: baseUrl, changefreq: 'daily', priority: '1.0' },
{ url: `${baseUrl}/channels`, changefreq: 'hourly', priority: '0.9' },
];
channels.forEach(c => {
urls.push({
url: `${baseUrl}/channels/${c.id}`,
lastmod: c.lastScrapedAt || c.updatedAt,
changefreq: 'daily',
priority: '0.7',
});
});
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => ` <url>
<loc>${u.url}</loc>
<lastmod>${u.lastmod ? new Date(u.lastmod).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]}</lastmod>
<changefreq>${u.changefreq}</changefreq>
<priority>${u.priority}</priority>
</url>`).join('\n')}
</urlset>`;
reply.header('Content-Type', 'application/xml');
return reply.send(xml);
});
// Robots.txt
app.get('/robots.txt', async (request, reply) => {
const baseUrl = 'https://your-domain.com'; // TODO: from config
const content = `User-agent: *
Allow: /
Sitemap: ${baseUrl}/sitemap.xml
`;
reply.header('Content-Type', 'text/plain');
return reply.send(content);
});
}