@umami/shiso 1.4.0 → 1.6.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 +17 -0
- package/bin/shiso.mjs +9 -1
- package/config.js +8 -0
- package/dist/chunks/App.js +255 -73
- package/dist/chunks/local.js +1 -62
- package/dist/chunks/search.js +64 -0
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +16 -5
- package/docs.schema.json +29 -26
- package/package.json +8 -2
- package/scripts/build-runtime.mjs +1 -0
- package/scripts/generate-search-index.mjs +4 -3
- package/scripts/load-shiso-config.mjs +167 -0
- package/scripts/prerender.mjs +4 -2
- package/scripts/validate-config.mjs +23 -10
- package/scripts/vite-docs-config.mjs +52 -13
- package/src/App.tsx +13 -6
- package/src/components/DocContent.tsx +5 -3
- package/src/components/Header.tsx +61 -24
- package/src/components/Search.tsx +14 -3
- package/src/components/TopNav.tsx +9 -7
- package/src/components/docs/Button.tsx +67 -0
- package/src/components/docs/PropertiesTable.tsx +4 -4
- package/src/components/docs/index.ts +1 -0
- package/src/declarations.d.ts +7 -0
- package/src/entry-server.tsx +20 -3
- package/src/lib/content.ts +10 -2
- package/src/lib/head.ts +15 -7
- package/src/lib/locale.ts +1 -1
- package/src/lib/paths.ts +11 -7
- package/src/lib/site-config.ts +21 -3
- package/src/lib/site-model.ts +7 -2
- package/src/lib/standalone-pages.ts +129 -0
- package/src/lib/types.ts +33 -3
- package/src/pages/StandalonePage.tsx +42 -0
- package/src/styles/global.css +19 -1
- package/src/styles/tokens.css +1 -1
- package/types/config.d.ts +19 -0
- package/vite.config.ts +58 -18
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { loadDocsConfig } from './load-docs-config.mjs';
|
|
3
|
+
import { SHISO_CONFIG_FILES, loadShisoConfig } from './load-shiso-config.mjs';
|
|
3
4
|
|
|
4
5
|
export const VIRTUAL_DOCS_CONFIG_ID = 'virtual:shiso-docs-config';
|
|
6
|
+
export const VIRTUAL_SHISO_CONFIG_ID = 'virtual:shiso-config';
|
|
5
7
|
const RESOLVED_DOCS_CONFIG_ID = `\0${VIRTUAL_DOCS_CONFIG_ID}`;
|
|
8
|
+
const RESOLVED_SHISO_CONFIG_ID = `\0${VIRTUAL_SHISO_CONFIG_ID}`;
|
|
6
9
|
|
|
7
10
|
function renderConfigModule(config) {
|
|
8
11
|
return `export default ${JSON.stringify(config)};`;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
/**
|
|
12
|
-
* Creates the single
|
|
13
|
-
* modules.
|
|
14
|
-
* bundle
|
|
15
|
+
* Creates the single config state shared by a Vite build and application
|
|
16
|
+
* modules. Two virtual modules keep Node-only file loading out of the browser
|
|
17
|
+
* bundle: `virtual:shiso-docs-config` carries docs.json (with `$ref`s
|
|
18
|
+
* resolved) and `virtual:shiso-config` carries the resolved shiso.config.*
|
|
19
|
+
* options with defaults already applied.
|
|
15
20
|
*/
|
|
16
21
|
export async function createDocsConfigModule({
|
|
17
22
|
root = process.cwd(),
|
|
@@ -19,35 +24,69 @@ export async function createDocsConfigModule({
|
|
|
19
24
|
} = {}) {
|
|
20
25
|
const options = { root, configFile };
|
|
21
26
|
let loaded = await loadDocsConfig(options);
|
|
27
|
+
let loadedShiso = await loadShisoConfig({ root });
|
|
28
|
+
// Candidate names are matched in handleHotUpdate (the dev watcher already
|
|
29
|
+
// covers the project root), so creating shiso.config.ts is picked up live.
|
|
30
|
+
// Only existing files may go through addWatchFile: Vite's dev import
|
|
31
|
+
// analysis re-resolves watched files from a load hook and errors on paths
|
|
32
|
+
// that do not exist.
|
|
33
|
+
const shisoCandidatePaths = SHISO_CONFIG_FILES.map(name => path.resolve(root, name));
|
|
22
34
|
|
|
23
35
|
return {
|
|
24
36
|
getConfig: () => loaded.config,
|
|
25
|
-
|
|
37
|
+
getShisoConfig: () => loadedShiso.config,
|
|
38
|
+
getSourcePaths: () => [...loaded.sourcePaths, ...loadedShiso.sourcePaths],
|
|
26
39
|
sourcePath: loaded.sourcePath,
|
|
40
|
+
shisoSourcePath: loadedShiso.sourcePath,
|
|
27
41
|
plugin: {
|
|
28
42
|
name: 'shiso-docs-config',
|
|
29
43
|
enforce: 'pre',
|
|
30
44
|
resolveId(id) {
|
|
31
|
-
|
|
45
|
+
if (id === VIRTUAL_DOCS_CONFIG_ID) {
|
|
46
|
+
return RESOLVED_DOCS_CONFIG_ID;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (id === VIRTUAL_SHISO_CONFIG_ID) {
|
|
50
|
+
return RESOLVED_SHISO_CONFIG_ID;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return undefined;
|
|
32
54
|
},
|
|
33
55
|
load(id) {
|
|
34
|
-
if (id
|
|
35
|
-
|
|
56
|
+
if (id === RESOLVED_DOCS_CONFIG_ID) {
|
|
57
|
+
for (const sourcePath of loaded.sourcePaths) {
|
|
58
|
+
this.addWatchFile(sourcePath);
|
|
59
|
+
}
|
|
60
|
+
return renderConfigModule(loaded.config);
|
|
36
61
|
}
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
if (id === RESOLVED_SHISO_CONFIG_ID) {
|
|
64
|
+
for (const sourcePath of loadedShiso.sourcePaths) {
|
|
65
|
+
this.addWatchFile(sourcePath);
|
|
66
|
+
}
|
|
67
|
+
return renderConfigModule(loadedShiso.config);
|
|
40
68
|
}
|
|
41
|
-
|
|
69
|
+
|
|
70
|
+
return undefined;
|
|
42
71
|
},
|
|
43
72
|
async handleHotUpdate(context) {
|
|
44
|
-
|
|
73
|
+
const changedPath = path.resolve(context.file);
|
|
74
|
+
const isDocsSource = loaded.sourcePaths.includes(changedPath);
|
|
75
|
+
const isShisoSource = shisoCandidatePaths.includes(changedPath);
|
|
76
|
+
|
|
77
|
+
if (!isDocsSource && !isShisoSource) {
|
|
45
78
|
return;
|
|
46
79
|
}
|
|
47
80
|
|
|
48
|
-
|
|
81
|
+
const resolvedId = isDocsSource ? RESOLVED_DOCS_CONFIG_ID : RESOLVED_SHISO_CONFIG_ID;
|
|
82
|
+
|
|
83
|
+
if (isDocsSource) {
|
|
84
|
+
loaded = await loadDocsConfig(options);
|
|
85
|
+
} else {
|
|
86
|
+
loadedShiso = await loadShisoConfig({ root });
|
|
87
|
+
}
|
|
49
88
|
|
|
50
|
-
const configModule = context.server.moduleGraph.getModuleById(
|
|
89
|
+
const configModule = context.server.moduleGraph.getModuleById(resolvedId);
|
|
51
90
|
|
|
52
91
|
if (configModule) {
|
|
53
92
|
context.server.moduleGraph.invalidateModule(configModule);
|
package/src/App.tsx
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import '@fontsource/inter/
|
|
2
|
-
import '@fontsource/inter/700.css';
|
|
3
|
-
import '@fontsource/inter/800.css';
|
|
1
|
+
import '@fontsource-variable/inter/index.css';
|
|
4
2
|
import '@fontsource/jetbrains-mono/400.css';
|
|
5
3
|
import 'highlight.js/styles/github.css';
|
|
6
4
|
import '@umami/shiso/styles.css';
|
|
@@ -11,8 +9,9 @@ import { CodeBlock } from '@/components/CodeBlock';
|
|
|
11
9
|
import * as docsComponents from '@/components/docs/index';
|
|
12
10
|
import { Layout } from '@/components/Layout';
|
|
13
11
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
|
14
|
-
import { docsHomeUrl, siteModel } from '@/lib/site-config';
|
|
12
|
+
import { docsHomeUrl, hasRootStandalonePage, siteModel, standalonePages } from '@/lib/site-config';
|
|
15
13
|
import { DocPage } from '@/pages/DocPage';
|
|
14
|
+
import { StandalonePageView } from '@/pages/StandalonePage';
|
|
16
15
|
|
|
17
16
|
const mdxComponents = {
|
|
18
17
|
...docsComponents,
|
|
@@ -26,8 +25,16 @@ export function App() {
|
|
|
26
25
|
<MDXProvider components={mdxComponents}>
|
|
27
26
|
<Layout site={siteModel}>
|
|
28
27
|
<Routes>
|
|
29
|
-
{
|
|
30
|
-
|
|
28
|
+
{standalonePages.map(page => (
|
|
29
|
+
<Route
|
|
30
|
+
key={page.path}
|
|
31
|
+
path={page.path}
|
|
32
|
+
element={<StandalonePageView page={page} site={siteModel} />}
|
|
33
|
+
/>
|
|
34
|
+
))}
|
|
35
|
+
{/* When the default scope's landing page is the root, or a
|
|
36
|
+
standalone page owns "/", there is nothing to redirect. */}
|
|
37
|
+
{docsHomeUrl !== '/' && !hasRootStandalonePage ? (
|
|
31
38
|
<Route path="/" element={<Navigate to={docsHomeUrl} replace />} />
|
|
32
39
|
) : null}
|
|
33
40
|
<Route path="*" element={<DocPage site={siteModel} />} />
|
|
@@ -101,16 +101,18 @@ export function DocContent({ page, doc, site }: DocContentProps) {
|
|
|
101
101
|
|
|
102
102
|
return (
|
|
103
103
|
<article className="min-w-0 grow" {...pagefindAttrs}>
|
|
104
|
-
{eyebrow && <div className="text-sm font-
|
|
104
|
+
{eyebrow && <div className="text-sm font-medium text-primary">{eyebrow}</div>}
|
|
105
105
|
<div className="flex items-start justify-between gap-4">
|
|
106
106
|
{title && (
|
|
107
|
-
<h1 className="mt-2 text-4xl text-foreground leading-[1.2] tracking-[-0.03em] [font-family:var(--font-heading)] [font-weight:var(--font-heading-weight,
|
|
107
|
+
<h1 className="mt-2 text-4xl text-foreground leading-[1.2] tracking-[-0.03em] [font-family:var(--font-heading)] [font-weight:var(--font-heading-weight,700)]">
|
|
108
108
|
{title}
|
|
109
109
|
</h1>
|
|
110
110
|
)}
|
|
111
111
|
<ContextualMenu options={contextualOptions} labels={site.labels} />
|
|
112
112
|
</div>
|
|
113
|
-
{description &&
|
|
113
|
+
{description && (
|
|
114
|
+
<p className="mt-3 mb-8 text-lg text-muted-foreground leading-relaxed">{description}</p>
|
|
115
|
+
)}
|
|
114
116
|
<div className="docs-markdown">
|
|
115
117
|
<Content />
|
|
116
118
|
</div>
|
|
@@ -1,31 +1,58 @@
|
|
|
1
|
-
import { useLocation } from 'react-router';
|
|
1
|
+
import { Link, useLocation } from 'react-router';
|
|
2
2
|
import { ConfiguredIcon } from '@/components/ConfiguredIcon';
|
|
3
3
|
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
|
|
4
4
|
import { Search } from '@/components/Search';
|
|
5
5
|
import { ThemeToggle } from '@/components/ThemeToggle';
|
|
6
6
|
import { TopNav } from '@/components/TopNav';
|
|
7
7
|
import { VersionSwitcher } from '@/components/VersionSwitcher';
|
|
8
|
-
import {
|
|
8
|
+
import { isExternalHref } from '@/lib/paths';
|
|
9
|
+
import { docsHomeUrl, getScopeByPathname, hasRootStandalonePage } from '@/lib/site-config';
|
|
9
10
|
import type { NormalizedLink, SiteModel } from '@/lib/types';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Header hrefs come from config, so they may point inside the site or off it.
|
|
14
|
+
* In-app routes go through react-router; anything external (or explicitly
|
|
15
|
+
* opened in a new tab) stays a plain anchor and triggers a document load.
|
|
16
|
+
*/
|
|
17
|
+
function isRoutedHref(href: string, target?: string): boolean {
|
|
18
|
+
return href.startsWith('/') && !isExternalHref(href) && target !== '_blank';
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
function NavbarLinkItem({ link, primary = false }: { link: NormalizedLink; primary?: boolean }) {
|
|
12
22
|
const iconOnly = !link.label;
|
|
13
23
|
const accessibleLabel = link.ariaLabel || link.icon || link.href;
|
|
14
24
|
|
|
25
|
+
const className = primary
|
|
26
|
+
? `ml-1 inline-flex items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground hover:opacity-90 ${iconOnly ? 'size-8' : 'gap-1.5 px-3.5 py-1.5'}`
|
|
27
|
+
: `inline-flex items-center rounded-md text-sm font-medium text-foreground hover:bg-accent hover:text-foreground ${iconOnly ? 'size-8 justify-center' : 'gap-1.5 px-2.5 py-1.5'}`;
|
|
28
|
+
const content = (
|
|
29
|
+
<>
|
|
30
|
+
<ConfiguredIcon icon={link.icon} />
|
|
31
|
+
{link.label}
|
|
32
|
+
</>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (isRoutedHref(link.href, link.target)) {
|
|
36
|
+
return (
|
|
37
|
+
<Link
|
|
38
|
+
to={link.href}
|
|
39
|
+
className={className}
|
|
40
|
+
aria-label={iconOnly ? accessibleLabel : undefined}
|
|
41
|
+
>
|
|
42
|
+
{content}
|
|
43
|
+
</Link>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
return (
|
|
16
48
|
<a
|
|
17
49
|
href={link.href}
|
|
18
|
-
className={
|
|
19
|
-
primary
|
|
20
|
-
? `ml-1 inline-flex items-center justify-center rounded-full bg-primary text-sm font-semibold text-primary-foreground hover:opacity-90 ${iconOnly ? 'size-8' : 'gap-1.5 px-3.5 py-1.5'}`
|
|
21
|
-
: `inline-flex items-center rounded-md text-sm font-medium text-foreground hover:bg-accent hover:text-foreground ${iconOnly ? 'size-8 justify-center' : 'gap-1.5 px-2.5 py-1.5'}`
|
|
22
|
-
}
|
|
50
|
+
className={className}
|
|
23
51
|
target={link.target}
|
|
24
52
|
rel={link.target === '_blank' ? 'noreferrer' : undefined}
|
|
25
53
|
aria-label={iconOnly ? accessibleLabel : undefined}
|
|
26
54
|
>
|
|
27
|
-
|
|
28
|
-
{link.label}
|
|
55
|
+
{content}
|
|
29
56
|
</a>
|
|
30
57
|
);
|
|
31
58
|
}
|
|
@@ -35,28 +62,38 @@ export function Header({ site }: { site: SiteModel }) {
|
|
|
35
62
|
const { pathname } = useLocation();
|
|
36
63
|
// The header renders the navigation of whichever scope owns the current page.
|
|
37
64
|
const docs = getScopeByPathname(pathname).docs;
|
|
38
|
-
|
|
65
|
+
// The brand links to the standalone home page when one owns "/".
|
|
66
|
+
const brandHref = logo?.href || (hasRootStandalonePage ? '/' : docsHomeUrl);
|
|
39
67
|
const hasBrand = !!name || !!logo?.light || !!logo?.dark;
|
|
68
|
+
const brandClassName =
|
|
69
|
+
'inline-flex items-center gap-2 text-xl font-bold text-foreground tracking-[-0.03em]';
|
|
70
|
+
const brandContent = (
|
|
71
|
+
<>
|
|
72
|
+
{logo?.light ? <img src={logo.light} alt="" className="h-6 w-auto dark:hidden" /> : null}
|
|
73
|
+
{logo?.dark ? <img src={logo.dark} alt="" className="hidden h-6 w-auto dark:block" /> : null}
|
|
74
|
+
{name ? <span>{name}</span> : null}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
40
77
|
|
|
41
78
|
return (
|
|
42
79
|
<header className="sticky top-0 z-50 h-[var(--header-height)] shrink-0 border-border border-b bg-[color-mix(in_srgb,var(--background)_92%,transparent)] backdrop-blur-md">
|
|
43
80
|
<div className="mx-auto grid h-full max-w-[1600px] grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-5">
|
|
44
81
|
<div className="flex min-w-0 items-center gap-5 justify-self-start">
|
|
45
82
|
{hasBrand ? (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
isRoutedHref(brandHref, logo?.target) ? (
|
|
84
|
+
<Link to={brandHref} className={brandClassName}>
|
|
85
|
+
{brandContent}
|
|
86
|
+
</Link>
|
|
87
|
+
) : (
|
|
88
|
+
<a
|
|
89
|
+
href={brandHref}
|
|
90
|
+
target={logo?.target}
|
|
91
|
+
rel={logo?.target === '_blank' ? 'noreferrer' : undefined}
|
|
92
|
+
className={brandClassName}
|
|
93
|
+
>
|
|
94
|
+
{brandContent}
|
|
95
|
+
</a>
|
|
96
|
+
)
|
|
60
97
|
) : null}
|
|
61
98
|
<div className="hidden items-center gap-2 lg:flex">
|
|
62
99
|
<VersionSwitcher />
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
CommandList,
|
|
12
12
|
} from '@/components/ui/command';
|
|
13
13
|
import { Dialog, DialogContent, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
14
|
-
import type
|
|
14
|
+
import { highlightTerms, type SearchResult } from '@/lib/search';
|
|
15
15
|
import type { ResolvedSearchConfig } from '@/lib/search/config';
|
|
16
16
|
import { resolveSearchProvider, type SearchProvider } from '@/lib/search/provider';
|
|
17
17
|
import { getScopeByPathname } from '@/lib/site-config';
|
|
@@ -44,6 +44,12 @@ function renderSnippet(snippet: string) {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function renderWithQueryHighlight(text: string, query: string) {
|
|
48
|
+
const terms = query.trim().split(/\s+/).filter(Boolean);
|
|
49
|
+
if (!terms.length) return text;
|
|
50
|
+
return renderSnippet(highlightTerms(text, terms));
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/**
|
|
48
54
|
* Provider-neutral search dialog. The selected provider and its index or
|
|
49
55
|
* client are loaded on demand, so search stays out of the initial bundle.
|
|
@@ -224,8 +230,13 @@ export function Search({ config, labels }: { config: ResolvedSearchConfig; label
|
|
|
224
230
|
onSelect={() => select(result)}
|
|
225
231
|
>
|
|
226
232
|
<div className="text-[0.9rem] font-semibold text-foreground">
|
|
227
|
-
{result.page}
|
|
228
|
-
{result.heading ?
|
|
233
|
+
{renderWithQueryHighlight(result.page, query)}
|
|
234
|
+
{result.heading ? (
|
|
235
|
+
<>
|
|
236
|
+
{' › '}
|
|
237
|
+
{renderWithQueryHighlight(result.heading, query)}
|
|
238
|
+
</>
|
|
239
|
+
) : null}
|
|
229
240
|
</div>
|
|
230
241
|
{result.snippet && (
|
|
231
242
|
<div className="mt-[0.15rem] line-clamp-2 text-[0.8rem] text-muted-foreground">
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { cn } from '@/lib/utils';
|
|
2
1
|
import { Link, useLocation, useNavigate } from 'react-router';
|
|
3
2
|
import { ConfiguredIcon } from '@/components/ConfiguredIcon';
|
|
4
3
|
import { ChevronRight } from '@/components/icons';
|
|
@@ -8,7 +7,9 @@ import {
|
|
|
8
7
|
DropdownMenuItem,
|
|
9
8
|
DropdownMenuTrigger,
|
|
10
9
|
} from '@/components/ui/dropdown-menu';
|
|
10
|
+
import { getStandalonePage } from '@/lib/site-config';
|
|
11
11
|
import type { DocsTab, LinkTarget, NavNode, NormalizedDocsConfig } from '@/lib/types';
|
|
12
|
+
import { cn } from '@/lib/utils';
|
|
12
13
|
|
|
13
14
|
interface MenuLink {
|
|
14
15
|
label: string;
|
|
@@ -55,12 +56,13 @@ export function TopNav({ docs, label }: { docs: NormalizedDocsConfig; label: str
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const page = docs.pages.find(item => item.url === pathname);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
const matchedTabId = [...tabs]
|
|
60
|
+
.sort((a, b) => b.url.length - a.url.length)
|
|
61
|
+
.find(tab => pathname === tab.url || pathname.startsWith(`${tab.url}/`))?.id;
|
|
62
|
+
// Standalone pages live outside the docs tree, so no tab owns them. Only
|
|
63
|
+
// unmatched *docs* routes fall back to highlighting the first tab.
|
|
64
|
+
const fallbackTabId = getStandalonePage(pathname) ? undefined : tabs[0]?.id;
|
|
65
|
+
const selected = page?.tabId || matchedTabId || fallbackTabId;
|
|
64
66
|
const tabClass = (tab: DocsTab) =>
|
|
65
67
|
cn(
|
|
66
68
|
'flex h-full items-center gap-1 whitespace-nowrap border-transparent border-b-2 font-medium',
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Link } from 'react-router';
|
|
3
|
+
import { Button as ButtonPrimitive, buttonVariants } from '@/components/ui/button';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import { resolveIcon } from './utils';
|
|
6
|
+
|
|
7
|
+
export interface ButtonProps {
|
|
8
|
+
href?: string;
|
|
9
|
+
variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'destructive' | 'link';
|
|
10
|
+
size?: 'default' | 'xs' | 'sm' | 'lg';
|
|
11
|
+
icon?: ReactNode | string;
|
|
12
|
+
className?: string;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* MDX-facing button. Renders a link when `href` is set (internal routes go
|
|
18
|
+
* through react-router), otherwise a plain button element.
|
|
19
|
+
*/
|
|
20
|
+
export function Button({
|
|
21
|
+
href,
|
|
22
|
+
variant = 'default',
|
|
23
|
+
size = 'default',
|
|
24
|
+
icon,
|
|
25
|
+
className,
|
|
26
|
+
children,
|
|
27
|
+
}: ButtonProps) {
|
|
28
|
+
const resolvedIcon = resolveIcon(icon, 16);
|
|
29
|
+
const content = (
|
|
30
|
+
<>
|
|
31
|
+
{resolvedIcon}
|
|
32
|
+
{children}
|
|
33
|
+
</>
|
|
34
|
+
);
|
|
35
|
+
// MDX wraps block-level children in <p>; strip its margins so the label
|
|
36
|
+
// stays centered against the icon.
|
|
37
|
+
const baseClassName = cn(resolvedIcon ? 'gap-2' : '', '[&_p]:m-0', className);
|
|
38
|
+
|
|
39
|
+
if (!href) {
|
|
40
|
+
return (
|
|
41
|
+
<ButtonPrimitive variant={variant} size={size} className={baseClassName}>
|
|
42
|
+
{content}
|
|
43
|
+
</ButtonPrimitive>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const linkClassName = cn(
|
|
48
|
+
buttonVariants({ variant, size }),
|
|
49
|
+
'no-underline hover:no-underline active:no-underline',
|
|
50
|
+
baseClassName,
|
|
51
|
+
);
|
|
52
|
+
const external = /^https?:\/\//i.test(href);
|
|
53
|
+
|
|
54
|
+
if (external) {
|
|
55
|
+
return (
|
|
56
|
+
<a href={href} className={linkClassName} target="_blank" rel="noreferrer">
|
|
57
|
+
{content}
|
|
58
|
+
</a>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Link to={href} className={linkClassName}>
|
|
64
|
+
{content}
|
|
65
|
+
</Link>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -24,16 +24,16 @@ export function PropertiesTable({ children }: PropertiesTableProps) {
|
|
|
24
24
|
|
|
25
25
|
return (
|
|
26
26
|
<div className="my-4 overflow-x-auto rounded-lg border border-border px-6 py-2">
|
|
27
|
-
<table className="m-0 min-w-[40rem] table-fixed border-collapse">
|
|
27
|
+
<table className="m-0 min-w-[40rem] table-fixed border-collapse border-0">
|
|
28
28
|
<thead>
|
|
29
29
|
<tr>
|
|
30
|
-
<th className="w-1/
|
|
30
|
+
<th className="w-1/5 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
|
|
31
31
|
Name
|
|
32
32
|
</th>
|
|
33
|
-
<th className="w-1/
|
|
33
|
+
<th className="w-1/4 border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 pr-6 text-left text-sm font-semibold text-foreground">
|
|
34
34
|
Type
|
|
35
35
|
</th>
|
|
36
|
-
<th className="
|
|
36
|
+
<th className="border-x-0 border-t-0 border-b border-border bg-transparent px-0 py-2 text-left text-sm font-semibold text-foreground">
|
|
37
37
|
Description
|
|
38
38
|
</th>
|
|
39
39
|
</tr>
|
package/src/declarations.d.ts
CHANGED
|
@@ -7,3 +7,10 @@ declare module 'virtual:shiso-docs-config' {
|
|
|
7
7
|
const config: DocsConfig;
|
|
8
8
|
export default config;
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
declare module 'virtual:shiso-config' {
|
|
12
|
+
import type { ResolvedShisoConfig } from '@/lib/types';
|
|
13
|
+
|
|
14
|
+
const config: ResolvedShisoConfig;
|
|
15
|
+
export default config;
|
|
16
|
+
}
|
package/src/entry-server.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getRedirects,
|
|
13
13
|
getSeo,
|
|
14
14
|
siteName,
|
|
15
|
+
standalonePages,
|
|
15
16
|
} from '@/lib/site-config';
|
|
16
17
|
|
|
17
18
|
export interface RenderResult {
|
|
@@ -28,7 +29,7 @@ export interface SitemapEntry {
|
|
|
28
29
|
|
|
29
30
|
/** Base-relative routes for every scope. The prerenderer prepends the deploy base itself. */
|
|
30
31
|
export function getRoutes(): string[] {
|
|
31
|
-
return docsSite.pages.map(page => page.url);
|
|
32
|
+
return [...docsSite.pages.map(page => page.url), ...standalonePages.map(page => page.path)];
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** Redirect rules with exact-match sources, for static redirect pages. */
|
|
@@ -41,12 +42,15 @@ export { getRedirects };
|
|
|
41
42
|
* "/content/docs/index.mdx", resolved against the project root.
|
|
42
43
|
*/
|
|
43
44
|
export function getMarkdownPages(): { route: string; filePath: string }[] {
|
|
44
|
-
return
|
|
45
|
+
return [
|
|
46
|
+
...docsSite.pages.map(page => ({ route: page.url, filePath: page.filePath })),
|
|
47
|
+
...standalonePages.map(page => ({ route: page.path, filePath: page.filePath })),
|
|
48
|
+
];
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Absolute URLs for the sitemap, honoring `seo.indexing` and per-page
|
|
49
|
-
* noindex. Empty when
|
|
53
|
+
* noindex. Empty when the shiso.config `siteUrl` is not configured, since a sitemap
|
|
50
54
|
* of relative URLs is invalid.
|
|
51
55
|
*/
|
|
52
56
|
export function getSitemapEntries(): SitemapEntry[] {
|
|
@@ -71,6 +75,19 @@ export function getSitemapEntries(): SitemapEntry[] {
|
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
// Standalone pages are always navigable; only frontmatter noindex opts out.
|
|
79
|
+
for (const page of standalonePages) {
|
|
80
|
+
if (getDocModule(page.filePath)?.frontmatter?.noindex === true) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const url = toAbsoluteUrl(page.path);
|
|
85
|
+
|
|
86
|
+
if (url) {
|
|
87
|
+
entries.push({ url, lastmod: getLastModified(page.filePath) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
return entries;
|
|
75
92
|
}
|
|
76
93
|
|
package/src/lib/content.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LAST_MODIFIED } from '@/generated/last-modified';
|
|
2
|
-
import { CONTENT_DIR } from '@/lib/paths';
|
|
2
|
+
import { CONTENT_DIR, PAGES_DIR } from '@/lib/paths';
|
|
3
3
|
import type { DocModule } from '@/lib/types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -12,7 +12,7 @@ import type { DocModule } from '@/lib/types';
|
|
|
12
12
|
* without Suspense, at the cost of bundling all pages together.
|
|
13
13
|
*
|
|
14
14
|
* The glob pattern must be a literal for Vite to statically analyze it, so it
|
|
15
|
-
* covers all of `content/` and the configured
|
|
15
|
+
* covers all of `content/` and the configured shiso.config `contentDir` is applied at
|
|
16
16
|
* lookup time instead. That also lets later versioned/localized content roots
|
|
17
17
|
* (`content/v2`, `content/es`) work without touching this glob.
|
|
18
18
|
*/
|
|
@@ -30,6 +30,14 @@ export function resolveDocFile(fileSlug: string, contentDir = CONTENT_DIR): stri
|
|
|
30
30
|
return candidates.find(candidate => candidate in docModules);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Resolves a standalone page slug (docs.json `pages[].page`) to a module key
|
|
35
|
+
* under the fixed content/pages root.
|
|
36
|
+
*/
|
|
37
|
+
export function resolvePageFile(fileSlug: string): string | undefined {
|
|
38
|
+
return resolveDocFile(fileSlug, PAGES_DIR);
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
export function getDocModule(filePath: string): DocModule | undefined {
|
|
34
42
|
return docModules[filePath];
|
|
35
43
|
}
|
package/src/lib/head.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
getPageTitle,
|
|
9
9
|
getScopeByPathname,
|
|
10
10
|
getSeo,
|
|
11
|
+
getStandalonePage,
|
|
11
12
|
showTimestamp,
|
|
12
13
|
siteConfig,
|
|
13
14
|
siteModel,
|
|
@@ -49,18 +50,25 @@ function escapeJsonLd(value: string): string {
|
|
|
49
50
|
|
|
50
51
|
export function buildHead(pathname: string): HeadTag[] {
|
|
51
52
|
const page = getPageByPathname(pathname);
|
|
52
|
-
const
|
|
53
|
+
const standalone = page ? null : getStandalonePage(pathname);
|
|
54
|
+
const filePath = page?.filePath || standalone?.filePath;
|
|
55
|
+
const doc = filePath ? getDocModule(filePath) : undefined;
|
|
53
56
|
const frontmatter = doc?.frontmatter;
|
|
54
57
|
|
|
55
58
|
const seo = getSeo();
|
|
56
|
-
const pageTitle = frontmatter?.title || page?.label;
|
|
59
|
+
const pageTitle = frontmatter?.title || standalone?.title || page?.label;
|
|
57
60
|
const title = getPageTitle(pageTitle);
|
|
58
61
|
const description = frontmatter?.description || siteConfig.description;
|
|
59
|
-
const canonical = page
|
|
62
|
+
const canonical = page
|
|
63
|
+
? toAbsoluteUrl(page.url)
|
|
64
|
+
: standalone
|
|
65
|
+
? toAbsoluteUrl(standalone.path)
|
|
66
|
+
: undefined;
|
|
60
67
|
// `seo.indexing: "all"` opts hidden pages (and hidden versions/languages)
|
|
61
68
|
// into the index; explicit per-page `noindex` frontmatter always wins.
|
|
62
69
|
const hidden = !!page && (!!page.hidden || !!getScopeByPathname(pathname).hidden);
|
|
63
|
-
const noindex =
|
|
70
|
+
const noindex =
|
|
71
|
+
(!page && !standalone) || frontmatter?.noindex === true || (hidden && seo.indexing !== 'all');
|
|
64
72
|
|
|
65
73
|
const tags: HeadTag[] = [{ tag: 'title', children: title }];
|
|
66
74
|
|
|
@@ -76,9 +84,9 @@ export function buildHead(pathname: string): HeadTag[] {
|
|
|
76
84
|
tags.push({ tag: 'meta', attrs: { name: 'robots', content: 'noindex' } });
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
// Open Graph
|
|
87
|
+
// Open Graph. Standalone pages (home, landing) are websites, not articles.
|
|
80
88
|
tags.push(
|
|
81
|
-
{ tag: 'meta', attrs: { property: 'og:type', content: 'article' } },
|
|
89
|
+
{ tag: 'meta', attrs: { property: 'og:type', content: standalone ? 'website' : 'article' } },
|
|
82
90
|
{ tag: 'meta', attrs: { property: 'og:title', content: title } },
|
|
83
91
|
);
|
|
84
92
|
|
|
@@ -106,7 +114,7 @@ export function buildHead(pathname: string): HeadTag[] {
|
|
|
106
114
|
|
|
107
115
|
// Last-modified time, when the timestamp feature is on for this page.
|
|
108
116
|
const lastModified =
|
|
109
|
-
|
|
117
|
+
filePath && showTimestamp(frontmatter?.timestamp) ? getLastModified(filePath) : undefined;
|
|
110
118
|
|
|
111
119
|
if (lastModified) {
|
|
112
120
|
tags.push({ tag: 'meta', attrs: { property: 'article:modified_time', content: lastModified } });
|
package/src/lib/locale.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function isValidLocale(value: string | undefined): value is string {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Locale for a page: its scope's language code when valid, then the
|
|
21
|
-
* site-wide
|
|
21
|
+
* site-wide shiso.config `locale`, then en-US.
|
|
22
22
|
*/
|
|
23
23
|
export function resolveLocale(language: string | undefined, fallback: string | undefined): string {
|
|
24
24
|
if (isValidLocale(language)) {
|