Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 63 additions & 3 deletions __tests__/sitemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ describe('GET /sitemaps/archive-index.xml', () => {
rankingType: ArchiveRankingType.Best,
};

it('should return index pages for tags and sources with archives', async () => {
it('should return index pages for global, tag, and source archives', async () => {
const createdAt = new Date('2025-03-01T10:00:00.000Z');

await con.getRepository(Archive).save([
Expand Down Expand Up @@ -1190,11 +1190,14 @@ describe('GET /sitemaps/archive-index.xml', () => {
expect(res.text).toContain(
'<loc>http://localhost:5002/tags/rust/best-of</loc>',
);
// Global archives should not appear
expect(res.text).not.toContain('/best-of</loc>\n');
expect(res.text).toContain(
'<loc>http://localhost:5002/posts/best-of</loc>',
);
// Only one entry for rust (two archives but one index)
const rustMatches = res.text.match(/\/tags\/rust\/best-of<\/loc>/g);
expect(rustMatches).toHaveLength(1);
const globalMatches = res.text.match(/\/posts\/best-of<\/loc>/g);
expect(globalMatches).toHaveLength(1);
});

it('should exclude source archives when the source has been deleted', async () => {
Expand Down Expand Up @@ -1334,6 +1337,52 @@ describe('GET /sitemaps/archive-pages-:scopeType-:periodType-:page.xml', () => {
);
});

it('should return global monthly archive pages', async () => {
const createdAt = new Date('2025-04-01T10:00:00.000Z');

await con.getRepository(Archive).save([
{
...archiveBase,
scopeType: ArchiveScopeType.Global,
scopeId: null,
periodType: ArchivePeriodType.Month,
periodStart: new Date('2025-03-01T00:00:00.000Z'),
createdAt,
},
]);

const res = await request(app.server)
.get('/sitemaps/archive-pages-global-month-0.xml')
.expect(200);

expect(res.text).toContain(
'<loc>http://localhost:5002/posts/best-of/2025/03</loc>',
);
});

it('should return global yearly archive pages', async () => {
const createdAt = new Date('2025-04-01T10:00:00.000Z');

await con.getRepository(Archive).save([
{
...archiveBase,
scopeType: ArchiveScopeType.Global,
scopeId: null,
periodType: ArchivePeriodType.Year,
periodStart: new Date('2024-01-01T00:00:00.000Z'),
createdAt,
},
]);

const res = await request(app.server)
.get('/sitemaps/archive-pages-global-year-0.xml')
.expect(200);

expect(res.text).toContain(
'<loc>http://localhost:5002/posts/best-of/2024</loc>',
);
});

it('should return 404 for invalid scopeType', async () => {
await request(app.server)
.get('/sitemaps/archive-pages-invalid-month-0.xml')
Expand Down Expand Up @@ -1416,6 +1465,14 @@ describe('GET /sitemaps/index.xml (archive entries)', () => {

it('should include archive-index and paginated archive-pages sitemaps', async () => {
await con.getRepository(Archive).save([
{
...archiveBase,
scopeType: ArchiveScopeType.Global,
scopeId: null,
periodType: ArchivePeriodType.Month,
periodStart: new Date('2025-02-01T00:00:00.000Z'),
createdAt: new Date(),
},
{
...archiveBase,
scopeType: ArchiveScopeType.Tag,
Expand All @@ -1441,6 +1498,9 @@ describe('GET /sitemaps/index.xml (archive entries)', () => {
expect(res.text).toContain(
'<loc>http://localhost:5002/api/sitemaps/archive-index.xml</loc>',
);
expect(res.text).toContain(
'<loc>http://localhost:5002/api/sitemaps/archive-pages-global-month-0.xml</loc>',
);
expect(res.text).toContain(
'<loc>http://localhost:5002/api/sitemaps/archive-pages-tag-month-0.xml</loc>',
);
Expand Down
63 changes: 47 additions & 16 deletions src/routes/sitemaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,17 +418,32 @@ const zeroPadMonth = (month: number): string =>
const getArchiveBestOfUrl = (
prefix: string,
scopeType: ArchiveScopeType,
scopeId: string,
scopeId: string | null,
): string => {
const segment = scopeType === ArchiveScopeType.Tag ? 'tags' : 'sources';
switch (scopeType) {
case ArchiveScopeType.Global:
return `${prefix}/posts/best-of`;
case ArchiveScopeType.Tag:
if (!scopeId) {
throw new Error('Archive tag sitemap URL requires a scopeId');
}

return `${prefix}/tags/${encodeURIComponent(scopeId)}/best-of`;
case ArchiveScopeType.Source:
if (!scopeId) {
throw new Error('Archive source sitemap URL requires a scopeId');
}

return `${prefix}/sources/${encodeURIComponent(scopeId)}/best-of`;
}

return `${prefix}/${segment}/${encodeURIComponent(scopeId)}/best-of`;
throw new Error(`Unsupported archive scope type: ${scopeType}`);
};

const getArchivePageUrl = (
prefix: string,
scopeType: ArchiveScopeType,
scopeId: string,
scopeId: string | null,
periodType: ArchivePeriodType,
periodStart: Date,
): string => {
Expand Down Expand Up @@ -462,7 +477,11 @@ const buildArchiveIndexSitemapQuery = (
`a."scopeType" = '${ArchiveScopeType.Source}' AND s.id = a."scopeId"`,
)
.where('a."scopeType" IN (:...scopeTypes)', {
scopeTypes: [ArchiveScopeType.Tag, ArchiveScopeType.Source],
scopeTypes: [
ArchiveScopeType.Global,
ArchiveScopeType.Tag,
ArchiveScopeType.Source,
],
})
.andWhere(
`CASE WHEN a."scopeType" = '${ArchiveScopeType.Source}' THEN s.handle IS NOT NULL AND s.type != '${SourceType.Squad}' ELSE TRUE END`,
Expand All @@ -479,6 +498,7 @@ const buildArchiveIndexSitemapQuery = (
.limit(DEFAULT_SITEMAP_LIMIT);

const VALID_ARCHIVE_SCOPE_TYPES = new Set<string>([
ArchiveScopeType.Global,
ArchiveScopeType.Tag,
ArchiveScopeType.Source,
]);
Expand All @@ -497,7 +517,11 @@ const buildArchivePagesPaginatedQuery = (
.createQueryBuilder()
.select('a."scopeType"', 'scopeType')
.addSelect(
scopeType === ArchiveScopeType.Source ? 's.handle' : 'a."scopeId"',
scopeType === ArchiveScopeType.Global
? 'NULL'
: scopeType === ArchiveScopeType.Source
? 's.handle'
: 'a."scopeId"',
'scopeId',
)
.addSelect('a."periodType"', 'periodType')
Expand All @@ -507,15 +531,18 @@ const buildArchivePagesPaginatedQuery = (
.where('a."scopeType" = :scopeType', { scopeType })
.andWhere('a."periodType" = :periodType', { periodType });

if (scopeType === ArchiveScopeType.Source) {
qb.innerJoin(
Source,
's',
`s.id = a."scopeId" AND s.type != '${SourceType.Squad}'`,
);
qb.orderBy('s.handle', 'ASC');
} else {
qb.orderBy('a."scopeId"', 'ASC');
switch (scopeType) {
case ArchiveScopeType.Source:
qb.innerJoin(
Source,
's',
`s.id = a."scopeId" AND s.type != '${SourceType.Squad}'`,
);
qb.orderBy('s.handle', 'ASC');
break;
case ArchiveScopeType.Tag:
qb.orderBy('a."scopeId"', 'ASC');
break;
}

qb.addOrderBy('a."periodStart"', 'ASC')
Expand All @@ -538,7 +565,11 @@ const getArchivePagesCount = async (
.addSelect('COUNT(*)', 'count')
.from(Archive, 'a')
.where('a."scopeType" IN (:...scopeTypes)', {
scopeTypes: [ArchiveScopeType.Tag, ArchiveScopeType.Source],
scopeTypes: [
ArchiveScopeType.Global,
ArchiveScopeType.Tag,
ArchiveScopeType.Source,
],
})
.groupBy('a."scopeType"')
.addGroupBy('a."periodType"')
Expand Down
Loading