render('home.html.twig'); } /** * @throws Exception */ #[Route('/newsstand', name: 'newsstand')] public function newsstand(): Response { return $this->render('pages/newsstand.html.twig'); } /** * @throws Exception */ #[Route('/lists', name: 'lists')] public function lists(): Response { return $this->render('pages/lists.html.twig'); } /** * @throws InvalidArgumentException */ #[Route('/latest', name: 'latest')] public function latest() : Response { $cacheKey = 'home-latest-articles'; $latest = $this->redisCache->get($cacheKey, function (ItemInterface $item) { $item->expiresAfter(13600); // about 4 hours // get latest articles $q = new Query(); $q->setSize(12); $q->setSort(['createdAt' => ['order' => 'desc']]); $col = new Collapse(); $col->setFieldname('pubkey'); $q->setCollapse($col); return $this->finder->find($q); }); return $this->render('pages/latest.html.twig', [ 'latest' => $latest ]); } /** * Magazine front page: title, summary, category links, featured list. * @throws InvalidArgumentException */ #[Route('/mag/{mag}', name: 'magazine-index')] public function magIndex(string $mag, RedisCacheService $redisCache) : Response { // redis cache lookup of magazine index by slug $magazine = $redisCache->getMagazineIndex($mag); return $this->render('magazine/magazine-front.html.twig', [ 'magazine' => $magazine, 'mag' => $mag, ]); } /** * @throws InvalidArgumentException */ #[Route('/mag/{mag}/cat/{slug}', name: 'magazine-category')] public function magCategory($mag, $slug, CacheInterface $redisCache, RedisCacheService $redisCacheService, FinderInterface $finder, LoggerInterface $logger): Response { $magazine = $redisCacheService->getMagazineIndex($mag); $catIndex = $redisCache->get('magazine-' . $slug, function (){ throw new Exception('Not found'); }); $list = []; $coordinates = []; // Store full coordinates (kind:author:slug) $category = []; // Extract category metadata and article coordinates foreach ($catIndex->getTags() as $tag) { if ($tag[0] === 'title') { $category['title'] = $tag[1]; } if ($tag[0] === 'summary') { $category['summary'] = $tag[1]; } if ($tag[0] === 'a') { $coordinates[] = $tag[1]; // Store the full coordinate } } if (!empty($coordinates)) { // Extract slugs for elasticsearch query $slugs = array_map(function($coordinate) { $parts = explode(':', $coordinate, 3); return end($parts); }, $coordinates); $slugs = array_filter($slugs); // Remove empty values // First filter to only include articles with the slugs we want $termsQuery = new Terms('slug', array_values($slugs)); // Create a Query object to set the size parameter $query = new Query($termsQuery); $query->setSize(200); // Set size to exceed the number of articles we expect $articles = $finder->find($query); // Create a map of slug => item to remove duplicates $slugMap = []; foreach ($articles as $item) { $slug = $item->getSlug(); if ($slug !== '') { // If the slugMap doesn't contain it yet, add it if (!isset($slugMap[$slug])) { $slugMap[$slug] = $item; } else { // If it already exists, compare created_at timestamps and save newest $existingItem = $slugMap[$slug]; if ($item->getCreatedAt() > $existingItem->getCreatedAt()) { $slugMap[$slug] = $item; } } } } // Find missing coordinates $missingCoordinates = []; foreach ($coordinates as $coordinate) { $parts = explode(':', $coordinate, 3); if (!isset($slugMap[end($parts)])) { $missingCoordinates[] = $coordinate; } } // If we have missing articles, fetch them directly using NostrClient's getArticlesByCoordinates if (!empty($missingCoordinates)) { $logger->info('There were missing articles', [ 'missing' => $missingCoordinates ]); // try { // $nostrArticles = $nostrClient->getArticlesByCoordinates($missingCoordinates); // // foreach ($nostrArticles as $coordinate => $event) { // $parts = explode(':', $coordinate); // if (count($parts) === 3) { // $article = $articleFactory->createFromLongFormContentEvent($event); // // Save article to database for future queries // $nostrClient->saveEachArticleToTheDatabase($article); // // Add to the slugMap // $slugMap[$article->getSlug()] = $article; // } // } // } catch (\Exception $e) { // $logger->error('Error fetching missing articles', [ // 'error' => $e->getMessage() // ]); // } } // Build ordered list based on original coordinates order foreach ($coordinates as $coordinate) { $parts = explode(':', $coordinate,3); if (isset($slugMap[end($parts)])) { $list[] = $slugMap[end($parts)]; } } } return $this->render('pages/category.html.twig', [ 'mag' => $mag, 'magazine' => $magazine, 'list' => $list, 'category' => $category, 'index' => $catIndex ]); } /** * @throws InvalidArgumentException */ #[Route('/mag/{mag}/cat/{cat}/d/{slug}', name: 'magazine-category-article')] public function magArticle($mag, $cat, $slug, RedisCacheService $redisCacheService, CacheItemPoolInterface $articlesCache, EntityManagerInterface $entityManager, Converter $converter, LoggerInterface $logger): Response { $magazine = $redisCacheService->getMagazineIndex($mag); $article = null; // check if an item with same eventId already exists in the db $repository = $entityManager->getRepository(Article::class); // slug might be url encoded, decode it $slug = urldecode($slug); $articles = $repository->findBy(['slug' => $slug]); $revisions = count($articles); if ($revisions === 0) { throw $this->createNotFoundException('The article could not be found'); } if ($revisions > 1) { // sort articles by created at date usort($articles, function ($a, $b) { return $b->getCreatedAt() <=> $a->getCreatedAt(); }); } $article = $articles[0]; $cacheKey = 'article_' . $article->getEventId(); $cacheItem = $articlesCache->getItem($cacheKey); if (!$cacheItem->isHit()) { $cacheItem->set($converter->convertToHTML($article->getContent())); $articlesCache->save($cacheItem); } $key = new Key(); $npub = $key->convertPublicKeyToBech32($article->getPubkey()); $author = $redisCacheService->getMetadata($npub); // set canonical url to this article as article-slug path $canonical = $this->generateUrl('article-slug', [ 'slug' => $article->getSlug() ], 0); return $this->render('pages/article.html.twig', [ 'magazine' => $magazine, 'mag' => $mag, 'article' => $article, 'author' => $author, 'npub' => $npub, 'content' => $cacheItem->get(), 'canEdit' => false, 'canonical' => $canonical ]); } /** * @throws InvalidArgumentException */ #[Route('/list/{slug}', name: 'reading-list')] public function readingList($slug, CacheInterface $redisCache, FinderInterface $finder, LoggerInterface $logger): Response { return new Response('Not implemented yet', 501); } /** * OG Preview endpoint for URLs */ #[Route('/og-preview/', name: 'og_preview', methods: ['POST'])] public function ogPreview(RequestStack $requestStack): Response { $request = $requestStack->getCurrentRequest(); $data = json_decode($request->getContent(), true); $url = $data['url'] ?? null; if (!$url) { return new Response('