From a6e831db20977c802fb1d3f8c21ec4778f523b2a Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 25 Apr 2026 12:16:50 +0200 Subject: [PATCH] add pagination to articles and categories clean up replies --- .../controllers/comment_reply_controller.js | 30 +------- src/Controller/ArticleController.php | 23 +++++- src/Controller/AuthorController.php | 38 ++++------ src/Controller/DefaultController.php | 7 +- src/Controller/FeaturedAuthorsController.php | 18 ++++- src/Controller/SearchController.php | 34 ++++++++- src/Repository/ArticleRepository.php | 58 ++++++++++++++ src/Repository/FeaturedAuthorRepository.php | 25 +++++++ src/Service/ArticleCommentThreadLoader.php | 39 ++++++++++ src/Service/CommentReplyService.php | 75 +++++++------------ src/Service/MagazineContentService.php | 25 ++++++- templates/pages/author.html.twig | 13 ++++ templates/pages/category.html.twig | 33 +++++++- templates/pages/featured_authors.html.twig | 14 ++++ templates/pages/search.html.twig | 35 ++++++++- 15 files changed, 355 insertions(+), 112 deletions(-) diff --git a/assets/controllers/comment_reply_controller.js b/assets/controllers/comment_reply_controller.js index edc63a4..c75a580 100644 --- a/assets/controllers/comment_reply_controller.js +++ b/assets/controllers/comment_reply_controller.js @@ -13,7 +13,6 @@ export default class extends Controller { articleEventId: String, fragmentUrl: String, refreshAfter: { type: Boolean, default: true }, - blurbLabel: String, expectedTags: Array, parentKind: Number, parentId: String, @@ -68,16 +67,12 @@ export default class extends Controller { return; } this.setHint('Preparing event…'); - // `nostr-tools` entry pulls @noble/curves (bare spec → breaks in AssetMapper). NIP-19 only needs bech32 helpers. - const { naddrEncode, neventEncode } = await import('nostr-tools/nip19'); - const link = this.buildParentBech32(naddrEncode, neventEncode); - // NIP-22 quote line: must still mention nostr:… for server validation; UI strips this (see formatReplyBlurbForDisplay). - const blurb = `> Replying to **${this.blurbLabelValue}** (nostr:${link})\n\n`; const unsigned = { kind: 1111, created_at: Math.floor(Date.now() / 1000), tags: this._tags, - content: blurb + text, + // Keep user-authored content clean; reply context is encoded in NIP-22 tags. + content: text, }; let signed; try { @@ -138,27 +133,6 @@ export default class extends Controller { return typeof window.nostr !== 'undefined' && typeof window.nostr.signEvent === 'function'; } - /** - * @param {function(object): string} naddrEncode - * @param {function(object): string} neventEncode - */ - buildParentBech32(naddrEncode, neventEncode) { - const allZero = /^0{64}$/.test(this.parentIdValue); - const parts = (this.expectedCoordinateValue || '').split(':'); - const k = parts[0] ? parseInt(parts[0], 10) : 30023; - const pub = parts[1] || this.authorPubkeyValue; - const d = parts[2] || ''; - if (allZero && d !== '') { - return naddrEncode({ kind: k, pubkey: pub, identifier: d, relays: [] }); - } - return neventEncode({ - id: this.parentIdValue, - kind: this.parentKindValue, - pubkey: this.authorPubkeyValue, - relays: [], - }); - } - /** * Reload the section HTML from the article comments fragment. After publishing, relays can lag; * if `expectedEventIdHex` is set, re-fetch with backoff until the new note appears (or a cap is hit). diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index f74e9c2..1b7f3e4 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -569,16 +569,25 @@ class ArticleController extends AbstractController } /** - * Display latest 20 community articles + * Display latest community articles (paginated). */ #[Route('/articles', name: 'articles')] - public function latestArticles(EntityManagerInterface $entityManager): Response + public function latestArticles(Request $request, EntityManagerInterface $entityManager): Response { set_time_limit(300); // 5 minutes ini_set('max_execution_time', '300'); - $articles = $entityManager->getRepository(Article::class) - ->findBy([], ['createdAt' => 'DESC'], 20); + $perPage = 25; + $page = max(1, $request->query->getInt('page', 1)); + $offset = ($page - 1) * $perPage; + $repo = $entityManager->getRepository(Article::class); + $total = $repo->count([]); + $lastPage = max(1, (int) ceil($total / $perPage)); + if ($page > $lastPage) { + $page = $lastPage; + $offset = ($page - 1) * $perPage; + } + $articles = $repo->findBy([], ['createdAt' => 'DESC'], $perPage, $offset); $category = (object) [ 'title' => 'Community Articles', @@ -589,6 +598,12 @@ class ArticleController extends AbstractController 'category' => $category, 'list' => $articles, 'sync_slug' => '', + 'pagination' => [ + 'page' => $page, + 'per_page' => $perPage, + 'total' => $total, + 'last_page' => $lastPage, + ], ]); } diff --git a/src/Controller/AuthorController.php b/src/Controller/AuthorController.php index 3315224..b72ab15 100644 --- a/src/Controller/AuthorController.php +++ b/src/Controller/AuthorController.php @@ -14,6 +14,7 @@ use App\Service\ProfilePaymentLinksBuilder; use Exception; use swentel\nostr\Key\Key; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -24,6 +25,7 @@ class AuthorController extends AbstractController */ #[Route('/p/{npub}', name: 'author-profile', requirements: ['npub' => '^npub1.*'])] public function index( + Request $request, $npub, NostrClient $nostrClient, CacheService $cacheService, @@ -44,29 +46,15 @@ class AuthorController extends AbstractController $bundle = $cacheService->getMetadataBundle($npub); $author = $bundle['content']; $kind0Tags = $bundle['kind0_tags']; - // Retrieve long-form content for the author - try { - $list = $nostrClient->getLongFormContentForPubkey($npub); - } catch (Exception $e) { - $list = []; + $perPage = 25; + $page = max(1, $request->query->getInt('page', 1)); + $total = $articleRepository->countByPubkey($pubkey); + $lastPage = max(1, (int) ceil($total / $perPage)); + if ($page > $lastPage) { + $page = $lastPage; } - - // Also look for articles in the database by pubkey - $dbArticles = $articleRepository->findByPubkey($pubkey, 25); - $list = array_merge($list, $dbArticles); - - $articles = []; - // Deduplicate by slugs - foreach ($list as $item) { - if (!key_exists((string) $item->getSlug(), $articles)) { - $articles[(string) $item->getSlug()] = $item; - } - } - - // Sort articles by date - usort($articles, function ($a, $b) { - return $b->getCreatedAt() <=> $a->getCreatedAt(); - }); + $offset = ($page - 1) * $perPage; + $articles = $articleRepository->findByPubkeyPaginated($pubkey, $perPage, $offset); $kind10133 = []; try { @@ -92,6 +80,12 @@ class AuthorController extends AbstractController 'profile_websites' => $profileIdentityLinks->buildWebsites($author, $kind0Tags), 'profile_nip05' => $profileNip05, 'profile_payment_links' => $profilePaymentLinks->buildPaymentRows($author, $kind0Tags, $extraPayto), + 'pagination' => [ + 'page' => $page, + 'per_page' => $perPage, + 'total' => $total, + 'last_page' => $lastPage, + ], ]); } diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index e4a2cdd..951d872 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -8,6 +8,7 @@ use App\Service\MagazineContentService; use Exception; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -27,13 +28,15 @@ class DefaultController extends AbstractController } #[Route('/cat/{slug}', name: 'magazine-category')] - public function magCategory(string $slug): Response + public function magCategory(Request $request, string $slug): Response { - $data = $this->magazineContent->getCategoryPageData($slug); + $page = max(1, $request->query->getInt('page', 1)); + $data = $this->magazineContent->getCategoryPageData($slug, $page, 25); return $this->render('pages/category.html.twig', [ 'list' => $data['list'], 'category' => $data['category'], + 'pagination' => $data['pagination'], 'sync_slug' => $slug, ]); } diff --git a/src/Controller/FeaturedAuthorsController.php b/src/Controller/FeaturedAuthorsController.php index c275498..41fb794 100644 --- a/src/Controller/FeaturedAuthorsController.php +++ b/src/Controller/FeaturedAuthorsController.php @@ -12,6 +12,7 @@ use App\Service\ProfilePaymentLinksBuilder; use swentel\nostr\Key\Key; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -22,6 +23,7 @@ final class FeaturedAuthorsController extends AbstractController { #[Route('/featured-authors', name: 'featured_authors', methods: ['GET'])] public function index( + Request $request, FeaturedAuthorRepository $featuredAuthorRepository, CacheService $cacheService, NostrClient $nostrClient, @@ -31,8 +33,16 @@ final class FeaturedAuthorsController extends AbstractController ): Response { $domain = trim((string) $params->get('nip05_domain')); $keys = new Key(); + $perPage = 25; + $page = max(1, $request->query->getInt('page', 1)); + $total = $featuredAuthorRepository->countListed(); + $lastPage = max(1, (int) ceil($total / $perPage)); + if ($page > $lastPage) { + $page = $lastPage; + } + $offset = ($page - 1) * $perPage; $authors = []; - foreach ($featuredAuthorRepository->findAllListedOrderByLocalPart() as $fa) { + foreach ($featuredAuthorRepository->findListedOrderByLocalPartPaginated($perPage, $offset) as $fa) { $npub = $keys->convertPublicKeyToBech32($fa->getPubkeyHex()); $bundle = $cacheService->getMetadataBundle($npub); $author = $bundle['content']; @@ -54,6 +64,12 @@ final class FeaturedAuthorsController extends AbstractController return $this->render('pages/featured_authors.html.twig', [ 'authors' => $authors, 'nip05_domain' => $domain, + 'pagination' => [ + 'page' => $page, + 'per_page' => $perPage, + 'total' => $total, + 'last_page' => $lastPage, + ], ]); } } diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php index d5edc3a..24a3ca9 100644 --- a/src/Controller/SearchController.php +++ b/src/Controller/SearchController.php @@ -4,15 +4,43 @@ declare(strict_types=1); namespace App\Controller; +use App\Repository\ArticleRepository; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; class SearchController extends AbstractController { - #[Route('/search')] - public function index(): Response + #[Route('/search', name: 'search', methods: ['GET'])] + public function index(Request $request, ArticleRepository $articleRepository): Response { - return $this->render('pages/search.html.twig'); + $query = trim((string) $request->query->get('q', '')); + $perPage = 25; + $page = max(1, $request->query->getInt('page', 1)); + $total = 0; + $results = []; + $lastPage = 1; + + if ($query !== '') { + $total = $articleRepository->countSearchArticles($query); + $lastPage = max(1, (int) ceil($total / $perPage)); + if ($page > $lastPage) { + $page = $lastPage; + } + $offset = ($page - 1) * $perPage; + $results = $articleRepository->searchArticles($query, $perPage, $offset); + } + + return $this->render('pages/search.html.twig', [ + 'query' => $query, + 'results' => $results, + 'pagination' => [ + 'page' => $page, + 'per_page' => $perPage, + 'total' => $total, + 'last_page' => $lastPage, + ], + ]); } } diff --git a/src/Repository/ArticleRepository.php b/src/Repository/ArticleRepository.php index 6261945..dba0d01 100644 --- a/src/Repository/ArticleRepository.php +++ b/src/Repository/ArticleRepository.php @@ -54,6 +54,42 @@ class ArticleRepository extends ServiceEntityRepository ->getResult(); } + public function countSearchArticles(string $query): int + { + $qb = $this->createQueryBuilder('a') + ->select('COUNT(a.id)'); + + $searchTerms = explode(' ', trim($query)); + $conditions = $qb->expr()->orX(); + + foreach ($searchTerms as $index => $term) { + $term = trim($term); + if (empty($term)) { + continue; + } + + $paramName = 'term' . $index; + $termCondition = $qb->expr()->orX( + $qb->expr()->like('a.title', ':' . $paramName), + $qb->expr()->like('a.content', ':' . $paramName), + $qb->expr()->like('a.summary', ':' . $paramName) + ); + $conditions->add($termCondition); + $qb->setParameter($paramName, '%' . $term . '%'); + } + + if (\count($conditions->getParts()) === 0) { + return 0; + } + + return (int) $qb + ->where($conditions) + ->andWhere('a.content IS NOT NULL') + ->andWhere('LENGTH(a.content) > 250') + ->getQuery() + ->getSingleScalarResult(); + } + /** * List-card fields only: avoids loading `content` / `raw` (can be very large) for home/category featured rows. * @@ -169,6 +205,28 @@ class ArticleRepository extends ServiceEntityRepository ->getResult(); } + public function findByPubkeyPaginated(string $pubkey, int $limit, int $offset): array + { + return $this->createQueryBuilder('a') + ->where('a.pubkey = :pubkey') + ->setParameter('pubkey', $pubkey) + ->orderBy('a.createdAt', 'DESC') + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + public function countByPubkey(string $pubkey): int + { + return (int) $this->createQueryBuilder('a') + ->select('COUNT(a.id)') + ->where('a.pubkey = :pubkey') + ->setParameter('pubkey', $pubkey) + ->getQuery() + ->getSingleScalarResult(); + } + /** * Published or archived long-form rows for sitemap/Atom (may include multiple rows per slug); * callers should dedupe by slug if URLs are slug-only. diff --git a/src/Repository/FeaturedAuthorRepository.php b/src/Repository/FeaturedAuthorRepository.php index 8323537..99e362a 100644 --- a/src/Repository/FeaturedAuthorRepository.php +++ b/src/Repository/FeaturedAuthorRepository.php @@ -51,4 +51,29 @@ class FeaturedAuthorRepository extends ServiceEntityRepository ->getResult(); } + /** + * @return list + */ + public function findListedOrderByLocalPartPaginated(int $limit, int $offset): array + { + return $this->createQueryBuilder('f') + ->where('f.isListed = :t') + ->setParameter('t', true) + ->orderBy('f.localPart', 'ASC') + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getQuery() + ->getResult(); + } + + public function countListed(): int + { + return (int) $this->createQueryBuilder('f') + ->select('COUNT(f.id)') + ->where('f.isListed = :t') + ->setParameter('t', true) + ->getQuery() + ->getSingleScalarResult(); + } + } diff --git a/src/Service/ArticleCommentThreadLoader.php b/src/Service/ArticleCommentThreadLoader.php index 63c6752..fb0ded7 100644 --- a/src/Service/ArticleCommentThreadLoader.php +++ b/src/Service/ArticleCommentThreadLoader.php @@ -302,6 +302,9 @@ final readonly class ArticleCommentThreadLoader $raw = isset($ev->content) ? (string) $ev->content : ''; $split = $this->splitNip22ReplyBlurb($raw); $blurb = $split['blurb']; + if ($blurb === null || trim($blurb) === '') { + $blurb = $this->replyBlurbFromAddressTag($ev); + } if (($blurb === null || trim($blurb) === '') && $id !== '' && isset($parentOf[$id])) { $pid = $parentOf[$id]; if (isset($idToEvent[$pid])) { @@ -363,6 +366,42 @@ final readonly class ArticleCommentThreadLoader return ['blurb' => $first, 'body' => $rest]; } + private function replyBlurbFromAddressTag(object $event): ?string + { + if (!isset($event->tags) || !\is_array($event->tags)) { + return null; + } + foreach ($event->tags as $row) { + if (!\is_array($row) || ($row[0] ?? null) === null || ($row[1] ?? null) === null) { + continue; + } + $name = (string) $row[0]; + if ($name !== 'a' && $name !== 'A') { + continue; + } + $coord = (string) $row[1]; + if ($coord === '') { + continue; + } + $parts = explode(':', $coord, 3); + if (\count($parts) !== 3) { + continue; + } + $kind = ctype_digit((string) $parts[0]) ? (int) $parts[0] : 0; + if (!\in_array($kind, [30023, 30024], true)) { + continue; + } + $dTag = trim((string) $parts[2]); + if ($dTag === '') { + $dTag = $coord; + } + + return '> *'.'Replying to'.'* — '."\n> ".$dTag; + } + + return null; + } + /** * Truncated single-line text from a parent’s content (strips a leading NIP-22 quote block when present), * similar in spirit to Jumble’s {@see ParentNotePreview} + compact ContentPreview. diff --git a/src/Service/CommentReplyService.php b/src/Service/CommentReplyService.php index 96fab56..e2b74b2 100644 --- a/src/Service/CommentReplyService.php +++ b/src/Service/CommentReplyService.php @@ -6,7 +6,6 @@ namespace App\Service; use App\Entity\User; use App\Enum\KindsEnum; -use nostriphant\NIP19\Bech32; use Psr\Log\LoggerInterface; use swentel\nostr\Event\Event as NostrWireEvent; use swentel\nostr\Key\Key; @@ -83,13 +82,8 @@ final readonly class CommentReplyService return ['ok' => false, 'error' => 'Tags must include a/A for this article', 'code' => 400]; } - if (!$this->contentBlurbReferencesParent( - $wire->getContent(), - $expectedCoordinate, - $parentKind, - $parentId - )) { - return ['ok' => false, 'error' => 'Reply must start with a quote line (>) linking the parent via nostr:nevent1 / naddr1 (reply blurb)', 'code' => 400]; + if (!$this->tagsReferenceParent($wire->getTags(), $expectedCoordinate, $parentKind, $parentId)) { + return ['ok' => false, 'error' => 'Tags must reference the selected parent (a/A for article or e/E for comment)', 'code' => 400]; } $rawParentAuthor = isset($payload['parent_author_pubkey']) && \is_string($payload['parent_author_pubkey']) @@ -142,53 +136,42 @@ final readonly class CommentReplyService return false; } - private function contentBlurbReferencesParent( - string $content, + /** + * @param array $tags + */ + private function tagsReferenceParent( + array $tags, string $articleCoordinate, int $parentKind, string $parentIdHex ): bool { - $head = \strlen($content) > 800 ? substr($content, 0, 800) : $content; - if (!str_contains($head, "\n\n")) { - return false; - } - [$blurb] = explode("\n\n", $head, 2); - $blurb = trim($blurb); - if ($blurb === '' || !str_starts_with($blurb, '>')) { - return false; - } - if (!preg_match('/nostr:(nevent1[0-9a-z]+|naddr1[0-9a-z]+|note1[0-9a-z]+)/i', $blurb, $m)) { - return false; - } - try { - $decoded = new Bech32($m[1]); - } catch (\Throwable) { - return false; - } - if ($decoded->type === 'nevent') { - $id = $decoded->data->id ?? null; - - return \is_string($id) && 64 === \strlen($id) && ctype_xdigit($id) && hash_equals($parentIdHex, $id); - } - if ($decoded->type === 'note') { - $id = $decoded->data->identifier ?? null; + if (\in_array($parentKind, [KindsEnum::LONGFORM->value, KindsEnum::LONGFORM_DRAFT->value], true)) { + foreach ($tags as $row) { + if (!\is_array($row) || ($row[0] ?? null) === null) { + continue; + } + $n = (string) $row[0]; + if (($n === 'a' || $n === 'A') && ($row[1] ?? '') === $articleCoordinate) { + return true; + } + } - return \is_string($id) && 64 === \strlen($id) && ctype_xdigit($id) && hash_equals($parentIdHex, $id); + return false; } - if ($decoded->type === 'naddr') { - $d = $decoded->data; - $coord = $d->kind.':'.$d->pubkey.':'.$d->identifier; - if (!\in_array($parentKind, [KindsEnum::LONGFORM->value, KindsEnum::LONGFORM_DRAFT->value], true)) { - return false; - } - if (!hash_equals($articleCoordinate, $coord)) { - return false; + if ($parentKind === KindsEnum::COMMENTS->value) { + foreach ($tags as $row) { + if (!\is_array($row) || ($row[0] ?? null) === null) { + continue; + } + $n = (string) $row[0]; + if (($n === 'e' || $n === 'E') && \is_string($row[1] ?? null) && hash_equals($parentIdHex, (string) $row[1])) { + return true; + } } - $zero = str_repeat('0', 64); - return hash_equals($parentIdHex, $zero); + return false; } - return false; + return true; } } diff --git a/src/Service/MagazineContentService.php b/src/Service/MagazineContentService.php index 3838cc3..8ed900f 100644 --- a/src/Service/MagazineContentService.php +++ b/src/Service/MagazineContentService.php @@ -196,9 +196,13 @@ final class MagazineContentService * Category listing from the persisted 30040 index and DB only. Does not call relays. * Rows come from MySQL only; run `app:prewarm` to sync new `a` tags and replaceable revisions. * - * @return array{list: list
, category: array{title: string, summary: string}} + * @return array{ + * list: list
, + * category: array{title: string, summary: string}, + * pagination: array{page: int, per_page: int, total: int, last_page: int} + * } */ - public function getCategoryPageData(string $slug): array + public function getCategoryPageData(string $slug, int $page = 1, int $perPage = 25): array { $this->warmCategoryIndexIfMissing($slug); $catIndex = $this->store->getCategory($slug); @@ -256,9 +260,24 @@ final class MagazineContentService $category['title'] = $category['title'] ?? ''; $category['summary'] = $category['summary'] ?? ''; + $perPage = max(1, $perPage); + $page = max(1, $page); + $total = \count($list); + $lastPage = max(1, (int) \ceil($total / $perPage)); + if ($page > $lastPage) { + $page = $lastPage; + } + $offset = ($page - 1) * $perPage; + return [ - 'list' => $list, + 'list' => \array_slice($list, $offset, $perPage), 'category' => $category, + 'pagination' => [ + 'page' => $page, + 'per_page' => $perPage, + 'total' => $total, + 'last_page' => $lastPage, + ], ]; } diff --git a/templates/pages/author.html.twig b/templates/pages/author.html.twig index 6513cb8..6887620 100644 --- a/templates/pages/author.html.twig +++ b/templates/pages/author.html.twig @@ -13,6 +13,19 @@
+ {% if pagination is defined and pagination.last_page > 1 %} + {% set _page = pagination.page|default(1) %} + {% set _last = pagination.last_page|default(1) %} + + {% endif %} {% endblock %} diff --git a/templates/pages/category.html.twig b/templates/pages/category.html.twig index f459d3e..b307a84 100644 --- a/templates/pages/category.html.twig +++ b/templates/pages/category.html.twig @@ -12,13 +12,21 @@ {% set _title = category.title|default('') %} {% set _summary = category.summary|default('')|striptags|u.truncate(159, '…') %} {% set _og_image = absolute_url(asset('og-image.jpg')) %} + {% set _is_articles_route = app.request.attributes.get('_route') == 'articles' %} + {% set _is_category_route = app.request.attributes.get('_route') == 'magazine-category' %} + {% set _articles_page = app.request.query.getInt('page', 1) %} + {% set _articles_url = _articles_page > 1 ? url('articles', { page: _articles_page }) : url('articles') %} + {% set _category_slug = sync_slug|default(app.request.attributes.get('slug')) %} + {% set _category_page = app.request.query.getInt('page', 1) %} + {% set _category_url = _category_page > 1 ? url('magazine-category', {slug: _category_slug, page: _category_page}) : url('magazine-category', {slug: _category_slug}) %} + {% set _canonical_url = _is_articles_route ? _articles_url : (_is_category_route ? _category_url : url('magazine-category', {slug: _category_slug})) %} - + - + @@ -39,6 +47,27 @@
+{% if pagination is defined and pagination.last_page > 1 %} + {% set _page = pagination.page|default(1) %} + {% set _last = pagination.last_page|default(1) %} + {% set _is_articles_route = app.request.attributes.get('_route') == 'articles' %} + {% set _slug = sync_slug|default(app.request.attributes.get('slug')) %} + {% set _prev_url = _is_articles_route + ? path('articles', _page > 2 ? { page: _page - 1 } : {}) + : path('magazine-category', _page > 2 ? { slug: _slug, page: _page - 1 } : { slug: _slug }) %} + {% set _next_url = _is_articles_route + ? path('articles', { page: _page + 1 }) + : path('magazine-category', { slug: _slug, page: _page + 1 }) %} + +{% endif %} {% endblock %} {% block aside %} diff --git a/templates/pages/featured_authors.html.twig b/templates/pages/featured_authors.html.twig index 30f04b3..7329906 100644 --- a/templates/pages/featured_authors.html.twig +++ b/templates/pages/featured_authors.html.twig @@ -35,6 +35,20 @@ {% else %}

No featured authors are listed yet. They appear when authors are added to magazine category indices and synced.

{% endfor %} + + {% if pagination is defined and pagination.last_page > 1 %} + {% set _page = pagination.page|default(1) %} + {% set _last = pagination.last_page|default(1) %} + + {% endif %} {% endblock %} diff --git a/templates/pages/search.html.twig b/templates/pages/search.html.twig index 05a3cdd..1b2b245 100644 --- a/templates/pages/search.html.twig +++ b/templates/pages/search.html.twig @@ -4,5 +4,38 @@ {% endblock %} {% block body %} - +
+
+ +
+ + {% if results|default([]) is not empty %} + + {% elseif query|default('') is not empty %} +

{{ 'text.noResults'|trans }}

+ {% endif %} + + {% if pagination is defined and pagination.last_page > 1 %} + {% set _page = pagination.page|default(1) %} + {% set _last = pagination.last_page|default(1) %} + {% set _query = query|default('') %} + + {% endif %} +
{% endblock %}