- /{edition}/feed und /{edition}/category/{slug}/feed (FeedController,
RSS 2.0 mit atom:link self, dc:creator, category; jüngste 50 der Ausgabe,
Branche inkl. aktiver Unterkategorien, nur visibleInArchive).
- Cache über WebPageCache (Key-Gruppe feed), Hub leitet per 301 aufs Portal
(canonical.portal_only_content_routes).
- Autodiscovery über @yield('feed_links') auf Start-, Branchen- und
Archivseiten, sichtbarer Link in der Archiv-Leiste, llms.txt nennt die
Feed-Muster, Block „RSS-Feeds" im Panel „Auslieferung"
(VisibilityStats::feedLinks).
- Übersetzungen unter web.rss (web.feed ist die Startseiten-Liste).
- Tests: FeedTest (7), VisibilityStatsTest erweitert.
- dev/2026-09-01: Sichtung 18.09. (Teil 11), zweite Teil-Serie des
Protokolls jetzt 7–10 mit nachgezogenen Verweisen, Anhang 1 abgeglichen,
E6 abgehakt, 2.6 extern, 1c.5/E8 wartet auf Daten, 3.1 abgehakt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
168 lines
8.3 KiB
PHP
168 lines
8.3 KiB
PHP
<?php
|
||
|
||
use App\Enums\Portal;
|
||
use App\Models\Category;
|
||
use App\Models\Company;
|
||
use App\Models\PressRelease;
|
||
use App\Support\WebCaches;
|
||
use App\Support\WebPageCache;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Tests\TestCase;
|
||
|
||
/**
|
||
* RSS-Feeds je Branche und je Portal (Roadmap 3.1): /{edition}/feed und
|
||
* /{edition}/category/{slug}/feed.
|
||
*/
|
||
function feedCategory(Portal $portal = Portal::Presseecho, string $slug = 'handel', string $name = 'Handel', ?Category $parent = null): Category
|
||
{
|
||
$category = Category::factory()->create(['portal' => $portal, 'is_active' => true, 'parent_id' => $parent?->id]);
|
||
$category->translations()->create(['locale' => 'de', 'name' => $name, 'slug' => $slug, 'description' => 'Meldungen aus dem Bereich '.$name.'.']);
|
||
$category->translations()->create(['locale' => 'en', 'name' => $name.' EN', 'slug' => $slug.'-en']);
|
||
|
||
return $category;
|
||
}
|
||
|
||
function feedRelease(Category $category, string $slug, array $attributes = [], Portal $portal = Portal::Presseecho): PressRelease
|
||
{
|
||
return PressRelease::factory()
|
||
->published()
|
||
->forPortal($portal)
|
||
->for($category)
|
||
->for(Company::factory()->presseecho()->create(['name' => 'Feed GmbH & Co.']))
|
||
->create(['slug' => $slug, 'published_at' => now()->subHour(), 'language' => 'de', ...$attributes]);
|
||
}
|
||
|
||
test('the category feed lists the newest releases of the category and its children on the own portal', function () {
|
||
/** @var TestCase $this */
|
||
$category = feedCategory();
|
||
$child = feedCategory(Portal::Presseecho, 'einzelhandel', 'Einzelhandel', $category);
|
||
$other = feedCategory(Portal::Presseecho, 'energie', 'Energie');
|
||
|
||
feedRelease($category, 'neu', ['title' => 'Neue Meldung & mehr', 'text' => '<p>Erster Absatz der Meldung.</p>', 'published_at' => now()->subHour()]);
|
||
feedRelease($child, 'kind', ['title' => 'Aus der Unterkategorie', 'published_at' => now()->subHours(2)]);
|
||
feedRelease($other, 'andere-branche');
|
||
feedRelease($category, 'fremdes-portal', [], Portal::Businessportal24);
|
||
feedRelease($category, 'terminiert', ['published_at' => now()->addHour()]);
|
||
feedRelease($category, 'englisch', ['language' => 'en']);
|
||
feedRelease($category, 'beide-portale', ['published_at' => now()->subHours(3)], Portal::Both);
|
||
|
||
$response = $this->get('https://presseecho.test/de/category/handel/feed')->assertSuccessful();
|
||
|
||
expect($response->headers->get('Content-Type'))->toBe('application/rss+xml; charset=UTF-8');
|
||
|
||
$xml = (string) $response->getContent();
|
||
expect($xml)
|
||
->toStartWith('<?xml version="1.0" encoding="UTF-8"?>')
|
||
->toContain('<title>Pressemitteilungen Handel – presseecho</title>')
|
||
->toContain('<link>https://presseecho.test/de/category/handel</link>')
|
||
->toContain('<atom:link href="https://presseecho.test/de/category/handel/feed" rel="self" type="application/rss+xml" />')
|
||
->toContain('<description>Meldungen aus dem Bereich Handel.</description>')
|
||
->toContain('<language>de</language>')
|
||
->toContain('<title>Neue Meldung & mehr</title>')
|
||
->toContain('<guid isPermaLink="true">https://presseecho.test/de/press-release/neu</guid>')
|
||
->toContain('<description>Erster Absatz der Meldung.</description>')
|
||
->toContain('<category>Handel</category>')
|
||
->toContain('<dc:creator>Feed GmbH & Co.</dc:creator>')
|
||
->toContain('<link>https://presseecho.test/de/press-release/kind</link>')
|
||
->toContain('<link>https://presseecho.test/de/press-release/beide-portale</link>');
|
||
|
||
// Neueste zuerst.
|
||
expect(strpos($xml, 'press-release/neu'))->toBeLessThan((int) strpos($xml, 'press-release/kind'));
|
||
|
||
foreach (['andere-branche', 'fremdes-portal', 'terminiert', 'press-release/englisch'] as $absent) {
|
||
$this->assertStringNotContainsString($absent, $xml);
|
||
}
|
||
|
||
// Wohlgeformt: die Ausgabe muss ein XML-Parser lesen können.
|
||
$document = simplexml_load_string($xml);
|
||
expect($document)->not->toBeFalse()
|
||
->and(count($document->channel->item))->toBe(3);
|
||
});
|
||
|
||
test('the portal feed lists releases of all categories, newest first, capped', function () {
|
||
/** @var TestCase $this */
|
||
$handel = feedCategory();
|
||
$energie = feedCategory(Portal::Presseecho, 'energie', 'Energie');
|
||
feedRelease($handel, 'handel-neu', ['published_at' => now()->subHour()]);
|
||
feedRelease($energie, 'energie-alt', ['published_at' => now()->subDay()]);
|
||
feedRelease($handel, 'fremd', [], Portal::Businessportal24);
|
||
|
||
$xml = (string) $this->get('https://presseecho.test/de/feed')->assertSuccessful()->getContent();
|
||
|
||
expect($xml)
|
||
->toContain('<title>Pressemitteilungen – presseecho</title>')
|
||
->toContain('<link>https://presseecho.test/de</link>')
|
||
->toContain('<atom:link href="https://presseecho.test/de/feed" rel="self" type="application/rss+xml" />')
|
||
->toContain('press-release/handel-neu')
|
||
->toContain('press-release/energie-alt');
|
||
expect(strpos($xml, 'handel-neu'))->toBeLessThan((int) strpos($xml, 'energie-alt'));
|
||
$this->assertStringNotContainsString('press-release/fremd', $xml);
|
||
});
|
||
|
||
test('an unknown or inactive category has no feed', function () {
|
||
/** @var TestCase $this */
|
||
$inactive = feedCategory(Portal::Presseecho, 'veraltet', 'Veraltet');
|
||
$inactive->update(['is_active' => false]);
|
||
|
||
$this->get('https://presseecho.test/de/category/gibt-es-nicht/feed')->assertNotFound();
|
||
$this->get('https://presseecho.test/de/category/veraltet/feed')->assertNotFound();
|
||
});
|
||
|
||
test('the english edition serves the feed under the english slug with english releases only', function () {
|
||
/** @var TestCase $this */
|
||
$category = feedCategory();
|
||
feedRelease($category, 'deutsch');
|
||
feedRelease($category, 'english', ['language' => 'en', 'title' => 'English release']);
|
||
|
||
$xml = (string) $this->get('https://presseecho.test/en/category/handel-en/feed')->assertSuccessful()->getContent();
|
||
|
||
expect($xml)
|
||
->toContain('<title>Handel EN press releases – presseecho</title>')
|
||
->toContain('<language>en</language>')
|
||
->toContain('https://presseecho.test/en/press-release/english');
|
||
$this->assertStringNotContainsString('press-release/deutsch', $xml);
|
||
});
|
||
|
||
test('category and home pages announce the feeds for autodiscovery and link them', function () {
|
||
/** @var TestCase $this */
|
||
$category = feedCategory();
|
||
feedRelease($category, 'irgendwas');
|
||
|
||
$this->get('https://presseecho.test/de/category/handel')->assertSuccessful()
|
||
->assertSee('<link rel="alternate" type="application/rss+xml" title="RSS-Feed: Pressemitteilungen Handel – presseecho" href="https://presseecho.test/de/category/handel/feed">', false)
|
||
->assertSee('<link rel="alternate" type="application/rss+xml" title="RSS-Feed: alle Pressemitteilungen – presseecho" href="https://presseecho.test/de/feed">', false)
|
||
->assertSee('href="https://presseecho.test/de/category/handel/feed" type="application/rss+xml"', false);
|
||
|
||
$this->get('https://presseecho.test/de')->assertSuccessful()
|
||
->assertSee('type="application/rss+xml" title="RSS-Feed: alle Pressemitteilungen – presseecho" href="https://presseecho.test/de/feed"', false);
|
||
|
||
$this->get('https://presseecho.test/llms.txt')->assertSuccessful()
|
||
->assertSee('https://presseecho.test/de/feed', false)
|
||
->assertSee('https://presseecho.test/de/category/{slug}/feed', false);
|
||
});
|
||
|
||
test('the hub redirects feed addresses to the portal', function () {
|
||
/** @var TestCase $this */
|
||
feedCategory(Portal::Both);
|
||
|
||
$this->get('https://pressekonto.test/de/feed')
|
||
->assertRedirect('https://presseecho.test/de/feed')
|
||
->assertStatus(301);
|
||
$this->get('https://pressekonto.test/de/category/handel/feed')
|
||
->assertRedirect('https://presseecho.test/de/category/handel/feed')
|
||
->assertStatus(301);
|
||
});
|
||
|
||
test('the feed is cached with the page data and cleared with the public pages', function () {
|
||
/** @var TestCase $this */
|
||
$category = feedCategory();
|
||
feedRelease($category, 'erste');
|
||
|
||
$this->get('https://presseecho.test/de/category/handel/feed')->assertSuccessful();
|
||
$key = WebPageCache::key('feed', ['presseecho', 'de', 'handel']);
|
||
expect(Cache::has($key))->toBeTrue();
|
||
|
||
WebCaches::forgetPublicPages();
|
||
|
||
expect(Cache::has(WebPageCache::key('feed', ['presseecho', 'de', 'handel'])))->toBeFalse();
|
||
});
|