Browse Source

Latest

imwald
Nuša Pukšič 3 months ago
parent
commit
4230730fe5
  1. 19
      src/Command/RssFetchCommand.php
  2. 23
      src/Controller/DefaultController.php
  3. 88
      src/Service/NzineCategoryIndexService.php
  4. 3
      templates/layout.html.twig
  5. 21
      templates/pages/latest-articles.html.twig

19
src/Command/RssFetchCommand.php

@ -401,6 +401,18 @@ class RssFetchCommand extends Command
// Don't return - continue processing without a category // Don't return - continue processing without a category
} }
// Ensure matched category has a slug field
if ($matchedCategory && empty($matchedCategory['slug'])) {
// Generate slug from title if not present
$slugger = new \Symfony\Component\String\Slugger\AsciiSlugger();
$matchedCategory['slug'] = $slugger->slug($matchedCategory['title'] ?? $matchedCategory['name'] ?? '')->lower()->toString();
$this->logger->debug('Generated slug for matched category', [
'category_title' => $matchedCategory['title'] ?? $matchedCategory['name'] ?? 'unknown',
'generated_slug' => $matchedCategory['slug'],
]);
}
if ($isDryRun) { if ($isDryRun) {
$categoryLabel = $matchedCategory $categoryLabel = $matchedCategory
? ($matchedCategory['name'] ?? $matchedCategory['title'] ?? $matchedCategory['slug'] ?? 'unknown') ? ($matchedCategory['name'] ?? $matchedCategory['title'] ?? $matchedCategory['slug'] ?? 'unknown')
@ -467,11 +479,16 @@ class RssFetchCommand extends Command
); );
try { try {
$this->categoryIndexService->addArticleToCategoryIndex( // addArticleToCategoryIndex now returns a NEW event entity
$updatedCategoryIndex = $this->categoryIndexService->addArticleToCategoryIndex(
$categoryIndices[$categorySlug], $categoryIndices[$categorySlug],
$articleCoordinate, $articleCoordinate,
$nzine $nzine
); );
// Update the reference in the array to point to the new event
$categoryIndices[$categorySlug] = $updatedCategoryIndex;
// Flush to ensure the category index is saved to the database // Flush to ensure the category index is saved to the database
$this->entityManager->flush(); $this->entityManager->flush();

23
src/Controller/DefaultController.php

@ -46,6 +46,29 @@ class DefaultController extends AbstractController
return $this->render('pages/newsstand.html.twig'); return $this->render('pages/newsstand.html.twig');
} }
/**
* @throws Exception
*/
#[Route('/latest-articles', name: 'latest_articles')]
public function latestArticles(FinderInterface $finder): Response
{
// Query all articles and sort by created_at descending
$query = new Query();
$query->setSize(50);
$query->setSort(['createdAt' => ['order' => 'desc']]);
// Use collapse to deduplicate by slug field
$collapse = new Collapse();
$collapse->setFieldname('slug');
$query->setCollapse($collapse);
$articles = $finder->find($query);
return $this->render('pages/latest-articles.html.twig', [
'articles' => $articles,
]);
}
/** /**
* @throws Exception * @throws Exception
*/ */

88
src/Service/NzineCategoryIndexService.php

@ -84,12 +84,13 @@ class NzineCategoryIndexService
} }
$title = $category['title']; $title = $category['title'];
$slug = !empty($category['slug']) $slug = $category['slug'];
? $category['slug']
: $slugger->slug($title)->lower()->toString();
// Check if category index already exists // Check if category index already exists
if (isset($existingBySlug[$slug])) { if (isset($existingBySlug[$slug])) {
// FIX: Add existing index to return array
$categoryIndices[$slug] = $existingBySlug[$slug];
$this->logger->debug('Using existing category index', [ $this->logger->debug('Using existing category index', [
'category_slug' => $slug, 'category_slug' => $slug,
'title' => $title, 'title' => $title,
@ -137,6 +138,7 @@ class NzineCategoryIndexService
$this->logger->info('Category indices ready', [ $this->logger->info('Category indices ready', [
'nzine_id' => $nzine->getId(), 'nzine_id' => $nzine->getId(),
'total_categories' => count($categories), 'total_categories' => count($categories),
'total_indices_returned' => count($categoryIndices),
'indexed_by_slug' => array_keys($categoryIndices), 'indexed_by_slug' => array_keys($categoryIndices),
]); ]);
@ -158,19 +160,25 @@ class NzineCategoryIndexService
/** /**
* Add an article to a category index * Add an article to a category index
* Creates a new signed event with the article added to the existing tags
* *
* @param EventEntity $categoryIndex The category index event * @param EventEntity $categoryIndex The category index event
* @param string $articleCoordinate The article coordinate (kind:pubkey:slug) * @param string $articleCoordinate The article coordinate (kind:pubkey:slug)
* @param Nzine $nzine The nzine entity (needed for signing) * @param Nzine $nzine The nzine entity (needed for signing)
* @return EventEntity The new category index event (with updated article list)
*/ */
public function addArticleToCategoryIndex(EventEntity $categoryIndex, string $articleCoordinate, Nzine $nzine): void public function addArticleToCategoryIndex(EventEntity $categoryIndex, string $articleCoordinate, Nzine $nzine): EventEntity
{ {
// Check if article already exists in the index // Check if article already exists in the index
$tags = $categoryIndex->getTags(); $existingTags = $categoryIndex->getTags();
foreach ($tags as $tag) { foreach ($existingTags as $tag) {
if ($tag[0] === 'a' && isset($tag[1]) && $tag[1] === $articleCoordinate) { if ($tag[0] === 'a' && isset($tag[1]) && $tag[1] === $articleCoordinate) {
// Article already in index // Article already in index, return existing event
return; $this->logger->debug('Article already in category index', [
'article_coordinate' => $articleCoordinate,
'event_id' => $categoryIndex->getId(),
]);
return $categoryIndex;
} }
} }
@ -187,42 +195,43 @@ class NzineCategoryIndexService
throw new \RuntimeException('Cannot sign category index: bot private key not found'); throw new \RuntimeException('Cannot sign category index: bot private key not found');
} }
// Add article coordinate to tags // Create a new Event object with ALL existing tags PLUS the new article tag
$tags[] = ['a', $articleCoordinate];
// Create a new Event object with updated tags
$event = new Event(); $event = new Event();
$event->setKind($categoryIndex->getKind()); $event->setKind($categoryIndex->getKind());
$event->setContent($categoryIndex->getContent() ?? ''); $event->setContent($categoryIndex->getContent() ?? '');
$event->setPublicKey($categoryIndex->getPubkey()); $event->setPublicKey($categoryIndex->getPubkey());
// Add all tags including the new article coordinate // Add ALL existing tags first
foreach ($tags as $tag) { foreach ($existingTags as $tag) {
$event->addTag($tag); $event->addTag($tag);
} }
// Add the new article coordinate tag
$event->addTag(['a', $articleCoordinate]);
// Sign the event with current timestamp // Sign the event with current timestamp
$signer = new Sign(); $signer = new Sign();
$signer->signEvent($event, $privateKey); $signer->signEvent($event, $privateKey);
// Convert to JSON and back to get all properties including sig // Convert to JSON and deserialize to NEW EventEntity
$eventJson = $event->toJson(); $serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$eventData = json_decode($eventJson, true); $newEventEntity = $serializer->deserialize($event->toJson(), EventEntity::class, 'json');
// Update the EventEntity with new tags, signature, ID, and timestamp // Persist the NEW event entity
$categoryIndex->setTags($tags); $this->entityManager->persist($newEventEntity);
$categoryIndex->setSig($eventData['sig']);
$categoryIndex->setId($eventData['id']);
$categoryIndex->setEventId($eventData['id']);
$categoryIndex->setCreatedAt($eventData['created_at']);
$this->entityManager->persist($categoryIndex); $articleCount = count(array_filter($newEventEntity->getTags(), fn($tag) => $tag[0] === 'a'));
$this->logger->debug('Added article to category index and re-signed', [ $this->logger->debug('Created new category index event with article added', [
'category_slug' => $this->extractSlugFromTags($tags), 'category_slug' => $this->extractSlugFromTags($newEventEntity->getTags()),
'article_coordinate' => $articleCoordinate, 'article_coordinate' => $articleCoordinate,
'event_id' => $eventData['id'], 'old_event_id' => $categoryIndex->getId(),
'new_event_id' => $newEventEntity->getId(),
'total_tags' => count($newEventEntity->getTags()),
'article_count' => $articleCount,
]); ]);
return $newEventEntity;
} }
/** /**
@ -272,25 +281,20 @@ class NzineCategoryIndexService
$event->addTag($tag); $event->addTag($tag);
} }
// Sign the event with current timestamp // Sign the event with current timestamp (creates new ID)
$signer->signEvent($event, $privateKey); $signer->signEvent($event, $privateKey);
// Convert to JSON and back to get all properties including sig // Deserialize to a NEW EventEntity (not updating the old one)
$eventJson = $event->toJson(); $newEventEntity = $serializer->deserialize($event->toJson(), EventEntity::class, 'json');
$eventData = json_decode($eventJson, true);
// Update the EventEntity with new signature and timestamp
$categoryIndex->setSig($eventData['sig']);
$categoryIndex->setId($eventData['id']);
$categoryIndex->setEventId($eventData['id']);
$categoryIndex->setCreatedAt($eventData['created_at']);
$this->entityManager->persist($categoryIndex); // Persist the NEW event entity
$this->entityManager->persist($newEventEntity);
$this->logger->info('Re-signed category index', [ $this->logger->info('Created new category index event (re-signed)', [
'category_slug' => $slug, 'category_slug' => $slug,
'event_id' => $eventData['id'], 'old_event_id' => $categoryIndex->getId(),
'article_count' => count(array_filter($categoryIndex->getTags(), fn($tag) => $tag[0] === 'a')), 'new_event_id' => $newEventEntity->getId(),
'article_count' => count(array_filter($newEventEntity->getTags(), fn($tag) => $tag[0] === 'a')),
]); ]);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Failed to re-sign category index', [ $this->logger->error('Failed to re-sign category index', [
@ -302,7 +306,7 @@ class NzineCategoryIndexService
$this->entityManager->flush(); $this->entityManager->flush();
$this->logger->info('Category indices re-signed', [ $this->logger->info('Category indices re-signed and new events created', [
'nzine_id' => $nzine->getId(), 'nzine_id' => $nzine->getId(),
'count' => count($categoryIndices), 'count' => count($categoryIndices),
]); ]);

3
templates/layout.html.twig

@ -11,6 +11,9 @@
<li> <li>
<a href="{{ path('newsstand') }}">Newsstand</a> <a href="{{ path('newsstand') }}">Newsstand</a>
</li> </li>
<li>
<a href="{{ path('latest_articles') }}">Latest articles</a>
</li>
<li> <li>
<a href="{{ path('reading_list_index') }}">Reading Lists</a> <a href="{{ path('reading_list_index') }}">Reading Lists</a>
</li> </li>

21
templates/pages/latest-articles.html.twig

@ -0,0 +1,21 @@
{% extends 'layout.html.twig' %}
{% block body %}
<section class="d-flex gap-3 center ln-section--latest-articles">
<div class="container mt-5 mb-5">
<h1>Latest Articles</h1>
<p class="eyebrow">Fresh off the presses</p>
</div>
</section>
<section class="container mb-5">
{% if articles is empty %}
<div class="alert alert-info">
No published articles found.
</div>
{% else %}
<twig:Organisms:CardList :list="articles" class="article-list" />
{% endif %}
</section>
{% endblock %}
Loading…
Cancel
Save