web: RSS-Feeds je Branche und je Portal (Roadmap 3.1); dev: Sichtung des Ordners, Protokoll-Teile umnummeriert
- /{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>
This commit is contained in:
parent
e2ef4dee31
commit
9d5e5a0b54
20 changed files with 547 additions and 28 deletions
|
|
@ -3,6 +3,7 @@
|
|||
use App\Enums\IndexState;
|
||||
use App\Enums\Portal;
|
||||
use App\Enums\PressReleaseEventType;
|
||||
use App\Models\Category;
|
||||
use App\Models\Company;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\PressReleaseCrawlerHit;
|
||||
|
|
@ -108,7 +109,9 @@ test('the daily series is gapless, marks days before measurement as unmeasured a
|
|||
test('the release summary reports sitemap presence only for public releases and lists crawler fetches', function () {
|
||||
/** @var TestCase $this */
|
||||
[, $company] = claimant();
|
||||
$public = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'published_at' => now()->subDay(), 'hits' => 9]);
|
||||
$category = Category::factory()->create(['portal' => Portal::Businessportal24]);
|
||||
$category->translations()->create(['locale' => 'de', 'name' => 'Handel', 'slug' => 'handel']);
|
||||
$public = PressRelease::factory()->published()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'category_id' => $category->id, 'published_at' => now()->subDay(), 'hits' => 9]);
|
||||
$draft = PressRelease::factory()->forPortal(Portal::Businessportal24)->create(['company_id' => $company->id, 'status' => 'draft']);
|
||||
|
||||
PressReleaseEvent::query()->create(['press_release_id' => $public->id, 'type' => PressReleaseEventType::Share, 'channel' => 'linkedin']);
|
||||
|
|
@ -126,12 +129,18 @@ test('the release summary reports sitemap presence only for public releases and
|
|||
->and($summary)->not->toHaveKey('search_url')
|
||||
->and(collect($summary['crawlers'])->pluck('fetches', 'service')->sortKeys()->all())->toBe(['google' => 2, 'openai' => 1])
|
||||
->and(collect($summary['crawlers'])->firstWhere('service', 'google')['label'])->toBe('Google Suche')
|
||||
->and($summary['first_view_at'])->not->toBeNull();
|
||||
->and($summary['first_view_at'])->not->toBeNull()
|
||||
// RSS-Feeds (3.1): Branchen-Feed und Portal-Feed des eigenen Portals.
|
||||
->and(collect($summary['feeds'])->pluck('url')->all())->toBe([
|
||||
'https://businessportal24.test/de/category/handel/feed',
|
||||
'https://businessportal24.test/de/feed',
|
||||
]);
|
||||
|
||||
$draftSummary = $stats->releaseSummary($draft);
|
||||
|
||||
expect($draftSummary['in_sitemap_since'])->toBeNull()
|
||||
->and($draftSummary['crawlers'])->toBe([]);
|
||||
->and($draftSummary['crawlers'])->toBe([])
|
||||
->and($draftSummary['feeds'])->toBe([]);
|
||||
});
|
||||
|
||||
test('the release page shows the delivery panel with daily bars, sitemap date and crawler fetches, but no search link', function () {
|
||||
|
|
@ -154,6 +163,8 @@ test('the release page shows the delivery panel with daily bars, sitemap date an
|
|||
->assertSeeText('Anthropic / Claude')
|
||||
->assertSeeText('1 Abruf')
|
||||
->assertSeeText('Ein Abruf belegt, dass der Crawler des Dienstes die Seite geholt hat')
|
||||
->assertSeeText('RSS-Feeds')
|
||||
->assertSee('https://businessportal24.test/de/feed', false)
|
||||
// Indexierungsstatus bleibt zu (E8), solange die Kundenansicht nicht freigegeben ist.
|
||||
->assertDontSeeText('Google-Index')
|
||||
->assertSee('<svg', false)
|
||||
|
|
|
|||
168
tests/Feature/Web/FeedTest.php
Normal file
168
tests/Feature/Web/FeedTest.php
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<?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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue