-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Core: Add sitemap.xml handler #33243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import { join } from 'node:path'; | ||
| import { gunzipSync } from 'node:zlib'; | ||
|
|
||
| import { beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
|
|
@@ -79,6 +80,13 @@ | |
| on: vi.fn(), | ||
| } as any; | ||
|
|
||
| const defaultOptions = { | ||
| host: 'localhost', | ||
| port: 6006, | ||
| https: false, | ||
| initialPath: '', | ||
| } as any; | ||
|
|
||
| beforeEach(async () => { | ||
| use.mockClear(); | ||
| end.mockClear(); | ||
|
|
@@ -101,10 +109,12 @@ | |
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
| console.timeEnd('useStoriesJson'); | ||
|
|
||
| expect(use).toHaveBeenCalledTimes(1); | ||
| // /index.json and /sitemap.xml | ||
| expect(use).toHaveBeenCalledTimes(2); | ||
|
Check failure on line 117 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| const route = use.mock.calls[0][1]; | ||
|
|
||
| console.time('route'); | ||
|
|
@@ -477,9 +487,10 @@ | |
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| expect(use).toHaveBeenCalledTimes(1); | ||
| expect(use).toHaveBeenCalledTimes(2); | ||
|
Check failure on line 493 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| const route = use.mock.calls[0][1]; | ||
|
|
||
| const firstPromise = route(request, response); | ||
|
|
@@ -509,9 +520,10 @@ | |
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| expect(use).toHaveBeenCalledTimes(1); | ||
| expect(use).toHaveBeenCalledTimes(2); | ||
|
Check failure on line 526 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| const route = use.mock.calls[0][1]; | ||
|
|
||
| await route(request, response); | ||
|
|
@@ -543,9 +555,10 @@ | |
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| expect(use).toHaveBeenCalledTimes(1); | ||
| expect(use).toHaveBeenCalledTimes(2); | ||
|
Check failure on line 561 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| const route = use.mock.calls[0][1]; | ||
|
|
||
| // Don't wait for the first request here before starting the second | ||
|
|
@@ -586,9 +599,10 @@ | |
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| expect(use).toHaveBeenCalledTimes(1); | ||
| expect(use).toHaveBeenCalledTimes(2); | ||
|
Check failure on line 605 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| const route = use.mock.calls[0][1]; | ||
|
|
||
| await route(request, response); | ||
|
|
@@ -621,4 +635,151 @@ | |
| expect(mockServerChannel.emit).toHaveBeenCalledTimes(2); | ||
| }); | ||
| }); | ||
|
|
||
| describe('Sitemap endpoint', () => { | ||
| beforeEach(() => { | ||
| use.mockClear(); | ||
| end.mockClear(); | ||
| }); | ||
|
|
||
| it('generates sitemap with http://localhost', async () => { | ||
| const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; | ||
| useStoriesJson({ | ||
| app, | ||
| serverChannel: mockServerChannel, | ||
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| expect(use).toHaveBeenCalledTimes(2); | ||
|
Check failure on line 656 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| const sitemapRoute = use.mock.calls[1][1]; | ||
|
|
||
| await sitemapRoute(request, response); | ||
|
|
||
| expect(end).toHaveBeenCalledTimes(1); | ||
| expect(response.setHeader).toHaveBeenCalledWith('Content-Type', 'application/xml'); | ||
| expect(response.setHeader).toHaveBeenCalledWith('Content-Encoding', 'gzip'); | ||
|
|
||
| const gzippedBuffer = end.mock.calls[0][0]; | ||
| const decompressed = gunzipSync(gzippedBuffer).toString('utf-8'); | ||
|
|
||
| expect(decompressed).toContain('<?xml version="1.0" encoding="UTF-8"?>'); | ||
| expect(decompressed).toContain('<urlset'); | ||
| expect(decompressed).toContain('http://localhost:6006'); | ||
| expect(decompressed).toContain('/?path=/docs/a--metaof'); | ||
| expect(decompressed).toContain('/?path=/story/a--story-one'); | ||
| expect(decompressed).toContain('/?path=/settings/about'); | ||
| expect(decompressed).toContain('/?path=/settings/whats-new'); | ||
| expect(decompressed).toContain('/?path=/settings/guide'); | ||
| expect(decompressed).toContain('/?path=/settings/shortcuts'); | ||
| }); | ||
|
|
||
| it('generates sitemap with https protocol', async () => { | ||
| const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; | ||
| const httpsOptions = { ...defaultOptions, https: true }; | ||
|
|
||
| useStoriesJson({ | ||
| app, | ||
| serverChannel: mockServerChannel, | ||
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: httpsOptions, | ||
| }); | ||
|
|
||
| const sitemapRoute = use.mock.calls[1][1]; | ||
|
Check failure on line 692 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| await sitemapRoute(request, response); | ||
|
|
||
| const gzippedBuffer = end.mock.calls[0][0]; | ||
| const decompressed = gunzipSync(gzippedBuffer).toString('utf-8'); | ||
|
|
||
| expect(decompressed).toContain('https://localhost:6006'); | ||
| expect(decompressed).not.toContain('http://localhost:6006'); | ||
| }); | ||
|
|
||
| it('generates sitemap for production website (example.com)', async () => { | ||
| const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; | ||
| const productionOptions = { | ||
| host: 'example.com', | ||
| port: 443, | ||
| https: true, | ||
| initialPath: '', | ||
| } as any; | ||
|
|
||
| useStoriesJson({ | ||
| app, | ||
| serverChannel: mockServerChannel, | ||
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: productionOptions, | ||
| }); | ||
|
|
||
| const sitemapRoute = use.mock.calls[1][1]; | ||
|
Check failure on line 720 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| await sitemapRoute(request, response); | ||
|
|
||
| const gzippedBuffer = end.mock.calls[0][0]; | ||
| const decompressed = gunzipSync(gzippedBuffer).toString('utf-8'); | ||
|
|
||
| expect(decompressed).toContain('https://example.com'); | ||
| expect(decompressed).toContain('/?path=/docs/a--metaof'); | ||
| expect(decompressed).toContain('/?path=/story/a--story-one'); | ||
| }); | ||
|
|
||
| it('uses cached sitemap on subsequent requests', async () => { | ||
| const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; | ||
| useStoriesJson({ | ||
| app, | ||
| serverChannel: mockServerChannel, | ||
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| const sitemapRoute = use.mock.calls[1][1]; | ||
|
Check failure on line 742 in code/core/src/core-server/utils/stories-json.test.ts
|
||
|
|
||
| // First request | ||
| await sitemapRoute(request, response); | ||
| const firstBuffer = end.mock.calls[0][0] as Buffer; | ||
|
|
||
| // Second request | ||
| const secondResponse = { | ||
| ...response, | ||
| end: vi.fn(), | ||
| setHeader: vi.fn(), | ||
| on: vi.fn(), | ||
| }; | ||
| await sitemapRoute(request, secondResponse); | ||
| const secondBuffer = secondResponse.end.mock.calls[0][0] as Buffer; | ||
|
|
||
| // Should return the same cached buffer | ||
| expect(firstBuffer).toBe(secondBuffer); | ||
| }); | ||
|
|
||
| it('excludes test stories from sitemap', async () => { | ||
| const mockServerChannel = { emit: vi.fn() } as any as ServerChannel; | ||
| useStoriesJson({ | ||
| app, | ||
| serverChannel: mockServerChannel, | ||
| workingDir, | ||
| normalizedStories, | ||
| initializedStoryIndexGenerator: getInitializedStoryIndexGenerator(), | ||
| options: defaultOptions, | ||
| }); | ||
|
|
||
| const sitemapRoute = use.mock.calls[1][1]; | ||
|
Check failure on line 773 in code/core/src/core-server/utils/stories-json.test.ts
|
||
| await sitemapRoute(request, response); | ||
|
|
||
| const gzippedBuffer = end.mock.calls[0][0]; | ||
| const decompressed = gunzipSync(gzippedBuffer).toString('utf-8'); | ||
|
|
||
| // Verify test stories are not included | ||
| // Note: If test stories exist in mock data with subtype: 'test', they should not appear | ||
| expect(decompressed).toContain('/?path=/story/a--story-one'); | ||
| expect(decompressed).toContain('/?path=/docs/a--metaof'); | ||
| }); | ||
| }); | ||
|
Comment on lines
+639
to
+784
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sitemap endpoint tests are strong overall; tighten the “excludes test stories” assertion The new sitemap tests nicely validate hostname/protocol handling, presence of key paths, and cache reuse. However, in 🤖 Prompt for AI Agents |
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sidnioulz, minor item here. Don't forget to add the appropriate documentation to reflect this CLI flag, to avoid potential questions/issues, and ensure parity between what's available and what's documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course 👍
I will copy my TODO list for this feature into the description to dispel any concern :)