From 9f7beb7ff304d424c7fbbe304289a1fe77db4112 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 30 Apr 2026 10:48:31 +0200 Subject: [PATCH] headlines implemented --- assets/styles/app.css | 97 +++++++++++++++++++ config/unfold.yaml | 4 +- src/Controller/ArticleController.php | 21 +--- src/Service/ArticleBodyHtmlRenderer.php | 36 +++++++ src/Service/MagazineContentService.php | 7 +- .../Organisms/HomeCurationHeadlines.html.twig | 48 +++++++++ templates/home.html.twig | 8 +- 7 files changed, 194 insertions(+), 27 deletions(-) create mode 100644 src/Service/ArticleBodyHtmlRenderer.php create mode 100644 templates/components/Organisms/HomeCurationHeadlines.html.twig diff --git a/assets/styles/app.css b/assets/styles/app.css index 64ebd22..f8b3bc2 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -146,6 +146,103 @@ svg.icon { gap: 3.5rem; } +/* Home: NIP-51 30004 “headlines” — editorial section title + full-width article stack (not masonry). */ +.home-curation-landmark { + width: 100%; + min-width: 0; +} + +.home-curation-landmark__title { + font-family: var(--heading-font), serif; + font-size: clamp(1.85rem, 4.2vw, 2.85rem); + font-weight: 700; + line-height: 1.12; + letter-spacing: -0.02em; + color: var(--color-primary); + margin: 0 0 2.25rem; + max-width: 40rem; +} + +.home-curation-landmark__articles { + display: flex; + flex-direction: column; + gap: 0; +} + +.curation-article-display { + padding: 0 0 3.25rem; + margin: 0 0 3.25rem; + border-bottom: 1px solid var(--color-border); +} + +.curation-article-display:last-child { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; +} + +.curation-article-display__media a { + display: block; + text-decoration: none; +} + +.curation-article-display__title-link { + color: inherit; + text-decoration: none; +} + +.curation-article-display__title-link:hover { + text-decoration: underline; + text-underline-offset: 0.12em; +} + +.curation-article-display__title-link:focus-visible { + outline: 2px solid var(--color-focus-ring); + outline-offset: 2px; + border-radius: 0.2rem; +} + +.curation-article-display__media { + margin: 0 0 1.25rem; + max-width: 400px; + border-radius: 0.35rem; + overflow: hidden; + background: var(--color-bg-light); +} + +.curation-article-display__media img { + display: block; + max-width: 100%; + width: auto; + height: auto; +} + +.curation-article-display__media img[src*='favicon-96x96'] { + padding: 1.5rem 2rem; + box-sizing: border-box; + max-height: 180px; + margin-inline: auto; +} + +.curation-article-display__body { + max-width: min(100%, 48rem); +} + +.curation-article-display__headline { + font-family: var(--heading-font), serif; + font-size: clamp(1.6rem, 3.2vw, 2.25rem); + font-weight: 700; + line-height: 1.18; + color: var(--color-primary); + margin: 0 0 1rem; + letter-spacing: -0.02em; +} + +.curation-article-display__main { + margin-top: 0.25rem; + max-width: none; +} + /* List pages: space header / form from content (same intent as .home-body gap) */ .search-page { display: flex; diff --git a/config/unfold.yaml b/config/unfold.yaml index c48cfaa..61dde3b 100644 --- a/config/unfold.yaml +++ b/config/unfold.yaml @@ -47,8 +47,8 @@ parameters: # Kind 30040 magazine root #d (NIP-33). Exposed as `d_tag` for backward compatibility. d_tag_magazine: 'newsroom-magazine-on-imwald-by-laeserin' d_tag: '%d_tag_magazine%' - # NIP-51 kind 30004 curation set #d for `npub` (home “spotlight” strip): ordered `a` for kind 30023 only; other `a` kinds and `e` tags are ignored. Empty or `d-tag-goes-here` disables. - d_tag_curation_set: 'd-tag-goes-here' + # NIP-51 kind 30004 curation set #d for `npub` (home landing stack): optional `title` tag = section heading; ordered `a` for kind 30023 only (full-width article blocks). Other `a` kinds and `e` tags ignored. Empty or `d-tag-goes-here` disables. + d_tag_curation_set: 'nostr-curated-headlines' community_articles: true # Domain for site-assigned NIP-05 for featured (magazine category) authors; must match the host serving /.well-known/nostr.json nip05_domain: 'blog.imwald.eu' diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index d021e3e..aee5399 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -3,9 +3,8 @@ namespace App\Controller; use App\Entity\Article; -use App\Repository\ArticleHighlightRepository; use App\Repository\ArticleRepository; -use App\Service\ArticleBodyHighlightInjector; +use App\Service\ArticleBodyHtmlRenderer; use App\Enum\KindsEnum; use App\Nostr\Nip10Kind1ArticleReplyTags; use App\Nostr\Nip22CommentTags; @@ -307,10 +306,8 @@ class ArticleController extends AbstractController string $slug, EntityManagerInterface $entityManager, CacheService $cacheService, - Converter $converter, ArticleCommentThreadLoader $commentThreadLoader, - ArticleHighlightRepository $articleHighlightRepository, - ArticleBodyHighlightInjector $articleBodyHighlightInjector, + ArticleBodyHtmlRenderer $articleBodyHtmlRenderer, NostrKeyHelper $nostrKeyHelper, ): Response { $article = $this->loadLatestArticleBySlug($entityManager, $slug); @@ -324,10 +321,8 @@ class ArticleController extends AbstractController return $this->renderArticle( $article, $cacheService, - $converter, $commentThreadLoader, - $articleHighlightRepository, - $articleBodyHighlightInjector, + $articleBodyHtmlRenderer, $nostrKeyHelper ); } @@ -366,16 +361,14 @@ class ArticleController extends AbstractController private function renderArticle( Article $article, CacheService $cacheService, - Converter $converter, ArticleCommentThreadLoader $commentThreadLoader, - ArticleHighlightRepository $articleHighlightRepository, - ArticleBodyHighlightInjector $articleBodyHighlightInjector, + ArticleBodyHtmlRenderer $articleBodyHtmlRenderer, NostrKeyHelper $nostrKeyHelper, ): Response { set_time_limit(300); // 5 minutes ini_set('max_execution_time', '300'); - $html = $converter->convertToHtml($article->getContent()); + $html = $articleBodyHtmlRenderer->renderForArticle($article); $npub = $nostrKeyHelper->convertPublicKeyToBech32($article->getPubkey()); $author = $cacheService->getMetadata($npub); @@ -403,10 +396,6 @@ class ArticleController extends AbstractController $commentsPreloaded = true; } - $highlights = $articleHighlightRepository->findByArticle($article); - $injection = $articleBodyHighlightInjector->inject($html, $highlights); - $html = $injection['html']; - return $this->render('pages/article.html.twig', [ 'article' => $article, 'author' => $author, diff --git a/src/Service/ArticleBodyHtmlRenderer.php b/src/Service/ArticleBodyHtmlRenderer.php new file mode 100644 index 0000000..5925f05 --- /dev/null +++ b/src/Service/ArticleBodyHtmlRenderer.php @@ -0,0 +1,36 @@ +getContent() ?? ''); + try { + $html = $this->converter->convertToHTML($raw); + } catch (CommonMarkException) { + $html = htmlspecialchars($raw, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + $highlights = $this->articleHighlightRepository->findByArticle($article); + + return $this->articleBodyHighlightInjector->inject($html, $highlights)['html']; + } +} diff --git a/src/Service/MagazineContentService.php b/src/Service/MagazineContentService.php index 8947cb3..9c7a6dd 100644 --- a/src/Service/MagazineContentService.php +++ b/src/Service/MagazineContentService.php @@ -27,6 +27,7 @@ final class MagazineContentService private readonly ArticleRepository $articleRepository, private readonly NostrClient $nostrClient, private readonly RequestStack $requestStack, + private readonly ArticleBodyHtmlRenderer $articleBodyHtmlRenderer, ) { } @@ -703,7 +704,7 @@ final class MagazineContentService * Home strip from NIP-51 kind 30004 (curation set): `d_tag_curation_set` on `npub`, ordered `a` tags for * kind **30023** only (other kinds and `e` tags are ignored). Tiles resolve from the local `article` table. * - * @return array{heading: string, tiles: list>} + * @return array{heading: string, tiles: list} */ public function buildHomeCurationWallData(): array { @@ -749,7 +750,7 @@ final class MagazineContentService } $indexed = $this->articleRepository->findByAuthorAndSlugIndexed($pairsArg); } - $heading = $parsed['title'] !== '' ? $parsed['title'] : 'Spotlight'; + $heading = trim($parsed['title']); $tiles = []; $seenArticle = []; foreach ($parsed['items'] as $it) { @@ -764,7 +765,7 @@ final class MagazineContentService $seenArticle[$key] = true; $tiles[] = [ 'article' => FeaturedArticleCard::fromArticle($article), - 'categoryTitle' => $heading, + 'body_html' => $this->articleBodyHtmlRenderer->renderForArticle($article), ]; } if ($tiles === []) { diff --git a/templates/components/Organisms/HomeCurationHeadlines.html.twig b/templates/components/Organisms/HomeCurationHeadlines.html.twig new file mode 100644 index 0000000..094a0f5 --- /dev/null +++ b/templates/components/Organisms/HomeCurationHeadlines.html.twig @@ -0,0 +1,48 @@ +{# + NIP-51 30004 home strip: section title from list `title` tag; each item reads like a static landing + section — illustration (max 400px, natural aspect), headline, then full article body (Markdown + + highlights as on /p/…/d/…). Body is not wrapped in a single (markdown may contain links). +#} +{% if tiles is not empty %} +
+ {% if section_title|default('') != '' %} +

{{ section_title|e }}

+ {% endif %} +
+
+{% endif %} diff --git a/templates/home.html.twig b/templates/home.html.twig index ca6d469..7975d3b 100644 --- a/templates/home.html.twig +++ b/templates/home.html.twig @@ -28,13 +28,9 @@ {% block body %}
{% if home_curation_tiles|default([]) is not empty %} - {% if home_curation_heading|default('') != '' %} -

{{ home_curation_heading|e }}

- {% endif %} - {% include 'components/Organisms/FeaturedWall.html.twig' with { + {% include 'components/Organisms/HomeCurationHeadlines.html.twig' with { tiles: home_curation_tiles, - region_aria_label: home_curation_heading|default('') != '' ? (home_curation_heading ~ ' — curated articles') : (website_name ~ ' — curated articles'), - wall_extra_class: 'featured-list--curation', + section_title: home_curation_heading|default(''), } only %} {% endif %} {% include 'components/Organisms/FeaturedWall.html.twig' with { tiles: home_featured_tiles|default([]) } only %}