Skip to content

Commit 4d0815f

Browse files
SoonIterclaude
andauthored
feat(core): auto-extract description from first contentful line in extractPageData (#3006)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6a14daa commit 4d0815f

File tree

10 files changed

+274
-49
lines changed

10 files changed

+274
-49
lines changed

packages/core/src/node/route/RoutePage.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import path from 'node:path';
2-
import type { RouteMeta } from '@rspress/shared';
2+
import type { PageIndexInfo, RouteMeta } from '@rspress/shared';
33
import { getPageKey } from '../utils/getPageKey';
44
import { normalizePath, slash } from '../utils/normalizePath';
55
import { RouteService } from './RouteService';
@@ -10,8 +10,11 @@ export class RoutePage {
1010
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: use this field in the future
1111
#docDir: string;
1212

13-
// TODO: add pageIndexInfo
14-
// pageIndexInfo: PageIndexInfo;
13+
pageIndexInfo?: PageIndexInfo;
14+
15+
setPageIndexInfo(info: PageIndexInfo): void {
16+
this.pageIndexInfo = info;
17+
}
1518

1619
static create(absolutePath: string, docsDir: string): RoutePage {
1720
const routeMeta = absolutePathToRouteMeta(

packages/core/src/node/route/RouteService.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ describe('RouteService', async () => {
3030
expect(routeData).toMatchInlineSnapshot(`
3131
Map {
3232
"/a" => RoutePage {
33+
"pageIndexInfo": undefined,
3334
"routeMeta": {
3435
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/a.mdx",
3536
"lang": "",
@@ -40,6 +41,7 @@ describe('RouteService', async () => {
4041
},
4142
},
4243
"/guide/__e" => RoutePage {
44+
"pageIndexInfo": undefined,
4345
"routeMeta": {
4446
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/__e.mdx",
4547
"lang": "",
@@ -50,6 +52,7 @@ describe('RouteService', async () => {
5052
},
5153
},
5254
"/guide/b" => RoutePage {
55+
"pageIndexInfo": undefined,
5356
"routeMeta": {
5457
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/b.mdx",
5558
"lang": "",
@@ -60,6 +63,7 @@ describe('RouteService', async () => {
6063
},
6164
},
6265
"/guide/c" => RoutePage {
66+
"pageIndexInfo": undefined,
6367
"routeMeta": {
6468
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/c.tsx",
6569
"lang": "",
@@ -70,6 +74,7 @@ describe('RouteService', async () => {
7074
},
7175
},
7276
"/guide/" => RoutePage {
77+
"pageIndexInfo": undefined,
7378
"routeMeta": {
7479
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/index.md",
7580
"lang": "",
@@ -80,6 +85,7 @@ describe('RouteService', async () => {
8085
},
8186
},
8287
"/" => RoutePage {
88+
"pageIndexInfo": undefined,
8389
"routeMeta": {
8490
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/index.mdx",
8591
"lang": "",
@@ -141,6 +147,7 @@ describe('RouteService', async () => {
141147
expect(routeData).toMatchInlineSnapshot(`
142148
Map {
143149
"/a" => RoutePage {
150+
"pageIndexInfo": undefined,
144151
"routeMeta": {
145152
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/a.mdx",
146153
"lang": "",
@@ -151,6 +158,7 @@ describe('RouteService', async () => {
151158
},
152159
},
153160
"/guide/__e" => RoutePage {
161+
"pageIndexInfo": undefined,
154162
"routeMeta": {
155163
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/__e.mdx",
156164
"lang": "",
@@ -161,6 +169,7 @@ describe('RouteService', async () => {
161169
},
162170
},
163171
"/guide/c" => RoutePage {
172+
"pageIndexInfo": undefined,
164173
"routeMeta": {
165174
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/c.tsx",
166175
"lang": "",
@@ -171,6 +180,7 @@ describe('RouteService', async () => {
171180
},
172181
},
173182
"/guide/" => RoutePage {
183+
"pageIndexInfo": undefined,
174184
"routeMeta": {
175185
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/index.md",
176186
"lang": "",
@@ -181,6 +191,7 @@ describe('RouteService', async () => {
181191
},
182192
},
183193
"/" => RoutePage {
194+
"pageIndexInfo": undefined,
184195
"routeMeta": {
185196
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/index.mdx",
186197
"lang": "",
@@ -236,6 +247,7 @@ describe('RouteService', async () => {
236247
expect(routeData).toMatchInlineSnapshot(`
237248
Map {
238249
"/a" => RoutePage {
250+
"pageIndexInfo": undefined,
239251
"routeMeta": {
240252
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/a.mdx",
241253
"lang": "",
@@ -246,6 +258,7 @@ describe('RouteService', async () => {
246258
},
247259
},
248260
"/guide/__e" => RoutePage {
261+
"pageIndexInfo": undefined,
249262
"routeMeta": {
250263
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/__e.mdx",
251264
"lang": "",
@@ -256,6 +269,7 @@ describe('RouteService', async () => {
256269
},
257270
},
258271
"/guide/b" => RoutePage {
272+
"pageIndexInfo": undefined,
259273
"routeMeta": {
260274
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/b.mdx",
261275
"lang": "",
@@ -266,6 +280,7 @@ describe('RouteService', async () => {
266280
},
267281
},
268282
"/guide/" => RoutePage {
283+
"pageIndexInfo": undefined,
269284
"routeMeta": {
270285
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/guide/index.md",
271286
"lang": "",
@@ -276,6 +291,7 @@ describe('RouteService', async () => {
276291
},
277292
},
278293
"/" => RoutePage {
294+
"pageIndexInfo": undefined,
279295
"routeMeta": {
280296
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/basic/index.mdx",
281297
"lang": "",
@@ -348,6 +364,7 @@ describe('RouteService with i18n', async () => {
348364
expect(routeData).toMatchInlineSnapshot(`
349365
Map {
350366
"/guide/basic/install" => RoutePage {
367+
"pageIndexInfo": undefined,
351368
"routeMeta": {
352369
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/en/guide/basic/install.mdx",
353370
"lang": "en",
@@ -358,6 +375,7 @@ describe('RouteService with i18n', async () => {
358375
},
359376
},
360377
"/guide/basic/quick-start" => RoutePage {
378+
"pageIndexInfo": undefined,
361379
"routeMeta": {
362380
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/en/guide/basic/quick-start.mdx",
363381
"lang": "en",
@@ -368,6 +386,7 @@ describe('RouteService with i18n', async () => {
368386
},
369387
},
370388
"/guide/" => RoutePage {
389+
"pageIndexInfo": undefined,
371390
"routeMeta": {
372391
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/en/guide/index.mdx",
373392
"lang": "en",
@@ -378,6 +397,7 @@ describe('RouteService with i18n', async () => {
378397
},
379398
},
380399
"/" => RoutePage {
400+
"pageIndexInfo": undefined,
381401
"routeMeta": {
382402
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/en/index.mdx",
383403
"lang": "en",
@@ -388,6 +408,7 @@ describe('RouteService with i18n', async () => {
388408
},
389409
},
390410
"/zh/guide/basic/install" => RoutePage {
411+
"pageIndexInfo": undefined,
391412
"routeMeta": {
392413
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/zh/guide/basic/install.mdx",
393414
"lang": "zh",
@@ -398,6 +419,7 @@ describe('RouteService with i18n', async () => {
398419
},
399420
},
400421
"/zh/guide/basic/quick-start" => RoutePage {
422+
"pageIndexInfo": undefined,
401423
"routeMeta": {
402424
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/zh/guide/basic/quick-start.mdx",
403425
"lang": "zh",
@@ -408,6 +430,7 @@ describe('RouteService with i18n', async () => {
408430
},
409431
},
410432
"/zh/guide/" => RoutePage {
433+
"pageIndexInfo": undefined,
411434
"routeMeta": {
412435
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/zh/guide/index.mdx",
413436
"lang": "zh",
@@ -418,6 +441,7 @@ describe('RouteService with i18n', async () => {
418441
},
419442
},
420443
"/zh/" => RoutePage {
444+
"pageIndexInfo": undefined,
421445
"routeMeta": {
422446
"absolutePath": "<ROOT>/packages/core/src/node/route/fixtures/locales/zh/index.mdx",
423447
"lang": "zh",

packages/core/src/node/route/extractPageData.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ describe('extractPageData', async () => {
6262
version: '',
6363
},
6464
}),
65+
getRoutePageByRoutePath: () => undefined,
6566
} as RouteService,
6667
{
6768
alias: {},
@@ -78,6 +79,7 @@ describe('extractPageData', async () => {
7879
",
7980
"_relativePath": "a.mdx",
8081
"content": "",
82+
"description": undefined,
8183
"frontmatter": {
8284
"__content": undefined,
8385
},
@@ -93,6 +95,7 @@ describe('extractPageData', async () => {
9395
",
9496
"_relativePath": "guide/b.mdx",
9597
"content": "",
98+
"description": undefined,
9699
"frontmatter": {
97100
"__content": undefined,
98101
},
@@ -120,6 +123,7 @@ describe('extractPageData', async () => {
120123
",
121124
"_relativePath": "index.mdx",
122125
"content": "",
126+
"description": undefined,
123127
"frontmatter": {
124128
"__content": undefined,
125129
},
@@ -132,8 +136,10 @@ describe('extractPageData', async () => {
132136
]
133137
`);
134138
});
139+
});
135140

136-
it('getPageIndexInfoByRoute - recursive', async () => {
141+
describe('getPageIndexInfoByRoute', async () => {
142+
it('recursive', async () => {
137143
const fixtureRecursiveDir = join(__dirname, './fixtures/recursive');
138144
const absolutize = (relativePath: string) => {
139145
return join(fixtureRecursiveDir, `.${relativePath}`);
@@ -182,6 +188,7 @@ describe('extractPageData', async () => {
182188
183189
Comp in Comp content \`code\`
184190
",
191+
"description": undefined,
185192
"frontmatter": {
186193
"__content": undefined,
187194
},
@@ -245,6 +252,7 @@ describe('extractPageData', async () => {
245252
246253
More content here.
247254
",
255+
"description": "Some text before code. Some text after code.",
248256
"frontmatter": {
249257
"__content": undefined,
250258
},
@@ -307,6 +315,7 @@ describe('extractPageData', async () => {
307315
308316
More content here.
309317
",
318+
"description": "Some text before code. Some text after code.",
310319
"frontmatter": {
311320
"__content": undefined,
312321
},
@@ -363,6 +372,7 @@ describe('extractPageData', async () => {
363372
364373
Final text.
365374
",
375+
"description": "Here is an image: And some text after.",
366376
"frontmatter": {
367377
"__content": undefined,
368378
},
@@ -419,6 +429,7 @@ describe('extractPageData', async () => {
419429
420430
Visit [our docs]() for more info.
421431
",
432+
"description": "This is a link to Google in text.",
422433
"frontmatter": {
423434
"__content": undefined,
424435
},
@@ -477,6 +488,7 @@ describe('extractPageData', async () => {
477488
478489
More content.
479490
",
491+
"description": "Some intro text.",
480492
"frontmatter": {
481493
"__content": undefined,
482494
},
@@ -526,6 +538,7 @@ describe('extractPageData', async () => {
526538
527539
More content here.
528540
",
541+
"description": "A page with frontmatter",
529542
"frontmatter": {
530543
"__content": undefined,
531544
"description": "A page with frontmatter",
@@ -546,4 +559,82 @@ describe('extractPageData', async () => {
546559
}
547560
`);
548561
});
562+
563+
it('should extract description from first paragraph before h2 when frontmatter.description is not set', async () => {
564+
const pageIndexInfo = await getPageIndexInfoByRoute(
565+
createRoute('with-description.mdx', fixtureContentProcessingDir),
566+
{
567+
alias: {},
568+
replaceRules: [],
569+
root: fixtureContentProcessingDir,
570+
searchCodeBlocks: false,
571+
},
572+
);
573+
574+
// description field should have extracted value
575+
expect(pageIndexInfo.description).toBe(
576+
'This is the first paragraph that should be used as description.',
577+
);
578+
// frontmatter.description should remain undefined
579+
expect(pageIndexInfo.frontmatter.description).toBeUndefined();
580+
});
581+
582+
it('should use frontmatter description when provided instead of extracting from content', async () => {
583+
const pageIndexInfo = await getPageIndexInfoByRoute(
584+
createRoute('with-frontmatter.mdx', fixtureContentProcessingDir),
585+
{
586+
alias: {},
587+
replaceRules: [],
588+
root: fixtureContentProcessingDir,
589+
searchCodeBlocks: false,
590+
},
591+
);
592+
593+
// description field should use frontmatter value
594+
expect(pageIndexInfo.description).toBe('A page with frontmatter');
595+
// frontmatter.description should preserve original value
596+
expect(pageIndexInfo.frontmatter.description).toBe(
597+
'A page with frontmatter',
598+
);
599+
});
600+
601+
it('should skip code blocks when extracting description', async () => {
602+
const pageIndexInfo = await getPageIndexInfoByRoute(
603+
createRoute('with-code.mdx', fixtureContentProcessingDir),
604+
{
605+
alias: {},
606+
replaceRules: [],
607+
root: fixtureContentProcessingDir,
608+
searchCodeBlocks: false,
609+
},
610+
);
611+
612+
// Code blocks should be skipped, only paragraph text collected
613+
expect(pageIndexInfo.description).toBe(
614+
'Some text before code. Some text after code.',
615+
);
616+
// frontmatter.description should remain undefined
617+
expect(pageIndexInfo.frontmatter.description).toBeUndefined();
618+
});
619+
620+
it('should collect all text between h1 and h2 for description', async () => {
621+
const pageIndexInfo = await getPageIndexInfoByRoute(
622+
createRoute(
623+
'multi-paragraph-description.mdx',
624+
fixtureContentProcessingDir,
625+
),
626+
{
627+
alias: {},
628+
replaceRules: [],
629+
root: fixtureContentProcessingDir,
630+
searchCodeBlocks: false,
631+
},
632+
);
633+
634+
// Should collect all paragraphs and list items between h1 and h2
635+
expect(pageIndexInfo.description).toBe(
636+
'This is the first paragraph. This is the second paragraph with bold and italic. List item oneList item two',
637+
);
638+
expect(pageIndexInfo.frontmatter.description).toBeUndefined();
639+
});
549640
});

0 commit comments

Comments
 (0)