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 @@ -401,6 +401,18 @@ class RssFetchCommand extends Command
// 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) {
$categoryLabel = $matchedCategory
? ($matchedCategory['name'] ?? $matchedCategory['title'] ?? $matchedCategory['slug'] ?? 'unknown')
@ -467,11 +479,16 @@ class RssFetchCommand extends Command @@ -467,11 +479,16 @@ class RssFetchCommand extends Command
);
try {
$this->categoryIndexService->addArticleToCategoryIndex(
// addArticleToCategoryIndex now returns a NEW event entity
$updatedCategoryIndex = $this->categoryIndexService->addArticleToCategoryIndex(
$categoryIndices[$categorySlug],
$articleCoordinate,
$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
$this->entityManager->flush();

23
src/Controller/DefaultController.php

@ -46,6 +46,29 @@ class DefaultController extends AbstractController @@ -46,6 +46,29 @@ class DefaultController extends AbstractController
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
*/

88
src/Service/NzineCategoryIndexService.php

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

3
templates/layout.html.twig

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

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

@ -0,0 +1,21 @@ @@ -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