feat(site): perform /docs redirect client-side

This commit is contained in:
Darius Cepulis
2026-02-09 09:12:12 -06:00
parent 5eab1dbb2f
commit 6b67428de3
6 changed files with 105 additions and 25 deletions
@@ -0,0 +1,22 @@
import { useStore } from '@nanostores/react';
import { currentFramework } from '@/stores/preferences';
import { buildDocsUrl } from '@/utils/docs/routing';
import { findFirstGuide } from '@/utils/docs/sidebar';
import useIsHydrated from '@/utils/useIsHydrated';
export interface GetStartedLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children?: React.ReactNode;
}
export default function GetStartedLink({ children, ...props }: GetStartedLinkProps) {
const framework = useStore(currentFramework);
const isHydrated = useIsHydrated();
const href = isHydrated && framework ? buildDocsUrl(framework, findFirstGuide(framework)) : '/docs';
return (
<a {...props} href={href}>
{children}
</a>
);
}
+19 -13
View File
@@ -3,6 +3,7 @@ import clsx from 'clsx';
import { ArrowUpRight, Menu, X } from 'lucide-react';
import { DISCORD_INVITE_URL, GITHUB_REPO_URL } from '@/consts';
import FilmGrain from '../FilmGrain';
import GetStartedLink from './GetStartedLink';
interface NavLink {
href: string;
@@ -74,19 +75,24 @@ export default function MobileNav({ navLinks, currentPath, dark = false, childre
{/* Navigation links */}
<nav className="flex flex-col py-3">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
className={clsx(
'text-lg px-6 py-3',
link.matchPath && currentPath.startsWith(link.matchPath) ? 'underline' : 'intent:underline'
)}
aria-current={link.matchPath && currentPath.startsWith(link.matchPath) ? 'page' : undefined}
>
{link.label} {link.external ? <ArrowUpRight size="1em" /> : null}
</a>
))}
{navLinks.map((link) => {
const isActive = link.matchPath && currentPath.startsWith(link.matchPath);
const className = clsx('text-lg px-6 py-3', isActive ? 'underline' : 'intent:underline');
if (link.href === '/docs') {
return (
<GetStartedLink key={link.href} className={className} aria-current={isActive ? 'page' : undefined}>
{link.label}
</GetStartedLink>
);
}
return (
<a key={link.href} href={link.href} className={className} aria-current={isActive ? 'page' : undefined}>
{link.label} {link.external ? <ArrowUpRight size="1em" /> : null}
</a>
);
})}
<a href={GITHUB_REPO_URL} className="text-lg px-6 py-3 inline-flex items-center gap-1 intent:underline">
GitHub <ArrowUpRight size="1em" />
</a>
+17 -3
View File
@@ -1,11 +1,13 @@
---
import clsx from 'clsx';
import { ArrowUpRight } from 'lucide-react';
import Discord from '@/components/icons/Discord';
import GitHub from '@/components/icons/GitHub';
import Search from '@/components/Search';
import { DISCORD_INVITE_URL, GITHUB_REPO_URL } from '@/consts';
import FilmGrain from '../FilmGrain';
import GetStartedLink from './GetStartedLink';
import MobileNav from './MobileNav';
export interface Props {
@@ -25,6 +27,8 @@ const navLinks: NavLink[] = [
];
const currentPath = Astro.url.pathname;
const isDocsActive = currentPath.startsWith('/docs');
const otherNavLinks = navLinks.filter((link) => link.href !== '/docs');
const { class: className, dark = false } = Astro.props;
---
@@ -47,8 +51,18 @@ const { class: className, dark = false } = Astro.props;
{/* Desktop nav links */}
<div class="hidden sm:flex -mb-px pr-3">
<GetStartedLink
client:idle
className={clsx(
'h-full flex items-center px-3 border-b gap-1',
isDocsActive ? 'border-current' : 'border-transparent intent:border-current',
)}
aria-current={isDocsActive ? 'page' : undefined}
>
Docs
</GetStartedLink>
{
navLinks.map((link) => (
otherNavLinks.map((link) => (
<a
href={link.href}
class:list={[
@@ -84,7 +98,7 @@ const { class: className, dark = false } = Astro.props;
</div>
{/* Mobile nav */}
<MobileNav client:idle navLinks={navLinks} currentPath={currentPath} dark={dark}>
<!-- <MobileNav client:idle navLinks={navLinks} currentPath={currentPath} dark={dark}>
<slot name="mobile-nav" />
</MobileNav>
</MobileNav> -->
</nav>
+2 -6
View File
@@ -1,7 +1,6 @@
import { useStore } from '@nanostores/react';
import { useEffect } from 'react';
import { currentFramework } from '@/stores/preferences';
import type { SupportedFramework } from '@/types/docs';
import { getFrameworkPreferenceClient, setFrameworkPreferenceClient } from '@/utils/docs/preferences';
/**
@@ -20,16 +19,13 @@ export function PreferenceSync() {
// Initialize store from cookies on mount
useEffect(() => {
const prefs = getFrameworkPreferenceClient();
if (prefs) {
currentFramework.set(prefs);
}
currentFramework.set(getFrameworkPreferenceClient());
}, []);
// Sync store changes to cookies
useEffect(() => {
if (framework) {
setFrameworkPreferenceClient(framework as SupportedFramework);
setFrameworkPreferenceClient(framework);
}
}, [framework]);
@@ -1,8 +1,9 @@
import type { AstroCookies } from 'astro';
import { describe, expect, it, vi } from 'vitest';
import { SUPPORTED_FRAMEWORKS } from '@/types/docs';
import { DEFAULT_FRAMEWORK, SUPPORTED_FRAMEWORKS } from '@/types/docs';
import {
FRAMEWORK_COOKIE,
getFrameworkPreferenceClient,
getPreferencesServer,
STYLE_STORAGE_KEY_PREFIX,
setFrameworkPreferenceClient,
@@ -76,6 +77,47 @@ describe('preferences utilities', () => {
});
});
describe('getFrameworkPreferenceClient', () => {
it('should return DEFAULT_FRAMEWORK when no cookie is set', () => {
Object.defineProperty(document, 'cookie', {
get: () => '',
configurable: true,
});
expect(getFrameworkPreferenceClient()).toBe(DEFAULT_FRAMEWORK);
});
it('should return the framework from cookie when valid', () => {
for (const framework of SUPPORTED_FRAMEWORKS) {
Object.defineProperty(document, 'cookie', {
get: () => `${FRAMEWORK_COOKIE}=${framework}`,
configurable: true,
});
expect(getFrameworkPreferenceClient()).toBe(framework);
}
});
it('should return DEFAULT_FRAMEWORK when cookie has invalid value', () => {
Object.defineProperty(document, 'cookie', {
get: () => `${FRAMEWORK_COOKIE}=invalid-framework`,
configurable: true,
});
expect(getFrameworkPreferenceClient()).toBe(DEFAULT_FRAMEWORK);
});
it('should return null when document is undefined (SSR)', () => {
const originalDocument = globalThis.document;
// @ts-expect-error Testing SSR scenario
globalThis.document = undefined;
expect(getFrameworkPreferenceClient()).toBeNull();
globalThis.document = originalDocument;
});
});
describe('setFrameworkPreferenceClient', () => {
it('should set framework cookie', () => {
// Mock document.cookie
+2 -2
View File
@@ -1,6 +1,6 @@
import type { AstroCookies } from 'astro';
import type { AnySupportedStyle, SupportedFramework, SupportedStyle } from '@/types/docs';
import { isValidFramework, isValidStyleForFramework } from '@/types/docs';
import { DEFAULT_FRAMEWORK, isValidFramework, isValidStyleForFramework } from '@/types/docs';
// Cookie name for framework (server-side redirects)
export const FRAMEWORK_COOKIE = 'vjs_docs_framework';
@@ -48,7 +48,7 @@ export function getFrameworkPreferenceClient(): SupportedFramework | null {
);
const framework = cookies[FRAMEWORK_COOKIE];
return framework && isValidFramework(framework) ? framework : null;
return framework && isValidFramework(framework) ? framework : DEFAULT_FRAMEWORK;
}
export function setFrameworkPreferenceClient(framework: SupportedFramework): void {