Browse Source

revamp landing page blocks and add an article feed on the left

imwald
Silberengel 2 days ago
parent
commit
b1c9bb990f
  1. 20
      assets/controllers/user_highlight_tooltip_controller.js
  2. 279
      assets/styles/app.css
  3. 105
      assets/styles/article.css
  4. 367
      assets/styles/layout.css
  5. 5
      config/unfold.yaml
  6. 1
      src/Controller/DefaultController.php
  7. 8
      src/Service/ArticleBodyHighlightInjector.php
  8. 106
      src/Service/MagazineContentService.php
  9. 4
      src/Service/TopicIndexService.php
  10. 2
      src/Twig/MagazineJumbleExtension.php
  11. 2
      src/Twig/TopTopicsExtension.php
  12. 2
      templates/base.html.twig
  13. 2
      templates/components/Footer.html.twig
  14. 33
      templates/components/Organisms/FeaturedWall.html.twig
  15. 38
      templates/components/Organisms/HomeCurationHeadlines.html.twig
  16. 14
      templates/components/Organisms/HomeHighlightsAside.html.twig
  17. 13
      templates/components/Organisms/SidebarMagazineCategoryRecent.html.twig
  18. 3
      templates/home.html.twig
  19. 1
      translations/messages.en.yaml

20
assets/controllers/user_highlight_tooltip_controller.js

@ -20,6 +20,18 @@ function shortNpub(n) {
return `${n.slice(0, 12)}${n.slice(-6)}`; return `${n.slice(0, 12)}${n.slice(-6)}`;
} }
/** @param {Date} d */
function formatHighlightDateUtc(d) {
try {
return new Intl.DateTimeFormat(undefined, {
dateStyle: 'medium',
timeZone: 'UTC',
}).format(d);
} catch {
return d.toISOString().slice(0, 10);
}
}
/** /**
* In-article highlight marks: hover/focus to show a tooltip of user-badges for everyone * In-article highlight marks: hover/focus to show a tooltip of user-badges for everyone
* who highlighted the same passage (data-hl JSON from {@see \App\Service\ArticleBodyHighlightInjector}). * who highlighted the same passage (data-hl JSON from {@see \App\Service\ArticleBodyHighlightInjector}).
@ -205,7 +217,7 @@ export default class extends Controller {
this._doHide(); this._doHide();
return; return;
} }
/** @type {Array<{e?: string, n: string, a?: string, p?: string}>} */ /** @type {Array<{e?: string, n: string, a?: string, p?: string, t?: number}>} */
let rows; let rows;
try { try {
rows = JSON.parse(raw); rows = JSON.parse(raw);
@ -253,6 +265,12 @@ export default class extends Controller {
} }
const nm = el('span', 'user-badge__name', a); const nm = el('span', 'user-badge__name', a);
nm.appendChild(document.createTextNode(label)); nm.appendChild(document.createTextNode(label));
if (typeof row.t === 'number' && row.t > 0) {
const timeEl = el('time', 'user-highlight__tip-date', li);
const d = new Date(row.t * 1000);
timeEl.setAttribute('datetime', d.toISOString());
timeEl.textContent = formatHighlightDateUtc(d);
}
} }
requestAnimationFrame(() => { requestAnimationFrame(() => {

279
assets/styles/app.css

@ -35,6 +35,13 @@ h1.brand {
line-height: 1.05; line-height: 1.05;
} }
/* Site chrome only (Header.html.twig): keep the wordmark readable but less dominant than body + nav. */
#site-header h1.brand {
font-size: clamp(1.45rem, 2.6vw, 2.55rem);
line-height: 1.06;
gap: 0.32em;
}
h1.brand a, h1.brand a,
@ -144,6 +151,15 @@ svg.icon {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 3.5rem; gap: 3.5rem;
width: 100%;
min-width: 0;
box-sizing: border-box;
}
@media (max-width: 1024px) {
.home-body.home-body--wall {
gap: 2.5rem;
}
} }
/* Home: NIP-51 30004 “headlines” — editorial section title + full-width article stack (not masonry). */ /* Home: NIP-51 30004 “headlines” — editorial section title + full-width article stack (not masonry). */
@ -280,6 +296,29 @@ svg.icon {
} }
} }
/* Home only: fixed 2-column picture grid (not masonry columns). */
.featured-list.featured-list--wall.featured-list--picture-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
column-count: unset;
column-gap: unset;
column-fill: unset;
gap: 1rem 1.1rem;
}
@media (min-width: 640px) {
.featured-list.featured-list--wall.featured-list--picture-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1100px) {
.featured-list.featured-list--wall.featured-list--picture-grid {
column-count: unset;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.featured-tile { .featured-tile {
--tile-hue: 140; --tile-hue: 140;
break-inside: avoid; break-inside: avoid;
@ -327,22 +366,195 @@ svg.icon {
border-radius: 0.35rem; border-radius: 0.35rem;
} }
/* Home picture blocks: sharp frame, full-bleed image, bottom text overlay */
.featured-list--picture-grid .featured-tile--picture-block {
break-inside: unset;
margin: 0;
border-radius: 2px;
border: 1px solid color-mix(in srgb, var(--color-text) 14%, var(--color-border));
box-shadow:
0 0 0 1px color-mix(in srgb, var(--color-bg) 100%, transparent),
0 1px 0 color-mix(in srgb, var(--color-text) 8%, transparent);
overflow: hidden;
background: var(--color-bg);
transition:
border-color 0.18s ease,
box-shadow 0.18s ease,
transform 0.18s ease;
}
.featured-list--picture-grid .featured-tile--picture-block:has(.featured-tile__link--picture:hover),
.featured-list--picture-grid .featured-tile--picture-block:has(.featured-tile__link--picture:focus-visible) {
border-color: color-mix(in srgb, hsl(var(--tile-hue) 42% 42%) 55%, var(--color-border) 45%);
box-shadow:
0 0 0 1px color-mix(in srgb, hsl(var(--tile-hue) 38% 48%) 22%, transparent),
0 12px 32px color-mix(in srgb, var(--color-text) 12%, transparent);
transform: translateY(-1px);
}
.featured-list--picture-grid .featured-tile__link--picture {
display: block;
color: inherit;
text-decoration: none;
}
.featured-list--picture-grid .featured-tile__link--picture:hover,
.featured-list--picture-grid .featured-tile__link--picture:focus-visible {
text-decoration: none;
}
.featured-list--picture-grid .featured-tile__link--picture:focus-visible {
outline: none;
}
.featured-list--picture-grid .featured-tile__picture {
position: relative;
aspect-ratio: 5 / 4;
overflow: hidden;
background: var(--color-bg-light);
}
.featured-list--picture-grid .featured-tile__picture-img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
display: block;
transform: scale(1.02);
transition: transform 0.35s ease;
}
.featured-list--picture-grid .featured-tile--picture-block:has(.featured-tile__link--picture:hover) .featured-tile__picture-img,
.featured-list--picture-grid .featured-tile--picture-block:has(.featured-tile__link--picture:focus-visible) .featured-tile__picture-img {
transform: scale(1.06);
}
.featured-list--picture-grid .featured-tile__picture-img[src*="favicon-96x96"] {
object-fit: contain;
padding: 2rem;
box-sizing: border-box;
transform: none;
}
.featured-list--picture-grid .featured-tile__picture-scrim {
position: absolute;
inset: 0;
pointer-events: none;
background: linear-gradient(
to top,
color-mix(in srgb, #0a0a0a 88%, transparent) 0%,
color-mix(in srgb, #0a0a0a 35%, transparent) 42%,
transparent 68%
);
}
.featured-list--picture-grid .featured-tile__picture-overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0.65rem 0.85rem 0.75rem;
z-index: 1;
display: flex;
flex-direction: column;
gap: 0.28rem;
min-width: 0;
}
.featured-list--picture-grid .featured-tile__picture-cat {
align-self: flex-start;
display: inline-block;
font-family: var(--font-family), system-ui, sans-serif;
font-size: 0.76rem;
font-weight: 800;
letter-spacing: 0.11em;
text-transform: uppercase;
line-height: 1.2;
padding: 0.32em 0.65em 0.28em;
border-radius: 0.28rem;
color: color-mix(in srgb, #fafafa 92%, hsl(var(--tile-hue) 70% 88%) 8%);
background: color-mix(in srgb, hsl(var(--tile-hue) 32% 12%) 72%, #000 28%);
border: 1px solid color-mix(in srgb, #fff 18%, hsl(var(--tile-hue) 45% 40%) 82%);
text-shadow: 0 1px 2px color-mix(in srgb, #000 65%, transparent);
box-shadow: 0 1px 0 color-mix(in srgb, #fff 12%, transparent);
transition:
color 0.22s ease,
background-color 0.22s ease,
border-color 0.22s ease,
box-shadow 0.22s ease,
text-shadow 0.22s ease,
transform 0.22s ease;
}
.featured-list--picture-grid .featured-tile--picture-block:has(.featured-tile__link:hover) .featured-tile__picture-cat,
.featured-list--picture-grid .featured-tile--picture-block:has(.featured-tile__link:focus-visible) .featured-tile__picture-cat {
color: #fff;
background: color-mix(in srgb, hsl(var(--tile-hue) 48% 52%) 78%, #fff 22%);
border-color: color-mix(in srgb, #fff 55%, hsl(var(--tile-hue) 55% 70%) 45%);
text-shadow:
0 0 14px color-mix(in srgb, hsl(var(--tile-hue) 72% 70%) 70%, transparent),
0 1px 3px color-mix(in srgb, #000 45%, transparent);
box-shadow:
0 0 22px color-mix(in srgb, hsl(var(--tile-hue) 62% 58%) 55%, transparent),
0 1px 0 color-mix(in srgb, #fff 35%, transparent);
transform: translateY(-1px);
}
.featured-list--picture-grid .featured-tile__picture-title {
font-family: var(--heading-font), ui-serif, serif;
font-size: clamp(1.05rem, 2.4vw, 1.35rem);
font-weight: 700;
line-height: 1.2;
color: #fafafa;
margin: 0;
letter-spacing: -0.02em;
text-shadow: 0 1px 3px color-mix(in srgb, #000 70%, transparent);
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
line-clamp: 3;
overflow: hidden;
}
.featured-tile__head { .featured-tile__head {
padding: 0.4rem 0.75rem 0.45rem; padding: 0.48rem 0.85rem 0.52rem;
background: color-mix(in srgb, hsl(var(--tile-hue) 34% 46%) 14%, var(--color-bg) 86%); background: color-mix(in srgb, hsl(var(--tile-hue) 34% 46%) 14%, var(--color-bg) 86%);
border-bottom: 1px solid color-mix(in srgb, hsl(var(--tile-hue) 32% 38%) 24%, var(--color-border) 76%); border-bottom: 1px solid color-mix(in srgb, hsl(var(--tile-hue) 32% 38%) 24%, var(--color-border) 76%);
transition:
background-color 0.22s ease,
border-color 0.22s ease,
box-shadow 0.22s ease;
} }
.featured-tile__cat { .featured-tile__cat {
display: block; display: block;
font-family: var(--font-family), sans-serif; font-family: var(--font-family), sans-serif;
font-size: 0.66rem; font-size: 0.8rem;
font-weight: 700; font-weight: 800;
letter-spacing: 0.12em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
/* Blend hue toward body mid-gray so every tile hue stays ≥4.5:1 on dark head strip */ /* Blend hue toward body mid-gray so every tile hue stays ≥4.5:1 on dark head strip */
color: color-mix(in srgb, hsl(var(--tile-hue) 26% 50%) 38%, var(--color-text-mid) 62%); color: color-mix(in srgb, hsl(var(--tile-hue) 26% 50%) 38%, var(--color-text-mid) 62%);
line-height: 1.35; line-height: 1.35;
transition:
color 0.22s ease,
text-shadow 0.22s ease,
transform 0.22s ease;
}
.featured-tile:has(.featured-tile__link:hover) .featured-tile__head,
.featured-tile:has(.featured-tile__link:focus-visible) .featured-tile__head {
background: color-mix(in srgb, hsl(var(--tile-hue) 42% 44%) 28%, var(--color-bg) 72%);
border-bottom-color: color-mix(in srgb, hsl(var(--tile-hue) 48% 46%) 40%, var(--color-border) 60%);
box-shadow: inset 0 0 0 1px color-mix(in srgb, hsl(var(--tile-hue) 55% 55%) 22%, transparent);
}
.featured-tile:has(.featured-tile__link:hover) .featured-tile__cat,
.featured-tile:has(.featured-tile__link:focus-visible) .featured-tile__cat {
color: color-mix(in srgb, hsl(var(--tile-hue) 22% 22%) 55%, var(--color-primary) 45%);
text-shadow: 0 0 12px color-mix(in srgb, hsl(var(--tile-hue) 58% 62%) 35%, transparent);
} }
.featured-tile__media { .featured-tile__media {
@ -586,8 +798,8 @@ svg.icon {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 0.4rem 0.6rem; gap: 0.45rem 0.65rem;
padding: 0.35rem 0.5rem 0.5rem; padding: 0.45rem 0.65rem 0.55rem;
margin: 0; margin: 0;
} }
@ -595,23 +807,23 @@ svg.icon {
list-style: none; list-style: none;
} }
/* Top category row: current section + clear hover affordance (passive “clean” list → scannable) */ /* Top category row: primary navigation — weight + contrast above the brand wordmark. */
.header__cat-link { .header__cat-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box; box-sizing: border-box;
padding: 0.4rem 0.75rem; padding: 0.42rem 0.85rem;
font-family: var(--font-family), sans-serif; font-family: var(--font-family), sans-serif;
font-size: 0.82rem; font-size: 0.92rem;
font-weight: 600; font-weight: 700;
letter-spacing: 0.04em; letter-spacing: 0.06em;
text-transform: uppercase; text-transform: uppercase;
text-decoration: none; text-decoration: none;
color: var(--color-text-mid); color: color-mix(in srgb, var(--color-primary) 78%, var(--color-text-mid) 22%);
background: transparent; background: color-mix(in srgb, var(--color-primary) 5%, var(--color-bg) 95%);
border: 1px solid transparent; border: 1px solid color-mix(in srgb, var(--color-primary) 14%, var(--color-border) 86%);
border-radius: 5px; border-radius: 6px;
transition: transition:
color 0.16s ease, color 0.16s ease,
background-color 0.16s ease, background-color 0.16s ease,
@ -622,7 +834,8 @@ svg.icon {
.header__cat-link:hover { .header__cat-link:hover {
text-decoration: none; text-decoration: none;
color: var(--color-primary); color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg) 92%); background: color-mix(in srgb, var(--color-primary) 11%, var(--color-bg) 89%);
border-color: color-mix(in srgb, var(--color-primary) 28%, var(--color-border) 72%);
box-shadow: 0 2px 0 0 var(--color-secondary); box-shadow: 0 2px 0 0 var(--color-secondary);
} }
@ -634,8 +847,9 @@ svg.icon {
.header__cat-link--active { .header__cat-link--active {
color: var(--color-primary); color: var(--color-primary);
font-weight: 700; font-weight: 800;
background: color-mix(in srgb, var(--color-primary) 14%, var(--color-bg) 86%); background: color-mix(in srgb, var(--color-primary) 16%, var(--color-bg) 84%);
border-color: color-mix(in srgb, var(--color-primary) 35%, var(--color-border) 65%);
box-shadow: inset 0 -2px 0 0 var(--color-primary); box-shadow: inset 0 -2px 0 0 var(--color-primary);
} }
@ -672,10 +886,10 @@ svg.icon {
@media (max-width: 1024px) { @media (max-width: 1024px) {
.header__logo .brand { .header__logo .brand {
font-size: clamp(1rem, 4.2vw, 1.45rem); font-size: clamp(0.95rem, 3.9vw, 1.32rem);
gap: 0.35rem; gap: 0.3rem;
/* Tight line-height + overflow:hidden on .brand__title clip ascenders; keep room for type. */ /* Tight line-height + overflow:hidden on .brand__title clip ascenders; keep room for type. */
line-height: 1.35; line-height: 1.3;
justify-content: flex-start; justify-content: flex-start;
text-align: left; text-align: left;
} }
@ -686,14 +900,14 @@ svg.icon {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
line-height: 1.35; line-height: 1.3;
/* Padding inside the clipping box so Lobster/serif caps aren’t sheared at the top */ /* Padding inside the clipping box so Lobster/serif caps aren’t sheared at the top */
padding: 0.2em 0 0.12em; padding: 0.12em 0 0.08em;
} }
.header__logo-circle { #site-header .header__logo-circle {
width: 44px; width: 40px;
height: 44px; height: 40px;
} }
.hamburger { .hamburger {
@ -721,6 +935,11 @@ svg.icon {
vertical-align: middle; vertical-align: middle;
} }
#site-header .header__logo-circle {
width: 48px;
height: 48px;
}
.header__logo-circle > img { .header__logo-circle > img {
position: absolute; position: absolute;
inset: 0; inset: 0;
@ -1316,12 +1535,12 @@ a:focus-visible {
@media (max-width: 600px) { @media (max-width: 600px) {
.header__logo .brand { .header__logo .brand {
font-size: clamp(0.95rem, 4.8vw, 1.25rem); font-size: clamp(0.88rem, 4.4vw, 1.12rem);
} }
.header__logo-circle { #site-header .header__logo-circle {
width: 40px; width: 36px;
height: 40px; height: 36px;
} }
} }

105
assets/styles/article.css

@ -1,3 +1,27 @@
/* Shared “reading pane” surface: full article pages and home NIP-51 30004 headline bodies. */
:root {
--article-reading-pane-bg: color-mix(in srgb, var(--color-bg) 70%, #ffffff 30%);
--article-reading-prose-color: color-mix(in srgb, var(--color-text-mid) 35%, var(--color-text) 65%);
}
.article-page-root > .card,
.article-page-root > .card-body {
background-color: var(--article-reading-pane-bg);
}
.article-page-root > .card {
margin-bottom: 0;
padding: 0.75rem max(1rem, env(safe-area-inset-left, 0px)) 0;
box-sizing: border-box;
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 55%, var(--article-reading-pane-bg) 45%);
}
.article-page-root > .card-body {
padding: 1rem max(1rem, env(safe-area-inset-left, 0px)) 2rem max(1rem, env(safe-area-inset-right, 0px));
margin-bottom: 0;
box-sizing: border-box;
}
.article-main { .article-main {
margin-top: 30px; margin-top: 30px;
} }
@ -25,6 +49,21 @@
line-height: 1.75; line-height: 1.75;
} }
.article-page-root .article-main p,
.article-page-root .article-main ul,
.article-page-root .article-main ol,
.article-page-root .article-main li,
.article-page-root .article-main blockquote,
.article-page-root .article-main table,
.home-curation-landmark .article-main p,
.home-curation-landmark .article-main ul,
.home-curation-landmark .article-main ol,
.home-curation-landmark .article-main li,
.home-curation-landmark .article-main blockquote,
.home-curation-landmark .article-main table {
color: var(--article-reading-prose-color);
}
.article-main table { .article-main table {
font-size: 1.3rem; font-size: 1.3rem;
} }
@ -72,6 +111,10 @@
max-width: none; max-width: none;
} }
.article-page-root > .card-body > .lede {
color: var(--article-reading-prose-color);
}
/* Sibling .category-body would paint over the ⋯ popover; lift the title card above the list. */ /* Sibling .category-body would paint over the ⋯ popover; lift the title card above the list. */
.category-page__header-card { .category-page__header-card {
position: relative; position: relative;
@ -123,6 +166,28 @@
padding-left: 30px; padding-left: 30px;
} }
.article-page-root .article-main blockquote p,
.home-curation-landmark .article-main blockquote p {
color: var(--article-reading-prose-color);
}
/* Home curated headline stack: one reading pane behind cover + headline + body (not only .__body). */
.home-curation-landmark .curation-article-display__pane {
background-color: var(--article-reading-pane-bg);
padding: 1rem 1.15rem 1.5rem;
box-sizing: border-box;
border-radius: 0.35rem;
}
.home-curation-landmark .curation-article-display__body {
padding: 0;
background: transparent;
}
.home-curation-landmark .curation-article-display__media {
background: transparent;
}
.table-of-contents { .table-of-contents {
border-left: var(--color-secondary) 6px solid; border-left: var(--color-secondary) 6px solid;
margin: 2em 0; margin: 2em 0;
@ -433,11 +498,11 @@
font-family: var(--main-body-font), serif; font-family: var(--main-body-font), serif;
} }
/* In-flow + aside: same NIP-84 mark treatment; scroll-margin in article for #highlight-… links */ /* In-flow: NIP-84 mark treatment on article pages only (scroll-margin for #highlight-… links below). */
.article-main mark.user-highlight__marker, .article-main mark.user-highlight__marker {
.home-aside-highlights__quote--html mark.user-highlight__marker {
margin: 0; margin: 0;
padding: 0.08em 0.1em 0.12em; padding: 0.08em 0.1em 0.12em;
border: none;
border-radius: 0.12em; border-radius: 0.12em;
font: inherit; font: inherit;
line-height: inherit; line-height: inherit;
@ -448,9 +513,24 @@
-webkit-box-decoration-break: clone; -webkit-box-decoration-break: clone;
} }
/* Home highlights feed: same <mark> markup, no highlight fill — reads as body copy. */
.home-aside-highlights__quote--html mark.user-highlight__marker {
margin: 0;
padding: 0;
border: none;
border-radius: 0;
font: inherit;
line-height: inherit;
color: inherit;
background: transparent;
box-shadow: none;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.article-main mark.user-highlight__marker, .article-main mark.user-highlight__marker,
.article-main .user-highlight__fragment-target { .article-main .user-highlight__fragment-target {
scroll-margin-top: calc(var(--site-fixed-header-offset, 140px) + 0.75rem); scroll-margin-top: calc(var(--site-fixed-header-offset, 112px) + 0.75rem);
} }
/* Invisible #highlight-{eid} anchors (same group as an older mark) — zero visual footprint. */ /* Invisible #highlight-{eid} anchors (same group as an older mark) — zero visual footprint. */
@ -482,7 +562,7 @@
padding: 0.5rem 0.65rem 0.6rem; padding: 0.5rem 0.65rem 0.6rem;
border-radius: 0.35rem; border-radius: 0.35rem;
background: var(--color-bg, #fff); background: var(--color-bg, #fff);
border: 1px solid color-mix(in srgb, var(--color-text-mid) 18%, var(--color-bg) 82%); border: none;
box-shadow: 0 0.15rem 0.75rem color-mix(in srgb, #000 12%, transparent); box-shadow: 0 0.15rem 0.75rem color-mix(in srgb, #000 12%, transparent);
font-size: 0.88rem; font-size: 0.88rem;
line-height: 1.35; line-height: 1.35;
@ -514,6 +594,21 @@
.user-highlight__tip-item { .user-highlight__tip-item {
margin: 0; margin: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.2rem;
min-width: 0;
}
.user-highlight__tip-date {
margin: 0 0 0 1.85rem;
font-size: 0.68rem;
font-weight: 400;
font-style: normal;
font-variant-numeric: tabular-nums;
color: var(--color-text-mid, #6b6b6b);
line-height: 1.25;
} }
.user-highlight__tip-popover .user-badge--in-tip { .user-highlight__tip-popover .user-badge--in-tip {

367
assets/styles/layout.css

@ -61,6 +61,55 @@
display: none; display: none;
} }
/* “Latest in categories” (only rendered on home when data exists). Hidden until wide layout or home stacked nav. */
.sidebar-magazine-recent {
display: none;
}
.sidebar-magazine-recent__title {
margin: 0 0 0.5rem;
font-family: var(--font-family), sans-serif;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-text-mid) 72%, var(--color-bg) 28%);
line-height: 1.3;
}
.sidebar-magazine-recent__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.layout > nav .sidebar-magazine-recent__item {
margin: 0;
}
.sidebar-magazine-recent__link {
display: block;
font-family: var(--font-family), system-ui, sans-serif;
font-size: 0.78rem;
font-weight: 500;
line-height: 1.35;
color: var(--color-primary);
text-decoration: none;
padding: 0.15rem 0;
border-bottom: 1px solid transparent;
transition: color 0.15s ease, border-color 0.15s ease;
}
.layout > nav .sidebar-magazine-recent__link:hover,
.layout > nav .sidebar-magazine-recent__link:focus-visible {
color: color-mix(in srgb, var(--color-secondary) 55%, var(--color-primary));
text-decoration: none;
border-bottom-color: color-mix(in srgb, var(--color-text-mid) 22%, var(--color-border));
}
@media (min-width: 1025px) { @media (min-width: 1025px) {
.sidebar-featured-authors { .sidebar-featured-authors {
display: block; display: block;
@ -204,6 +253,13 @@
color: color-mix(in srgb, var(--color-primary) 42%, var(--color-text-mid)); color: color-mix(in srgb, var(--color-primary) 42%, var(--color-text-mid));
text-decoration: none; text-decoration: none;
} }
.sidebar-magazine-recent {
display: block;
margin-top: 1.05rem;
padding-top: 0.85rem;
border-top: 1px solid var(--color-border);
}
} }
/* Only the app chrome in Header.html.twig (#site-header). A bare `header` rule also /* Only the app chrome in Header.html.twig (#site-header). A bare `header` rule also
@ -222,7 +278,7 @@
.header__logo padding in the max-width block below. */ .header__logo padding in the max-width block below. */
@media (min-width: 1025px) { @media (min-width: 1025px) {
#site-header { #site-header {
padding-top: max(0.65rem, env(safe-area-inset-top, 0px)); padding-top: max(0.4rem, env(safe-area-inset-top, 0px));
} }
} }
@ -387,9 +443,9 @@ a.nostr-share-menu__action {
box-sizing: border-box; box-sizing: border-box;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.45rem;
/* Top: safe area (notch) + room so the site title isn’t flush under the browser chrome */ /* Top: safe area (notch) + tighter bar so content starts sooner */
padding: max(0.5rem, env(safe-area-inset-top, 0px)) max(0.65rem, env(safe-area-inset-left)) 0.45rem max(0.65rem, env(safe-area-inset-right)); padding: max(0.32rem, env(safe-area-inset-top, 0px)) max(0.55rem, env(safe-area-inset-left)) 0.32rem max(0.55rem, env(safe-area-inset-right));
} }
.header__brand { .header__brand {
@ -402,8 +458,8 @@ a.nostr-share-menu__action {
.header__categories { .header__categories {
display: none; display: none;
flex-direction: column; flex-direction: column;
padding-top: 10px; padding-top: 0.35rem;
padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px)); padding-bottom: max(0.85rem, env(safe-area-inset-bottom, 0px));
} }
.header__categories.active { .header__categories.active {
@ -416,24 +472,33 @@ a.nostr-share-menu__action {
} }
.header__categories ul { .header__categories ul {
flex-direction: column; flex-direction: row;
gap: 0.35rem; flex-wrap: wrap;
align-items: stretch; justify-content: center;
align-items: center;
gap: 0.4rem 0.55rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
padding: 0.15rem 0.35rem 0.35rem;
} }
.header__cat-link { .header__cat-link {
width: 100%; width: auto;
min-height: 2.6rem; min-width: 0;
min-height: 2.35rem;
padding: 0.35rem 0.7rem;
box-sizing: border-box;
} }
/* Log in / account block below category links in the hamburger */ /* Log in / account block below category links in the hamburger */
.header__mobile-account { .header__mobile-account {
align-self: stretch; align-self: stretch;
text-align: left; text-align: center;
width: 100%; width: 100%;
max-width: 32rem; max-width: 32rem;
margin: 0 auto; margin: 0 auto;
padding: 0.75rem 0.25rem 0; padding: 0.65rem 0.35rem 0;
border-top: 1px solid var(--color-border); border-top: 1px solid var(--color-border);
} }
} }
@ -465,7 +530,7 @@ a.nostr-share-menu__action {
/* Main content */ /* Main content */
:root { :root {
/* Clears fixed #site-header; keep in sync with main margin-top per breakpoint below. */ /* Clears fixed #site-header; keep in sync with main margin-top per breakpoint below. */
--site-fixed-header-offset: 140px; --site-fixed-header-offset: 112px;
} }
/* #:target / in-page links: scroll position leaves room under the fixed bar (scroll-margin on inline <mark> is unreliable alone). */ /* #:target / in-page links: scroll position leaves room under the fixed bar (scroll-margin on inline <mark> is unreliable alone). */
@ -483,7 +548,7 @@ main {
.user-menu { .user-menu {
position: fixed; position: fixed;
top: 150px; top: calc(var(--site-fixed-header-offset, 112px) + 0.35rem);
width: calc(21vw - 10px); width: calc(21vw - 10px);
min-width: 150px; min-width: 150px;
max-width: 270px; max-width: 270px;
@ -491,7 +556,7 @@ main {
@media (min-width: 1025px) { @media (min-width: 1025px) {
:root { :root {
--site-fixed-header-offset: 152px; --site-fixed-header-offset: 120px;
} }
/* Match extra header padding-top so content and menu clear the fixed bar */ /* Match extra header padding-top so content and menu clear the fixed bar */
@ -616,25 +681,22 @@ aside {
.home-aside-highlights__item-inner { .home-aside-highlights__item-inner {
position: relative; position: relative;
color: color-mix(in srgb, var(--color-text-mid) 90%, var(--color-primary) 10%); color: color-mix(in srgb, var(--color-text-mid) 90%, var(--color-primary) 10%);
padding: 0.1rem 0 0.15rem 0.55rem; padding: 0.1rem 0 0.15rem 0;
border: none; border: none;
border-left: 1px solid color-mix(in srgb, var(--color-text-mid) 7%, var(--color-border) 93%);
border-radius: 0; border-radius: 0;
background: transparent; background: transparent;
line-height: 1.45; line-height: 1.45;
font-size: 0.78rem; font-size: 0.78rem;
transition: color 0.18s ease, border-left-color 0.18s ease, background 0.18s ease; transition: color 0.18s ease, background 0.18s ease;
} }
.home-aside-highlights__item-inner:hover { .home-aside-highlights__item-inner:hover {
color: var(--color-primary); color: var(--color-primary);
border-left-color: color-mix(in srgb, var(--color-primary) 32%, var(--color-border) 68%);
background: color-mix(in srgb, var(--color-primary) 4%, var(--color-bg) 96%); background: color-mix(in srgb, var(--color-primary) 4%, var(--color-bg) 96%);
} }
.home-aside-highlights__item-inner:has(.home-aside-highlights__hit:focus-visible) { .home-aside-highlights__item-inner:has(.home-aside-highlights__hit:focus-visible) {
color: var(--color-primary); color: var(--color-primary);
border-left-color: color-mix(in srgb, var(--color-primary) 38%, var(--color-border) 62%);
background: color-mix(in srgb, var(--color-primary) 4%, var(--color-bg) 96%); background: color-mix(in srgb, var(--color-primary) 4%, var(--color-bg) 96%);
outline: 2px solid var(--color-focus-ring, var(--color-primary)); outline: 2px solid var(--color-focus-ring, var(--color-primary));
outline-offset: 3px; outline-offset: 3px;
@ -647,51 +709,6 @@ aside {
text-decoration: none; text-decoration: none;
} }
/* Highlight author (small badge link) + date above quote; badge is clickable, rest of row opens article. */
.home-aside-highlights__byline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.3rem 0.5rem;
margin: 0 0 0.35rem;
position: relative;
z-index: 3;
font-family: var(--font-family), system-ui, sans-serif;
font-size: 0.68rem;
line-height: 1.2;
pointer-events: none;
}
.home-aside-highlights__who {
display: inline-flex;
max-width: 100%;
pointer-events: auto;
}
.home-aside-highlights__byline .user-badge {
gap: 0.28rem;
}
.home-aside-highlights__byline .user-badge__avatar {
width: 1.125rem;
height: 1.125rem;
}
.home-aside-highlights__byline .user-badge__name {
font-size: 0.68rem;
max-width: 7.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-aside-highlights__time {
font-size: 0.65rem;
color: color-mix(in srgb, var(--color-text-mid) 88%, var(--color-bg) 12%);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Let clicks go to the overlay; quote/meta stay visible above background only visually (no pointer on text). */ /* Let clicks go to the overlay; quote/meta stay visible above background only visually (no pointer on text). */
.home-aside-highlights__item-inner .home-aside-highlights__quote, .home-aside-highlights__item-inner .home-aside-highlights__quote,
.home-aside-highlights__item-inner .home-aside-highlights__meta { .home-aside-highlights__item-inner .home-aside-highlights__meta {
@ -744,11 +761,6 @@ aside {
color: color-mix(in srgb, var(--color-primary) 45%, var(--color-text-mid) 55%); color: color-mix(in srgb, var(--color-primary) 45%, var(--color-text-mid) 55%);
} }
.home-aside-highlights__item-inner:hover .home-aside-highlights__time,
.home-aside-highlights__item-inner:has(.home-aside-highlights__hit:focus-visible) .home-aside-highlights__time {
color: color-mix(in srgb, var(--color-primary) 32%, var(--color-text-mid) 68%);
}
table { table {
width: 100%; width: 100%;
margin: 20px 0; margin: 20px 0;
@ -776,26 +788,233 @@ dt {
aside { aside {
display: none; /* Hide the sidebars on small screens */ display: none; /* Hide the sidebars on small screens */
} }
/* Home: keep highlights — stack <aside> under the featured wall (same DOM order; row layout would squeeze it beside main). */ /* Home: main first, then left chrome (topics + lists), then highlights — single column, full viewport width. */
.layout:has(.home-body--wall) { .layout:has(.home-body--wall) {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
width: 100%;
max-width: 100%;
min-width: 0;
}
.layout:has(.home-body--wall) > main {
order: 1;
width: 100%;
min-width: 0;
box-sizing: border-box;
padding-left: max(1em, env(safe-area-inset-left, 0px));
padding-right: max(1em, env(safe-area-inset-right, 0px));
}
.layout:has(.home-body--wall) > nav {
display: block;
order: 2;
width: 100%;
max-width: none;
min-width: 0;
flex-shrink: 0;
margin-top: 0.25rem;
padding: 0.75rem max(1rem, env(safe-area-inset-left, 0px)) 1.25rem max(1rem, env(safe-area-inset-right, 0px));
box-sizing: border-box;
border-top: 1px solid var(--color-border);
}
.layout:has(.home-body--wall) > nav .user-menu {
position: static;
width: 100%;
min-width: 0;
max-width: none;
top: auto;
left: auto;
}
.layout:has(.home-body--wall) .sidebar-top-topics {
display: block;
margin-top: 0.35rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border);
}
.layout:has(.home-body--wall) .sidebar-featured-authors {
display: block;
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--color-border);
}
.layout:has(.home-body--wall) .sidebar-magazine-recent {
display: block;
margin-top: 0.65rem;
padding-top: 0.65rem;
border-top: 1px solid var(--color-border);
} }
.layout:has(.home-body--wall) > aside { .layout:has(.home-body--wall) > aside {
display: block; display: block;
order: 3;
width: 100%; width: 100%;
max-width: none; max-width: none;
min-width: 0; min-width: 0;
flex-shrink: 0; flex-shrink: 0;
margin-top: 0.5rem; margin-top: 0.5rem;
padding: 0 1em 1.75rem; padding: 0 max(1rem, env(safe-area-inset-left, 0px)) 1.75rem max(1rem, env(safe-area-inset-right, 0px));
box-sizing: border-box; box-sizing: border-box;
} }
/* Sidebar chrome is styled only inside min-width:1025px elsewhere; restyle here for stacked home nav. */
.layout:has(.home-body--wall) > nav .sidebar-featured-authors__grid > li,
.layout:has(.home-body--wall) > nav .sidebar-top-topics__list > li,
.layout:has(.home-body--wall) > nav .sidebar-magazine-recent__list > li {
margin: 0;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__title,
.layout:has(.home-body--wall) .sidebar-top-topics__title,
.layout:has(.home-body--wall) .sidebar-magazine-recent__title {
margin: 0 0 0.55rem;
font-family: var(--font-family), sans-serif;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-text-mid) 72%, var(--color-bg) 28%);
line-height: 1.3;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(3.75rem, 1fr));
gap: 0.75rem 0.65rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
list-style: none;
margin: 0;
padding: 0;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__item {
margin: 0;
padding: 0;
list-style: none;
min-width: 0;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__link {
display: block;
width: 100%;
aspect-ratio: 1;
max-width: 100%;
border-radius: 50%;
overflow: hidden;
line-height: 0;
color: inherit;
text-decoration: none;
box-sizing: border-box;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__link:hover {
text-decoration: none;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__link:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__avatar {
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
background: var(--color-bg-light);
box-shadow: 0 0 0 1px var(--color-border);
}
.layout:has(.home-body--wall) .sidebar-featured-authors__avatar img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__avatar img[src*="favicon-96x96"] {
object-fit: contain;
object-position: center;
padding: 0.2rem;
box-sizing: border-box;
}
.layout:has(.home-body--wall) .sidebar-featured-authors__link:hover .sidebar-featured-authors__avatar {
box-shadow: 0 0 0 2px var(--color-secondary);
}
.layout:has(.home-body--wall) .sidebar-top-topics__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.45rem 0.4rem;
width: 100%;
max-width: 100%;
box-sizing: border-box;
align-items: flex-start;
}
.layout:has(.home-body--wall) > nav a.topic-badge.sidebar-top-topics__link,
.layout:has(.home-body--wall) > nav a.topic-badge {
display: inline-block;
max-width: 100%;
background-color: color-mix(in srgb, var(--color-text-mid) 7%, var(--color-bg));
color: color-mix(in srgb, var(--color-text-mid) 78%, var(--color-bg) 22%);
padding: 0.28rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
line-height: 1.35;
font-weight: 400;
text-decoration: none;
border: none;
box-sizing: border-box;
word-break: break-word;
transition: background-color 0.2s ease, color 0.2s ease;
}
.layout:has(.home-body--wall) > nav a.topic-badge.sidebar-top-topics__link:hover,
.layout:has(.home-body--wall) > nav a.topic-badge:hover,
.layout:has(.home-body--wall) > nav a.topic-badge.sidebar-top-topics__link:focus-visible,
.layout:has(.home-body--wall) > nav a.topic-badge:focus-visible {
background-color: color-mix(in srgb, var(--color-text-mid) 12%, var(--color-bg));
color: color-mix(in srgb, var(--color-primary) 42%, var(--color-text-mid));
text-decoration: none;
}
.layout:has(.home-body--wall) .sidebar-magazine-recent__list {
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.layout:has(.home-body--wall) .sidebar-magazine-recent__link {
max-width: 100%;
box-sizing: border-box;
}
.layout:has(.home-body--wall) .home-aside-highlights {
width: 100%;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
}
/* Fixed header is taller than 90px (safe-area + logo row + title padding). Match it or the first /* Fixed header is taller than 90px (safe-area + logo row + title padding). Match it or the first
main content (e.g. featured authors intro) sits under the bar and looks cut off at the top. */ main content (e.g. featured authors intro) sits under the bar and looks cut off at the top. */
:root { :root {
--site-fixed-header-offset: max(7.25rem, calc(4.8rem + env(safe-area-inset-top, 0px))); --site-fixed-header-offset: max(5.85rem, calc(3.85rem + env(safe-area-inset-top, 0px)));
} }
main { main {

5
config/unfold.yaml

@ -49,6 +49,7 @@ parameters:
d_tag: '%d_tag_magazine%' d_tag: '%d_tag_magazine%'
# 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. # 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' d_tag_curation_set: 'nostr-curated-headlines'
# Whether to show community articles on the home page
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'
@ -67,5 +68,5 @@ parameters:
url: "https://git.imwald.eu/silberengel/unfold/src/branch/imwald" url: "https://git.imwald.eu/silberengel/unfold/src/branch/imwald"
description: "This site’s Unfold source (imwald branch)." description: "This site’s Unfold source (imwald branch)."
- title: "Decent Newsroom" - title: "Decent Newsroom"
url: "https://decentnewsroom.com/" url: "https://decentnewsroom.com/mag/newsroom-magazine-on-imwald-by-laeserin"
description: "Decentralized magazine platform." description: "Decentralized magazine platform. View the magazine on Decent Newsroom."

1
src/Controller/DefaultController.php

@ -31,6 +31,7 @@ class DefaultController extends AbstractController
'home_curation_heading' => $curation['heading'], 'home_curation_heading' => $curation['heading'],
'home_curation_tiles' => $curation['tiles'], 'home_curation_tiles' => $curation['tiles'],
'home_featured_tiles' => $this->magazineContent->buildHomeMixedFeaturedWallTiles($categoryATags), 'home_featured_tiles' => $this->magazineContent->buildHomeMixedFeaturedWallTiles($categoryATags),
'home_sidebar_category_recent' => $this->magazineContent->buildHomeSidebarCategorizedRecent($categoryATags),
'home_highlights' => $this->articleHighlightRepository->findRecentForHome(40), 'home_highlights' => $this->articleHighlightRepository->findRecentForHome(40),
]); ]);
} }

8
src/Service/ArticleBodyHighlightInjector.php

@ -332,6 +332,7 @@ final class ArticleBodyHighlightInjector
/** /**
* NIP-84: same highlighted passage → one mark, dedupe authors by npub, profile from cache. * NIP-84: same highlighted passage → one mark, dedupe authors by npub, profile from cache.
* JSON objects: e (event id hex), n (npub), a (display name), p (picture URL), t (created_at unix, optional).
* *
* @param list<ArticleHighlight> $group * @param list<ArticleHighlight> $group
*/ */
@ -371,12 +372,17 @@ final class ArticleBodyHighlightInjector
} }
} catch (\Throwable) { } catch (\Throwable) {
} }
$byNpub[$npub] = [ $created = $h->getEventCreatedAt();
$row = [
'e' => \strtolower($eidH), 'e' => \strtolower($eidH),
'n' => $npub, 'n' => $npub,
'a' => $name, 'a' => $name,
'p' => $pic, 'p' => $pic,
]; ];
if ($created > 0) {
$row['t'] = $created;
}
$byNpub[$npub] = $row;
} }
return \json_encode(\array_values($byNpub), \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR); return \json_encode(\array_values($byNpub), \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR);

106
src/Service/MagazineContentService.php

@ -776,7 +776,7 @@ final class MagazineContentService
} }
/** /**
* Interleaves up to four articles per home category in round-robin order (one “wall” mixing all topics). * Interleaves up to two articles per home category in round-robin order (one “wall” mixing all topics).
* Duplicate slugs across categories are skipped so each article appears at most once. * Duplicate slugs across categories are skipped so each article appears at most once.
* *
* @param list<array<int, string>> $categoryATags * @param list<array<int, string>> $categoryATags
@ -835,7 +835,98 @@ final class MagazineContentService
} }
/** /**
* Same resolution as {@see \App\Twig\Components\Organisms\FeaturedList} (4 cards per category). * Distinct articles referenced by any home magazine category index (`a` tags), newest by display date
* (published or created). For the left nav list below topic badges.
*
* @param list<array<int, string>> $categoryATags
*
* @return list<FeaturedArticleCard>
*/
public function buildHomeSidebarCategorizedRecent(array $categoryATags, int $limit = 24): array
{
if ($limit < 1) {
return [];
}
$slugSet = [];
foreach ($categoryATags as $row) {
$coord = \trim((string) ($row[1] ?? ''));
if ($coord === '') {
continue;
}
foreach ($this->slugsFromCategoryCoord($coord, 200) as $s) {
if ($s !== '') {
$slugSet[$s] = true;
}
}
}
$unionSlugs = \array_keys($slugSet);
if ($unionSlugs === []) {
return [];
}
$articles = $this->articleRepository->findFeaturedCardsBySlugs($unionSlugs);
$slugMap = [];
foreach ($articles as $article) {
$articleSlug = \trim((string) $article->getSlug());
if ($articleSlug === '') {
continue;
}
if (!isset($slugMap[$articleSlug]) || $this->featuredCardIsNewer($article, $slugMap[$articleSlug])) {
$slugMap[$articleSlug] = $article;
}
}
$list = \array_values($slugMap);
\usort($list, static function (FeaturedArticleCard $a, FeaturedArticleCard $b): int {
$da = $a->getDisplayAt();
$db = $b->getDisplayAt();
if ($da === $db) {
return 0;
}
if ($da === null) {
return 1;
}
if ($db === null) {
return -1;
}
return $db <=> $da;
});
return \array_slice($list, 0, $limit);
}
/**
* @return list<string> `#d` slugs from kind-30023 `a` tags in category index order (trimmed, non-empty)
*/
private function slugsFromCategoryCoord(string $categoryCoord, int $maxA): array
{
if ($maxA < 1) {
return [];
}
$parts = explode(':', $categoryCoord, 3);
if (\count($parts) < 3) {
return [];
}
$slug = $parts[2];
$catIndex = $this->store->getCategory($slug);
if ($catIndex === null) {
return [];
}
$slugs = [];
foreach ($catIndex->getTags() as $tag) {
if (($tag[0] ?? null) === 'a' && isset($tag[1])) {
$segs = explode(':', (string) $tag[1], 3);
$slugs[] = \trim((string) end($segs));
if (\count($slugs) >= $maxA) {
break;
}
}
}
return \array_values(\array_filter($slugs, static fn (string $s): bool => $s !== ''));
}
/**
* Same resolution as {@see \App\Twig\Components\Organisms\FeaturedList} index tags; at most two cards per category for the home wall.
* *
* @return null|array{title: string, cards: list<FeaturedArticleCard>} * @return null|array{title: string, cards: list<FeaturedArticleCard>}
*/ */
@ -852,23 +943,16 @@ final class MagazineContentService
} }
$title = ''; $title = '';
$slugs = [];
foreach ($catIndex->getTags() as $tag) { foreach ($catIndex->getTags() as $tag) {
if (($tag[0] ?? null) === 'title' && isset($tag[1])) { if (($tag[0] ?? null) === 'title' && isset($tag[1])) {
$title = (string) $tag[1]; $title = (string) $tag[1];
} }
if (($tag[0] ?? null) === 'a' && isset($tag[1])) {
$segs = explode(':', (string) $tag[1], 3);
$slugs[] = \trim((string) end($segs));
if (\count($slugs) >= 5) {
break;
}
}
} }
if ($title === '') { if ($title === '') {
$title = $slug; $title = $slug;
} }
$slugs = $this->slugsFromCategoryCoord($categoryCoord, 40);
if ($slugs === []) { if ($slugs === []) {
return null; return null;
} }
@ -892,7 +976,7 @@ final class MagazineContentService
$orderedList[] = $slugMap[$articleSlug]; $orderedList[] = $slugMap[$articleSlug];
} }
} }
$cards = \array_slice($orderedList, 0, 4); $cards = \array_slice($orderedList, 0, 2);
return ['title' => $title, 'cards' => $cards]; return ['title' => $title, 'cards' => $cards];
} }

4
src/Service/TopicIndexService.php

@ -20,11 +20,11 @@ final class TopicIndexService
} }
/** /**
* Up to 25 most relevant topic strings, scored by count + 5× featured (magazine home cards). * Most relevant topic strings (default cap 10 in sidebar), scored by count + 5× featured (magazine home cards).
* *
* @return list<string> topic labels (lowercase, no #) * @return list<string> topic labels (lowercase, no #)
*/ */
public function getTopTopicLabels(int $limit = 25): array public function getTopTopicLabels(int $limit = 10): array
{ {
$conn = $this->articleRepository->getEntityManager()->getConnection(); $conn = $this->articleRepository->getEntityManager()->getConnection();
$slugs = $this->magazineContent->collectFeaturedArticleSlugsForHome( $slugs = $this->magazineContent->collectFeaturedArticleSlugsForHome(

2
src/Twig/MagazineJumbleExtension.php

@ -12,7 +12,7 @@ use Twig\Extension\AbstractExtension;
use Twig\TwigFunction; use Twig\TwigFunction;
/** /**
* Footer “View this magazine on Jumble”: Jumble /feed/notes/{naddr} for the site root kind 30040 index. * Footer “View this magazine on Jumble Imwald”: Jumble /feed/notes/{naddr} for the site root kind 30040 index.
*/ */
final class MagazineJumbleExtension extends AbstractExtension final class MagazineJumbleExtension extends AbstractExtension
{ {

2
src/Twig/TopTopicsExtension.php

@ -18,7 +18,7 @@ final class TopTopicsExtension extends AbstractExtension
public function getFunctions(): array public function getFunctions(): array
{ {
return [ return [
new TwigFunction('top_topic_labels', function (int $limit = 25): array { new TwigFunction('top_topic_labels', function (int $limit = 10): array {
return $this->topicIndexService->getTopTopicLabels($limit); return $this->topicIndexService->getTopTopicLabels($limit);
}), }),
]; ];

2
templates/base.html.twig

@ -41,7 +41,7 @@
{% if _sidebar_fa is not empty %} {% if _sidebar_fa is not empty %}
{% include 'components/Organisms/SidebarFeaturedAuthors.html.twig' with { rows: _sidebar_fa } only %} {% include 'components/Organisms/SidebarFeaturedAuthors.html.twig' with { rows: _sidebar_fa } only %}
{% endif %} {% endif %}
{% set _top_topics = top_topic_labels(25) %} {% set _top_topics = top_topic_labels(10) %}
{% if _top_topics is not empty %} {% if _top_topics is not empty %}
{% include 'components/Organisms/SidebarTopTopics.html.twig' with { labels: _top_topics } only %} {% include 'components/Organisms/SidebarTopTopics.html.twig' with { labels: _top_topics } only %}
{% endif %} {% endif %}

2
templates/components/Footer.html.twig

@ -25,7 +25,7 @@
</div> </div>
<div class="site-footer__main"> <div class="site-footer__main">
<p class="site-footer__jumble"> <p class="site-footer__jumble">
<a class="site-footer__link" href="{{ magazine_on_jumble_url()|e('html_attr') }}" target="_blank" rel="nofollow noopener noreferrer">View this magazine on Jumble</a> <a class="site-footer__link" href="{{ magazine_on_jumble_url()|e('html_attr') }}" target="_blank" rel="nofollow noopener noreferrer">View this magazine on Jumble Imwald</a>
</p> </p>
<div class="footer-links"> <div class="footer-links">
{% for link in footer_links %} {% for link in footer_links %}

33
templates/components/Organisms/FeaturedWall.html.twig

@ -1,38 +1,35 @@
{# {#
Single masonry wall: mixed categories (round-robin), same tile styling as the former per-section list. Home wall: two picture blocks per row (responsive → one column), title + category on image overlay.
#} #}
{% if tiles is not empty %} {% if tiles is not empty %}
<div <div
class="featured-list featured-list--wall{{ wall_extra_class|default('') != '' ? ' ' ~ wall_extra_class : '' }}" class="featured-list featured-list--wall featured-list--picture-grid{{ wall_extra_class|default('') != '' ? ' ' ~ wall_extra_class : '' }}"
role="region" role="region"
aria-label="{{ (region_aria_label|default(website_name ~ ' — featured articles'))|e('html_attr') }}" aria-label="{{ (region_aria_label|default(website_name ~ ' — featured articles'))|e('html_attr') }}"
> >
{% for tile in tiles %} {% for tile in tiles %}
{% set _hue = (tile.categoryTitle|default('x')|length * 47) % 360 %} {% set _hue = (tile.categoryTitle|default('x')|length * 47) % 360 %}
{% set item = tile.article %} {% set item = tile.article %}
<article class="featured-tile" style="--tile-hue: {{ _hue }};"> {% 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="featured-tile featured-tile--picture-block" style="--tile-hue: {{ _hue }};">
<a <a
class="featured-tile__link" class="featured-tile__link featured-tile__link--picture"
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 }) }}" href="{{ article_href }}"
aria-label="{{ (item.title ~ ' — ' ~ tile.categoryTitle)|e('html_attr') }}"
> >
<div class="featured-tile__head"> <div class="featured-tile__picture">
<span class="featured-tile__cat">{{ tile.categoryTitle }}</span>
</div>
<div class="featured-tile__media featured-tile__media--ar{{ loop.index0 % 4 }}">
<img <img
class="featured-tile__picture-img"
src="{{ article_card_cover(item.image, item.pubkey) }}" src="{{ article_card_cover(item.image, item.pubkey) }}"
alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}" alt=""
width="1200"
height="675"
loading="lazy" loading="lazy"
decoding="async" decoding="async"
> >
</div> <div class="featured-tile__picture-scrim" aria-hidden="true"></div>
<div class="card-body"> <div class="featured-tile__picture-overlay">
<h2 class="card-title">{{ item.title }}</h2> <span class="featured-tile__picture-cat">{{ tile.categoryTitle|e }}</span>
<p class="lede truncate"> <h2 class="featured-tile__picture-title">{{ item.title|e }}</h2>
{{ item.summary }} </div>
</p>
</div> </div>
</a> </a>
</article> </article>

38
templates/components/Organisms/HomeCurationHeadlines.html.twig

@ -20,25 +20,27 @@
{% set item = tile.article %} {% 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 }) %} {% 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"> <article class="curation-article-display">
<div class="curation-article-display__media"> <div class="curation-article-display__pane">
<a href="{{ article_href }}" tabindex="-1" aria-hidden="true"> <div class="curation-article-display__media">
<img <a href="{{ article_href }}" tabindex="-1" aria-hidden="true">
src="{{ article_card_cover(item.image, item.pubkey) }}" <img
alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}" src="{{ article_card_cover(item.image, item.pubkey) }}"
loading="{{ loop.first ? 'eager' : 'lazy' }}" alt="{{ ('Illustration for ' ~ item.title)|e('html_attr') }}"
decoding="async" 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"
> >
</a> {{ tile.body_html|raw }}
</div> </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>
</div> </div>
</article> </article>

14
templates/components/Organisms/HomeHighlightsAside.html.twig

@ -17,20 +17,6 @@
> >
<span class="visually-hidden">{{ 'sidebar.highlight_view'|trans({ '%title%': art.title|default('') }) }}</span> <span class="visually-hidden">{{ 'sidebar.highlight_view'|trans({ '%title%': art.title|default('') }) }}</span>
</a> </a>
{% if h.authorPubkey|default('')|length == 64 %}
<div class="home-aside-highlights__byline">
<span class="home-aside-highlights__who">
<twig:Molecules:UserFromNpub ident="{{ h.authorPubkey }}" />
</span>
{% if h.eventCreatedAt|default(0) > 0 %}
<time class="home-aside-highlights__time" datetime="{{ h.eventCreatedAt|date('c', 'UTC') }}">{{ h.eventCreatedAt|date('M j, Y', 'UTC') }}</time>
{% endif %}
</div>
{% elseif h.eventCreatedAt|default(0) > 0 %}
<div class="home-aside-highlights__byline">
<time class="home-aside-highlights__time" datetime="{{ h.eventCreatedAt|date('c', 'UTC') }}">{{ h.eventCreatedAt|date('M j, Y', 'UTC') }}</time>
</div>
{% endif %}
{% set _html = h.bodyHtml|default('')|trim %} {% set _html = h.bodyHtml|default('')|trim %}
{% if _html != '' %} {% if _html != '' %}
<div class="home-aside-highlights__quote home-aside-highlights__quote--html user-highlight__body">{{ _html|raw }}</div> <div class="home-aside-highlights__quote home-aside-highlights__quote--html user-highlight__body">{{ _html|raw }}</div>

13
templates/components/Organisms/SidebarMagazineCategoryRecent.html.twig

@ -0,0 +1,13 @@
{% if articles is defined and articles is not empty %}
<section class="sidebar-magazine-recent" aria-label="{{ 'sidebar.category_recent'|trans }}">
<h2 class="sidebar-magazine-recent__title">{{ 'sidebar.category_recent'|trans }}</h2>
<ol class="sidebar-magazine-recent__list" role="list">
{% for item in articles %}
{% set 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 }) %}
<li class="sidebar-magazine-recent__item">
<a class="sidebar-magazine-recent__link" href="{{ href }}">{{ item.title|e }}</a>
</li>
{% endfor %}
</ol>
</section>
{% endif %}

3
templates/home.html.twig

@ -23,6 +23,9 @@
{% endblock %} {% endblock %}
{% block nav %} {% block nav %}
{% if home_sidebar_category_recent|default([]) is not empty %}
{% include 'components/Organisms/SidebarMagazineCategoryRecent.html.twig' with { articles: home_sidebar_category_recent } only %}
{% endif %}
{% endblock %} {% endblock %}
{% block body %} {% block body %}

1
translations/messages.en.yaml

@ -1,6 +1,7 @@
sidebar: sidebar:
featured_authors: 'Featured authors' featured_authors: 'Featured authors'
topics: 'Topics' topics: 'Topics'
category_recent: 'Latest in categories'
highlights: 'Highlights' highlights: 'Highlights'
highlight_view: 'View highlight: %title%' highlight_view: 'View highlight: %title%'
topic: topic:

Loading…
Cancel
Save