Browse Source

restyle the blog

imwald
Silberengel 5 days ago
parent
commit
dd680430c6
  1. 281
      assets/styles/app.css
  2. 25
      assets/styles/article.css
  3. 2
      assets/styles/card.css
  4. 7
      assets/styles/event.css
  5. 26
      assets/styles/layout.css
  6. 96
      src/Twig/ArticleCardCoverExtension.php
  7. 7
      templates/components/Header.html.twig
  8. 12
      templates/components/Molecules/Card.html.twig
  9. 10
      templates/components/Molecules/CategoryLink.html.twig
  10. 16
      templates/components/Organisms/FeaturedList.html.twig

281
assets/styles/app.css

@ -139,9 +139,45 @@ svg.icon { @@ -139,9 +139,45 @@ svg.icon {
border-radius: 0; /* Sharp edges */
}
/* Landing (home): clear hierarchy — section label / title / excerpt / spacing */
.home-body {
display: flex;
flex-direction: column;
gap: 3.5rem;
}
/* List pages: space header / form from content (same intent as .home-body gap) */
.search-page {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.category-page__header-card {
margin-bottom: 2.5rem;
}
/* Author profile: space articles block below header+divider (header is multiple nodes; do not use flex+gap on .author-profile) */
.author-profile > .author-profile__divider {
margin: 2.5rem 0;
border: 0;
border-top: 1px solid var(--color-border);
}
.featured-cat {
border-bottom: 2px solid var(--color-border);
padding-left: 10px;
border-bottom: 1px solid var(--color-border);
padding: 0 0 0.75rem 10px;
margin-bottom: 1.25rem;
}
.featured-cat small {
display: block;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-text-mid) 55%, var(--color-bg) 45%);
line-height: 1.35;
}
.featured-list {
@ -149,18 +185,50 @@ svg.icon { @@ -149,18 +185,50 @@ svg.icon {
flex-direction: row;
flex-wrap: nowrap;
align-items: flex-start;
gap: 1.25rem;
}
.featured-list > * {
box-sizing: border-box; /* so padding/border don't break the layout */
margin-bottom: 10px;
padding: 10px;
box-sizing: border-box;
margin: 0;
padding: 0;
min-width: 0;
}
/* Uniform text padding for lead + side cards; image is full-bleed above. */
.featured-list .card-body {
box-sizing: border-box;
padding: 1rem 1.125rem 1.2rem;
}
/* Lead card only: 16:9 cover frame (Molecules/FeaturedList — side cards have no .card-header). */
.featured-list .card > a > .card-header {
margin: 0;
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
background-color: var(--color-bg-light);
}
.featured-list .card > a > .card-header img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
}
/* Site logo fallback (small square asset in a wide frame) */
.featured-list .card > a > .card-header img[src*="favicon-96x96"] {
object-fit: contain;
padding: 2.25rem;
box-sizing: border-box;
}
@media (max-width: 1024px) {
.featured-list {
flex-direction: column !important;
gap: 1.5rem;
}
.featured-list > div:first-child,
@ -168,19 +236,6 @@ svg.icon { @@ -168,19 +236,6 @@ svg.icon {
flex: 1 1 auto;
width: 100%;
}
.featured-list .card-header {
margin-top: 20px;
}
.featured-list .card {
border-bottom: 1px solid var(--color-border) !important;
}
.featured-list > * {
margin-bottom: 10px;
padding: 0;
}
}
div:nth-child(odd) .featured-list {
flex-direction: row-reverse;
@ -197,25 +252,81 @@ div:nth-child(odd) .featured-list { @@ -197,25 +252,81 @@ div:nth-child(odd) .featured-list {
min-width: 0;
}
/* Each column: vertical rhythm between hero / stacked cards (replaces ad-hoc card margins). */
.featured-list > div {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
/* List card titles + excerpts: home (featured), category, search, author */
.featured-list h2.card-title,
.article-list h2.card-title {
font-family: var(--heading-font), serif;
font-size: 1.95rem;
font-weight: 700;
line-height: 1.18;
color: var(--color-primary);
margin: 0.15rem 0 0.55rem;
}
/* Home featured grid only: two-line title + two-line deck for even rhythm. */
.featured-list h2.card-title {
font-size: 1.5rem;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
min-height: 2.36em;
margin-top: 0;
margin-bottom: 0.4rem;
}
.featured-list p.lede {
font-size: 1.4rem;
.featured-list p.lede,
.article-list p.lede {
font-family: var(--main-body-font), serif;
font-size: 1.1rem;
font-weight: 400;
line-height: 1.55;
color: var(--color-text-mid);
margin-top: 0.15rem;
}
.featured-list .card {
margin-bottom: 10px;
.featured-list p.lede.truncate {
-webkit-line-clamp: 2;
line-clamp: 2;
}
.featured-list .card:not(:last-child) {
border-bottom: 1px solid var(--color-border);
.featured-list__meta {
font-family: var(--font-family), sans-serif;
font-size: 0.78rem;
font-weight: 400;
line-height: 1.35;
color: color-mix(in srgb, var(--color-text-mid) 48%, var(--color-bg) 52%);
margin: 0.15rem 0 0.45rem;
}
.featured-list .card-header img {
max-height: 500px;
aspect-ratio: 1;
.featured-list__meta time {
color: inherit;
}
/* Whole-card link is `color: inherit`; keep excerpt + date subdued on hover. */
.featured-list .card a:hover p.lede,
.article-list .card a:hover p.lede {
color: color-mix(in srgb, var(--color-text-mid) 88%, var(--color-text) 12%);
text-decoration: none;
}
.featured-list .card a:hover .featured-list__meta {
color: color-mix(in srgb, var(--color-text-mid) 58%, var(--color-text) 42%);
}
.featured-list .card {
margin: 0;
border: 1px solid var(--color-border);
box-sizing: border-box;
min-width: 0;
background: var(--color-bg);
}
.article-list .metadata {
@ -225,11 +336,48 @@ div:nth-child(odd) .featured-list { @@ -225,11 +336,48 @@ div:nth-child(odd) .featured-list {
align-items: center;
gap: 0.75rem;
min-width: 0;
font-family: var(--font-family), sans-serif;
font-size: 0.78rem;
font-weight: 400;
line-height: 1.35;
color: color-mix(in srgb, var(--color-text-mid) 48%, var(--color-bg) 52%);
}
.article-list .metadata p {
margin: 0;
min-width: 0;
color: inherit;
}
.article-list .metadata a {
color: color-mix(in srgb, var(--color-text-mid) 72%, var(--color-text) 28%);
text-decoration: none;
}
.article-list .metadata a:hover,
.article-list .metadata a:focus-visible {
color: var(--color-link-hover);
text-decoration: underline;
text-underline-offset: 2px;
}
/* List cards: same site-logo treatment when the hero is the default mark */
.article-list .card-header img[src*="favicon-96x96"] {
object-fit: contain;
padding: 1.25rem;
box-sizing: border-box;
background: var(--color-bg-light);
}
/* Optional category label above cover (see Molecules/Card) */
.article-list .card-header small {
display: block;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-text-mid) 55%, var(--color-bg) 45%);
margin-bottom: 0.35rem;
}
.truncate {
@ -294,21 +442,79 @@ div:nth-child(odd) .featured-list { @@ -294,21 +442,79 @@ div:nth-child(odd) .featured-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
padding: 0;
align-items: center;
gap: 0.4rem 0.6rem;
padding: 0.35rem 0.5rem 0.5rem;
margin: 0;
}
.header__categories li {
list-style: none;
}
.header__categories li a:hover {
/* Top category row: current section + clear hover affordance (passive “clean” list → scannable) */
.header__cat-link {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 0.4rem 0.75rem;
font-family: var(--font-family), sans-serif;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
text-decoration: none;
font-weight: bold;
color: var(--color-text-mid);
background: transparent;
border: 1px solid transparent;
border-radius: 5px;
transition:
color 0.16s ease,
background-color 0.16s ease,
border-color 0.16s ease,
box-shadow 0.16s ease;
}
.header__categories a.active {
font-weight: bold;
.header__cat-link:hover {
text-decoration: none;
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg) 92%);
box-shadow: 0 2px 0 0 var(--color-secondary);
}
.header__cat-link:focus-visible {
text-decoration: none;
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.header__cat-link--active {
color: var(--color-primary);
font-weight: 700;
background: color-mix(in srgb, var(--color-primary) 14%, var(--color-bg) 86%);
box-shadow: inset 0 -2px 0 0 var(--color-primary);
}
/* Strong CTA: “Latest Articles” (outline + fill when you’re on that list) */
.header__cat-link--cta {
border-color: color-mix(in srgb, var(--color-secondary) 55%, var(--color-border) 45%);
color: var(--color-secondary);
background: color-mix(in srgb, var(--color-secondary) 7%, var(--color-bg) 93%);
}
.header__cat-link--cta:hover {
color: var(--color-link-hover);
background: color-mix(in srgb, var(--color-secondary) 14%, var(--color-bg) 86%);
box-shadow: 0 2px 0 0 var(--color-secondary);
}
.header__cat-link--cta.header__cat-link--active {
color: var(--color-text-contrast);
background: var(--color-primary);
border-color: var(--color-primary);
font-weight: 700;
box-shadow: none;
}
.header__logo h1 {
@ -541,6 +747,11 @@ footer a { @@ -541,6 +747,11 @@ footer a {
.author-profile__title {
margin-top: 0.25em;
font-family: var(--heading-font), serif;
font-size: clamp(1.65rem, 3.2vw, 2.35rem);
font-weight: 700;
line-height: 1.12;
color: var(--color-primary);
}
.author-profile__header-meta {

25
assets/styles/article.css

@ -50,6 +50,26 @@ @@ -50,6 +50,26 @@
flex: 1 1 12rem;
min-width: 0;
margin: 0;
font-family: var(--heading-font), serif;
font-weight: 700;
line-height: 1.12;
color: var(--color-primary);
}
/* Article + category page headers: global h1 is 300 weight; titles should read as the clear focal point. */
.card-header--article h1.card-title {
font-size: clamp(1.85rem, 2.8vw, 2.75rem);
}
/* Hero summary: same “excerpt” level as list cards, not .lede’s 1.6rem body scale */
.card > .card-body > .lede {
font-family: var(--main-body-font), serif;
font-size: 1.1rem;
line-height: 1.55;
font-weight: 400;
color: var(--color-text-mid);
margin: 0 0 1.25rem;
max-width: none;
}
/* Sibling .category-body would paint over the ⋯ popover; lift the title card above the list. */
@ -76,7 +96,10 @@ @@ -76,7 +96,10 @@
margin: 2rem 0;
padding-top: 0.5rem;
border-top: 1px solid var(--color-border);
font-size: 1rem;
font-size: 0.88rem;
font-weight: 400;
color: color-mix(in srgb, var(--color-text-mid) 58%, var(--color-bg) 42%);
font-family: var(--font-family), sans-serif;
}
.byline__author {

2
assets/styles/card.css

@ -36,7 +36,7 @@ h2.card-title { @@ -36,7 +36,7 @@ h2.card-title {
}
.article-list .card {
margin-bottom: 1rem;
margin-bottom: 1.75rem;
min-width: 0; /* column flex: do not let cover images set unshrinkable row width */
}

7
assets/styles/event.css

@ -94,8 +94,11 @@ @@ -94,8 +94,11 @@
flex-wrap: wrap;
gap: 0.5rem;
width: 100%;
color: var(--color-text-mid);
font-size: 0.95rem;
font-family: var(--font-family), sans-serif;
font-size: 0.78rem;
font-weight: 400;
line-height: 1.35;
color: color-mix(in srgb, var(--color-text-mid) 50%, var(--color-bg) 50%);
}
.event-page a:focus-visible {

26
assets/styles/layout.css

@ -144,7 +144,11 @@ @@ -144,7 +144,11 @@
width: 100%;
text-align: left;
padding: 0.45rem 0.75rem;
font: inherit;
font-family: var(--font-family), sans-serif;
font-size: 0.9rem;
font-weight: 400;
line-height: 1.3;
text-transform: none;
color: var(--color-text, inherit);
text-decoration: none;
background: none;
@ -258,7 +262,13 @@ a.nostr-share-menu__action { @@ -258,7 +262,13 @@ a.nostr-share-menu__action {
.header__categories ul {
flex-direction: column;
gap: 10px;
gap: 0.35rem;
align-items: stretch;
}
.header__cat-link {
width: 100%;
min-height: 2.6rem;
}
/* Log in / account block below category links in the hamburger */
@ -641,6 +651,9 @@ footer .footer-links { @@ -641,6 +651,9 @@ footer .footer-links {
max-width: 48rem;
margin: 0 auto;
padding: 0 0.5rem 2rem;
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.featured-authors-grid {
@ -709,7 +722,7 @@ footer .footer-links { @@ -709,7 +722,7 @@ footer .footer-links {
}
.featured-authors__intro {
margin-bottom: 2rem;
margin-bottom: 0;
overflow: visible; /* do not clip heading ascenders */
}
@ -718,7 +731,7 @@ footer .footer-links { @@ -718,7 +731,7 @@ footer .footer-links {
margin: 0 0 0.5rem;
font-size: clamp(1.35rem, 2.6vw, 2.05rem);
line-height: 1.28;
font-weight: 500;
font-weight: 700;
font-family: var(--heading-font), serif;
color: var(--color-primary);
padding: 0.2em 0 0.05em;
@ -807,17 +820,14 @@ footer .footer-links { @@ -807,17 +820,14 @@ footer .footer-links {
text-align: center;
}
/* Narrow: smaller page title + intro; flex gap avoids margin collapse with first author card. */
/* Narrow: smaller page title + intro; flex gap avoids margin collapse with first author block. */
@media (max-width: 1024px) {
.featured-authors {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 1.75rem;
}
.featured-authors__intro {
margin-bottom: 0;
/* Contain the intro <p> margin so it doesn’t collapse with the first author block */
display: flow-root;
}

96
src/Twig/ArticleCardCoverExtension.php

@ -0,0 +1,96 @@ @@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Twig;
use App\Service\CacheService;
use App\Service\NostrPathHelper;
use Symfony\Component\Asset\Packages;
use Throwable;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
/**
* Resolves a card hero image: article image, Nostr kind-0 profile {@see picture}, then site default image.
*/
final class ArticleCardCoverExtension extends AbstractExtension
{
/**
* Used when the article has no image and the author has no (or no usable) NIP-01 {@see picture} URL.
* Same asset as the header mark so empty hero slots read as the site, not a blank gray field.
*/
private const DEFAULT_PACKAGE_IMAGE = 'icons/favicon-96x96.png';
/**
* @var array<string, string> lowercase 64-hex pubkey → resolved cover URL (author picture or site default)
*/
private array $authorCoverMemo = [];
public function __construct(
private readonly CacheService $cacheService,
private readonly NostrPathHelper $nostrPathHelper,
private readonly Packages $packages,
) {
}
public function getFunctions(): array
{
return [
new TwigFunction('article_card_cover', $this->articleCardCover(...)),
];
}
/**
* @param string|null $articleImage Cover URL stored on the article, if any
* @param string|null $pubkeyHex 64-char hex (lowercase) Nostr public key, if any
*/
public function articleCardCover(?string $articleImage, ?string $pubkeyHex): string
{
if ($articleImage !== null && trim($articleImage) !== '') {
return trim($articleImage);
}
$pubkeyHex = $pubkeyHex !== null ? strtolower(trim($pubkeyHex)) : '';
if (64 !== strlen($pubkeyHex) || !ctype_xdigit($pubkeyHex)) {
return $this->defaultSiteImageUrl();
}
if (\array_key_exists($pubkeyHex, $this->authorCoverMemo)) {
return $this->authorCoverMemo[$pubkeyHex];
}
try {
$npub = $this->nostrPathHelper->npubFromPubkeyHex($pubkeyHex);
if ($npub === '') {
$url = $this->defaultSiteImageUrl();
$this->authorCoverMemo[$pubkeyHex] = $url;
return $url;
}
$meta = $this->cacheService->getMetadata($npub);
$pic = isset($meta->picture) ? trim((string) $meta->picture) : '';
if ($pic !== '') {
$this->authorCoverMemo[$pubkeyHex] = $pic;
return $pic;
}
} catch (Throwable) {
$out = $this->defaultSiteImageUrl();
$this->authorCoverMemo[$pubkeyHex] = $out;
return $out;
}
$out = $this->defaultSiteImageUrl();
$this->authorCoverMemo[$pubkeyHex] = $out;
return $out;
}
private function defaultSiteImageUrl(): string
{
return $this->packages->getUrl(self::DEFAULT_PACKAGE_IMAGE);
}
}

7
templates/components/Header.html.twig

@ -18,8 +18,13 @@ @@ -18,8 +18,13 @@
<li><twig:Molecules:CategoryLink :category="category" /></li>
{% endfor %}
{% if magazine_community_articles %}
{% set articles_nav_active = app.request.attributes.get('_route') == 'articles' %}
<li>
<a href="{{ path('articles') }}">Latest Articles</a>
<a
class="header__cat-link header__cat-link--cta{{ articles_nav_active ? ' header__cat-link--active' : '' }}"
href="{{ path('articles') }}"
{% if articles_nav_active %}aria-current="page"{% endif %}
>Latest Articles</a>
</li>
{% endif %}
</ul>

12
templates/components/Molecules/Card.html.twig

@ -14,9 +14,15 @@ @@ -14,9 +14,15 @@
<a href="{{ (article.pubkey and npub_from_hex(article.pubkey) != '') ? path('article', { npub: npub_from_hex(article.pubkey), slug: article.slug }) : path('article-legacy-redirect', { slug: article.slug }) }}">
<div class="card-header">
{% if category %}<small class="text-uppercase">{{ category }}</small>{% endif %}
{% if article.image %}
<img src="{{ article.image }}" alt="Cover image for {{ card_title != '' ? card_title : (article.slug|default('')) }}" onerror="this.style.display='none';" >
{% endif %}
<img
src="{{ article_card_cover(article.image, article.pubkey) }}"
alt="{{ ('Illustration for ' ~ (card_title != '' ? card_title : (article.slug|default('')|replace({'-': ' '})|title)))|e('html_attr') }}"
width="800"
height="450"
loading="lazy"
decoding="async"
onerror="this.onerror=null;this.style.display='none';"
>
</div>
<div class="card-body">
<h2 class="card-title">{% if card_title != '' %}{{ card_title }}{% else %}{{ article.slug|default('')|replace({'-': ' '})|title }}{% endif %}</h2>

10
templates/components/Molecules/CategoryLink.html.twig

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
<a {% if path('magazine-category', { 'slug' : slug }) in app.request.uri %}class="active"{% endif %}
href="{{ path('magazine-category', { 'slug' : slug }) }}">
{{ title }}
</a>
{% set nav_active = app.request.attributes.get('_route') == 'magazine-category' and app.request.attributes.get('slug') == slug %}
<a
class="header__cat-link{{ nav_active ? ' header__cat-link--active' : '' }}"
href="{{ path('magazine-category', { slug: slug }) }}"
{% if nav_active %}aria-current="page"{% endif %}
>{{ title }}</a>

16
templates/components/Organisms/FeaturedList.html.twig

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
<div>
{% if list %}
<div class="featured-cat">
<small><b>{{ title }}</b></small>
<small>{{ title }}</small>
</div>
<div {{ attributes }}>
<div>
@ -9,12 +9,15 @@ @@ -9,12 +9,15 @@
<div class="card">
<a href="{{ (feature.pubkey and npub_from_hex(feature.pubkey) != '') ? path('article', { npub: npub_from_hex(feature.pubkey), slug: feature.slug }) : path('article-legacy-redirect', { slug: feature.slug }) }}">
<div class="card-header">
{% if feature.image %}
<img src="{{ feature.image }}" alt="Cover image for {{ feature.title }}">
{% endif %}
<img src="{{ article_card_cover(feature.image, feature.pubkey) }}" alt="{{ ('Illustration for ' ~ feature.title)|e('html_attr') }}" width="1200" height="675" loading="lazy" decoding="async">
</div>
<div class="card-body">
<h2 class="card-title">{{ feature.title }}</h2>
{% if feature.createdAt %}
<p class="featured-list__meta">
<time datetime="{{ feature.createdAt|date('c') }}">{{ feature.createdAt|date('F j, Y') }}</time>
</p>
{% endif %}
<p class="lede truncate">
{{ feature.summary }}
</p>
@ -29,6 +32,11 @@ @@ -29,6 +32,11 @@
<a 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 }) }}">
<div class="card-body">
<h2 class="card-title">{{ item.title }}</h2>
{% if item.createdAt %}
<p class="featured-list__meta">
<time datetime="{{ item.createdAt|date('c') }}">{{ item.createdAt|date('F j, Y') }}</time>
</p>
{% endif %}
<p class="lede truncate">
{{ item.summary }}
</p>

Loading…
Cancel
Save