Browse Source

bug-fix comments

adjust footer
imwald
Silberengel 5 days ago
parent
commit
acb0c25ec2
  1. 12
      assets/controllers/article_comments_controller.js
  2. 25
      assets/styles/app.css
  3. 67
      assets/styles/layout.css
  4. 3
      config/services.yaml
  5. 244
      src/Controller/ArticleController.php
  6. 9
      src/Controller/DefaultController.php
  7. 71
      src/Service/ArticleCommentThreadLoader.php
  8. 21
      src/Twig/Components/Footer.php
  9. 38
      templates/components/Footer.html.twig
  10. 12
      templates/home.html.twig
  11. 12
      templates/pages/article.html.twig

12
assets/controllers/article_comments_controller.js

@ -6,6 +6,7 @@ import { Controller } from '@hotwired/stimulus'; @@ -6,6 +6,7 @@ import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static values = {
url: String,
preloaded: { type: Boolean, default: false },
};
static targets = ['container'];
@ -14,6 +15,17 @@ export default class extends Controller { @@ -14,6 +15,17 @@ export default class extends Controller {
if (!this.hasContainerTarget || !this.urlValue) {
return;
}
if (this.preloadedValue) {
const run = () => {
void this.load();
};
if (typeof requestIdleCallback !== 'undefined') {
requestIdleCallback(run, { timeout: 15_000 });
} else {
setTimeout(run, 2_000);
}
return;
}
void this.load();
}

25
assets/styles/app.css

@ -848,31 +848,6 @@ a:focus-visible { @@ -848,31 +848,6 @@ a:focus-visible {
outline-offset: 2px;
}
.home-subscribe {
margin-bottom: 1.75rem;
padding: 1rem 0 0;
border-bottom: 1px solid var(--color-border, rgba(0, 0, 0, 0.08));
}
.home-subscribe__title {
font-size: 1.15rem;
margin: 0 0 0.35rem;
}
.home-subscribe__hint {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--color-text);
opacity: 0.85;
}
.home-subscribe__actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.6rem;
margin-bottom: 1.25rem;
}
@media (max-width: 600px) {
.header__logo .brand {
font-size: clamp(0.95rem, 4.8vw, 1.25rem);

67
assets/styles/layout.css

@ -204,15 +204,76 @@ dt { @@ -204,15 +204,76 @@ dt {
footer {
background-color: var(--color-footer-bg);
color: var(--color-footer-text);
text-align: center;
padding: 1em 0;
padding: 1.25rem 1rem 1.5rem;
position: relative;
width: 100%;
border-top: 1px solid var(--color-border);
}
.site-footer {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 1.75rem;
text-align: left;
}
.site-footer__syndication-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 0.35rem;
}
.site-footer__syndication-hint {
margin: 0 0 0.75rem;
font-size: 0.9rem;
color: var(--color-text);
opacity: 0.9;
max-width: 40rem;
}
.site-footer__syndication-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 0.6rem;
}
.site-footer__main {
text-align: center;
}
.site-footer__legal {
margin: 1rem 0 0;
font-size: 0.95rem;
}
@media (min-width: 900px) {
.site-footer {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
gap: 2rem 3rem;
}
.site-footer__syndication {
flex: 0 1 50%;
}
.site-footer__main {
flex: 0 1 auto;
min-width: min(20rem, 100%);
text-align: right;
}
.site-footer__legal {
text-align: right;
}
}
footer .footer-links {
margin: 24px 0;
margin: 0 0 0.5rem;
}
.footer-links a {

3
config/services.yaml

@ -37,6 +37,9 @@ services: @@ -37,6 +37,9 @@ services:
$articleRelayUrls: '%article_relays%'
$profileRelayUrls: '%profile_relays%'
$projectDir: '%kernel.project_dir%'
App\Service\ArticleCommentThreadLoader:
arguments:
$appCachePool: '@cache.app'
App\Twig\FooterLinksExtension:
arguments:
$footerLinksPath: '%footer_links%'

244
src/Controller/ArticleController.php

@ -35,8 +35,8 @@ class ArticleController extends AbstractController @@ -35,8 +35,8 @@ class ArticleController extends AbstractController
{
// {@see NostrClient::getArticleDiscussion} runs per-relay work in parallel CLI workers; allow headroom
// for all processes + Symfony (45s was too low and caused an uncatchable max-execution fatal → HTTP 500).
@set_time_limit(120);
@ini_set('max_execution_time', '120');
@set_time_limit(300);
@ini_set('max_execution_time', '300');
$t0 = microtime(true);
$coordinate = $request->query->getString('coordinate');
@ -56,35 +56,6 @@ class ArticleController extends AbstractController @@ -56,35 +56,6 @@ class ArticleController extends AbstractController
if (strlen($articleTitle) > 200) {
$articleTitle = substr($articleTitle, 0, 200);
}
$coordparts = explode(':', $coordinate, 3);
$articleKind = isset($coordparts[0]) && ctype_digit($coordparts[0]) ? (int) $coordparts[0] : 30023;
$articleAuthorPubkey = $coordparts[1] ?? '';
$articleReplyTags = null;
if ($articleAuthorPubkey !== '' && 64 === \strlen($articleAuthorPubkey) && ctype_xdigit($articleAuthorPubkey)) {
$articleReplyTags = Nip22CommentTags::forReplyToArticle($coordinate, $articleAuthorPubkey);
}
$parentIdForNaddr = str_repeat('0', 64);
$articleParentId = $articleEventId ?? $parentIdForNaddr;
if ($articleEventId !== null && 64 === \strlen($articleEventId) && ctype_xdigit($articleEventId)) {
$articleParentId = $articleEventId;
} else {
$articleParentId = $parentIdForNaddr;
}
$threadReplyRows = [];
$userMayReply = $this->isGranted('ROLE_USER');
if ($userMayReply && $articleReplyTags !== null) {
$threadReplyRows[] = [
'mode' => 'article',
'blurbLabel' => $articleTitle !== '' ? $articleTitle : 'Article',
'parentKind' => $articleKind,
'parentId' => $articleParentId,
'authorPubkey' => $articleAuthorPubkey,
'expectedTags' => $articleReplyTags,
];
}
$logger->info('http.fragment.comments_start', [
'coordinate' => $coordinate,
@ -98,69 +69,17 @@ class ArticleController extends AbstractController @@ -98,69 +69,17 @@ class ArticleController extends AbstractController
try {
$data = $loader->load($coordinate, $articleEventId);
if ($userMayReply && $articleReplyTags !== null) {
/** @var array<int, object> $list */
$list = $data['list'] ?? [];
foreach ($list as $row) {
if (!\is_object($row)) {
continue;
}
$k = (int) ($row->kind ?? 0);
if ($k !== KindsEnum::COMMENTS->value) {
continue;
}
$cid = (string) ($row->id ?? '');
$cpk = (string) ($row->pubkey ?? '');
if ($cid === '' || 64 !== \strlen($cid) || !ctype_xdigit($cid)) {
continue;
}
if ($cpk === '' || 64 !== \strlen($cpk) || !ctype_xdigit($cpk)) {
continue;
}
$rawTags = json_decode(json_encode($row->tags ?? []), true);
if (!\is_array($rawTags)) {
$rawTags = [];
}
$snippet = trim((string) ($row->content ?? ''));
if (strlen($snippet) > 120) {
$snippet = substr($snippet, 0, 117).'…';
}
if ($snippet === '') {
$snippet = 'Comment';
}
try {
$expectedTags = Nip22CommentTags::forReplyToComment($cid, $cpk, $k, $rawTags);
} catch (\Throwable) {
continue;
}
$threadReplyRows[] = [
'mode' => 'comment',
'blurbLabel' => $snippet,
'parentKind' => $k,
'parentId' => $cid,
'authorPubkey' => $cpk,
'expectedTags' => $expectedTags,
];
}
}
$data = $this->enrichCommentDataWithReplyContext(
$data,
$coordinate,
$articleEventId,
$articleTitle
);
$logger->info('http.fragment.comments_after_load', [
'elapsed_ms' => (int) round((microtime(true) - $t0) * 1000),
]);
$tRender = microtime(true);
$fragmentQuery = ['coordinate' => $coordinate, 'title' => $articleTitle];
if ($articleEventId !== null) {
$fragmentQuery['e'] = $articleEventId;
}
$data['comment_reply_context'] = [
'can_publish' => $userMayReply,
'coordinate' => $coordinate,
'article_event_id' => $articleEventId,
'parent_kind' => $articleKind,
'rows' => $threadReplyRows,
'fragment_url' => $this->generateUrl('article_comments_fragment', $fragmentQuery),
];
$response = $this->render('components/Organisms/Comments.html.twig', $data, new Response(
'',
Response::HTTP_OK,
@ -183,6 +102,127 @@ class ArticleController extends AbstractController @@ -183,6 +102,127 @@ class ArticleController extends AbstractController
}
}
/**
* Adds `comment_reply_context` for the reply composer (same data as the HTML fragment, used for full-page SSR when cache hits).
*
* @param array{
* list: array<int, object>,
* quotes: array<int, object>,
* commentLinks: array<string, array<int, mixed>>,
* quoteLinks: array<string, array<int, mixed>>,
* processedContent: array<string, string>
* } $data
*
* @return array{
* list: array<int, object>,
* quotes: array<int, object>,
* commentLinks: array<string, array<int, mixed>>,
* quoteLinks: array<string, array<int, mixed>>,
* processedContent: array<string, string>,
* comment_reply_context: array{
* can_publish: bool,
* coordinate: string,
* article_event_id: ?string,
* parent_kind: int,
* rows: array<int, array<string, mixed>>,
* fragment_url: string
* }
* }
*/
private function enrichCommentDataWithReplyContext(
array $data,
string $coordinate,
?string $articleEventId,
string $articleTitle
): array {
$coordparts = explode(':', $coordinate, 3);
$articleKind = isset($coordparts[0]) && ctype_digit($coordparts[0]) ? (int) $coordparts[0] : 30023;
$articleAuthorPubkey = $coordparts[1] ?? '';
$articleReplyTags = null;
if ($articleAuthorPubkey !== '' && 64 === \strlen($articleAuthorPubkey) && ctype_xdigit($articleAuthorPubkey)) {
$articleReplyTags = Nip22CommentTags::forReplyToArticle($coordinate, $articleAuthorPubkey);
}
$parentIdForNaddr = str_repeat('0', 64);
if ($articleEventId !== null && 64 === \strlen($articleEventId) && ctype_xdigit($articleEventId)) {
$articleParentId = $articleEventId;
} else {
$articleParentId = $parentIdForNaddr;
}
$threadReplyRows = [];
$userMayReply = $this->isGranted('ROLE_USER');
if ($userMayReply && $articleReplyTags !== null) {
$threadReplyRows[] = [
'mode' => 'article',
'blurbLabel' => $articleTitle !== '' ? $articleTitle : 'Article',
'parentKind' => $articleKind,
'parentId' => $articleParentId,
'authorPubkey' => $articleAuthorPubkey,
'expectedTags' => $articleReplyTags,
];
/** @var array<int, object> $list */
$list = $data['list'] ?? [];
foreach ($list as $row) {
if (!\is_object($row)) {
continue;
}
$k = (int) ($row->kind ?? 0);
if ($k !== KindsEnum::COMMENTS->value) {
continue;
}
$cid = (string) ($row->id ?? '');
$cpk = (string) ($row->pubkey ?? '');
if ($cid === '' || 64 !== \strlen($cid) || !ctype_xdigit($cid)) {
continue;
}
if ($cpk === '' || 64 !== \strlen($cpk) || !ctype_xdigit($cpk)) {
continue;
}
$rawTags = json_decode(json_encode($row->tags ?? []), true);
if (!\is_array($rawTags)) {
$rawTags = [];
}
$snippet = trim((string) ($row->content ?? ''));
if (strlen($snippet) > 120) {
$snippet = substr($snippet, 0, 117).'…';
}
if ($snippet === '') {
$snippet = 'Comment';
}
try {
$expectedTags = Nip22CommentTags::forReplyToComment($cid, $cpk, $k, $rawTags);
} catch (\Throwable) {
continue;
}
$threadReplyRows[] = [
'mode' => 'comment',
'blurbLabel' => $snippet,
'parentKind' => $k,
'parentId' => $cid,
'authorPubkey' => $cpk,
'expectedTags' => $expectedTags,
];
}
}
$fragmentQuery = ['coordinate' => $coordinate, 'title' => $articleTitle];
if ($articleEventId !== null) {
$fragmentQuery['e'] = $articleEventId;
}
$data['comment_reply_context'] = [
'can_publish' => $userMayReply,
'coordinate' => $coordinate,
'article_event_id' => $articleEventId,
'parent_kind' => $articleKind,
'rows' => $threadReplyRows,
'fragment_url' => $this->generateUrl('article_comments_fragment', $fragmentQuery),
];
return $data;
}
private static function isValidNostrCoordinate(string $coordinate): bool
{
$parts = explode(':', $coordinate, 3);
@ -243,7 +283,8 @@ class ArticleController extends AbstractController @@ -243,7 +283,8 @@ class ArticleController extends AbstractController
EntityManagerInterface $entityManager,
CacheService $cacheService,
CacheItemPoolInterface $articlesCache,
Converter $converter
Converter $converter,
ArticleCommentThreadLoader $commentThreadLoader
): Response
{
@ -282,12 +323,34 @@ class ArticleController extends AbstractController @@ -282,12 +323,34 @@ class ArticleController extends AbstractController
$npub = $key->convertPublicKeyToBech32($article->getPubkey());
$author = $cacheService->getMetadata($npub);
$kind = $article->getKind()?->value ?? 30023;
$pubkey = (string) $article->getPubkey();
$articleSlug = (string) ($article->getSlug() ?? $slug);
$coordinate = $kind.':'.$pubkey.':'.$articleSlug;
$eid = $article->getEventId();
$eid = ($eid !== null && $eid !== '' && self::isValidHexEventId($eid)) ? $eid : null;
$articleTitle = (string) ($article->getTitle() ?? '');
$commentsData = null;
$commentsPreloaded = false;
$cached = $commentThreadLoader->tryLoadFromCacheOnly($coordinate, $eid);
if (null !== $cached) {
$commentsData = $this->enrichCommentDataWithReplyContext(
$cached,
$coordinate,
$eid,
$articleTitle
);
$commentsPreloaded = true;
}
return $this->render('pages/article.html.twig', [
'article' => $article,
'author' => $author,
'npub' => $npub,
'content' => $cacheItem->get(),
'comments_data' => $commentsData,
'comments_preloaded' => $commentsPreloaded,
]);
}
@ -450,6 +513,7 @@ class ArticleController extends AbstractController @@ -450,6 +513,7 @@ class ArticleController extends AbstractController
'article' => $article,
'content' => $content,
'author' => $user->getMetadata(),
'comments_preloaded' => false,
]);
}

9
src/Controller/DefaultController.php

@ -21,17 +21,8 @@ class DefaultController extends AbstractController @@ -21,17 +21,8 @@ class DefaultController extends AbstractController
#[Route('/', name: 'home')]
public function index(): Response
{
$categoriesForFeed = [];
foreach ($this->magazineContent->getCategorySlugsFromStore() as $slug) {
$categoriesForFeed[] = [
'slug' => $slug,
'title' => $this->magazineContent->getCategoryDisplayTitle($slug),
];
}
return $this->render('home.html.twig', [
'indices' => $this->magazineContent->getHomeCategoryIndexTags(),
'categories_for_feed' => $categoriesForFeed,
]);
}

71
src/Service/ArticleCommentThreadLoader.php

@ -4,6 +4,8 @@ declare(strict_types=1); @@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Service;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
@ -13,14 +15,52 @@ use Symfony\Contracts\Cache\ItemInterface; @@ -13,14 +15,52 @@ use Symfony\Contracts\Cache\ItemInterface;
*/
final readonly class ArticleCommentThreadLoader
{
/** PSR-6 pool backing {@see $cache}; used for true cache-only reads (SSR) without invoking Nostr. */
public function __construct(
private NostrClient $nostrClient,
private NostrLinkParser $nostrLinkParser,
private CacheInterface $cache,
private CacheItemPoolInterface $appCachePool,
private LoggerInterface $logger,
) {
}
/**
* @return array{
* list: array<int, object>,
* quotes: array<int, object>,
* commentLinks: array<string, array<int, mixed>>,
* quoteLinks: array<string, array<int, mixed>>,
* processedContent: array<string, string>
* }|null
*/
public function tryLoadFromCacheOnly(string $coordinate, ?string $articleEventHexId = null): ?array
{
$key = $this->cacheKeyForThread($coordinate, $articleEventHexId);
try {
$item = $this->appCachePool->getItem($key);
} catch (InvalidArgumentException) {
return null;
}
if (!$item->isHit()) {
return null;
}
$discussion = $item->get();
if (!\is_array($discussion)) {
return null;
}
if (($discussion['thread'] ?? []) === [] && ($discussion['quotes'] ?? []) === []) {
$this->logger->info('comments.loader.cache_hit_empty', ['coordinate' => $coordinate]);
} else {
$this->logger->info('comments.loader.cache_hit_only', [
'coordinate' => $coordinate,
'thread' => \count($discussion['thread'] ?? []),
]);
}
return $this->expandFromDiscussion($discussion, microtime(true));
}
/**
* @return array{
* list: array<int, object>,
@ -33,8 +73,7 @@ final readonly class ArticleCommentThreadLoader @@ -33,8 +73,7 @@ final readonly class ArticleCommentThreadLoader
public function load(string $coordinate, ?string $articleEventHexId = null): array
{
$t0 = microtime(true);
$aggrSuffix = $this->nostrClient->getNostrLandAggrReaderCacheSuffix();
$cacheKey = 'comments_v5_'.hash('sha256', $coordinate."\0".($articleEventHexId ?? '')."\0".$aggrSuffix);
$cacheKey = $this->cacheKeyForThread($coordinate, $articleEventHexId);
$this->logger->info('comments.loader.start', [
'cache_key_suffix' => substr($cacheKey, -16),
'coordinate' => $coordinate,
@ -43,7 +82,8 @@ final readonly class ArticleCommentThreadLoader @@ -43,7 +82,8 @@ final readonly class ArticleCommentThreadLoader
try {
$discussion = $this->cache->get($cacheKey, function (ItemInterface $item) use ($coordinate, $articleEventHexId, $t0): array {
$item->expiresAfter(120);
// 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),
]);
@ -66,6 +106,31 @@ final readonly class ArticleCommentThreadLoader @@ -66,6 +106,31 @@ final readonly class ArticleCommentThreadLoader
$discussion = ['thread' => [], 'quotes' => []];
}
return $this->expandFromDiscussion($discussion, $t0);
}
/**
* Same key for CLI prewarm, anonymous, and logged-in readers so cached threads are shared.
* (Relay selection for misses may still add aggr for signed-in users in {@see NostrClient::getArticleDiscussion}.)
*/
private function cacheKeyForThread(string $coordinate, ?string $articleEventHexId): string
{
return 'comments_v5_'.hash('sha256', $coordinate."\0".($articleEventHexId ?? ''));
}
/**
* @param array{thread: array<int, object>, quotes: array<int, object>} $discussion
*
* @return array{
* list: array<int, object>,
* quotes: array<int, object>,
* commentLinks: array<string, array<int, mixed>>,
* quoteLinks: array<string, array<int, mixed>>,
* processedContent: array<string, string>
* }
*/
private function expandFromDiscussion(array $discussion, float $t0): array
{
$list = $discussion['thread'] ?? [];
$quotes = $discussion['quotes'] ?? [];
$this->logger->info('comments.loader.cache_resolved', [

21
src/Twig/Components/Footer.php

@ -4,11 +4,28 @@ declare(strict_types=1); @@ -4,11 +4,28 @@ declare(strict_types=1);
namespace App\Twig\Components;
use App\Service\MagazineContentService;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Footer {
public function __construct()
class Footer
{
/** @var list<array{slug: string, title: string}> */
public array $categoriesForFeed = [];
public function __construct(
private readonly MagazineContentService $magazineContent,
) {
}
public function mount(): void
{
$this->categoriesForFeed = [];
foreach ($this->magazineContent->getCategorySlugsFromStore() as $slug) {
$this->categoriesForFeed[] = [
'slug' => $slug,
'title' => $this->magazineContent->getCategoryDisplayTitle($slug),
];
}
}
}

38
templates/components/Footer.html.twig

@ -1,13 +1,29 @@ @@ -1,13 +1,29 @@
<div class="footer-links">
{% for link in footer_links %}
<div class="footer-link">
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" title="{{ link.description|default(link.title) }}">{{ link.title }}</a>
{% if link.description %}
&mdash; <small>{{ link.description }}</small>
{% endif %}
<div class="site-footer">
<div class="site-footer__syndication" aria-label="Sitemap and syndication">
<h2 class="site-footer__syndication-title">Sitemap and feeds</h2>
<p class="site-footer__syndication-hint">For search engines and feed readers. Atom is supported by most clients.</p>
<div class="site-footer__syndication-actions">
<a class="btn btn-secondary" href="{{ path('sitemap') }}">Sitemap (XML)</a>
<a class="btn btn-secondary" href="{{ path('robots_txt') }}">Robots</a>
<a class="btn btn-secondary" href="{{ path('feed_magazine') }}">Atom — all articles</a>
{% for c in categoriesForFeed %}
<a class="btn btn-secondary" href="{{ path('feed_category', {slug: c.slug}) }}">Atom — {{ c.title }}</a>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="site-footer__main">
<div class="footer-links">
{% for link in footer_links %}
<div class="footer-link">
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" title="{{ link.description|default(link.title) }}">{{ link.title }}</a>
{% if link.description %}
&mdash; <small>{{ link.description }}</small>
{% endif %}
</div>
{% endfor %}
</div>
<p class="site-footer__legal">
{{ "now"|date("Y") }} {{ website_name }} <span class="publisher">by <twig:Molecules:UserFromNpub :ident="publisher_npub" /></span>
</p>
</div>
</div>
<p>{{ "now"|date("Y") }} {{ website_name }} <span class="publisher">by <twig:Molecules:UserFromNpub :ident="publisher_npub" /></span></p>

12
templates/home.html.twig

@ -28,18 +28,6 @@ @@ -28,18 +28,6 @@
{% endblock %}
{% block body %}
<div class="home-subscribe" aria-label="Sitemap and syndication">
<h2 class="home-subscribe__title">Sitemap and feeds</h2>
<p class="home-subscribe__hint">For search engines and feed readers. Atom is supported by most clients.</p>
<div class="home-subscribe__actions">
<a class="btn btn-secondary" href="{{ path('sitemap') }}">Sitemap (XML)</a>
<a class="btn btn-secondary" href="{{ path('robots_txt') }}">Robots</a>
<a class="btn btn-secondary" href="{{ path('feed_magazine') }}">Atom — all articles</a>
{% for c in categories_for_feed %}
<a class="btn btn-secondary" href="{{ path('feed_category', {slug: c.slug}) }}">Atom — {{ c.title }}</a>
{% endfor %}
</div>
</div>
<div class="home-body" data-magazine-sync-target="pageBody">
{% for item in indices %}
<twig:Organisms:FeaturedList :category="item" class="featured-list"/>

12
templates/pages/article.html.twig

@ -126,9 +126,17 @@ @@ -126,9 +126,17 @@
<div
data-controller="article-comments"
data-article-comments-url-value="{{ path('article_comments_fragment', comments_query)|e('html_attr') }}"
data-article-comments-preloaded-value="{{ (comments_preloaded|default(false)) ? 'true' : 'false' }}"
>
<div data-article-comments-target="container" class="comments comments--pending">
<p class="text-subtle">Loading comments…</p>
<div
data-article-comments-target="container"
class="comments {{ comments_preloaded|default(false) ? 'comments--from-cache' : 'comments--pending' }}"
>
{% if comments_preloaded|default(false) and comments_data is defined and comments_data is not null %}
{% include 'components/Organisms/Comments.html.twig' with comments_data %}
{% else %}
<p class="text-subtle">Loading comments…</p>
{% endif %}
</div>
</div>
</section>

Loading…
Cancel
Save