Skip to content

Commit 902c373

Browse files
Enes Yağızclaude
andcommitted
fix(seo): answer failed renders with 503 and shorten package page state
A page whose API call failed now answers 503 with Retry-After and noindex. A package page embeds its newest versions, and the browser fetches the rest after it loads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 51e5e19 commit 902c373

12 files changed

Lines changed: 95 additions & 16 deletions

File tree

‎src/backend/PkgDiff.API/Controllers/PackageController.cs‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace PkgDiff.API.Controllers;
1515
public class PackageController : ControllerBase
1616
{
1717
private const int MaxConcurrentAnalyses = 10;
18+
private const int MaxVersionLimit = 100;
1819

1920
private const int MaxConcurrentAnalysesPerAddress = 2;
2021
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, int> AnalysesPerAddress = new();
@@ -198,7 +199,11 @@ private async Task<bool> MayQueueScrapeAsync()
198199

199200
[HttpGet("{registry}/{packageId}")]
200201
[ValidatePackageRoute]
201-
public async Task<ActionResult<PackageMetadata>> GetPackageMetadata(string registry, string packageId)
202+
public async Task<ActionResult<PackageMetadata>> GetPackageMetadata(
203+
string registry,
204+
string packageId,
205+
[FromQuery] int? versionLimit = null,
206+
[FromQuery] string? includeVersion = null)
202207
{
203208
try
204209
{
@@ -222,6 +227,16 @@ public async Task<ActionResult<PackageMetadata>> GetPackageMetadata(string regis
222227
}
223228
}
224229

230+
if (versionLimit.HasValue)
231+
{
232+
var limit = Math.Clamp(versionLimit.Value, 1, MaxVersionLimit);
233+
metadata.Versions = metadata.Versions
234+
.Where((version, index) => index < limit
235+
|| version.Version == metadata.LatestVersion
236+
|| string.Equals(version.Version, includeVersion, StringComparison.OrdinalIgnoreCase))
237+
.ToList();
238+
}
239+
225240
return Ok(metadata);
226241
}
227242
catch (ArgumentException ex)
@@ -379,6 +394,7 @@ public async Task<IActionResult> ExportDiffToPdf([FromBody] AnalyzeRequest reque
379394
ProjectUrl = metadata.ProjectUrl,
380395
GitHubRepositoryUrl = metadata.GitHubRepositoryUrl,
381396
Versions = metadata.Versions,
397+
TotalVersions = metadata.Versions.Count,
382398
Trust = trust
383399
};
384400
}

‎src/backend/PkgDiff.Shared/DTOs/AnalysisModels.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class PackageMetadata
6969
public string? GitHubRepositoryUrl { get; set; }
7070
public string? SourceDirectory { get; set; }
7171
public List<VersionSummary> Versions { get; set; } = new();
72+
public int TotalVersions { get; set; }
7273
public PackageTrust? Trust { get; set; }
7374
}
7475

‎src/frontend/src/app/core/models/package.models.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export interface PackageMetadata {
3838
gitHubRepositoryUrl: string | null;
3939
sourceDirectory?: string | null;
4040
versions: VersionSummary[];
41+
totalVersions?: number;
4142
trust?: PackageTrust | null;
4243
}
4344

‎src/frontend/src/app/core/services/http-status.service.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Injectable, RESPONSE_INIT, inject } from '@angular/core';
22

3-
// Lets a server render answer a missing page with a real 404 instead of a 200
3+
const RETRY_AFTER_SECONDS = '120';
4+
5+
// Lets a server render answer a missing or failed page with its real status instead of a 200
46
@Injectable({ providedIn: 'root' })
57
export class HttpStatusService {
68
private response = inject(RESPONSE_INIT, { optional: true });
@@ -10,4 +12,13 @@ export class HttpStatusService {
1012
this.response.status = 404;
1113
}
1214
}
15+
16+
serviceUnavailable(): void {
17+
if (this.response) {
18+
const headers = new Headers(this.response.headers);
19+
headers.set('Retry-After', RETRY_AFTER_SECONDS);
20+
this.response.status = 503;
21+
this.response.headers = headers;
22+
}
23+
}
1324
}

‎src/frontend/src/app/core/services/package.service.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ export class PackageService {
1515
return this.api.get<RegistryInfo[]>('Package/registries');
1616
}
1717

18-
getPackageMetadata(registry: string, packageId: string): Observable<PackageMetadata> {
19-
return this.api.get<PackageMetadata>(`Package/${registry}/${encodeURIComponent(packageId)}`);
18+
getPackageMetadata(
19+
registry: string,
20+
packageId: string,
21+
versions?: { versionLimit: number; includeVersion?: string },
22+
): Observable<PackageMetadata> {
23+
return this.api.get<PackageMetadata>(`Package/${registry}/${encodeURIComponent(packageId)}`, versions);
2024
}
2125

2226
analyze(request: AnalyzeRequest): Observable<AnalysisResult> {

‎src/frontend/src/app/core/services/seo.service.ts‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ export class SeoService {
8484
this.clearPageData();
8585
}
8686

87+
// The page failed to load; the next successful updateMeta makes it indexable again
88+
setUnavailable(): void {
89+
this.setNoIndex();
90+
this.clearPageData();
91+
}
92+
93+
setNoIndex(): void {
94+
this.meta.updateTag({ name: 'robots', content: 'noindex, follow' });
95+
}
96+
8797
setStructuredData(data: StructuredData): void {
8898
this.setJsonLd('page', data);
8999
}
@@ -128,7 +138,7 @@ export class SeoService {
128138
}
129139

130140
setPackage(metadata: PackageMetadata, registry: string, version?: string): void {
131-
const versionCount = metadata.versions.length;
141+
const versionCount = metadata.totalVersions ?? metadata.versions.length;
132142
const latestVersion = metadata.latestVersion;
133143

134144
let description = metadata.packageId;

‎src/frontend/src/app/features/contributors/contributors.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RouterModule } from '@angular/router';
55
import { LucideAngularModule, Github, Users, RefreshCw } from 'lucide-angular';
66
import { ApiService } from '../../core/services/api';
77
import { SeoService } from '../../core/services/seo.service';
8+
import { HttpStatusService } from '../../core/services/http-status.service';
89

910
interface ContributorDto {
1011
gitHubUsername: string;
@@ -23,6 +24,7 @@ interface ContributorDto {
2324
export class ContributorsComponent {
2425
private api = inject(ApiService);
2526
private seo = inject(SeoService);
27+
private httpStatus = inject(HttpStatusService);
2628
private destroyRef = inject(DestroyRef);
2729

2830
readonly GithubIcon = Github;
@@ -63,7 +65,11 @@ export class ContributorsComponent {
6365
this.contributors.set(data);
6466
this.loading.set(false);
6567
},
66-
error: () => this.loading.set(false),
68+
error: () => {
69+
this.httpStatus.serviceUnavailable();
70+
this.seo.setNoIndex();
71+
this.loading.set(false);
72+
},
6773
});
6874
}
6975

‎src/frontend/src/app/features/package/components/releases-tab/releases-tab.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<h3 class="text-sm font-bold uppercase tracking-widest text-gray-400">Release History</h3>
44
<span
55
class="px-2 py-0.5 text-[10px] font-bold bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 rounded-full">
6-
{{ versions().length }} total
6+
{{ totalCount() ?? versions().length }} total
77
</span>
88
</div>
99
@if (selectedVersion()) {

‎src/frontend/src/app/features/package/components/releases-tab/releases-tab.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class ReleasesTabComponent {
2020
registry = input<string>('nuget');
2121
gitHubUrl = input<string | null>(null);
2222
selectedVersion = input<string | undefined>(undefined);
23+
totalCount = input<number | null>(null);
2324

2425
readonly GithubIcon = Github;
2526
readonly ExternalLinkIcon = ExternalLink;

‎src/frontend/src/app/features/package/package.html‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,8 @@ <h1 class="text-2xl md:text-3xl font-bold tracking-tight break-words min-w-0">{{
195195

196196
@if (activeTab() === 'releases') {
197197
<app-releases-tab [versions]="mappedVersions()" [packageId]="metadata()!.packageId" [registry]="registry()"
198-
[gitHubUrl]="metadata()!.gitHubRepositoryUrl" [selectedVersion]="version()" />
198+
[gitHubUrl]="metadata()!.gitHubRepositoryUrl" [selectedVersion]="version()"
199+
[totalCount]="version() ? null : (metadata()!.totalVersions ?? null)" />
199200
}
200201
@if (activeTab() === 'diff') {
201202
<app-diff-tab [versions]="mappedVersions()" [packageId]="metadata()!.packageId" [registry]="registry()" [trust]="trust()"

0 commit comments

Comments
 (0)