Skip to content

Commit 0c53076

Browse files
authored
feat(site): remove style from urls (#378)
1 parent 9ed608d commit 0c53076

26 files changed

Lines changed: 504 additions & 1141 deletions

site/CLAUDE.md

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ Documentation is generated for **multiple framework and style combinations** fro
145145

146146
**URL pattern:**
147147
```
148-
/docs/framework/{framework}/style/{style}/{...slug}/
148+
/docs/framework/{framework}/{...slug}/
149149
```
150150

151+
**Style handling:** Style is a **client-side preference** stored in localStorage per-framework (`vjs_docs_style_html`, `vjs_docs_style_react`). The `StyleInit.astro` component reads localStorage before paint and sets `html[data-style]`. CSS rules control content visibility via `[data-for-style]` attributes on `<StyleCase>` wrapped content.
152+
151153
**Example:**
152154
- `src/content/docs/how-to/installation.mdx` generates:
153-
- `/docs/framework/html/style/css/how-to/installation/`
154-
- `/docs/framework/react/style/css/how-to/installation/`
155+
- `/docs/framework/html/how-to/installation/`
156+
- `/docs/framework/react/how-to/installation/`
155157

156158
### Content Restriction Mechanisms
157159

@@ -171,21 +173,25 @@ Use `<FrameworkCase>` or `<StyleCase>` components to show framework/style-specif
171173

172174
**2. In sidebar config (`src/docs.config.ts`):**
173175

176+
Restrict entire guides to specific frameworks:
177+
174178
```ts
175179
const sidebar: Sidebar = [
176180
{
177181
sidebarLabel: 'Getting started',
178182
contents: [
179-
{ slug: 'how-to/installation' }, // Available to all
183+
{ slug: 'how-to/installation' }, // Available to all frameworks
180184
{
181185
slug: 'how-to/react-hooks',
182-
frameworks: ['react'] // Only for React
186+
frameworks: ['react'] // Only visible when viewing React docs
183187
},
184188
],
185189
},
186190
];
187191
```
188192

193+
**Note:** Style restrictions on sidebar items are no longer supported. All docs are visible to all styles; use `<StyleCase>` within docs to show style-specific content.
194+
189195
### Sidebar Configuration
190196

191197
**Structure** (`src/docs.config.ts`):
@@ -195,7 +201,6 @@ const sidebar: Sidebar = [
195201
- `slug`: Path relative to `src/content/docs/` (without `.mdx`)
196202
- `sidebarLabel` (optional): Override display name
197203
- `frameworks` (optional): Restrict to specific frameworks
198-
- `styles` (optional): Restrict to specific styles
199204
- `devOnly` (optional): Show only in development mode
200205

201206
**Example:**
@@ -216,11 +221,10 @@ export const sidebar: Sidebar = [
216221
### Key Utility Functions (`src/utils/docs/`)
217222

218223
**`sidebar.ts`** — Sidebar filtering and navigation:
219-
- `filterSidebar()`: Filter sidebar by framework/style, remove empty sections
220-
- `findFirstGuide()`: Get first available guide for framework/style combo
224+
- `filterSidebar()`: Filter sidebar by framework, remove empty sections
225+
- `findFirstGuide()`: Get first available guide for framework
221226
- `findGuideBySlug()`: Search sidebar recursively for a guide
222227
- `getAdjacentGuides()`: Get prev/next guides for navigation
223-
- `getValidStylesForGuide()`: Determine valid styles for a guide
224228
- `getSectionsForGuide()`: Get breadcrumb trail to a guide
225229

226230
**`routing.ts`** — URL building and redirect logic:
@@ -234,19 +238,19 @@ export const sidebar: Sidebar = [
234238

235239
**Nested index pages** handle redirects at each level:
236240
```
237-
/docs/ → redirect to first guide
238-
/docs/framework/ → redirect to first guide
239-
/docs/framework/{framework}/ → redirect to first guide
240-
/docs/framework/{framework}/style/ → redirect to first guide
241-
/docs/framework/{framework}/style/{style}/ → redirect to first guide
242-
/docs/framework/{framework}/style/{style}/{...slug} → render guide
241+
/docs/ → redirect to first guide
242+
/docs/framework/ → redirect to first guide
243+
/docs/framework/{framework}/ → redirect to first guide
244+
/docs/framework/{framework}/{slug} → render guide
243245
```
244246

245247
Each index page uses `resolveIndexRedirect()` to determine where to redirect based on:
246-
1. URL params (framework, style)
247-
2. User preferences (from localStorage via Nanostores)
248+
1. URL params (framework)
249+
2. User preferences (framework from cookies)
248250
3. Defaults (when invalid or missing)
249251

252+
**Style is not part of the URL.** Style preference is stored per-framework in localStorage and applied client-side via CSS.
253+
250254
## Content Collections
251255

252256
Defined in `src/content.config.ts` using Astro's Content Collections API.
@@ -453,18 +457,13 @@ Sidebar filtering is recursive because sections can contain guides or nested sec
453457

454458
```ts
455459
export function filterSidebar(
456-
sidebar: Sidebar,
457460
framework: SupportedFramework,
458-
style: AnySupportedStyle,
461+
sidebarToFilter?: Sidebar,
459462
): Sidebar {
460-
return sidebar
461-
.map((section) => ({
462-
...section,
463-
contents: section.contents.filter((item) =>
464-
isItemVisible(item, framework, style)
465-
),
466-
}))
467-
.filter((section) => section.contents.length > 0);
463+
const root = sidebarToFilter ?? sidebar;
464+
return root
465+
.map((item) => filterItem(item, framework))
466+
.filter(isNotFalsy);
468467
}
469468
```
470469

site/astro.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ export default defineConfig({
7474
vite: {
7575
plugins: [tailwindcss()],
7676
optimizeDeps: {
77-
exclude: ['@vjs/react'],
77+
exclude: ['@videojs/react-preview'],
7878
},
7979
resolve: {
80+
dedupe: ['react', 'react-dom'],
8081
alias: {
8182
'@': new URL('./src', import.meta.url).pathname,
8283
},

site/src/components/docs/DocsLink.astro

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
2-
32
import type { HTMLAttributes } from 'astro/types';
4-
import { isValidFramework, isValidStyleForFramework } from '@/types/docs';
3+
import { isValidFramework } from '@/types/docs';
54
import { resolveDocsLinkUrl } from '@/utils/docs/routing';
65
import A from '../typography/A.astro';
76
@@ -10,18 +9,14 @@ interface Props extends Omit<HTMLAttributes<'a'>, 'href'> {
109
}
1110
const { slug } = Astro.props;
1211
13-
const { framework: paramFramework, style: paramStyle } = Astro.params;
12+
const { framework: paramFramework } = Astro.params;
1413
if (!paramFramework || !isValidFramework(paramFramework)) {
1514
throw new Error(`Invalid or missing framework param "${paramFramework ?? 'undefined'}".`);
1615
}
17-
if (!paramStyle || !isValidStyleForFramework(paramFramework, paramStyle)) {
18-
throw new Error(`Invalid style param "${paramStyle}" for framework "${paramFramework}".`);
19-
}
2016
2117
const { url: href } = resolveDocsLinkUrl({
2218
targetSlug: slug,
2319
contextFramework: paramFramework,
24-
contextStyle: paramStyle,
2520
});
2621
---
2722

site/src/components/docs/DocsNavigation.astro

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
---
2-
32
import { ChevronDown } from 'lucide-react';
4-
import type { Guide, SupportedFramework, SupportedStyle } from '@/types/docs';
3+
import type { Guide, SupportedFramework } from '@/types/docs';
54
6-
type Props<F extends SupportedFramework = SupportedFramework> = {
5+
type Props = {
76
prev: Guide | null;
87
next: Guide | null;
9-
framework: F;
10-
style: SupportedStyle<F>;
8+
framework: SupportedFramework;
119
docTitles: Map<string, string>;
1210
};
1311
14-
const { prev, next, framework, style, docTitles } = Astro.props;
12+
const { prev, next, framework, docTitles } = Astro.props;
1513
16-
function getGuideUrl<F extends SupportedFramework>(framework: F, style: SupportedStyle<F>, slug: string): string {
17-
return `/docs/framework/${framework}/style/${style}/${slug}`;
14+
function getGuideUrl(framework: SupportedFramework, slug: string): string {
15+
return `/docs/framework/${framework}/${slug}`;
1816
}
1917
---
2018

2119
<nav class="w-full max-w-3xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-5">
2220
{
2321
prev ? (
2422
<a
25-
href={getGuideUrl(framework, style, prev.slug)}
23+
href={getGuideUrl(framework, prev.slug)}
2624
class="grid items-center gap-x-2 p-4 border border-light-40 dark:border-dark-80 rounded-lg intent:border-dark-40 dark:intent:border-dark-40"
2725
style="grid-template-areas: 'empty direction' 'icon title'; grid-template-columns: auto minmax(0,1fr); grid-template-rows: auto minmax(0,1fr);"
2826
>
@@ -43,7 +41,7 @@ function getGuideUrl<F extends SupportedFramework>(framework: F, style: Supporte
4341
{
4442
next ? (
4543
<a
46-
href={getGuideUrl(framework, style, next.slug)}
44+
href={getGuideUrl(framework, next.slug)}
4745
class="grid items-center gap-x-2 p-4 border border-light-40 dark:border-dark-80 rounded-lg intent:border-dark-40 text-right dark:intent:border-dark-40"
4846
style="grid-template-areas: 'direction empty' 'title icon';grid-template-columns: minmax(0,1fr) auto; grid-template-rows: auto minmax(0,1fr);"
4947
>
Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
11
---
2-
32
import clsx from 'clsx';
43
54
import { PreferenceUpdater } from '@/components/docs/PreferenceUpdater';
65
import SidebarItem from '@/components/docs/SidebarItem.astro';
7-
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
6+
import type { SupportedFramework } from '@/types/docs';
87
import { filterSidebar } from '@/utils/docs/sidebar';
98
10-
type Props<F extends SupportedFramework = SupportedFramework> = {
11-
framework: F;
12-
style: SupportedStyle<F>;
9+
type Props = {
10+
framework: SupportedFramework;
1311
docTitles: Map<string, string>;
1412
class?: string;
1513
};
1614
17-
const { framework, style, docTitles, class: className } = Astro.props;
18-
const filteredSidebar = filterSidebar(framework, style);
15+
const { framework, docTitles, class: className } = Astro.props;
16+
const filteredSidebar = filterSidebar(framework);
1917
---
2018

2119
<div class={clsx('bg-light-80 dark:bg-dark-100', className)}>
2220
<slot />
23-
<PreferenceUpdater client:idle currentFramework={framework} currentStyle={style} />
21+
<PreferenceUpdater client:idle currentFramework={framework} />
2422
<nav class="p-6">
25-
{
26-
filteredSidebar.map((item) => (
27-
<SidebarItem item={item} framework={framework} style={style} docTitles={docTitles} />
28-
))
29-
}
23+
{filteredSidebar.map((item) => <SidebarItem item={item} framework={framework} docTitles={docTitles} />)}
3024
</nav>
3125
</div>
Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,37 @@
11
import { useStore } from '@nanostores/react';
22
import { useEffect } from 'react';
3-
import { currentFramework, currentStyle } from '@/stores/preferences';
4-
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
5-
import { getPreferenceClient, setPreferenceClient } from '@/utils/docs/preferences';
3+
import { currentFramework } from '@/stores/preferences';
4+
import type { SupportedFramework } from '@/types/docs';
5+
import { getFrameworkPreferenceClient, setFrameworkPreferenceClient } from '@/utils/docs/preferences';
66

77
/**
8-
* PreferenceSync keeps the nanostore in sync with cookies.
8+
* PreferenceSync keeps the framework nanostore in sync with cookies.
99
*
1010
* On mount: Reads cookies → initializes store
1111
* On store change: Writes to cookies
1212
*
13-
* This component should be loaded with client:load in the base layout
13+
* Style preferences are handled via localStorage by StyleInit and PreferenceUpdater.
14+
*
15+
* This component should be loaded with client:idle in the base layout
1416
* to ensure preferences are available immediately.
1517
*/
1618
export function PreferenceSync() {
1719
const framework = useStore(currentFramework);
18-
const style = useStore(currentStyle);
1920

2021
// Initialize store from cookies on mount
2122
useEffect(() => {
22-
const prefs = getPreferenceClient();
23-
if (prefs.framework) {
24-
currentFramework.set(prefs.framework);
25-
}
26-
if (prefs.style) {
27-
currentStyle.set(prefs.style);
23+
const prefs = getFrameworkPreferenceClient();
24+
if (prefs) {
25+
currentFramework.set(prefs);
2826
}
2927
}, []);
3028

3129
// Sync store changes to cookies
3230
useEffect(() => {
33-
if (framework && style) {
34-
setPreferenceClient(framework as SupportedFramework, style as SupportedStyle<typeof framework>);
31+
if (framework) {
32+
setFrameworkPreferenceClient(framework as SupportedFramework);
3533
}
36-
}, [framework, style]);
34+
}, [framework]);
3735

3836
return null;
3937
}
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import { useEffect } from 'react';
22
import { currentFramework as frameworkStore, currentStyle as styleStore } from '@/stores/preferences';
3-
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
3+
import { getDefaultStyle, type SupportedFramework } from '@/types/docs';
4+
import { getStylePreferenceClient } from '@/utils/docs/preferences';
45

5-
interface PreferenceUpdaterProps<F extends SupportedFramework = SupportedFramework> {
6-
currentFramework: F;
7-
currentStyle: SupportedStyle<F>;
6+
interface PreferenceUpdaterProps {
7+
currentFramework: SupportedFramework;
88
}
99

1010
/**
11-
* PreferenceUpdater component updates the preference nanostore based on URL params.
11+
* PreferenceUpdater component updates the preference nanostore based on URL params and localStorage.
1212
* This component is loaded with client:idle directive on docs pages, making it non-blocking.
13-
* It updates the nanostore whenever framework or style from URL changes.
14-
* PreferenceSync handles persisting to cookies.
13+
* It updates the framework store from URL params and style store from localStorage.
14+
* PreferenceSync handles persisting framework to cookies.
1515
*/
16-
export function PreferenceUpdater<F extends SupportedFramework = SupportedFramework>({
17-
currentFramework,
18-
currentStyle,
19-
}: PreferenceUpdaterProps<F>) {
16+
export function PreferenceUpdater({ currentFramework }: PreferenceUpdaterProps) {
2017
useEffect(() => {
18+
// Update framework store from URL
2119
frameworkStore.set(currentFramework);
22-
styleStore.set(currentStyle);
23-
}, [currentFramework, currentStyle]);
20+
21+
// Read style from localStorage (StyleInit guarantees a valid value exists)
22+
// Fallback to default if React hydrates before StyleInit completes
23+
const style = getStylePreferenceClient(currentFramework) ?? getDefaultStyle(currentFramework);
24+
styleStore.set(style);
25+
}, [currentFramework]);
2426

2527
return null;
2628
}

0 commit comments

Comments
 (0)