Browse Source

headlines implemented

imwald
Silberengel 3 days ago
parent
commit
9f7beb7ff3
  1. 97
      assets/styles/app.css
  2. 4
      config/unfold.yaml
  3. 21
      src/Controller/ArticleController.php
  4. 36
      src/Service/ArticleBodyHtmlRenderer.php
  5. 7
      src/Service/MagazineContentService.php
  6. 48
      templates/components/Organisms/HomeCurationHeadlines.html.twig
  7. 8
      templates/home.html.twig

97
assets/styles/app.css

@ -146,6 +146,103 @@ svg.icon {
gap: 3.5rem; 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) */ /* List pages: space header / form from content (same intent as .home-body gap) */
.search-page { .search-page {
display: flex; display: flex;

4
config/unfold.yaml

@ -47,8 +47,8 @@ parameters:
# Kind 30040 magazine root #d (NIP-33). Exposed as `d_tag` for backward compatibility. # 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_magazine: 'newsroom-magazine-on-imwald-by-laeserin'
d_tag: '%d_tag_magazine%' 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. # 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: 'd-tag-goes-here' d_tag_curation_set: 'nostr-curated-headlines'
community_articles: true community_articles: true
# Domain for site-assigned NIP-05 for featured (magazine category) authors; must match the host serving /.well-known/nostr.json # 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' nip05_domain: 'blog.imwald.eu'

21
src/Controller/ArticleController.php

@ -3,9 +3,8 @@
namespace App\Controller; namespace App\Controller;
use App\Entity\Article; use App\Entity\Article;
use App\Repository\ArticleHighlightRepository;
use App\Repository\ArticleRepository; use App\Repository\ArticleRepository;
use App\Service\ArticleBodyHighlightInjector; use App\Service\ArticleBodyHtmlRenderer;
use App\Enum\KindsEnum; use App\Enum\KindsEnum;
use App\Nostr\Nip10Kind1ArticleReplyTags; use App\Nostr\Nip10Kind1ArticleReplyTags;
use App\Nostr\Nip22CommentTags; use App\Nostr\Nip22CommentTags;
@ -307,10 +306,8 @@ class ArticleController extends AbstractController
string $slug, string $slug,
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
CacheService $cacheService, CacheService $cacheService,
Converter $converter,
ArticleCommentThreadLoader $commentThreadLoader, ArticleCommentThreadLoader $commentThreadLoader,
ArticleHighlightRepository $articleHighlightRepository, ArticleBodyHtmlRenderer $articleBodyHtmlRenderer,
ArticleBodyHighlightInjector $articleBodyHighlightInjector,
NostrKeyHelper $nostrKeyHelper, NostrKeyHelper $nostrKeyHelper,
): Response { ): Response {
$article = $this->loadLatestArticleBySlug($entityManager, $slug); $article = $this->loadLatestArticleBySlug($entityManager, $slug);
@ -324,10 +321,8 @@ class ArticleController extends AbstractController
return $this->renderArticle( return $this->renderArticle(
$article, $article,
$cacheService, $cacheService,
$converter,
$commentThreadLoader, $commentThreadLoader,
$articleHighlightRepository, $articleBodyHtmlRenderer,
$articleBodyHighlightInjector,
$nostrKeyHelper $nostrKeyHelper
); );
} }
@ -366,16 +361,14 @@ class ArticleController extends AbstractController
private function renderArticle( private function renderArticle(
Article $article, Article $article,
CacheService $cacheService, CacheService $cacheService,
Converter $converter,
ArticleCommentThreadLoader $commentThreadLoader, ArticleCommentThreadLoader $commentThreadLoader,
ArticleHighlightRepository $articleHighlightRepository, ArticleBodyHtmlRenderer $articleBodyHtmlRenderer,
ArticleBodyHighlightInjector $articleBodyHighlightInjector,
NostrKeyHelper $nostrKeyHelper, NostrKeyHelper $nostrKeyHelper,
): Response { ): Response {
set_time_limit(300); // 5 minutes set_time_limit(300); // 5 minutes
ini_set('max_execution_time', '300'); ini_set('max_execution_time', '300');
$html = $converter->convertToHtml($article->getContent()); $html = $articleBodyHtmlRenderer->renderForArticle($article);
$npub = $nostrKeyHelper->convertPublicKeyToBech32($article->getPubkey()); $npub = $nostrKeyHelper->convertPublicKeyToBech32($article->getPubkey());
$author = $cacheService->getMetadata($npub); $author = $cacheService->getMetadata($npub);
@ -403,10 +396,6 @@ class ArticleController extends AbstractController
$commentsPreloaded = true; $commentsPreloaded = true;
} }
$highlights = $articleHighlightRepository->findByArticle($article);
$injection = $articleBodyHighlightInjector->inject($html, $highlights);
$html = $injection['html'];
return $this->render('pages/article.html.twig', [ return $this->render('pages/article.html.twig', [
'article' => $article, 'article' => $article,
'author' => $author, 'author' => $author,

36
src/Service/ArticleBodyHtmlRenderer.php

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Article;
use App\Repository\ArticleHighlightRepository;
use App\Util\CommonMark\Converter;
use League\CommonMark\Exception\CommonMarkException;
/**
* Markdown → HTML for an {@see Article}, with the same kind-9802 highlight injection as {@see \App\Controller\ArticleController}.
*/
final class ArticleBodyHtmlRenderer
{
public function __construct(
private readonly Converter $converter,
private readonly ArticleHighlightRepository $articleHighlightRepository,
private readonly ArticleBodyHighlightInjector $articleBodyHighlightInjector,
) {
}
public function renderForArticle(Article $article): string
{
$raw = (string) ($article->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'];
}
}

7
src/Service/MagazineContentService.php

@ -27,6 +27,7 @@ final class MagazineContentService
private readonly ArticleRepository $articleRepository, private readonly ArticleRepository $articleRepository,
private readonly NostrClient $nostrClient, private readonly NostrClient $nostrClient,
private readonly RequestStack $requestStack, 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 * 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. * kind **30023** only (other kinds and `e` tags are ignored). Tiles resolve from the local `article` table.
* *
* @return array{heading: string, tiles: list<array<string, mixed>>} * @return array{heading: string, tiles: list<array{article: FeaturedArticleCard, body_html: string}>}
*/ */
public function buildHomeCurationWallData(): array public function buildHomeCurationWallData(): array
{ {
@ -749,7 +750,7 @@ final class MagazineContentService
} }
$indexed = $this->articleRepository->findByAuthorAndSlugIndexed($pairsArg); $indexed = $this->articleRepository->findByAuthorAndSlugIndexed($pairsArg);
} }
$heading = $parsed['title'] !== '' ? $parsed['title'] : 'Spotlight'; $heading = trim($parsed['title']);
$tiles = []; $tiles = [];
$seenArticle = []; $seenArticle = [];
foreach ($parsed['items'] as $it) { foreach ($parsed['items'] as $it) {
@ -764,7 +765,7 @@ final class MagazineContentService
$seenArticle[$key] = true; $seenArticle[$key] = true;
$tiles[] = [ $tiles[] = [
'article' => FeaturedArticleCard::fromArticle($article), 'article' => FeaturedArticleCard::fromArticle($article),
'categoryTitle' => $heading, 'body_html' => $this->articleBodyHtmlRenderer->renderForArticle($article),
]; ];
} }
if ($tiles === []) { if ($tiles === []) {

48
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 <a> (markdown may contain links).
#}
{% if tiles is not empty %}
<section
class="home-curation-landmark"
{% if section_title|default('') != '' %}
aria-labelledby="home-curation-landmark-heading"
{% else %}
aria-label="{{ (website_name ~ ' — curated articles')|e('html_attr') }}"
{% endif %}
>
{% if section_title|default('') != '' %}
<h2 id="home-curation-landmark-heading" class="home-curation-landmark__title">{{ section_title|e }}</h2>
{% endif %}
<div class="home-curation-landmark__articles">
{% for tile in tiles %}
{% set item = tile.article %}
{% set article_href = (item.pubkey and npub_from_hex(item.pubkey) != '') ? path('article', { npub: npub_from_hex(item.pubkey), slug: item.slug }) : path('article-legacy-redirect', { slug: item.slug }) %}
<article class="curation-article-display">
<div class="curation-article-display__media">
<a href="{{ article_href }}" tabindex="-1" aria-hidden="true">
<img
src="{{ article_card_cover(item.image, item.pubkey) }}"
alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}"
loading="{{ loop.first ? 'eager' : 'lazy' }}"
decoding="async"
>
</a>
</div>
<div class="curation-article-display__body">
<h3 class="curation-article-display__headline">
<a class="curation-article-display__title-link" href="{{ article_href }}">{{ item.title|e }}</a>
</h3>
<div
class="article-main curation-article-display__main"
data-controller="user-highlight-tooltip"
>
{{ tile.body_html|raw }}
</div>
</div>
</article>
{% endfor %}
</div>
</section>
{% endif %}

8
templates/home.html.twig

@ -28,13 +28,9 @@
{% block body %} {% block body %}
<div class="home-body home-body--wall"> <div class="home-body home-body--wall">
{% if home_curation_tiles|default([]) is not empty %} {% if home_curation_tiles|default([]) is not empty %}
{% if home_curation_heading|default('') != '' %} {% include 'components/Organisms/HomeCurationHeadlines.html.twig' with {
<h2 class="home-curation-heading h5 mb-3 text-body-secondary">{{ home_curation_heading|e }}</h2>
{% endif %}
{% include 'components/Organisms/FeaturedWall.html.twig' with {
tiles: home_curation_tiles, tiles: home_curation_tiles,
region_aria_label: home_curation_heading|default('') != '' ? (home_curation_heading ~ ' — curated articles') : (website_name ~ ' — curated articles'), section_title: home_curation_heading|default(''),
wall_extra_class: 'featured-list--curation',
} only %} } only %}
{% endif %} {% endif %}
{% include 'components/Organisms/FeaturedWall.html.twig' with { tiles: home_featured_tiles|default([]) } only %} {% include 'components/Organisms/FeaturedWall.html.twig' with { tiles: home_featured_tiles|default([]) } only %}

Loading…
Cancel
Save