Browse Source

clean up changes

imwald
Silberengel 2 months ago
parent
commit
8b397fb34c
  1. 10
      assets/controllers/article_comments_controller.js
  2. 47
      assets/styles/app.css
  3. 29
      src/Controller/ArticleController.php
  4. 7
      src/Service/ArticleCommentThreadLoader.php
  5. 43
      src/Service/NostrClient.php
  6. 127
      src/Twig/Components/SearchComponent.php
  7. 57
      templates/components/Molecules/ArticleReplyComposer.html.twig
  8. 19
      templates/components/Molecules/Pagination.html.twig
  9. 61
      templates/components/Organisms/Comments.html.twig
  10. 29
      templates/components/SearchComponent.html.twig
  11. 2
      templates/components/UserMenu.html.twig
  12. 2
      templates/pages/article.html.twig
  13. 18
      templates/pages/author.html.twig
  14. 33
      templates/pages/category.html.twig
  15. 18
      templates/pages/featured_authors.html.twig
  16. 18
      templates/pages/search.html.twig

10
assets/controllers/article_comments_controller.js

@ -12,6 +12,7 @@ export default class extends Controller { @@ -12,6 +12,7 @@ export default class extends Controller {
static targets = ['container'];
connect() {
this.partialReloads = 0;
this.boundOnAuth = this.onAuthChanged.bind(this);
window.addEventListener('unfold:auth-changed', this.boundOnAuth);
if (!this.hasContainerTarget || !this.urlValue) {
@ -67,6 +68,15 @@ export default class extends Controller { @@ -67,6 +68,15 @@ export default class extends Controller {
return;
}
this.containerTarget.innerHTML = html;
const isPartial = /data-comments-partial="1"/.test(html);
if (isPartial && this.partialReloads < 2) {
this.partialReloads += 1;
window.setTimeout(() => {
if (this.hasContainerTarget) {
void this.load();
}
}, 1200);
}
const ms = Math.round(performance.now() - t0);
if (attempt > 1) {
console.info(`[article-comments] fragment OK in ${ms}ms (after ${attempt} attempts)`, this.urlValue);

47
assets/styles/app.css

@ -954,3 +954,50 @@ a:focus-visible { @@ -954,3 +954,50 @@ a:focus-visible {
height: 40px;
}
}
.pager {
margin-top: 1.25rem;
display: flex;
justify-content: center;
width: 100%;
}
.pager__inner {
width: min(100%, 36rem);
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
background: var(--color-bg-light);
}
.pager__status {
min-width: 8rem;
text-align: center;
}
.pager__btn {
min-width: 6.5rem;
text-align: center;
}
.pager__btn.is-disabled {
opacity: 0.55;
pointer-events: none;
}
@media (max-width: 640px) {
.pager__inner {
width: 100%;
gap: 0.5rem;
}
.pager__btn {
min-width: 5.25rem;
}
.pager__status {
min-width: 7rem;
font-size: 0.92rem;
}
}

29
src/Controller/ArticleController.php

@ -383,6 +383,7 @@ class ArticleController extends AbstractController @@ -383,6 +383,7 @@ class ArticleController extends AbstractController
$commentsData = null;
$commentsPreloaded = false;
$commentReplyContext = $this->buildArticleReplyContext($coordinate, $eid, $articleTitle);
$cached = $commentThreadLoader->tryLoadFromCacheOnly($coordinate, $eid);
if (null !== $cached) {
$commentsData = $this->enrichCommentDataWithReplyContext(
@ -391,6 +392,7 @@ class ArticleController extends AbstractController @@ -391,6 +392,7 @@ class ArticleController extends AbstractController
$eid,
$articleTitle
);
$commentReplyContext = $commentsData['comment_reply_context'] ?? $commentReplyContext;
$commentsPreloaded = true;
}
@ -401,9 +403,36 @@ class ArticleController extends AbstractController @@ -401,9 +403,36 @@ class ArticleController extends AbstractController
'content' => $html,
'comments_data' => $commentsData,
'comments_preloaded' => $commentsPreloaded,
'comment_reply_context' => $commentReplyContext,
]);
}
/**
* Base article-level reply context so the top "Reply" button can render before async comments load.
*
* @return array{
* can_publish: bool,
* coordinate: string,
* article_event_id: ?string,
* parent_kind: int,
* rows: array<int, array<string, mixed>>,
* fragment_url: string
* }
*/
private function buildArticleReplyContext(string $coordinate, ?string $articleEventId, string $articleTitle): array
{
$base = [
'list' => [],
'quotes' => [],
'commentLinks' => [],
'quoteLinks' => [],
'processedContent' => [],
];
$enriched = $this->enrichCommentDataWithReplyContext($base, $coordinate, $articleEventId, $articleTitle);
return $enriched['comment_reply_context'];
}
/**
* Fetch complete event to show as preview
* POST data contains an object with request params

7
src/Service/ArticleCommentThreadLoader.php

@ -93,18 +93,20 @@ final readonly class ArticleCommentThreadLoader @@ -93,18 +93,20 @@ final readonly class ArticleCommentThreadLoader
try {
$discussion = $this->cache->get($cacheKey, function (ItemInterface $item) use ($coordinate, $articleEventHexId, $t0): array {
// Prewarm + HTTP should share the same key; 2m expiry caused cold misses during normal use.
$item->expiresAfter(86400);
$this->logger->info('comments.loader.cache_miss', [
'elapsed_since_load_start_ms' => (int) round((microtime(true) - $t0) * 1000),
]);
$tNostr = microtime(true);
// On failure, let this throw: Symfony cache will not store a value, so a prior good thread is not replaced by [].
$out = $this->nostrClient->getArticleDiscussion($coordinate, $articleEventHexId);
$partial = (bool) ($out['partial'] ?? false);
// Partial relay snapshots are intentionally short-lived so the next request can pick up late relays.
$item->expiresAfter($partial ? 15 : 86400);
$this->logger->info('comments.loader.nostr_ok', [
'nostr_elapsed_ms' => (int) round((microtime(true) - $tNostr) * 1000),
'thread' => \count($out['thread'] ?? []),
'quotes' => \count($out['quotes'] ?? []),
'partial' => $partial,
]);
return $out;
@ -193,6 +195,7 @@ final readonly class ArticleCommentThreadLoader @@ -193,6 +195,7 @@ final readonly class ArticleCommentThreadLoader
'commentLinks' => $commentLinks,
'quoteLinks' => $quoteLinks,
'processedContent' => $processedContent,
'comments_partial' => (bool) ($discussion['partial'] ?? false),
];
}

43
src/Service/NostrClient.php

@ -34,6 +34,8 @@ class NostrClient @@ -34,6 +34,8 @@ class NostrClient
/** Extra wall time for {@see bin/nostr_relay_request_worker.php} process vs. WebSocket timeout. */
private const DISCUSSION_WORKER_GRACE_SEC = 5.0;
/** Soft wall-time for parallel discussion collection before returning partial results. */
private const DISCUSSION_PARALLEL_SOFT_DEADLINE_SEC = 3.5;
/**
* Hard cap on unique relay URLs for article discussion. More relays do not help much (indexers duplicate)
@ -1224,7 +1226,7 @@ class NostrClient @@ -1224,7 +1226,7 @@ class NostrClient
* @param string $coordinate kind:pubkey:d-identifier (e.g. longform address)
* @param null|string $rootEventHexId Published article event id (hex) for #e / #q matching
*
* @return array{thread: array<int, object>, quotes: array<int, object>}
* @return array{thread: array<int, object>, quotes: array<int, object>, partial?: bool}
*/
public function getArticleDiscussion(string $coordinate, ?string $rootEventHexId = null): array
{
@ -1315,6 +1317,8 @@ class NostrClient @@ -1315,6 +1317,8 @@ class NostrClient
throw new \RuntimeException('Nostr request failed for article discussion', 0, $e);
}
$respondedRelayCount = \count($response);
$partial = $respondedRelayCount < \count($plannedRelayUrls);
$tParse = microtime(true);
$this->processResponse($response, function ($event) use (&$byId) {
if (\is_object($event) && isset($event->id)) {
@ -1369,9 +1373,12 @@ class NostrClient @@ -1369,9 +1373,12 @@ class NostrClient
$this->logger->info('nostr.article_discussion.done', [
'thread_count' => \count($thread),
'quotes_count' => \count($quotes),
'partial' => $partial,
'responded_relays' => $respondedRelayCount,
'planned_relays' => \count($plannedRelayUrls),
]);
return ['thread' => $thread, 'quotes' => $quotes];
return ['thread' => $thread, 'quotes' => $quotes, 'partial' => $partial];
}
/**
@ -1449,8 +1456,14 @@ class NostrClient @@ -1449,8 +1456,14 @@ class NostrClient
}
$merged = [];
foreach ($procs as $wss => $p) {
$p->wait();
$pending = $procs;
$deadlineAt = microtime(true) + self::DISCUSSION_PARALLEL_SOFT_DEADLINE_SEC;
while ($pending !== []) {
foreach ($pending as $wss => $p) {
if ($p->isRunning()) {
continue;
}
unset($pending[$wss]);
if (!$p->isSuccessful()) {
$err = $p->getErrorOutput();
$this->logger->warning('nostr.article_discussion.relay_worker_failed', [
@ -1458,30 +1471,38 @@ class NostrClient @@ -1458,30 +1471,38 @@ class NostrClient
'exit_code' => $p->getExitCode(),
'stderr' => $err !== '' ? $err : null,
]);
$merged[$wss] = [];
continue;
}
$out = trim($p->getOutput());
if ($out === '') {
$merged[$wss] = [];
continue;
}
$decoded = base64_decode($out, true);
if ($decoded === false || $decoded === '') {
$merged[$wss] = [];
continue;
}
$chunk = unserialize($decoded, ['allowed_classes' => true]);
if (!\is_array($chunk)) {
$merged[$wss] = [];
continue;
}
$merged = array_replace($merged, $chunk);
}
if ($pending === []) {
break;
}
if (microtime(true) >= $deadlineAt) {
foreach ($pending as $wss => $p) {
$this->logger->warning('nostr.article_discussion.relay_worker_soft_timeout', [
'relay' => $wss,
'soft_deadline_sec' => self::DISCUSSION_PARALLEL_SOFT_DEADLINE_SEC,
]);
$p->stop(0.2);
}
break;
}
usleep(100_000);
}
return $merged;
} finally {

127
src/Twig/Components/SearchComponent.php

@ -1,127 +0,0 @@ @@ -1,127 +0,0 @@
<?php
namespace App\Twig\Components;
use App\Repository\ArticleRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\Contracts\Cache\CacheInterface;
#[AsLiveComponent]
final class SearchComponent
{
use DefaultActionTrait;
#[LiveProp(writable: true, useSerializerForHydration: true)]
public string $query = '';
public array $results = [];
public bool $interactive = true;
#[LiveProp]
public int $vol = 0;
#[LiveProp(writable: true)]
public int $page = 1;
#[LiveProp]
public int $resultsPerPage = 12;
private const SESSION_KEY = 'last_search_results';
private const SESSION_QUERY_KEY = 'last_search_query';
public function __construct(
private readonly ArticleRepository $articleRepository,
private readonly TokenStorageInterface $tokenStorage,
private readonly LoggerInterface $logger,
private readonly CacheInterface $cache,
private readonly RequestStack $requestStack
)
{
}
public function mount(): void
{
// Restore search results from session if available and no query provided
if (empty($this->query)) {
$session = $this->requestStack->getSession();
if ($session->has(self::SESSION_QUERY_KEY)) {
$this->query = $session->get(self::SESSION_QUERY_KEY);
$this->results = $session->get(self::SESSION_KEY, []);
$this->logger->info('Restored search results from session for query: ' . $this->query);
}
}
}
#[LiveAction]
public function search(): void
{
$this->logger->info("Query: {$this->query}");
if (empty($this->query)) {
$this->results = [];
$this->clearSearchCache();
return;
}
// Check if the same query exists in session
$session = $this->requestStack->getSession();
if ($session->has(self::SESSION_QUERY_KEY) &&
$session->get(self::SESSION_QUERY_KEY) === $this->query) {
$this->results = $session->get(self::SESSION_KEY, []);
$this->logger->info('Using cached search results for query: ' . $this->query);
return;
}
try {
$this->results = [];
// Use database-based search instead of Elasticsearch
$offset = ($this->page - 1) * $this->resultsPerPage;
$results = $this->articleRepository->searchArticles(
$this->query,
$this->resultsPerPage,
$offset
);
$this->logger->info('Search results count: ' . count($results));
$this->logger->info('Search results: ', ['results' => $results]);
$this->results = $results;
// Cache the search results in session
$this->saveSearchToSession($this->query, $this->results);
} catch (\Exception $e) {
$this->logger->error('Search error: ' . $e->getMessage());
$this->results = [];
}
}
/**
* Save search results to session
*/
private function saveSearchToSession(string $query, array $results): void
{
$session = $this->requestStack->getSession();
$session->set(self::SESSION_QUERY_KEY, $query);
$session->set(self::SESSION_KEY, $results);
$this->logger->info('Saved search results to session for query: ' . $query);
}
/**
* Clear search cache from session
*/
private function clearSearchCache(): void
{
$session = $this->requestStack->getSession();
$session->remove(self::SESSION_QUERY_KEY);
$session->remove(self::SESSION_KEY);
$this->logger->info('Cleared search cache from session');
}
}

57
templates/components/Molecules/ArticleReplyComposer.html.twig

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
{% set ctx = comment_reply_context|default(null) %}
{% if ctx and ctx.can_publish|default(false) %}
{% for row in ctx.rows|default([]) %}
{% if row.mode|default('') == 'article' %}
<div
class="comment-reply comment-reply--article card"
data-controller="comment-reply"
data-comment-reply-publish-url-value="{{ path('comment_reply_publish')|e('html_attr') }}"
data-comment-reply-csrf-value="{{ csrf_token('comment_reply')|e('html_attr') }}"
data-comment-reply-expected-coordinate-value="{{ ctx.coordinate|e('html_attr') }}"
data-comment-reply-article-event-id-value="{{ ctx.article_event_id|default('')|e('html_attr') }}"
data-comment-reply-fragment-url-value="{{ ctx.fragment_url|default('')|e('html_attr') }}"
data-comment-reply-refresh-after-value="1"
data-comment-reply-expected-tags-value='{{ row.expectedTags|default([])|json_encode|e('html_attr') }}'
data-comment-reply-parent-kind-value="{{ row.parentKind|default(0) }}"
data-comment-reply-parent-id-value="{{ row.parentId|default('')|e('html_attr') }}"
data-comment-reply-author-pubkey-value="{{ row.authorPubkey|default('')|e('html_attr') }}"
>
<div class="card-body comment-reply--article__inner">
<div class="comment-reply__toolbar">
<p class="comment-reply__lede text-subtle">Reply to this note on Nostr (kind 1111).</p>
<button
type="button"
class="btn btn-secondary btn-sm comment-reply__toggle"
data-comment-reply-target="toggleBtn"
data-action="click->comment-reply#togglePanel"
aria-expanded="false"
>Reply</button>
</div>
<div class="comment-reply__panel comment-reply__panel--hidden" data-comment-reply-target="panel">
<form
class="comment-reply__form"
data-action="submit->comment-reply#publish"
>
<div class="comment-reply__body">
<label class="visually-hidden" for="comment-reply-article-body">Your reply</label>
<textarea
class="form-control"
id="comment-reply-article-body"
name="body"
rows="4"
required
minlength="1"
placeholder="Write a NIP-22 comment (kind 1111)."
></textarea>
</div>
<div class="comment-reply__actions">
<button class="btn btn-primary" type="submit">Sign &amp; publish</button>
</div>
<p class="comment-reply__hint text-subtle" data-comment-reply-target="hint" aria-live="polite"></p>
</form>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}

19
templates/components/Molecules/Pagination.html.twig

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
{% set _page = page|default(1) %}
{% set _last = last_page|default(1) %}
{% if _last > 1 %}
<nav class="pager" aria-label="{{ aria_label|default('Pagination') }}">
<div class="pager__inner">
{% if prev_url is not empty %}
<a class="btn btn-outline-secondary pager__btn" href="{{ prev_url }}">Newer</a>
{% else %}
<span class="btn btn-outline-secondary pager__btn is-disabled" aria-disabled="true">Newer</span>
{% endif %}
<span class="pager__status text-subtle">Page {{ _page }} of {{ _last }}</span>
{% if next_url is not empty %}
<a class="btn btn-outline-secondary pager__btn" href="{{ next_url }}">Older</a>
{% else %}
<span class="btn btn-outline-secondary pager__btn is-disabled" aria-disabled="true">Older</span>
{% endif %}
</div>
</nav>
{% endif %}

61
templates/components/Organisms/Comments.html.twig

@ -1,64 +1,5 @@ @@ -1,64 +1,5 @@
{% set ctx = comment_reply_context|default(null) %}
{# `rows` can be non-empty for nested replies when the article-level NIP-22 row is missing; do not require length > 0. #}
{% if ctx and ctx.can_publish|default(false) %}
{% for row in ctx.rows %}
{% if row.mode|default('') == 'article' %}
<div
class="comment-reply comment-reply--article card"
data-controller="comment-reply"
data-comment-reply-publish-url-value="{{ path('comment_reply_publish')|e('html_attr') }}"
data-comment-reply-csrf-value="{{ csrf_token('comment_reply')|e('html_attr') }}"
data-comment-reply-expected-coordinate-value="{{ ctx.coordinate|e('html_attr') }}"
data-comment-reply-article-event-id-value="{{ ctx.article_event_id|default('')|e('html_attr') }}"
data-comment-reply-fragment-url-value="{{ ctx.fragment_url|default('')|e('html_attr') }}"
data-comment-reply-refresh-after-value="1"
data-comment-reply-blurb-label-value="{{ row.blurbLabel|default('Article')|e('html_attr') }}"
data-comment-reply-expected-tags-value='{{ row.expectedTags|default([])|json_encode|e('html_attr') }}'
data-comment-reply-parent-kind-value="{{ row.parentKind|default(0) }}"
data-comment-reply-parent-id-value="{{ row.parentId|default('')|e('html_attr') }}"
data-comment-reply-author-pubkey-value="{{ row.authorPubkey|default('')|e('html_attr') }}"
>
<div class="card-body comment-reply--article__inner">
<div class="comment-reply__toolbar">
<p class="comment-reply__lede text-subtle">Reply to this note on Nostr (kind 1111).</p>
<button
type="button"
class="btn btn-secondary btn-sm comment-reply__toggle"
data-comment-reply-target="toggleBtn"
data-action="click->comment-reply#togglePanel"
aria-expanded="false"
>Reply</button>
</div>
<div class="comment-reply__panel comment-reply__panel--hidden" data-comment-reply-target="panel">
<form
class="comment-reply__form"
data-action="submit->comment-reply#publish"
>
<div class="comment-reply__body">
<label class="visually-hidden" for="comment-reply-article-body">Your reply</label>
<textarea
class="form-control"
id="comment-reply-article-body"
name="body"
rows="4"
required
minlength="1"
placeholder="Write a NIP-22 comment (kind 1111). A quoted parent line is added when you publish."
></textarea>
</div>
<div class="comment-reply__actions">
<button class="btn btn-primary" type="submit">Sign &amp; publish</button>
</div>
<p class="comment-reply__hint text-subtle" data-comment-reply-target="hint" aria-live="polite"></p>
</form>
</div>
</div>
</div>
{% endif %}
{% endfor %}
{% endif %}
<div class="comments">
<div class="comments" data-comments-partial="{{ comments_partial|default(false) ? '1' : '0' }}">
{% for item in list %}
{% set cid = item.id|default('')|lower %}
{% set cpk = item.pubkey|default('') %}

29
templates/components/SearchComponent.html.twig

@ -1,29 +0,0 @@ @@ -1,29 +0,0 @@
<div {{ attributes }}>
{% if interactive %}
<form data-live-action-param="search"
data-action="live#action:prevent">
<label class="search">
<input type="search"
placeholder="{{ 'text.search'|trans }}"
data-model="norender|query"
value="{{ this.query }}"
/>
<button type="submit"><twig:ux:icon name="iconoir:search" class="icon" /></button>
</label>
</form>
<!-- Loading Indicator -->
<div style="text-align: center">
<div class="spinner" data-loading>
<div class="lds-dual-ring"></div>
</div>
</div>
{% endif %}
<!-- Results -->
{% if this.results is not empty %}
<twig:Organisms:CardList :list="this.results" class="article-list" />
{% elseif this.query is not empty %}
<p><small>{{ 'text.noResults'|trans }}</small></p>
{% endif %}
</div>

2
templates/components/UserMenu.html.twig

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
{# <a href="{{ path('editor-create') }}">Write an article</a>#}
{# </li>#}
<li>
<a href="{{ path('app_search_index') }}">{{ 'heading.search'|trans }}</a>
<a href="{{ path('search') }}">{{ 'heading.search'|trans }}</a>
</li>
{# <li>#}
{# <a href="{{ path('app_index_index') }}">{{ 'heading.index'|trans }}</a>#}

2
templates/pages/article.html.twig

@ -124,6 +124,8 @@ @@ -124,6 +124,8 @@
{# </pre>#}
{% set article_coordinate = (article.kind ? article.kind.value : 30023) ~ ':' ~ article.pubkey ~ ':' ~ article.slug %}
{% set comments_query = { coordinate: article_coordinate, title: article.title|default('') }|merge(article.eventId ? { e: article.eventId } : {}) %}
{% set _reply_ctx = comments_data.comment_reply_context|default(comment_reply_context|default(null)) %}
{% include 'components/Molecules/ArticleReplyComposer.html.twig' with { comment_reply_context: _reply_ctx } only %}
<section class="article-comments-async" id="article-comments" aria-label="Comments">
<div
data-controller="article-comments"

18
templates/pages/author.html.twig

@ -16,15 +16,15 @@ @@ -16,15 +16,15 @@
{% if pagination is defined and pagination.last_page > 1 %}
{% set _page = pagination.page|default(1) %}
{% set _last = pagination.last_page|default(1) %}
<nav class="category-pagination mt-3" aria-label="Author articles pagination">
{% if _page > 1 %}
<a class="btn btn-outline-secondary" href="{{ path('author-profile', _page > 2 ? { npub: npub, page: _page - 1 } : { npub: npub }) }}">Newer</a>
{% endif %}
<span class="mx-2 text-subtle">Page {{ _page }} of {{ _last }}</span>
{% if _page < _last %}
<a class="btn btn-outline-secondary" href="{{ path('author-profile', { npub: npub, page: _page + 1 }) }}">Older</a>
{% endif %}
</nav>
{% set _prev_url = _page > 1 ? path('author-profile', _page > 2 ? { npub: npub, page: _page - 1 } : { npub: npub }) : null %}
{% set _next_url = _page < _last ? path('author-profile', { npub: npub, page: _page + 1 }) : null %}
{% include 'components/Molecules/Pagination.html.twig' with {
page: _page,
last_page: _last,
prev_url: _prev_url,
next_url: _next_url,
aria_label: 'Author articles pagination',
} only %}
{% endif %}
</div>
{% endblock %}

33
templates/pages/category.html.twig

@ -18,8 +18,13 @@ @@ -18,8 +18,13 @@
{% 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})) %}
{% set _category_has_slug = (_category_slug|default('')|trim) != '' %}
{% set _category_url = _category_has_slug
? (_category_page > 1
? url('magazine-category', {slug: _category_slug, page: _category_page})
: url('magazine-category', {slug: _category_slug}))
: _articles_url %}
{% set _canonical_url = _is_articles_route ? _articles_url : (_is_category_route and _category_has_slug ? _category_url : _articles_url) %}
<meta property="og:title" content="{{ _title|e('html_attr') }}">
<meta property="og:type" content="website">
<meta property="og:url" content="{{ _canonical_url }}">
@ -52,21 +57,19 @@ @@ -52,21 +57,19 @@
{% 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
{% set _prev_url = _page > 1 ? (_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('magazine-category', _page > 2 ? { slug: _slug, page: _page - 1 } : { slug: _slug })) : null %}
{% set _next_url = _page < _last ? (_is_articles_route
? path('articles', { page: _page + 1 })
: path('magazine-category', { slug: _slug, page: _page + 1 }) %}
<nav class="category-pagination mt-3" aria-label="Articles pagination">
{% if _page > 1 %}
<a class="btn btn-outline-secondary" href="{{ _prev_url }}">Newer</a>
{% endif %}
<span class="mx-2 text-subtle">Page {{ _page }} of {{ _last }}</span>
{% if _page < _last %}
<a class="btn btn-outline-secondary" href="{{ _next_url }}">Older</a>
{% endif %}
</nav>
: path('magazine-category', { slug: _slug, page: _page + 1 })) : null %}
{% include 'components/Molecules/Pagination.html.twig' with {
page: _page,
last_page: _last,
prev_url: _prev_url,
next_url: _next_url,
aria_label: 'Articles pagination',
} only %}
{% endif %}
{% endblock %}

18
templates/pages/featured_authors.html.twig

@ -39,15 +39,15 @@ @@ -39,15 +39,15 @@
{% if pagination is defined and pagination.last_page > 1 %}
{% set _page = pagination.page|default(1) %}
{% set _last = pagination.last_page|default(1) %}
<nav class="category-pagination mt-3" aria-label="Featured authors pagination">
{% if _page > 1 %}
<a class="btn btn-outline-secondary" href="{{ path('featured_authors', _page > 2 ? { page: _page - 1 } : {}) }}">Newer</a>
{% endif %}
<span class="mx-2 text-subtle">Page {{ _page }} of {{ _last }}</span>
{% if _page < _last %}
<a class="btn btn-outline-secondary" href="{{ path('featured_authors', { page: _page + 1 }) }}">Older</a>
{% endif %}
</nav>
{% set _prev_url = _page > 1 ? path('featured_authors', _page > 2 ? { page: _page - 1 } : {}) : null %}
{% set _next_url = _page < _last ? path('featured_authors', { page: _page + 1 }) : null %}
{% include 'components/Molecules/Pagination.html.twig' with {
page: _page,
last_page: _last,
prev_url: _prev_url,
next_url: _next_url,
aria_label: 'Featured authors pagination',
} only %}
{% endif %}
</div>
{% endblock %}

18
templates/pages/search.html.twig

@ -27,15 +27,15 @@ @@ -27,15 +27,15 @@
{% set _page = pagination.page|default(1) %}
{% set _last = pagination.last_page|default(1) %}
{% set _query = query|default('') %}
<nav class="category-pagination mt-3" aria-label="Search pagination">
{% if _page > 1 %}
<a class="btn btn-outline-secondary" href="{{ path('search', _page > 2 ? { q: _query, page: _page - 1 } : { q: _query }) }}">Newer</a>
{% endif %}
<span class="mx-2 text-subtle">Page {{ _page }} of {{ _last }}</span>
{% if _page < _last %}
<a class="btn btn-outline-secondary" href="{{ path('search', { q: _query, page: _page + 1 }) }}">Older</a>
{% endif %}
</nav>
{% set _prev_url = _page > 1 ? path('search', _page > 2 ? { q: _query, page: _page - 1 } : { q: _query }) : null %}
{% set _next_url = _page < _last ? path('search', { q: _query, page: _page + 1 }) : null %}
{% include 'components/Molecules/Pagination.html.twig' with {
page: _page,
last_page: _last,
prev_url: _prev_url,
next_url: _next_url,
aria_label: 'Search pagination',
} only %}
{% endif %}
</div>
{% endblock %}

Loading…
Cancel
Save