Browse Source

give subcategories their own bricks

imwald
Silberengel 1 month ago
parent
commit
f0af60c7a9
  1. 187
      src/Service/MagazineContentService.php

187
src/Service/MagazineContentService.php

@ -702,14 +702,12 @@ final class MagazineContentService
if (!\is_string($coord) || $coord === '') { if (!\is_string($coord) || $coord === '') {
continue; continue;
} }
$b = $this->buildCategoryFeaturedBlock($coord); foreach ($this->buildFeaturedWallBlocksForCategoryTree($coord) as $b) {
if ($b === null) { foreach ($b['cards'] as $card) {
continue; $s = \trim((string) $card->getSlug());
} if ($s !== '') {
foreach ($b['cards'] as $card) { $out[$s] = true;
$s = \trim((string) $card->getSlug()); }
if ($s !== '') {
$out[$s] = true;
} }
} }
} }
@ -816,8 +814,10 @@ final class MagazineContentService
} }
/** /**
* Interleaves up to two articles per home category in round-robin order (one “wall” mixing all topics). * Interleaves up to two articles per wall “brick” in round-robin order (one picture wall mixing all topics).
* Duplicate slugs across categories are skipped so each article appears at most once. * Each root category and each nested kind-30040 subcategory is its own brick (direct long-form `a` tags only;
* subcategory articles are not folded into the parent’s title). Per brick, cards are the two newest by
* display date ({@see FeaturedArticleCard::getDisplayAt}). Duplicate slugs across bricks are skipped.
* *
* @param list<array<int, string>> $categoryATags * @param list<array<int, string>> $categoryATags
* *
@ -831,9 +831,10 @@ final class MagazineContentService
if (!\is_string($coord) || $coord === '') { if (!\is_string($coord) || $coord === '') {
continue; continue;
} }
$b = $this->buildCategoryFeaturedBlock($coord); foreach ($this->buildFeaturedWallBlocksForCategoryTree($coord) as $b) {
if ($b !== null && $b['cards'] !== []) { if ($b['cards'] !== []) {
$blocks[] = $b; $blocks[] = $b;
}
} }
} }
if ($blocks === []) { if ($blocks === []) {
@ -1010,39 +1011,56 @@ final class MagazineContentService
} }
/** /**
* Same article resolution as {@see \App\Twig\Components\Organisms\FeaturedList} (nested 30040 + long-form); * Long-form `#d` slugs from `a` tags on this index only (no descent into nested kind-30040), in tag order.
* at most two cards per root category for the home picture wall.
* *
* @return null|array{title: string, cards: list<FeaturedArticleCard>} * @return list<string>
*/ */
private function buildCategoryFeaturedBlock(string $categoryCoord): ?array private function directLongformSlugsFromCategoryIndex(Event $catIndex, int $maxA): array
{ {
$parts = explode(':', $categoryCoord, 3); if ($maxA < 1) {
if (\count($parts) < 3) { return [];
return null;
}
$slug = trim((string) $parts[2]);
$this->warmCategoryIndexIfMissing($slug);
$catIndex = $this->store->getCategory($slug);
if ($catIndex === null) {
return null;
} }
$slugs = [];
$title = '';
foreach ($catIndex->getTags() as $tag) { foreach ($catIndex->getTags() as $tag) {
if (($tag[0] ?? null) === 'title' && isset($tag[1])) { if (($tag[0] ?? null) !== 'a' || !isset($tag[1])) {
$title = (string) $tag[1]; continue;
}
$coord = (string) $tag[1];
$segs = explode(':', $coord, 3);
if (\count($segs) < 3) {
continue;
}
$kind = (int) ($segs[0] ?? 0);
$identifier = trim((string) $segs[2]);
if ($identifier === '') {
continue;
}
if (\in_array($kind, [KindsEnum::LONGFORM->value, KindsEnum::LONGFORM_DRAFT->value], true)) {
$slugs[] = $identifier;
if (\count($slugs) >= $maxA) {
return $slugs;
}
} }
} }
if ($title === '') { return $slugs;
$title = $slug; }
}
$slugs = $this->slugsFromCategoryCoord($categoryCoord, 40); /**
* One home wall brick: this index’s title and up to {@see $maxCards} featured cards from direct long-form
* `a` tags only, choosing the newest by display date ({@see FeaturedArticleCard::getDisplayAt}).
*
* @return null|array{title: string, cards: list<FeaturedArticleCard>}
*/
private function buildWallBlockForCategoryIndex(Event $catIndex, string $slug, int $maxCards = 2): ?array
{
$slugs = $this->directLongformSlugsFromCategoryIndex($catIndex, 40);
if ($slugs === []) { if ($slugs === []) {
return null; return null;
} }
$title = $this->categoryDisplayTitleFromIndexTags($catIndex, $slug);
$articles = $this->articleRepository->findFeaturedCardsBySlugs($slugs); $articles = $this->articleRepository->findFeaturedCardsBySlugs($slugs);
$slugMap = []; $slugMap = [];
foreach ($articles as $article) { foreach ($articles as $article) {
@ -1055,30 +1073,103 @@ final class MagazineContentService
} }
} }
} }
$orderedList = []; $resolved = [];
foreach ($slugs as $articleSlug) { foreach ($slugs as $articleSlug) {
$articleSlug = \trim((string) $articleSlug); $articleSlug = \trim((string) $articleSlug);
if ($articleSlug !== '' && isset($slugMap[$articleSlug])) { if ($articleSlug !== '' && isset($slugMap[$articleSlug])) {
$orderedList[] = $slugMap[$articleSlug]; $resolved[] = $slugMap[$articleSlug];
} }
} }
if ($orderedList !== [] && NostrEventTags::publicationIndexNestedDSlugs($catIndex->getTags()) !== []) { if ($resolved === []) {
\usort($orderedList, function (FeaturedArticleCard $a, FeaturedArticleCard $b): int { return null;
if ($this->featuredCardIsNewer($a, $b)) {
return -1;
}
if ($this->featuredCardIsNewer($b, $a)) {
return 1;
}
return 0;
});
} }
$cards = \array_slice($orderedList, 0, 2); \usort($resolved, function (FeaturedArticleCard $a, FeaturedArticleCard $b): int {
if ($this->featuredCardIsNewer($a, $b)) {
return -1;
}
if ($this->featuredCardIsNewer($b, $a)) {
return 1;
}
return 0;
});
$cards = \array_slice($resolved, 0, max(1, $maxCards));
return ['title' => $title, 'cards' => $cards]; return ['title' => $title, 'cards' => $cards];
} }
/**
* Ordered wall bricks: this category (direct long-form only), then each nested kind-30040 owned by the
* same pubkey, depth-first in `a` tag order.
*
* @return list<array{title: string, cards: list<FeaturedArticleCard>}>
*/
private function buildFeaturedWallBlocksForCategoryTree(string $categoryCoord, int $depth = 0, int $maxDepth = 8): array
{
if ($depth > $maxDepth) {
return [];
}
$parts = explode(':', $categoryCoord, 3);
if (\count($parts) < 3) {
return [];
}
$ownerHex = strtolower(trim((string) $parts[1]));
$slug = trim((string) $parts[2]);
if ($slug === '' || 64 !== \strlen($ownerHex) || !ctype_xdigit($ownerHex)) {
return [];
}
$this->warmCategoryIndexIfMissing($slug);
$catIndex = $this->store->getCategory($slug);
if ($catIndex === null) {
return [];
}
$blocks = [];
$own = $this->buildWallBlockForCategoryIndex($catIndex, $slug);
if ($own !== null) {
$blocks[] = $own;
}
foreach ($catIndex->getTags() as $tag) {
if (($tag[0] ?? null) !== 'a' || !isset($tag[1])) {
continue;
}
$coord = trim((string) $tag[1]);
$segs = explode(':', $coord, 3);
if (\count($segs) < 3) {
continue;
}
$kind = (int) ($segs[0] ?? 0);
if ($kind !== KindsEnum::PUBLICATION_INDEX->value) {
continue;
}
$pk = strtolower(trim((string) $segs[1]));
$childSlug = trim((string) $segs[2]);
if ($childSlug === '' || !hash_equals($ownerHex, $pk)) {
continue;
}
$childCoord = $kind.':'.$pk.':'.$childSlug;
foreach ($this->buildFeaturedWallBlocksForCategoryTree($childCoord, $depth + 1, $maxDepth) as $b) {
$blocks[] = $b;
}
}
return $blocks;
}
private function categoryDisplayTitleFromIndexTags(Event $catIndex, string $slugFallback): string
{
foreach ($catIndex->getTags() as $tag) {
if (($tag[0] ?? null) === 'title' && isset($tag[1])) {
$t = trim((string) $tag[1]);
return $t !== '' ? $t : $slugFallback;
}
}
return $slugFallback !== '' ? $slugFallback : 'Category';
}
private function featuredCardIsNewer(FeaturedArticleCard $a, FeaturedArticleCard $b): bool private function featuredCardIsNewer(FeaturedArticleCard $a, FeaturedArticleCard $b): bool
{ {
$ca = $a->getDisplayAt(); $ca = $a->getDisplayAt();

Loading…
Cancel
Save