@umami/shiso 0.61.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/bin/shiso.mjs +0 -0
- package/docs.schema.json +81 -20
- package/package.json +3 -2
- package/scripts/check-package.mjs +36 -0
- package/scripts/generate-search-index.mjs +79 -33
- package/scripts/lib/mdast.mjs +23 -0
- package/scripts/lib/slug.mjs +21 -0
- package/scripts/prerender.mjs +24 -17
- package/src/App.tsx +4 -5
- package/src/components/DocContent.tsx +7 -2
- package/src/components/Docs.tsx +15 -5
- package/src/components/Footer.tsx +6 -9
- package/src/components/Header.tsx +13 -3
- package/src/components/LanguageSwitcher.tsx +59 -0
- package/src/components/Layout.tsx +8 -0
- package/src/components/Search.tsx +12 -3
- package/src/components/SideNav.tsx +3 -5
- package/src/components/VersionSwitcher.tsx +60 -0
- package/src/components/docs/PropertiesTable.tsx +7 -7
- package/src/entry-server.tsx +20 -8
- package/src/lib/docs-config.ts +397 -93
- package/src/lib/head.ts +15 -4
- package/src/lib/locale.ts +39 -0
- package/src/lib/search/provider.ts +7 -2
- package/src/lib/search/providers/local.ts +3 -3
- package/src/lib/search.ts +28 -0
- package/src/lib/site-config.ts +35 -22
- package/src/lib/types.ts +39 -2
- package/types/search.d.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
All notable changes to `@umami/shiso` are documented here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 1.0.0 - 2026-08-15
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Full multi-version and multi-language navigation, including versions nested
|
|
11
|
+
inside languages and responsive header selectors.
|
|
12
|
+
- Per-scope prerendering, Markdown export, sitemap metadata, document locale,
|
|
13
|
+
text direction, and local search results.
|
|
14
|
+
- A multi-scope integration fixture covering visible and hidden pages,
|
|
15
|
+
versions, languages, redirects, search, and sitemap output.
|
|
16
|
+
- Public exports for the bundled `docs.json` schema.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Navigation containers now require exactly one non-empty navigation mode, and
|
|
21
|
+
default to the first visible version or language when none is marked.
|
|
22
|
+
- Hidden versions and languages remain buildable and directly reachable while
|
|
23
|
+
staying out of selectors and search.
|
|
24
|
+
- Redirect sources are exact paths; wildcard and parameter patterns are
|
|
25
|
+
rejected during configuration validation.
|
|
26
|
+
- Supported Node.js versions now match Vite: `^20.19.0 || >=22.12.0`.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Client-side navigation now updates page metadata, document language, and
|
|
31
|
+
left-to-right or right-to-left text direction.
|
|
32
|
+
|
|
6
33
|
## 0.61.0 - 2026-08-13
|
|
7
34
|
|
|
8
35
|
### Added
|
package/bin/shiso.mjs
CHANGED
|
File without changes
|
package/docs.schema.json
CHANGED
|
@@ -104,6 +104,33 @@
|
|
|
104
104
|
"navigation": {
|
|
105
105
|
"type": "object",
|
|
106
106
|
"description": "Site navigation. Define exactly one of: tabs, dropdowns, versions, languages, or top-level groups/pages.",
|
|
107
|
+
"anyOf": [
|
|
108
|
+
{ "required": ["$ref"] },
|
|
109
|
+
{ "required": ["tabs"] },
|
|
110
|
+
{ "required": ["dropdowns"] },
|
|
111
|
+
{ "required": ["versions"] },
|
|
112
|
+
{ "required": ["languages"] },
|
|
113
|
+
{ "required": ["groups"] },
|
|
114
|
+
{ "required": ["pages"] }
|
|
115
|
+
],
|
|
116
|
+
"not": {
|
|
117
|
+
"anyOf": [
|
|
118
|
+
{ "required": ["tabs", "dropdowns"] },
|
|
119
|
+
{ "required": ["tabs", "versions"] },
|
|
120
|
+
{ "required": ["tabs", "languages"] },
|
|
121
|
+
{ "required": ["tabs", "groups"] },
|
|
122
|
+
{ "required": ["tabs", "pages"] },
|
|
123
|
+
{ "required": ["dropdowns", "versions"] },
|
|
124
|
+
{ "required": ["dropdowns", "languages"] },
|
|
125
|
+
{ "required": ["dropdowns", "groups"] },
|
|
126
|
+
{ "required": ["dropdowns", "pages"] },
|
|
127
|
+
{ "required": ["versions", "languages"] },
|
|
128
|
+
{ "required": ["versions", "groups"] },
|
|
129
|
+
{ "required": ["versions", "pages"] },
|
|
130
|
+
{ "required": ["languages", "groups"] },
|
|
131
|
+
{ "required": ["languages", "pages"] }
|
|
132
|
+
]
|
|
133
|
+
},
|
|
107
134
|
"additionalProperties": false,
|
|
108
135
|
"properties": {
|
|
109
136
|
"$ref": { "$ref": "#/definitions/configRef" },
|
|
@@ -599,31 +626,31 @@
|
|
|
599
626
|
},
|
|
600
627
|
"redirectSourceValue": {
|
|
601
628
|
"anyOf": [
|
|
602
|
-
{ "type": "string", "pattern": "^/[
|
|
629
|
+
{ "type": "string", "pattern": "^/[^*:]*$" },
|
|
603
630
|
{ "$ref": "#/definitions/configRefValue" }
|
|
604
631
|
]
|
|
605
632
|
},
|
|
606
633
|
"pageItemList": {
|
|
607
634
|
"anyOf": [
|
|
608
|
-
{ "type": "array", "items": { "$ref": "#/definitions/pageItem" } },
|
|
635
|
+
{ "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/pageItem" } },
|
|
609
636
|
{ "$ref": "#/definitions/configRefValue" }
|
|
610
637
|
]
|
|
611
638
|
},
|
|
612
639
|
"groupList": {
|
|
613
640
|
"anyOf": [
|
|
614
|
-
{ "type": "array", "items": { "$ref": "#/definitions/group" } },
|
|
641
|
+
{ "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/group" } },
|
|
615
642
|
{ "$ref": "#/definitions/configRefValue" }
|
|
616
643
|
]
|
|
617
644
|
},
|
|
618
645
|
"dropdownList": {
|
|
619
646
|
"anyOf": [
|
|
620
|
-
{ "type": "array", "items": { "$ref": "#/definitions/dropdown" } },
|
|
647
|
+
{ "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/dropdown" } },
|
|
621
648
|
{ "$ref": "#/definitions/configRefValue" }
|
|
622
649
|
]
|
|
623
650
|
},
|
|
624
651
|
"tabList": {
|
|
625
652
|
"anyOf": [
|
|
626
|
-
{ "type": "array", "items": { "$ref": "#/definitions/tab" } },
|
|
653
|
+
{ "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/tab" } },
|
|
627
654
|
{ "$ref": "#/definitions/configRefValue" }
|
|
628
655
|
]
|
|
629
656
|
},
|
|
@@ -635,13 +662,13 @@
|
|
|
635
662
|
},
|
|
636
663
|
"versionList": {
|
|
637
664
|
"anyOf": [
|
|
638
|
-
{ "type": "array", "items": { "$ref": "#/definitions/version" } },
|
|
665
|
+
{ "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/version" } },
|
|
639
666
|
{ "$ref": "#/definitions/configRefValue" }
|
|
640
667
|
]
|
|
641
668
|
},
|
|
642
669
|
"languageList": {
|
|
643
670
|
"anyOf": [
|
|
644
|
-
{ "type": "array", "items": { "$ref": "#/definitions/language" } },
|
|
671
|
+
{ "type": "array", "minItems": 1, "items": { "$ref": "#/definitions/language" } },
|
|
645
672
|
{ "$ref": "#/definitions/configRefValue" }
|
|
646
673
|
]
|
|
647
674
|
},
|
|
@@ -723,7 +750,7 @@
|
|
|
723
750
|
},
|
|
724
751
|
"anchor": {
|
|
725
752
|
"type": "object",
|
|
726
|
-
"anyOf": [{ "required": ["$ref"] }, { "required": ["anchor"] }],
|
|
753
|
+
"anyOf": [{ "required": ["$ref"] }, { "required": ["anchor", "href"] }],
|
|
727
754
|
"additionalProperties": false,
|
|
728
755
|
"properties": {
|
|
729
756
|
"$ref": { "$ref": "#/definitions/configRef" },
|
|
@@ -763,7 +790,10 @@
|
|
|
763
790
|
},
|
|
764
791
|
"dropdown": {
|
|
765
792
|
"type": "object",
|
|
766
|
-
"
|
|
793
|
+
"allOf": [
|
|
794
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["dropdown"] }] },
|
|
795
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["groups"] }, { "required": ["pages"] }] }
|
|
796
|
+
],
|
|
767
797
|
"additionalProperties": false,
|
|
768
798
|
"properties": {
|
|
769
799
|
"$ref": { "$ref": "#/definitions/configRef" },
|
|
@@ -779,7 +809,10 @@
|
|
|
779
809
|
},
|
|
780
810
|
"tab": {
|
|
781
811
|
"type": "object",
|
|
782
|
-
"
|
|
812
|
+
"allOf": [
|
|
813
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["tab"] }] },
|
|
814
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["groups"] }, { "required": ["pages"] }, { "required": ["dropdowns"] }] }
|
|
815
|
+
],
|
|
783
816
|
"additionalProperties": false,
|
|
784
817
|
"properties": {
|
|
785
818
|
"$ref": { "$ref": "#/definitions/configRef" },
|
|
@@ -793,16 +826,28 @@
|
|
|
793
826
|
},
|
|
794
827
|
"version": {
|
|
795
828
|
"type": "object",
|
|
796
|
-
"
|
|
829
|
+
"allOf": [
|
|
830
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["version"] }] },
|
|
831
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["tabs"] }, { "required": ["dropdowns"] }, { "required": ["groups"] }, { "required": ["pages"] }] }
|
|
832
|
+
],
|
|
833
|
+
"not": {
|
|
834
|
+
"anyOf": [
|
|
835
|
+
{ "required": ["tabs", "dropdowns"] },
|
|
836
|
+
{ "required": ["tabs", "groups"] },
|
|
837
|
+
{ "required": ["tabs", "pages"] },
|
|
838
|
+
{ "required": ["dropdowns", "groups"] },
|
|
839
|
+
{ "required": ["dropdowns", "pages"] }
|
|
840
|
+
]
|
|
841
|
+
},
|
|
797
842
|
"additionalProperties": false,
|
|
798
843
|
"properties": {
|
|
799
844
|
"$ref": { "$ref": "#/definitions/configRef" },
|
|
800
845
|
"version": {
|
|
801
|
-
"allOf": [{ "$ref": "#/definitions/
|
|
802
|
-
"description": "Version label
|
|
846
|
+
"allOf": [{ "$ref": "#/definitions/nonEmptyStringValue" }],
|
|
847
|
+
"description": "Version label, shown in the version selector. Every version builds; page references determine its URLs."
|
|
803
848
|
},
|
|
804
|
-
"default": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Marks this version as the one
|
|
805
|
-
"hidden": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Hide this version." },
|
|
849
|
+
"default": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Marks this version as the default. At most one version may be the default; without one, the first visible version is used." },
|
|
850
|
+
"hidden": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Hide this version from the version selector, sitemap, and search. Its pages still build and stay reachable by URL." },
|
|
806
851
|
"tabs": { "$ref": "#/definitions/tabList" },
|
|
807
852
|
"dropdowns": { "$ref": "#/definitions/dropdownList" },
|
|
808
853
|
"groups": { "$ref": "#/definitions/groupList" },
|
|
@@ -811,19 +856,35 @@
|
|
|
811
856
|
},
|
|
812
857
|
"language": {
|
|
813
858
|
"type": "object",
|
|
814
|
-
"
|
|
859
|
+
"allOf": [
|
|
860
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["language"] }] },
|
|
861
|
+
{ "anyOf": [{ "required": ["$ref"] }, { "required": ["versions"] }, { "required": ["tabs"] }, { "required": ["dropdowns"] }, { "required": ["groups"] }, { "required": ["pages"] }] }
|
|
862
|
+
],
|
|
863
|
+
"not": {
|
|
864
|
+
"anyOf": [
|
|
865
|
+
{ "required": ["tabs", "dropdowns"] },
|
|
866
|
+
{ "required": ["tabs", "versions"] },
|
|
867
|
+
{ "required": ["tabs", "groups"] },
|
|
868
|
+
{ "required": ["tabs", "pages"] },
|
|
869
|
+
{ "required": ["dropdowns", "versions"] },
|
|
870
|
+
{ "required": ["dropdowns", "groups"] },
|
|
871
|
+
{ "required": ["dropdowns", "pages"] },
|
|
872
|
+
{ "required": ["versions", "groups"] },
|
|
873
|
+
{ "required": ["versions", "pages"] }
|
|
874
|
+
]
|
|
875
|
+
},
|
|
815
876
|
"additionalProperties": false,
|
|
816
877
|
"properties": {
|
|
817
878
|
"$ref": { "$ref": "#/definitions/configRef" },
|
|
818
879
|
"language": {
|
|
819
|
-
"allOf": [{ "$ref": "#/definitions/
|
|
820
|
-
"description": "Language code
|
|
880
|
+
"allOf": [{ "$ref": "#/definitions/nonEmptyStringValue" }],
|
|
881
|
+
"description": "Language code (e.g. \"es\"), shown in the language selector and used as the page locale when valid. Every language builds; page references determine its URLs."
|
|
821
882
|
},
|
|
822
883
|
"default": {
|
|
823
884
|
"allOf": [{ "$ref": "#/definitions/booleanValue" }],
|
|
824
|
-
"description": "Marks this language as the one
|
|
885
|
+
"description": "Marks this language as the default. At most one language may be the default; without one, the first visible language is used."
|
|
825
886
|
},
|
|
826
|
-
"hidden": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Hide this language." },
|
|
887
|
+
"hidden": { "allOf": [{ "$ref": "#/definitions/booleanValue" }], "description": "Hide this language from the language selector, sitemap, and search. Its pages still build and stay reachable by URL." },
|
|
827
888
|
"versions": { "$ref": "#/definitions/versionList" },
|
|
828
889
|
"tabs": { "$ref": "#/definitions/tabList" },
|
|
829
890
|
"dropdowns": { "$ref": "#/definitions/dropdownList" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umami/shiso",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Open-source documentation framework for Markdown and MDX sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"default": "./src/lib/search/provider.ts"
|
|
17
17
|
},
|
|
18
18
|
"./schema": "./docs.schema.json",
|
|
19
|
+
"./docs.schema.json": "./docs.schema.json",
|
|
19
20
|
"./package.json": "./package.json"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"test:package": "node scripts/check-package.mjs"
|
|
35
36
|
},
|
|
36
37
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
38
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
38
39
|
},
|
|
39
40
|
"repository": {
|
|
40
41
|
"type": "git",
|
|
@@ -6,6 +6,8 @@ const root = path.resolve(import.meta.dirname, '..');
|
|
|
6
6
|
for (const relativeFile of [
|
|
7
7
|
'bin/shiso.mjs',
|
|
8
8
|
'docs.schema.json',
|
|
9
|
+
'scripts/lib/mdast.mjs',
|
|
10
|
+
'scripts/lib/slug.mjs',
|
|
9
11
|
'src/entry-client.tsx',
|
|
10
12
|
'src/entry-server.tsx',
|
|
11
13
|
'types/client.d.ts',
|
|
@@ -15,4 +17,38 @@ for (const relativeFile of [
|
|
|
15
17
|
await fs.access(path.join(root, relativeFile));
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
// Every public export subpath must resolve to real files, including its types.
|
|
21
|
+
const metadata = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
|
|
22
|
+
|
|
23
|
+
for (const [subpath, target] of Object.entries(metadata.exports)) {
|
|
24
|
+
const files = typeof target === 'string' ? [target] : Object.values(target);
|
|
25
|
+
|
|
26
|
+
for (const file of files) {
|
|
27
|
+
await fs.access(path.join(root, file)).catch(() => {
|
|
28
|
+
throw new Error(`Export "${subpath}" points at missing file "${file}".`);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Node scripts must not import raw TypeScript: that would depend on Node's
|
|
34
|
+
// experimental type stripping, which the supported engine range does not have.
|
|
35
|
+
for (const directory of ['bin', 'scripts', 'scripts/lib']) {
|
|
36
|
+
const entries = await fs.readdir(path.join(root, directory), { withFileTypes: true });
|
|
37
|
+
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (!entry.isFile() || !/\.(mjs|js|cjs)$/.test(entry.name)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const source = await fs.readFile(path.join(root, directory, entry.name), 'utf8');
|
|
44
|
+
const rawTsImport = source.match(/^import\s[^\n]*from\s+'[^']+\.tsx?';?$/m);
|
|
45
|
+
|
|
46
|
+
if (rawTsImport) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${directory}/${entry.name} imports raw TypeScript: ${rawTsImport[0].trim()}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
18
54
|
console.log('shiso package contents are ready.');
|
|
@@ -21,8 +21,8 @@ import remarkGfm from 'remark-gfm';
|
|
|
21
21
|
import remarkMdx from 'remark-mdx';
|
|
22
22
|
import remarkParse from 'remark-parse';
|
|
23
23
|
import { unified } from 'unified';
|
|
24
|
-
import { headingText } from '
|
|
25
|
-
import { createSlugger } from '
|
|
24
|
+
import { headingText } from './lib/mdast.mjs';
|
|
25
|
+
import { createSlugger, slugifyId } from './lib/slug.mjs';
|
|
26
26
|
import { loadDocsConfig } from './load-docs-config.mjs';
|
|
27
27
|
|
|
28
28
|
const DEFAULT_ROOT = process.cwd();
|
|
@@ -49,6 +49,53 @@ function normalizePageReference(pageRef) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* One entry per navigation scope, mirroring collectScopeSources in
|
|
54
|
+
* src/lib/docs-config.ts: ordinary navigation, versions, languages, and
|
|
55
|
+
* versions nested inside languages. Hidden scopes are excluded from search,
|
|
56
|
+
* matching how hidden pages are excluded.
|
|
57
|
+
*/
|
|
58
|
+
function collectScopes(navigation) {
|
|
59
|
+
if (Array.isArray(navigation.versions)) {
|
|
60
|
+
return navigation.versions
|
|
61
|
+
.filter(version => !version.hidden)
|
|
62
|
+
.map(version => ({
|
|
63
|
+
id: slugifyId(version.version?.trim() || '', 'scope'),
|
|
64
|
+
version: version.version?.trim(),
|
|
65
|
+
container: version,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(navigation.languages)) {
|
|
70
|
+
return navigation.languages
|
|
71
|
+
.filter(language => !language.hidden)
|
|
72
|
+
.flatMap(language => {
|
|
73
|
+
const languageLabel = language.language?.trim();
|
|
74
|
+
|
|
75
|
+
if (Array.isArray(language.versions)) {
|
|
76
|
+
return language.versions
|
|
77
|
+
.filter(version => !version.hidden)
|
|
78
|
+
.map(version => ({
|
|
79
|
+
id: slugifyId(`${languageLabel}-${version.version?.trim()}`, 'scope'),
|
|
80
|
+
language: languageLabel,
|
|
81
|
+
version: version.version?.trim(),
|
|
82
|
+
container: version,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
id: slugifyId(languageLabel || '', 'scope'),
|
|
89
|
+
language: languageLabel,
|
|
90
|
+
container: language,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return [{ id: 'default', container: navigation }];
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
/**
|
|
53
100
|
* Collects `{ fileSlug, slug }` for every non-hidden page in the navigation
|
|
54
101
|
* tree. A simplified mirror of the config walker: search only needs page refs
|
|
@@ -62,15 +109,6 @@ function collectVisiblePages(container, pages = [], hidden = false) {
|
|
|
62
109
|
...(container.dropdowns || []),
|
|
63
110
|
];
|
|
64
111
|
|
|
65
|
-
// Versions and languages collapse to their default entry, like the app does.
|
|
66
|
-
for (const key of ['versions', 'languages']) {
|
|
67
|
-
const entries = container[key];
|
|
68
|
-
|
|
69
|
-
if (Array.isArray(entries) && entries.length) {
|
|
70
|
-
items.push(entries.find(entry => entry.default) || entries[0]);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
112
|
for (const item of items) {
|
|
75
113
|
if (typeof item === 'string') {
|
|
76
114
|
if (!hidden) {
|
|
@@ -172,36 +210,44 @@ export async function generateSearchIndex({
|
|
|
172
210
|
const seen = new Set();
|
|
173
211
|
const records = [];
|
|
174
212
|
|
|
175
|
-
for (const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
213
|
+
for (const scope of collectScopes(docsJson.navigation || {})) {
|
|
214
|
+
// Single-scope sites omit scope fields so their index stays unchanged.
|
|
215
|
+
const scopeFields =
|
|
216
|
+
scope.id === 'default'
|
|
217
|
+
? {}
|
|
218
|
+
: { scopeId: scope.id, language: scope.language, version: scope.version };
|
|
219
|
+
|
|
220
|
+
for (const { fileSlug, slug } of collectVisiblePages(scope.container)) {
|
|
221
|
+
if (seen.has(fileSlug)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
179
224
|
|
|
180
|
-
|
|
225
|
+
seen.add(fileSlug);
|
|
181
226
|
|
|
182
|
-
|
|
183
|
-
|
|
227
|
+
let source;
|
|
228
|
+
let filePath;
|
|
184
229
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
230
|
+
for (const extension of ['mdx', 'md']) {
|
|
231
|
+
filePath = path.join(root, contentDir, `${fileSlug}.${extension}`);
|
|
232
|
+
source = await fs.readFile(filePath, 'utf8').catch(() => undefined);
|
|
188
233
|
|
|
189
|
-
|
|
190
|
-
|
|
234
|
+
if (source !== undefined) {
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
191
237
|
}
|
|
192
|
-
}
|
|
193
238
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
239
|
+
if (source === undefined) {
|
|
240
|
+
// Missing files fail config normalization; search just skips them.
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
198
243
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
244
|
+
const tree = parser.parse(source);
|
|
245
|
+
const url = slug === 'index' ? docsPrefix || '/' : `${docsPrefix}/${slug}`;
|
|
246
|
+
const page = frontmatterTitle(tree) || fileSlug;
|
|
202
247
|
|
|
203
|
-
|
|
204
|
-
|
|
248
|
+
for (const { heading, id, text } of collectSections(tree)) {
|
|
249
|
+
records.push({ url, page, heading, id, text, ...scopeFields });
|
|
250
|
+
}
|
|
205
251
|
}
|
|
206
252
|
}
|
|
207
253
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors the mdast helpers in src/lib/mdast.ts for Node scripts, which must
|
|
3
|
+
* not import raw TypeScript (that would depend on Node's experimental type
|
|
4
|
+
* stripping). tests/script-mirrors.test.ts asserts the two stay in sync.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Concatenates the text content of a node, including JSX text children. */
|
|
8
|
+
export function toText(node) {
|
|
9
|
+
if (!node) {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof node.value === 'string') {
|
|
14
|
+
return node.value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (node.children || []).map(toText).join('');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Text of a heading node, trimmed. */
|
|
21
|
+
export function headingText(node) {
|
|
22
|
+
return toText(node).trim();
|
|
23
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors src/lib/slug.ts for Node scripts, which must not import raw
|
|
3
|
+
* TypeScript (that would depend on Node's experimental type stripping).
|
|
4
|
+
* tests/script-mirrors.test.ts asserts the two stay in sync.
|
|
5
|
+
*/
|
|
6
|
+
import GithubSlugger, { slug as slugifyOnce } from 'github-slugger';
|
|
7
|
+
|
|
8
|
+
/** Stateful slugger matching rehype-slug: repeated headings get -1, -2, ... */
|
|
9
|
+
export function createSlugger() {
|
|
10
|
+
return new GithubSlugger();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Stateless slugify for one-off ids that do not need de-duplication. */
|
|
14
|
+
export function slugify(value) {
|
|
15
|
+
return slugifyOnce(value.trim());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Slugifies a label into a config-level identifier, with a fallback. */
|
|
19
|
+
export function slugifyId(value, fallback) {
|
|
20
|
+
return slugify(value) || fallback;
|
|
21
|
+
}
|
package/scripts/prerender.mjs
CHANGED
|
@@ -16,7 +16,6 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import process from 'node:process';
|
|
18
18
|
import { pathToFileURL } from 'node:url';
|
|
19
|
-
import { loadDocsConfig } from './load-docs-config.mjs';
|
|
20
19
|
|
|
21
20
|
const DEFAULT_HEAD_OPEN = '<!--shiso-default-head-->';
|
|
22
21
|
const DEFAULT_HEAD_CLOSE = '<!--/shiso-default-head-->';
|
|
@@ -32,12 +31,8 @@ if (!template.includes('<!--app-html-->')) {
|
|
|
32
31
|
);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const { render, getRoutes, getRedirects, getSitemapEntries, getMarkdownPages } = await import(
|
|
39
|
-
pathToFileURL(path.join(root, 'dist', 'server', 'entry-server.js')).href
|
|
40
|
-
);
|
|
34
|
+
const { render, getRoutes, getRedirects, getSitemapEntries, getMarkdownPages, docsHomeUrl } =
|
|
35
|
+
await import(pathToFileURL(path.join(root, 'dist', 'server', 'entry-server.js')).href);
|
|
41
36
|
|
|
42
37
|
/** Vite's `base`, normalized to "" or "/prefix". */
|
|
43
38
|
function readBase() {
|
|
@@ -52,12 +47,21 @@ function withBase(routePath) {
|
|
|
52
47
|
return `${base}${routePath}`.replace(/\/{2,}/g, '/') || '/';
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
function fillTemplate(head, html) {
|
|
50
|
+
function fillTemplate(head, html, htmlAttrs) {
|
|
56
51
|
// Per-page head tags supersede the site-level defaults injected at build time.
|
|
57
|
-
|
|
52
|
+
let output = head
|
|
58
53
|
? template.replace(new RegExp(`${DEFAULT_HEAD_OPEN}[\\s\\S]*?${DEFAULT_HEAD_CLOSE}`), '')
|
|
59
54
|
: template;
|
|
60
55
|
|
|
56
|
+
// Per-page document language and direction, replacing the template's own.
|
|
57
|
+
if (htmlAttrs) {
|
|
58
|
+
output = output.replace(/<html([^>]*)>/, (_match, attrs) => {
|
|
59
|
+
const kept = attrs.replace(/\s+lang="[^"]*"/, '').replace(/\s+dir="[^"]*"/, '');
|
|
60
|
+
const dir = htmlAttrs.dir === 'rtl' ? ' dir="rtl"' : '';
|
|
61
|
+
return `<html${kept} lang="${htmlAttrs.lang}"${dir}>`;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
return output.replace('<!--app-head-->', head).replace('<!--app-html-->', html);
|
|
62
66
|
}
|
|
63
67
|
|
|
@@ -75,15 +79,15 @@ function outputPathFor(routePath) {
|
|
|
75
79
|
const routes = getRoutes();
|
|
76
80
|
|
|
77
81
|
for (const route of routes) {
|
|
78
|
-
const { html, head } = render(route);
|
|
79
|
-
await writePage(outputPathFor(route), fillTemplate(head, html));
|
|
82
|
+
const { html, head, htmlAttrs } = render(route);
|
|
83
|
+
await writePage(outputPathFor(route), fillTemplate(head, html, htmlAttrs));
|
|
80
84
|
}
|
|
81
85
|
|
|
82
|
-
// Root entry. When
|
|
83
|
-
// alone is slow and SEO-hostile, so pair it
|
|
84
|
-
// immediate history-replacing navigation.
|
|
85
|
-
if (
|
|
86
|
-
const target = withBase(`${
|
|
86
|
+
// Root entry. When the default scope's landing page is not the root itself the
|
|
87
|
+
// root is a redirect; a meta refresh alone is slow and SEO-hostile, so pair it
|
|
88
|
+
// with a canonical link and an immediate history-replacing navigation.
|
|
89
|
+
if (docsHomeUrl && docsHomeUrl !== '/') {
|
|
90
|
+
const target = withBase(`${docsHomeUrl}/`);
|
|
87
91
|
|
|
88
92
|
await writePage(
|
|
89
93
|
path.join(clientDir, 'index.html'),
|
|
@@ -166,7 +170,10 @@ if (sitemapEntries.length) {
|
|
|
166
170
|
// 404 fallback renders the app shell so client routing can take over
|
|
167
171
|
// on hosts that serve 404.html for unknown paths (e.g. GitHub Pages).
|
|
168
172
|
const notFound = render('/404');
|
|
169
|
-
await writePage(
|
|
173
|
+
await writePage(
|
|
174
|
+
path.join(clientDir, '404.html'),
|
|
175
|
+
fillTemplate(notFound.head, notFound.html, notFound.htmlAttrs),
|
|
176
|
+
);
|
|
170
177
|
|
|
171
178
|
// Guard against a base/route mismatch silently producing unreachable files.
|
|
172
179
|
const stray = routes.filter(route => !withBase(route).startsWith(base || '/'));
|
package/src/App.tsx
CHANGED
|
@@ -11,8 +11,7 @@ import { CodeBlock } from '@/components/CodeBlock';
|
|
|
11
11
|
import * as docsComponents from '@/components/docs/index';
|
|
12
12
|
import { Layout } from '@/components/Layout';
|
|
13
13
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
14
|
-
import {
|
|
15
|
-
import { siteModel } from '@/lib/site-config';
|
|
14
|
+
import { docsHomeUrl, siteModel } from '@/lib/site-config';
|
|
16
15
|
import { DocPage } from '@/pages/DocPage';
|
|
17
16
|
|
|
18
17
|
const mdxComponents = {
|
|
@@ -26,9 +25,9 @@ export function App() {
|
|
|
26
25
|
<MDXProvider components={mdxComponents}>
|
|
27
26
|
<Layout site={siteModel}>
|
|
28
27
|
<Routes>
|
|
29
|
-
{/* When
|
|
30
|
-
{
|
|
31
|
-
<Route path="/" element={<Navigate to={
|
|
28
|
+
{/* When the default scope's landing page is the root there is nothing to redirect. */}
|
|
29
|
+
{docsHomeUrl !== '/' ? (
|
|
30
|
+
<Route path="/" element={<Navigate to={docsHomeUrl} replace />} />
|
|
32
31
|
) : null}
|
|
33
32
|
<Route path="*" element={<DocPage site={siteModel} />} />
|
|
34
33
|
</Routes>
|
|
@@ -2,6 +2,9 @@ import { Link } from 'react-router';
|
|
|
2
2
|
import { ContextualMenu } from '@/components/ContextualMenu';
|
|
3
3
|
import { ChevronRight } from '@/components/icons';
|
|
4
4
|
import { getLastModified } from '@/lib/content';
|
|
5
|
+
import { getScopeForPage } from '@/lib/docs-config';
|
|
6
|
+
import { resolveLocale } from '@/lib/locale';
|
|
7
|
+
import { docsSite } from '@/lib/site-config';
|
|
5
8
|
import { resolveContextualOptions } from '@/lib/site-model';
|
|
6
9
|
import type { DocModule, NormalizedDocsPage, SiteModel } from '@/lib/types';
|
|
7
10
|
|
|
@@ -12,7 +15,8 @@ export interface DocContentProps {
|
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function DocContent({ page, doc, site }: DocContentProps) {
|
|
15
|
-
|
|
18
|
+
// Prev/next paging never crosses a version or language boundary.
|
|
19
|
+
const pagerPages = getScopeForPage(docsSite, page).docs.pages.filter(item => !item.hidden);
|
|
16
20
|
const pageIndex = pagerPages.findIndex(item => item.slug === page.slug);
|
|
17
21
|
const prev = pageIndex > 0 ? pagerPages[pageIndex - 1] : undefined;
|
|
18
22
|
const next = pageIndex >= 0 ? pagerPages[pageIndex + 1] : undefined;
|
|
@@ -30,7 +34,8 @@ export function DocContent({ page, doc, site }: DocContentProps) {
|
|
|
30
34
|
? [...new Set([page.tabLabel, page.section])].filter(Boolean).join(' / ')
|
|
31
35
|
: page.section;
|
|
32
36
|
const contextualOptions = resolveContextualOptions(site.contextualOptions, page, site.labels);
|
|
33
|
-
|
|
37
|
+
// Dates follow the page's language when it is a valid locale code.
|
|
38
|
+
const dateFormat = new Intl.DateTimeFormat(resolveLocale(page.language, site.locale), {
|
|
34
39
|
dateStyle: 'medium',
|
|
35
40
|
timeZone: 'UTC',
|
|
36
41
|
});
|