From 73dfda828838eadcded751bbbc7ed2ce662630ae Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 15:08:18 +0900 Subject: [PATCH 1/7] docs: write docs (does not build yet) --- packages/docs/src/App.tsx | 26 +- .../components/CodeBlock/CodeBlock.module.css | 72 +++++ .../src/components/CodeBlock/CodeBlock.tsx | 42 +++ .../src/components/Header/Header.module.css | 142 ++++++++ .../docs/src/components/Header/Header.tsx | 56 ++++ .../src/components/Layout/Layout.module.css | 152 +++++++++ .../docs/src/components/Layout/Layout.tsx | 33 ++ .../src/components/Sidebar/Sidebar.module.css | 81 +++++ .../docs/src/components/Sidebar/Sidebar.tsx | 48 +++ packages/docs/src/pages/GettingStarted.mdx | 119 ++++++- packages/docs/src/pages/Home.module.css | 304 ++++++++++++++++++ packages/docs/src/pages/Home.tsx | 231 ++++++++++++- packages/docs/src/pages/api/Defer.mdx | 133 ++++++++ .../docs/src/pages/api/FunstackStatic.mdx | 137 ++++++++ packages/docs/src/pages/concepts/RSC.mdx | 187 +++++++++++ packages/docs/src/root.tsx | 11 + packages/docs/src/styles/globals.css | 238 ++++++++++++++ packages/docs/src/types/css.d.ts | 9 + 18 files changed, 2009 insertions(+), 12 deletions(-) create mode 100644 packages/docs/src/components/CodeBlock/CodeBlock.module.css create mode 100644 packages/docs/src/components/CodeBlock/CodeBlock.tsx create mode 100644 packages/docs/src/components/Header/Header.module.css create mode 100644 packages/docs/src/components/Header/Header.tsx create mode 100644 packages/docs/src/components/Layout/Layout.module.css create mode 100644 packages/docs/src/components/Layout/Layout.tsx create mode 100644 packages/docs/src/components/Sidebar/Sidebar.module.css create mode 100644 packages/docs/src/components/Sidebar/Sidebar.tsx create mode 100644 packages/docs/src/pages/Home.module.css create mode 100644 packages/docs/src/pages/api/Defer.mdx create mode 100644 packages/docs/src/pages/api/FunstackStatic.mdx create mode 100644 packages/docs/src/pages/concepts/RSC.mdx create mode 100644 packages/docs/src/styles/globals.css create mode 100644 packages/docs/src/types/css.d.ts diff --git a/packages/docs/src/App.tsx b/packages/docs/src/App.tsx index fbc8eeb..b06a56e 100644 --- a/packages/docs/src/App.tsx +++ b/packages/docs/src/App.tsx @@ -1,17 +1,37 @@ import { Router } from "@funstack/router"; import { route, type RouteDefinition } from "@funstack/router/server"; +import { defer } from "@funstack/static/server"; +import { Layout } from "./components/Layout/Layout"; +import DeferApi from "./pages/api/Defer.mdx"; +import FunstackStaticApi from "./pages/api/FunstackStatic.mdx"; +import RSCConcept from "./pages/concepts/RSC.mdx"; import GettingStarted from "./pages/GettingStarted.mdx"; import { Home } from "./pages/Home"; -import { defer } from "@funstack/static/server"; const routes: RouteDefinition[] = [ route({ path: "/", - component: , + component: ( + + + + ), }), route({ path: "/getting-started", - component: defer(GettingStarted), + component: {defer(GettingStarted)}, + }), + route({ + path: "/api/funstack-static", + component: {defer(FunstackStaticApi)}, + }), + route({ + path: "/api/defer", + component: {defer(DeferApi)}, + }), + route({ + path: "/concepts/rsc", + component: {defer(RSCConcept)}, }), ]; diff --git a/packages/docs/src/components/CodeBlock/CodeBlock.module.css b/packages/docs/src/components/CodeBlock/CodeBlock.module.css new file mode 100644 index 0000000..9ad4cff --- /dev/null +++ b/packages/docs/src/components/CodeBlock/CodeBlock.module.css @@ -0,0 +1,72 @@ +.codeBlock { + position: relative; + margin: var(--spacing-lg) 0; + border-radius: var(--radius-lg); + overflow: hidden; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-sm) var(--spacing-md); + background-color: #161622; + border-bottom: 1px solid #2d2d4a; +} + +.language { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--color-text-muted); + text-transform: uppercase; +} + +.copyButton { + display: flex; + align-items: center; + gap: var(--spacing-xs); + padding: var(--spacing-xs) var(--spacing-sm); + font-size: var(--text-xs); + color: var(--color-text-muted); + background-color: transparent; + border: 1px solid #2d2d4a; + border-radius: var(--radius-sm); + cursor: pointer; + transition: + color var(--transition-fast), + border-color var(--transition-fast); +} + +.copyButton:hover { + color: var(--color-text); + border-color: var(--color-text-muted); +} + +.copyButton svg { + width: 14px; + height: 14px; +} + +.pre { + margin: 0; + padding: var(--spacing-lg); + background-color: var(--color-code-bg); + overflow-x: auto; +} + +.code { + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: 1.6; + color: var(--color-code-text); +} + +/* Inline code */ +.inlineCode { + font-family: var(--font-mono); + font-size: 0.9em; + padding: 0.15em 0.4em; + background-color: var(--color-bg-tertiary); + border-radius: var(--radius-sm); + color: var(--color-accent); +} diff --git a/packages/docs/src/components/CodeBlock/CodeBlock.tsx b/packages/docs/src/components/CodeBlock/CodeBlock.tsx new file mode 100644 index 0000000..a6c30ea --- /dev/null +++ b/packages/docs/src/components/CodeBlock/CodeBlock.tsx @@ -0,0 +1,42 @@ +import type React from "react"; +import styles from "./CodeBlock.module.css"; + +interface CodeBlockProps { + children: string; + language?: string; + showLineNumbers?: boolean; +} + +export const CodeBlock: React.FC = ({ + children, + language = "typescript", +}) => { + return ( +
+
+ {language} + +
+
+        {children}
+      
+
+ ); +}; + +interface InlineCodeProps { + children: React.ReactNode; +} + +export const InlineCode: React.FC = ({ children }) => { + return {children}; +}; diff --git a/packages/docs/src/components/Header/Header.module.css b/packages/docs/src/components/Header/Header.module.css new file mode 100644 index 0000000..89c0980 --- /dev/null +++ b/packages/docs/src/components/Header/Header.module.css @@ -0,0 +1,142 @@ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: var(--header-height); + background-color: var(--color-bg); + border-bottom: 1px solid var(--color-border); + z-index: 100; + backdrop-filter: blur(8px); + background-color: rgba(255, 255, 255, 0.9); +} + +@media (prefers-color-scheme: dark) { + .header { + background-color: rgba(15, 15, 26, 0.9); + } +} + +.container { + max-width: var(--page-max-width); + margin: 0 auto; + height: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--spacing-xl); +} + +.logo { + display: flex; + align-items: center; + gap: var(--spacing-sm); + font-weight: 700; + font-size: var(--text-lg); + color: var(--color-text); + text-decoration: none; +} + +.logo:hover { + color: var(--color-text); +} + +.logoIcon { + width: 32px; + height: 32px; + background: var(--gradient-accent); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: 700; + font-size: var(--text-sm); +} + +.logoText { + background: var(--gradient-accent); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.nav { + display: flex; + align-items: center; + gap: var(--spacing-lg); +} + +.navLink { + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-text-secondary); + text-decoration: none; + padding: var(--spacing-sm) var(--spacing-md); + border-radius: var(--radius-md); + transition: + color var(--transition-fast), + background-color var(--transition-fast); +} + +.navLink:hover { + color: var(--color-text); + background-color: var(--color-bg-tertiary); +} + +.navLinkActive { + color: var(--color-accent); +} + +.navLinkActive:hover { + color: var(--color-accent-dark); +} + +.githubLink { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: var(--radius-md); + color: var(--color-text-secondary); + transition: + color var(--transition-fast), + background-color var(--transition-fast); +} + +.githubLink:hover { + color: var(--color-text); + background-color: var(--color-bg-tertiary); +} + +.githubLink svg { + width: 20px; + height: 20px; +} + +/* Mobile menu button */ +.menuButton { + display: none; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + color: var(--color-text-secondary); +} + +.menuButton:hover { + background-color: var(--color-bg-tertiary); + color: var(--color-text); +} + +@media (max-width: 768px) { + .nav { + display: none; + } + + .menuButton { + display: flex; + } +} diff --git a/packages/docs/src/components/Header/Header.tsx b/packages/docs/src/components/Header/Header.tsx new file mode 100644 index 0000000..cbb1cae --- /dev/null +++ b/packages/docs/src/components/Header/Header.tsx @@ -0,0 +1,56 @@ +import styles from "./Header.module.css"; + +export const Header: React.FC = () => { + return ( +
+ +
+ ); +}; diff --git a/packages/docs/src/components/Layout/Layout.module.css b/packages/docs/src/components/Layout/Layout.module.css new file mode 100644 index 0000000..f5d3bbb --- /dev/null +++ b/packages/docs/src/components/Layout/Layout.module.css @@ -0,0 +1,152 @@ +.layout { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.main { + display: flex; + flex: 1; + margin-top: var(--header-height); +} + +.content { + flex: 1; + padding: var(--spacing-2xl); + max-width: var(--content-max-width); + margin: 0 auto; + width: 100%; +} + +/* Docs layout with sidebar */ +.docsLayout .content { + margin-left: var(--sidebar-width); + max-width: calc(100% - var(--sidebar-width)); + padding: var(--spacing-2xl) var(--spacing-3xl); +} + +/* Home page full width */ +.homeLayout .content { + max-width: 100%; + padding: 0; +} + +/* MDX Content Styling */ +.mdxContent h1 { + margin-bottom: var(--spacing-lg); + padding-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-border); +} + +.mdxContent h2 { + margin-top: var(--spacing-2xl); + margin-bottom: var(--spacing-md); +} + +.mdxContent h3 { + margin-top: var(--spacing-xl); + margin-bottom: var(--spacing-sm); +} + +.mdxContent p { + margin-bottom: var(--spacing-md); + line-height: 1.7; +} + +.mdxContent ul, +.mdxContent ol { + margin-bottom: var(--spacing-md); + padding-left: var(--spacing-xl); +} + +.mdxContent ul { + list-style: disc; +} + +.mdxContent ol { + list-style: decimal; +} + +.mdxContent li { + margin-bottom: var(--spacing-xs); + color: var(--color-text-secondary); +} + +.mdxContent pre { + margin: var(--spacing-lg) 0; + padding: var(--spacing-lg); + background-color: var(--color-code-bg); + border-radius: var(--radius-lg); + overflow-x: auto; +} + +.mdxContent pre code { + color: var(--color-code-text); + font-size: var(--text-sm); + line-height: 1.6; +} + +.mdxContent code { + font-size: var(--text-sm); +} + +.mdxContent blockquote { + margin: var(--spacing-lg) 0; + padding: var(--spacing-md) var(--spacing-lg); + border-left: 4px solid var(--color-accent); + background-color: var(--color-accent-bg); + border-radius: 0 var(--radius-md) var(--radius-md) 0; +} + +.mdxContent blockquote p { + margin: 0; + color: var(--color-text); +} + +.mdxContent hr { + margin: var(--spacing-2xl) 0; + border: none; + border-top: 1px solid var(--color-border); +} + +.mdxContent table { + width: 100%; + margin: var(--spacing-lg) 0; + border-collapse: collapse; +} + +.mdxContent th, +.mdxContent td { + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--color-border); + text-align: left; +} + +.mdxContent th { + background-color: var(--color-bg-secondary); + font-weight: 600; +} + +.mdxContent strong { + font-weight: 600; + color: var(--color-text); +} + +/* Responsive */ +@media (max-width: 1024px) { + .docsLayout .content { + margin-left: 0; + max-width: 100%; + padding: var(--spacing-xl); + } +} + +@media (max-width: 640px) { + .content { + padding: var(--spacing-md); + } + + .docsLayout .content { + padding: var(--spacing-md); + } +} diff --git a/packages/docs/src/components/Layout/Layout.tsx b/packages/docs/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..878989c --- /dev/null +++ b/packages/docs/src/components/Layout/Layout.tsx @@ -0,0 +1,33 @@ +import type React from "react"; +import { Header } from "../Header/Header"; +import { Sidebar } from "../Sidebar/Sidebar"; +import styles from "./Layout.module.css"; + +type LayoutVariant = "home" | "docs"; + +interface LayoutProps { + children: React.ReactNode; + variant?: LayoutVariant; +} + +export const Layout: React.FC = ({ + children, + variant = "docs", +}) => { + const layoutClass = + variant === "home" ? styles.homeLayout : styles.docsLayout; + + return ( +
+
+
+ {variant === "docs" && } +
+ {children} +
+
+
+ ); +}; diff --git a/packages/docs/src/components/Sidebar/Sidebar.module.css b/packages/docs/src/components/Sidebar/Sidebar.module.css new file mode 100644 index 0000000..756bf72 --- /dev/null +++ b/packages/docs/src/components/Sidebar/Sidebar.module.css @@ -0,0 +1,81 @@ +.sidebar { + position: fixed; + top: var(--header-height); + left: 0; + width: var(--sidebar-width); + height: calc(100vh - var(--header-height)); + background-color: var(--color-bg-secondary); + border-right: 1px solid var(--color-border); + overflow-y: auto; + padding: var(--spacing-lg); +} + +.section { + margin-bottom: var(--spacing-xl); +} + +.sectionTitle { + font-size: var(--text-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-muted); + margin-bottom: var(--spacing-sm); + padding: 0 var(--spacing-sm); +} + +.navList { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.navItem { + display: block; + padding: var(--spacing-sm) var(--spacing-md); + font-size: var(--text-sm); + color: var(--color-text-secondary); + text-decoration: none; + border-radius: var(--radius-md); + transition: + color var(--transition-fast), + background-color var(--transition-fast); +} + +.navItem:hover { + color: var(--color-text); + background-color: var(--color-bg-tertiary); +} + +.navItemActive { + color: var(--color-accent); + background-color: var(--color-accent-bg); + font-weight: 500; +} + +.navItemActive:hover { + color: var(--color-accent-dark); + background-color: var(--color-accent-bg); +} + +/* Mobile overlay */ +@media (max-width: 1024px) { + .sidebar { + display: none; + } + + .sidebarOpen { + display: block; + z-index: 50; + width: 100%; + max-width: 300px; + box-shadow: var(--shadow-xl); + } + + .overlay { + position: fixed; + inset: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 40; + } +} diff --git a/packages/docs/src/components/Sidebar/Sidebar.tsx b/packages/docs/src/components/Sidebar/Sidebar.tsx new file mode 100644 index 0000000..3aba444 --- /dev/null +++ b/packages/docs/src/components/Sidebar/Sidebar.tsx @@ -0,0 +1,48 @@ +import styles from "./Sidebar.module.css"; + +interface NavItem { + label: string; + href: string; +} + +interface NavSection { + title: string; + items: NavItem[]; +} + +const navigation: NavSection[] = [ + { + title: "Getting Started", + items: [{ label: "Introduction", href: "/getting-started" }], + }, + { + title: "API Reference", + items: [ + { label: "funstackStatic()", href: "/api/funstack-static" }, + { label: "defer()", href: "/api/defer" }, + ], + }, + { + title: "Concepts", + items: [{ label: "React Server Components", href: "/concepts/rsc" }], + }, +]; + +export const Sidebar: React.FC = () => { + return ( + + ); +}; diff --git a/packages/docs/src/pages/GettingStarted.mdx b/packages/docs/src/pages/GettingStarted.mdx index 5604cfc..ddac05f 100644 --- a/packages/docs/src/pages/GettingStarted.mdx +++ b/packages/docs/src/pages/GettingStarted.mdx @@ -1,15 +1,122 @@ # Getting Started -Welcome to **FUNSTACK Static**! This page is written in MDX. +Welcome to **FUNSTACK Static**! Build static sites powered by React Server Components with zero server runtime. ## Installation +Install `@funstack/static` and its peer dependencies: + +```bash +npm install @funstack/static react react-dom +``` + +Or with pnpm: + ```bash -npm install @funstack/static +pnpm add @funstack/static react react-dom +``` + +## Quick Start + +### 1. Configure Vite + +Create or update your `vite.config.ts`: + +```typescript +import { funstackStatic } from "@funstack/static"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + funstackStatic({ + root: "./src/root.tsx", + app: "./src/App.tsx", + }), + ], +}); +``` + +### 2. Create Your Root Component + +The root component wraps your entire application and defines the HTML structure: + +```tsx +// src/root.tsx +import type React from "react"; + +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + My Static Site + + {children} + + ); +} +``` + +### 3. Create Your App Component + +The app component defines your routes and page content: + +```tsx +// src/App.tsx +import { Router } from "@funstack/router"; +import { route } from "@funstack/router/server"; + +const routes = [ + route({ + path: "/", + component:

Welcome to my site!

, + }), + route({ + path: "/about", + component:

About Us

, + }), +]; + +export default function App() { + return ; +} +``` + +### 4. Start Development Server + +```bash +npm run dev +``` + +Your site is now running at `http://localhost:5173`! + +### 5. Build for Production + +```bash +npm run build +``` + +Static HTML files are generated in the `dist/public` directory, ready to deploy to any static hosting provider. + +## Project Structure + +A typical FUNSTACK Static project looks like this: + +``` +my-site/ +├── src/ +│ ├── root.tsx # HTML wrapper component +│ ├── App.tsx # Routes and main app +│ ├── components/ # Your React components +│ └── pages/ # Page components +├── public/ # Static assets +├── vite.config.ts # Vite configuration +└── package.json ``` -## Features +## What's Next? -- React Server Components without a server -- Static site generation -- Vite-powered development +- Learn about the [funstackStatic() API](/api/funstack-static) for configuration options +- Understand [defer()](/api/defer) for streaming content +- Dive into [React Server Components](/concepts/rsc) concepts diff --git a/packages/docs/src/pages/Home.module.css b/packages/docs/src/pages/Home.module.css new file mode 100644 index 0000000..e0a21b4 --- /dev/null +++ b/packages/docs/src/pages/Home.module.css @@ -0,0 +1,304 @@ +.home { + min-height: calc(100vh - var(--header-height)); +} + +/* Hero Section */ +.hero { + padding: var(--spacing-3xl) var(--spacing-xl); + text-align: center; + background: linear-gradient( + 180deg, + var(--color-bg) 0%, + var(--color-bg-secondary) 100% + ); +} + +.heroContent { + max-width: 800px; + margin: 0 auto; +} + +.badge { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-xs) var(--spacing-md); + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-accent); + background-color: var(--color-accent-bg); + border-radius: var(--radius-full); + margin-bottom: var(--spacing-lg); +} + +.title { + font-size: var(--text-5xl); + font-weight: 700; + line-height: 1.1; + margin-bottom: var(--spacing-lg); + background: var(--gradient-accent); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + font-size: var(--text-xl); + color: var(--color-text-secondary); + margin-bottom: var(--spacing-2xl); + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.buttons { + display: flex; + justify-content: center; + gap: var(--spacing-md); + flex-wrap: wrap; +} + +.primaryButton { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-xl); + font-size: var(--text-base); + font-weight: 600; + color: white; + background: var(--gradient-accent); + border-radius: var(--radius-md); + text-decoration: none; + box-shadow: var(--shadow-md); + transition: + transform var(--transition-fast), + box-shadow var(--transition-fast); +} + +.primaryButton:hover { + color: white; + transform: translateY(-2px); + box-shadow: var(--shadow-lg); +} + +.secondaryButton { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-xl); + font-size: var(--text-base); + font-weight: 600; + color: var(--color-text); + background-color: var(--color-bg); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + text-decoration: none; + transition: + background-color var(--transition-fast), + border-color var(--transition-fast); +} + +.secondaryButton:hover { + background-color: var(--color-bg-secondary); + border-color: var(--color-text-muted); + color: var(--color-text); +} + +/* Code Preview */ +.codePreview { + max-width: 700px; + margin: var(--spacing-3xl) auto 0; + text-align: left; + background-color: var(--color-code-bg); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + overflow: hidden; +} + +.codeHeader { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + background-color: #161622; + border-bottom: 1px solid #2d2d4a; +} + +.codeDot { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.codeDotRed { + background-color: #ff5f56; +} + +.codeDotYellow { + background-color: #ffbd2e; +} + +.codeDotGreen { + background-color: #27c93f; +} + +.codeContent { + padding: var(--spacing-lg); + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: 1.6; + color: var(--color-code-text); + overflow-x: auto; +} + +.codeContent code { + background: none; + padding: 0; +} + +/* Features Section */ +.features { + padding: var(--spacing-3xl) var(--spacing-xl); + background-color: var(--color-bg); +} + +.featuresContainer { + max-width: var(--page-max-width); + margin: 0 auto; +} + +.sectionTitle { + font-size: var(--text-3xl); + font-weight: 700; + text-align: center; + margin-bottom: var(--spacing-sm); +} + +.sectionSubtitle { + font-size: var(--text-lg); + color: var(--color-text-secondary); + text-align: center; + margin-bottom: var(--spacing-3xl); + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.featuresGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: var(--spacing-xl); +} + +.featureCard { + padding: var(--spacing-xl); + background-color: var(--color-bg-secondary); + border-radius: var(--radius-lg); + border: 1px solid var(--color-border-light); + transition: + transform var(--transition-fast), + box-shadow var(--transition-fast); +} + +.featureCard:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-lg); +} + +.featureIcon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: var(--gradient-accent); + border-radius: var(--radius-md); + margin-bottom: var(--spacing-md); + color: white; + font-size: 24px; +} + +.featureTitle { + font-size: var(--text-lg); + font-weight: 600; + margin-bottom: var(--spacing-sm); +} + +.featureDescription { + font-size: var(--text-sm); + color: var(--color-text-secondary); + line-height: 1.6; +} + +/* CTA Section */ +.cta { + padding: var(--spacing-3xl) var(--spacing-xl); + background: linear-gradient( + 135deg, + var(--color-accent) 0%, + var(--color-accent-dark) 100% + ); + text-align: center; +} + +.ctaTitle { + font-size: var(--text-3xl); + font-weight: 700; + color: white; + margin-bottom: var(--spacing-md); +} + +.ctaDescription { + font-size: var(--text-lg); + color: rgba(255, 255, 255, 0.9); + margin-bottom: var(--spacing-xl); + max-width: 500px; + margin-left: auto; + margin-right: auto; +} + +.ctaButton { + display: inline-flex; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-md) var(--spacing-xl); + font-size: var(--text-base); + font-weight: 600; + color: var(--color-accent); + background-color: white; + border-radius: var(--radius-md); + text-decoration: none; + box-shadow: var(--shadow-md); + transition: + transform var(--transition-fast), + box-shadow var(--transition-fast); +} + +.ctaButton:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-lg); + color: var(--color-accent-dark); +} + +/* Responsive */ +@media (max-width: 768px) { + .title { + font-size: var(--text-3xl); + } + + .subtitle { + font-size: var(--text-base); + } + + .hero { + padding: var(--spacing-2xl) var(--spacing-md); + } + + .features { + padding: var(--spacing-2xl) var(--spacing-md); + } + + .cta { + padding: var(--spacing-2xl) var(--spacing-md); + } +} diff --git a/packages/docs/src/pages/Home.tsx b/packages/docs/src/pages/Home.tsx index 6ca1be1..940708a 100644 --- a/packages/docs/src/pages/Home.tsx +++ b/packages/docs/src/pages/Home.tsx @@ -1,8 +1,233 @@ +import styles from "./Home.module.css"; + export const Home: React.FC = () => { return ( -
-

Welcome to FUNSTACK Static Docs

-

This is the home page of the FUNSTACK Static documentation site.

+
+ {/* Hero Section */} +
+
+ React Server Components +

Build Static Sites with RSC

+

+ FUNSTACK Static brings the power of React Server Components to + static site generation. No server required, just pure static output + with modern DX. +

+ + + {/* Code Preview */} +
+
+ + + +
+
+              {`// vite.config.ts
+import { funstackStatic } from "@funstack/static";
+
+export default {
+  plugins: [
+    funstackStatic({
+      root: "./src/root.tsx",
+      app: "./src/App.tsx",
+    }),
+  ],
+};`}
+            
+
+
+
+ + {/* Features Section */} +
+
+

Why FUNSTACK Static?

+

+ Modern static site generation with React Server Components, powered + by Vite. +

+ +
+
+
+ + + + +
+

Zero Runtime Overhead

+

+ Generate pure static HTML at build time. No JavaScript framework + shipped to the client unless you need it. +

+
+ +
+
+ + + + + +
+

React Server Components

+

+ Write components that run at build time. Fetch data, read files, + and render HTML without a server. +

+
+ +
+
+ + + +
+

Vite-Powered

+

+ Lightning fast HMR in development and optimized builds in + production. Enjoy the best DX with Vite. +

+
+ +
+
+ + + + + +
+

Streaming Support

+

+ Use defer() to stream content progressively. Perfect for large + pages and slow data sources. +

+
+ +
+
+ + + + +
+

MDX Support

+

+ Write content in MDX with full component support. Perfect for + documentation sites and blogs. +

+
+ +
+
+ + + + + +
+

File-Based Routing

+

+ Flexible routing with @funstack/router. Define routes + declaratively or use file-based conventions. +

+
+
+
+
+ + {/* CTA Section */} +
+

Ready to Get Started?

+

+ Build your next static site with the power of React Server Components. +

+ + Read the Documentation + + + + +
); }; diff --git a/packages/docs/src/pages/api/Defer.mdx b/packages/docs/src/pages/api/Defer.mdx new file mode 100644 index 0000000..a88b8dc --- /dev/null +++ b/packages/docs/src/pages/api/Defer.mdx @@ -0,0 +1,133 @@ +# defer() + +The `defer()` function enables deferred rendering for React Server Components, allowing content to be streamed progressively to the client. + +## Import + +```typescript +import { defer } from "@funstack/static/server"; +``` + +## Usage + +```tsx +import { defer } from "@funstack/static/server"; +import { route } from "@funstack/router/server"; +import HeavyComponent from "./HeavyComponent"; + +const routes = [ + route({ + path: "/", + component: defer(HeavyComponent), + }), +]; +``` + +## Signature + +```typescript +function defer>( + Component: T +): React.ReactNode; +``` + +### Parameters + +- **Component:** A React component to render with deferred streaming. + +### Returns + +A React node that will stream its content when rendered. + +## When to Use defer() + +Use `defer()` when you have components that: + +- Fetch data asynchronously (e.g., from APIs or databases) +- Perform expensive computations +- Include large amounts of content +- Have children that can be rendered independently + +```tsx +// Async component that fetches data +async function BlogPosts() { + const posts = await fetchPosts(); // Slow API call + + return ( +
    + {posts.map(post => ( +
  • {post.title}
  • + ))} +
+ ); +} + +// Wrap with defer() to stream +route({ + path: "/blog", + component: defer(BlogPosts), +}); +``` + +## How It Works + +1. **Without defer():** The entire page waits for all components to render before sending any HTML. + +2. **With defer():** + - The page shell renders immediately + - A placeholder is inserted for the deferred content + - The deferred content streams in once ready + - The placeholder is replaced with the actual content + +## Example: Mixing Static and Deferred Content + +```tsx +import { defer } from "@funstack/static/server"; + +// Fast, static header +function Header() { + return

My Blog

; +} + +// Slow, data-fetching component +async function RecentPosts() { + const posts = await fetchRecentPosts(); + return ; +} + +// Page component +function BlogPage() { + return ( +
+
{/* Renders immediately */} + {defer(RecentPosts)} {/* Streams when ready */} +
+ ); +} +``` + +## MDX Integration + +`defer()` works seamlessly with MDX files: + +```tsx +import { defer } from "@funstack/static/server"; +import BlogPost from "./BlogPost.mdx"; + +route({ + path: "/blog/post", + component: defer(BlogPost), +}); +``` + +## Best Practices + +1. **Defer at the route level** for pages with async data fetching +2. **Don't over-defer** - only use it when you have genuinely slow components +3. **Consider user experience** - streaming can cause layout shifts; design accordingly +4. **Test without defer** first to understand baseline performance + +## See Also + +- [funstackStatic()](/api/funstack-static) - Main plugin configuration +- [React Server Components](/concepts/rsc) - Understanding async components diff --git a/packages/docs/src/pages/api/FunstackStatic.mdx b/packages/docs/src/pages/api/FunstackStatic.mdx new file mode 100644 index 0000000..8401e96 --- /dev/null +++ b/packages/docs/src/pages/api/FunstackStatic.mdx @@ -0,0 +1,137 @@ +# funstackStatic() + +The `funstackStatic()` function is the main Vite plugin that enables React Server Components for static site generation. + +## Import + +```typescript +import { funstackStatic } from "@funstack/static"; +``` + +## Usage + +```typescript +// vite.config.ts +import { funstackStatic } from "@funstack/static"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + funstackStatic({ + root: "./src/root.tsx", + app: "./src/App.tsx", + }), + ], +}); +``` + +## Options + +### root (required) + +**Type:** `string` + +Path to the root component file. This component wraps your entire application and defines the HTML document structure (``, ``, ``). + +```typescript +funstackStatic({ + root: "./src/root.tsx", + // ... +}); +``` + +The root component receives `children` as a prop: + +```tsx +// src/root.tsx +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + My Site + + {children} + + ); +} +``` + +### app (required) + +**Type:** `string` + +Path to the app component file. This component defines your application's routes and content. + +```typescript +funstackStatic({ + root: "./src/root.tsx", + app: "./src/App.tsx", +}); +``` + +The app component typically uses the Router to define routes: + +```tsx +// src/App.tsx +import { Router } from "@funstack/router"; +import { route } from "@funstack/router/server"; + +const routes = [ + route({ path: "/", component: }), + route({ path: "/about", component: }), +]; + +export default function App() { + return ; +} +``` + +### publicOutDir (optional) + +**Type:** `string` +**Default:** `"dist/public"` + +Output directory for the generated static files. + +```typescript +funstackStatic({ + root: "./src/root.tsx", + app: "./src/App.tsx", + publicOutDir: "build/static", +}); +``` + +## Full Example + +```typescript +// vite.config.ts +import { funstackStatic } from "@funstack/static"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [ + funstackStatic({ + root: "./src/root.tsx", + app: "./src/App.tsx", + publicOutDir: "dist/public", + }), + ], +}); +``` + +## How It Works + +1. **Development:** The plugin starts a development server with hot module replacement. Your React Server Components run on-demand as you navigate. + +2. **Build:** During production build, the plugin: + - Discovers all routes defined in your app + - Renders each route to static HTML + - Outputs HTML files to the specified `publicOutDir` + - Handles streaming content with `defer()` + +## See Also + +- [Getting Started](/getting-started) - Quick start guide +- [defer()](/api/defer) - Deferred rendering for streaming +- [React Server Components](/concepts/rsc) - Understanding RSC diff --git a/packages/docs/src/pages/concepts/RSC.mdx b/packages/docs/src/pages/concepts/RSC.mdx new file mode 100644 index 0000000..acba11c --- /dev/null +++ b/packages/docs/src/pages/concepts/RSC.mdx @@ -0,0 +1,187 @@ +# React Server Components + +React Server Components (RSC) are a new paradigm for building React applications where components can run on the server (or at build time) rather than in the browser. + +## What Are Server Components? + +Server Components are React components that: + +- **Run on the server/build time** - Not in the browser +- **Can be async** - Use `async/await` directly in components +- **Have zero client bundle impact** - Their code never ships to the browser +- **Can access server-side resources** - Files, databases, environment variables + +```tsx +// This component runs at build time, not in the browser +async function UserList() { + // Direct file system access + const data = await fs.readFile("./data/users.json", "utf-8"); + const users = JSON.parse(data); + + return ( +
    + {users.map(user => ( +
  • {user.name}
  • + ))} +
+ ); +} +``` + +## Server Components vs Client Components + +| Feature | Server Components | Client Components | +|---------|------------------|-------------------| +| Runs on | Server/Build time | Browser | +| Can use hooks | No | Yes | +| Can use browser APIs | No | Yes | +| Can be async | Yes | No | +| Bundle size impact | Zero | Included | +| Data fetching | Direct | Via APIs | + +## FUNSTACK Static and RSC + +FUNSTACK Static leverages React Server Components for **static site generation**: + +1. Components run at **build time** (not on a live server) +2. Output is **pure static HTML** +3. No JavaScript framework runtime shipped by default +4. Perfect for documentation, blogs, marketing sites + +```tsx +// Build-time data fetching +async function BlogPost({ slug }: { slug: string }) { + // Runs during build, not at runtime + const content = await fetchMarkdownFile(`./posts/${slug}.md`); + + return ( +
+ +
+ ); +} +``` + +## Async Components + +One of the most powerful features of Server Components is native async support: + +```tsx +// No useEffect, no loading states - just async/await +async function WeatherWidget() { + const weather = await fetch("https://api.weather.com/current") + .then(r => r.json()); + + return ( +
+ {weather.temperature}° + {weather.condition} +
+ ); +} +``` + +> **Note:** In FUNSTACK Static, async data is fetched at build time. For truly dynamic data, you'll need client-side JavaScript. + +## Composing Server and Client Components + +While Server Components can't use hooks or browser APIs, they can render Client Components that do: + +```tsx +// Server Component (runs at build time) +async function ProductPage({ id }: { id: string }) { + const product = await fetchProduct(id); + + return ( +
+

{product.name}

+

{product.description}

+ + {/* Client Component for interactivity */} + +
+ ); +} +``` + +```tsx +// Client Component (runs in browser) +// Add "use client" directive at the top of your file + +import { useState } from "react"; + +export function AddToCartButton({ productId }: { productId: string }) { + const [added, setAdded] = useState(false); + + return ( + + ); +} +``` + +## Benefits for Static Sites + +### 1. Zero JavaScript by Default + +Server Components render to HTML. No React runtime ships unless you add Client Components. + +### 2. Build-Time Data Fetching + +Fetch data once at build time, not on every page view: + +```tsx +async function BlogIndex() { + // Fetched once during build + const posts = await fetchAllPosts(); + return ; +} +``` + +### 3. Direct File Access + +Read files, parse markdown, process images - all at build time: + +```tsx +async function Documentation() { + const files = await glob("./docs/**/*.md"); + const docs = await Promise.all( + files.map(f => parseMarkdown(f)) + ); + return ; +} +``` + +### 4. Type-Safe Data Flow + +Data flows from server to client with full TypeScript support: + +```tsx +interface Post { + id: string; + title: string; + content: string; +} + +async function BlogPost({ slug }: { slug: string }): Promise { + const post: Post = await fetchPost(slug); + return
; +} +``` + +## Limitations + +When using FUNSTACK Static, keep in mind: + +1. **Build-time only** - Data is fetched during build, not at runtime +2. **No request context** - No access to cookies, headers, or request data +3. **Static output** - Pages can't vary based on user or request + +For dynamic features, combine static pages with client-side JavaScript or consider a server-rendered solution. + +## See Also + +- [Getting Started](/getting-started) - Set up your first project +- [defer()](/api/defer) - Stream content progressively +- [funstackStatic()](/api/funstack-static) - Plugin configuration diff --git a/packages/docs/src/root.tsx b/packages/docs/src/root.tsx index b0d549e..c75e7c7 100644 --- a/packages/docs/src/root.tsx +++ b/packages/docs/src/root.tsx @@ -1,4 +1,5 @@ import type React from "react"; +import "./styles/globals.css"; export default function Root({ children }: { children: React.ReactNode }) { return ( @@ -7,6 +8,16 @@ export default function Root({ children }: { children: React.ReactNode }) { FUNSTACK Static - docs + + + {children} diff --git a/packages/docs/src/styles/globals.css b/packages/docs/src/styles/globals.css new file mode 100644 index 0000000..ab3578c --- /dev/null +++ b/packages/docs/src/styles/globals.css @@ -0,0 +1,238 @@ +/* CSS Variables */ +:root { + /* Colors - Light Mode */ + --color-bg: #ffffff; + --color-bg-secondary: #f8f9fc; + --color-bg-tertiary: #f1f3f9; + --color-text: #1a1a2e; + --color-text-secondary: #4a4a68; + --color-text-muted: #8888a4; + --color-border: #e2e4eb; + --color-border-light: #f0f1f5; + + /* Accent - Indigo/Purple Gradient */ + --color-accent: #6366f1; + --color-accent-light: #818cf8; + --color-accent-dark: #4f46e5; + --color-accent-bg: #eef2ff; + --gradient-accent: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + + /* Semantic Colors */ + --color-success: #10b981; + --color-warning: #f59e0b; + --color-error: #ef4444; + + /* Code Block Colors */ + --color-code-bg: #1e1e2e; + --color-code-text: #e2e8f0; + --color-code-comment: #6b7280; + --color-code-keyword: #c792ea; + --color-code-string: #a5d6ff; + --color-code-function: #82aaff; + + /* Typography */ + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; + --font-mono: "JetBrains Mono", "Fira Code", Consolas, monospace; + + /* Font Sizes */ + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.25rem; + --text-2xl: 1.5rem; + --text-3xl: 1.875rem; + --text-4xl: 2.25rem; + --text-5xl: 3rem; + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + --spacing-3xl: 4rem; + + /* Layout */ + --sidebar-width: 280px; + --header-height: 64px; + --content-max-width: 800px; + --page-max-width: 1400px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), + 0 2px 4px -1px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), + 0 4px 6px -2px rgba(0, 0, 0, 0.04); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), + 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 250ms ease; + --transition-slow: 350ms ease; +} + +/* Dark Mode */ +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #0f0f1a; + --color-bg-secondary: #1a1a2e; + --color-bg-tertiary: #252542; + --color-text: #f1f5f9; + --color-text-secondary: #cbd5e1; + --color-text-muted: #94a3b8; + --color-border: #2d2d4a; + --color-border-light: #1f1f38; + + --color-accent-bg: rgba(99, 102, 241, 0.15); + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), + 0 2px 4px -1px rgba(0, 0, 0, 0.2); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), + 0 4px 6px -2px rgba(0, 0, 0, 0.2); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), + 0 10px 10px -5px rgba(0, 0, 0, 0.3); + } +} + +/* Reset & Base Styles */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-sans); + font-size: var(--text-base); + line-height: 1.6; + color: var(--color-text); + background-color: var(--color-bg); +} + +/* Typography Base */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: 700; + line-height: 1.3; + color: var(--color-text); +} + +h1 { + font-size: var(--text-4xl); + letter-spacing: -0.02em; +} + +h2 { + font-size: var(--text-2xl); + letter-spacing: -0.01em; +} + +h3 { + font-size: var(--text-xl); +} + +h4 { + font-size: var(--text-lg); +} + +p { + color: var(--color-text-secondary); +} + +a { + color: var(--color-accent); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--color-accent-dark); +} + +code { + font-family: var(--font-mono); + font-size: 0.9em; + background-color: var(--color-bg-tertiary); + padding: 0.15em 0.4em; + border-radius: var(--radius-sm); +} + +pre { + font-family: var(--font-mono); + overflow-x: auto; +} + +pre code { + background: none; + padding: 0; +} + +/* Selection */ +::selection { + background-color: var(--color-accent); + color: white; +} + +/* Scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--color-bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: var(--radius-full); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-text-muted); +} + +/* Focus Styles */ +:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +/* Button Reset */ +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; +} + +/* List Reset */ +ul, +ol { + list-style: none; +} diff --git a/packages/docs/src/types/css.d.ts b/packages/docs/src/types/css.d.ts new file mode 100644 index 0000000..84e0170 --- /dev/null +++ b/packages/docs/src/types/css.d.ts @@ -0,0 +1,9 @@ +declare module "*.module.css" { + const classes: { [key: string]: string }; + export default classes; +} + +declare module "*.css" { + const content: string; + export default content; +} From 48f0b0a34dece2ce0f95a0839c45ff6d551f8aa7 Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 15:18:11 +0900 Subject: [PATCH 2/7] docs: fix mdx plugin usage --- packages/docs/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts index 712508a..d708f0f 100644 --- a/packages/docs/vite.config.ts +++ b/packages/docs/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ root: "./src/root.tsx", app: "./src/App.tsx", }), - mdx(), + { enforce: "pre", ...mdx() }, react({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }), ], }); From e1131167c6373c8e20d259854d2a955f9cd3ff58 Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 15:24:19 +0900 Subject: [PATCH 3/7] docs: update to emphasize SPAs --- packages/docs/src/pages/GettingStarted.mdx | 6 +- packages/docs/src/pages/Home.tsx | 33 ++++----- .../docs/src/pages/api/FunstackStatic.mdx | 10 +-- packages/docs/src/pages/concepts/RSC.mdx | 69 ++++++++++++------- 4 files changed, 69 insertions(+), 49 deletions(-) diff --git a/packages/docs/src/pages/GettingStarted.mdx b/packages/docs/src/pages/GettingStarted.mdx index ddac05f..4776b18 100644 --- a/packages/docs/src/pages/GettingStarted.mdx +++ b/packages/docs/src/pages/GettingStarted.mdx @@ -1,6 +1,6 @@ # Getting Started -Welcome to **FUNSTACK Static**! Build static sites powered by React Server Components with zero server runtime. +Welcome to **FUNSTACK Static**! Build high-performance Single Page Applications powered by React Server Components - no server required at runtime. ## Installation @@ -50,7 +50,7 @@ export default function Root({ children }: { children: React.ReactNode }) { - My Static Site + My App {children} @@ -97,7 +97,7 @@ Your site is now running at `http://localhost:5173`! npm run build ``` -Static HTML files are generated in the `dist/public` directory, ready to deploy to any static hosting provider. +Your SPA is pre-rendered to static files in the `dist/public` directory, ready to deploy to any static hosting provider. The app hydrates on the client for full SPA interactivity. ## Project Structure diff --git a/packages/docs/src/pages/Home.tsx b/packages/docs/src/pages/Home.tsx index 940708a..b3471a7 100644 --- a/packages/docs/src/pages/Home.tsx +++ b/packages/docs/src/pages/Home.tsx @@ -7,11 +7,11 @@ export const Home: React.FC = () => {
React Server Components -

Build Static Sites with RSC

+

SPAs Powered by RSC

- FUNSTACK Static brings the power of React Server Components to - static site generation. No server required, just pure static output - with modern DX. + Build high-performance Single Page Applications with React Server + Components. No server required at runtime - just pre-rendered HTML + with full SPA interactivity.

-

React Server Components

+

No Server Required

- Write components that run at build time. Fetch data, read files, - and render HTML without a server. + Deploy anywhere that serves static files. RSC benefits without + the complexity of server infrastructure.

@@ -198,10 +198,10 @@ export default {
-

File-Based Routing

+

Client-Side Navigation

- Flexible routing with @funstack/router. Define routes - declaratively or use file-based conventions. + Full SPA experience with @funstack/router. Instant page + transitions without full page reloads.

@@ -212,7 +212,8 @@ export default {

Ready to Get Started?

- Build your next static site with the power of React Server Components. + Build your next SPA with the performance benefits of React Server + Components.

Read the Documentation diff --git a/packages/docs/src/pages/api/FunstackStatic.mdx b/packages/docs/src/pages/api/FunstackStatic.mdx index 8401e96..8a6fa2b 100644 --- a/packages/docs/src/pages/api/FunstackStatic.mdx +++ b/packages/docs/src/pages/api/FunstackStatic.mdx @@ -1,6 +1,6 @@ # funstackStatic() -The `funstackStatic()` function is the main Vite plugin that enables React Server Components for static site generation. +The `funstackStatic()` function is the main Vite plugin that enables React Server Components for building SPAs without a runtime server. ## Import @@ -122,13 +122,13 @@ export default defineConfig({ ## How It Works -1. **Development:** The plugin starts a development server with hot module replacement. Your React Server Components run on-demand as you navigate. +1. **Development:** The plugin starts a development server with hot module replacement. Server Components run on-demand, and Client Components hot-reload instantly. 2. **Build:** During production build, the plugin: - Discovers all routes defined in your app - - Renders each route to static HTML - - Outputs HTML files to the specified `publicOutDir` - - Handles streaming content with `defer()` + - Pre-renders each route using React Server Components + - Bundles Client Components for hydration + - Outputs static files to `publicOutDir` that hydrate into a full SPA ## See Also diff --git a/packages/docs/src/pages/concepts/RSC.mdx b/packages/docs/src/pages/concepts/RSC.mdx index acba11c..3580f2a 100644 --- a/packages/docs/src/pages/concepts/RSC.mdx +++ b/packages/docs/src/pages/concepts/RSC.mdx @@ -41,12 +41,12 @@ async function UserList() { ## FUNSTACK Static and RSC -FUNSTACK Static leverages React Server Components for **static site generation**: +FUNSTACK Static leverages React Server Components to build **high-performance SPAs**: -1. Components run at **build time** (not on a live server) -2. Output is **pure static HTML** -3. No JavaScript framework runtime shipped by default -4. Perfect for documentation, blogs, marketing sites +1. Server Components run at **build time** to pre-render your app +2. Client Components hydrate in the browser for **full SPA interactivity** +3. No runtime server required - deploy anywhere that serves static files +4. Get RSC performance benefits without server infrastructure complexity ```tsx // Build-time data fetching @@ -121,41 +121,60 @@ export function AddToCartButton({ productId }: { productId: string }) { } ``` -## Benefits for Static Sites +## Benefits for SPAs -### 1. Zero JavaScript by Default +### 1. Instant Initial Load -Server Components render to HTML. No React runtime ships unless you add Client Components. +Server Components pre-render your app at build time. Users see content immediately while the SPA hydrates in the background. -### 2. Build-Time Data Fetching +### 2. Reduced Bundle Size -Fetch data once at build time, not on every page view: +Server Component code stays on the build server. Only Client Components and their dependencies ship to the browser. + +### 3. Build-Time Data Fetching + +Fetch data once at build time, embedded directly into your pre-rendered HTML: ```tsx async function BlogIndex() { - // Fetched once during build + // Fetched once during build, not on every page view const posts = await fetchAllPosts(); return ; } ``` -### 3. Direct File Access +### 4. Full SPA Interactivity -Read files, parse markdown, process images - all at build time: +After hydration, your app behaves like any SPA - client-side navigation, state management, and all the interactivity you need: ```tsx -async function Documentation() { - const files = await glob("./docs/**/*.md"); - const docs = await Promise.all( - files.map(f => parseMarkdown(f)) +// Client Component for interactive features +"use client"; + +import { useState } from "react"; +import { useNavigate } from "@funstack/router"; + +export function SearchBox() { + const [query, setQuery] = useState(""); + const navigate = useNavigate(); + + return ( + setQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + navigate(`/search?q=${query}`); + } + }} + /> ); - return ; } ``` -### 4. Type-Safe Data Flow +### 5. Type-Safe Data Flow -Data flows from server to client with full TypeScript support: +Data flows from Server Components to Client Components with full TypeScript support: ```tsx interface Post { @@ -170,15 +189,15 @@ async function BlogPost({ slug }: { slug: string }): Promise { } ``` -## Limitations +## Considerations When using FUNSTACK Static, keep in mind: -1. **Build-time only** - Data is fetched during build, not at runtime -2. **No request context** - No access to cookies, headers, or request data -3. **Static output** - Pages can't vary based on user or request +1. **Build-time data** - Server Component data is fetched during build. For runtime data, use Client Components with standard fetch patterns. +2. **No server context** - No access to cookies, headers, or request data in Server Components. +3. **Pre-rendered routes** - Each route is pre-rendered at build time. Dynamic route segments work, but the content is fixed at build. -For dynamic features, combine static pages with client-side JavaScript or consider a server-rendered solution. +This makes FUNSTACK Static ideal for apps where most content is known at build time, with Client Components handling any truly dynamic features. ## See Also From fdd6bec3a88317bba95bf3d3fdb619abbccfaab1 Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 15:44:46 +0900 Subject: [PATCH 4/7] docs: add syntax highlighting --- packages/docs/package.json | 8 +- .../components/CodeBlock/CodeBlock.module.css | 68 +++----- .../src/components/CodeBlock/CodeBlock.tsx | 41 +++-- packages/docs/src/pages/Home.module.css | 44 +----- packages/docs/src/pages/Home.tsx | 32 ++-- packages/docs/src/styles/globals.css | 24 +++ packages/docs/vite.config.ts | 18 ++- pnpm-lock.yaml | 148 ++++++++++++++++++ 8 files changed, 249 insertions(+), 134 deletions(-) diff --git a/packages/docs/package.json b/packages/docs/package.json index 4653eb6..f1a898b 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -13,9 +13,11 @@ "dependencies": { "@funstack/router": "0.0.3-alpha.1", "@funstack/static": "workspace:*", + "@shikijs/rehype": "^3.21.0", "@types/node": "catalog:", "react": "catalog:", - "react-dom": "catalog:" + "react-dom": "catalog:", + "shiki": "^3.21.0" }, "devDependencies": { "@mdx-js/rollup": "^3.1.0", @@ -24,7 +26,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "latest", "rsc-html-stream": "^0.0.7", - "vite": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "vite": "catalog:" } } diff --git a/packages/docs/src/components/CodeBlock/CodeBlock.module.css b/packages/docs/src/components/CodeBlock/CodeBlock.module.css index 9ad4cff..5076552 100644 --- a/packages/docs/src/components/CodeBlock/CodeBlock.module.css +++ b/packages/docs/src/components/CodeBlock/CodeBlock.module.css @@ -5,60 +5,36 @@ overflow: hidden; } -.header { - display: flex; - align-items: center; - justify-content: space-between; - padding: var(--spacing-sm) var(--spacing-md); - background-color: #161622; - border-bottom: 1px solid #2d2d4a; -} - -.language { - font-family: var(--font-mono); - font-size: var(--text-xs); - color: var(--color-text-muted); - text-transform: uppercase; -} - -.copyButton { - display: flex; - align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-xs) var(--spacing-sm); - font-size: var(--text-xs); - color: var(--color-text-muted); - background-color: transparent; - border: 1px solid #2d2d4a; - border-radius: var(--radius-sm); - cursor: pointer; - transition: - color var(--transition-fast), - border-color var(--transition-fast); -} - -.copyButton:hover { - color: var(--color-text); - border-color: var(--color-text-muted); -} - -.copyButton svg { - width: 14px; - height: 14px; +.codeContent { + font-size: var(--text-sm); + line-height: 1.6; } -.pre { +/* Style Shiki's generated pre element */ +.codeContent :global(pre) { margin: 0; padding: var(--spacing-lg); - background-color: var(--color-code-bg); overflow-x: auto; + border-radius: var(--radius-lg); } -.code { +.codeContent :global(code) { font-family: var(--font-mono); - font-size: var(--text-sm); - line-height: 1.6; - color: var(--color-code-text); +} + +/* Light/dark theme switching for Shiki dual themes */ +.codeContent :global(.shiki), +.codeContent :global(.shiki span) { + color: var(--shiki-light); + background-color: var(--shiki-light-bg); +} + +@media (prefers-color-scheme: dark) { + .codeContent :global(.shiki), + .codeContent :global(.shiki span) { + color: var(--shiki-dark); + background-color: var(--shiki-dark-bg); + } } /* Inline code */ diff --git a/packages/docs/src/components/CodeBlock/CodeBlock.tsx b/packages/docs/src/components/CodeBlock/CodeBlock.tsx index a6c30ea..8f352a6 100644 --- a/packages/docs/src/components/CodeBlock/CodeBlock.tsx +++ b/packages/docs/src/components/CodeBlock/CodeBlock.tsx @@ -1,42 +1,37 @@ -import type React from "react"; +import { codeToHtml } from "shiki"; import styles from "./CodeBlock.module.css"; interface CodeBlockProps { children: string; language?: string; - showLineNumbers?: boolean; } -export const CodeBlock: React.FC = ({ +export async function CodeBlock({ children, language = "typescript", -}) => { +}: CodeBlockProps) { + const html = await codeToHtml(children.trim(), { + lang: language, + themes: { + light: "github-light", + dark: "github-dark", + }, + }); + return (
-
- {language} - -
-
-        {children}
-      
+
); -}; +} interface InlineCodeProps { children: React.ReactNode; } -export const InlineCode: React.FC = ({ children }) => { +export function InlineCode({ children }: InlineCodeProps) { return {children}; -}; +} diff --git a/packages/docs/src/pages/Home.module.css b/packages/docs/src/pages/Home.module.css index e0a21b4..81ccfb2 100644 --- a/packages/docs/src/pages/Home.module.css +++ b/packages/docs/src/pages/Home.module.css @@ -109,53 +109,11 @@ max-width: 700px; margin: var(--spacing-3xl) auto 0; text-align: left; - background-color: var(--color-code-bg); - border-radius: var(--radius-lg); box-shadow: var(--shadow-xl); + border-radius: var(--radius-lg); overflow: hidden; } -.codeHeader { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-md); - background-color: #161622; - border-bottom: 1px solid #2d2d4a; -} - -.codeDot { - width: 12px; - height: 12px; - border-radius: 50%; -} - -.codeDotRed { - background-color: #ff5f56; -} - -.codeDotYellow { - background-color: #ffbd2e; -} - -.codeDotGreen { - background-color: #27c93f; -} - -.codeContent { - padding: var(--spacing-lg); - font-family: var(--font-mono); - font-size: var(--text-sm); - line-height: 1.6; - color: var(--color-code-text); - overflow-x: auto; -} - -.codeContent code { - background: none; - padding: 0; -} - /* Features Section */ .features { padding: var(--spacing-3xl) var(--spacing-xl); diff --git a/packages/docs/src/pages/Home.tsx b/packages/docs/src/pages/Home.tsx index b3471a7..4d46b0b 100644 --- a/packages/docs/src/pages/Home.tsx +++ b/packages/docs/src/pages/Home.tsx @@ -1,5 +1,18 @@ +import { CodeBlock } from "../components/CodeBlock/CodeBlock"; import styles from "./Home.module.css"; +const heroCode = `// vite.config.ts +import { funstackStatic } from "@funstack/static"; + +export default { + plugins: [ + funstackStatic({ + root: "./src/root.tsx", + app: "./src/App.tsx", + }), + ], +};`; + export const Home: React.FC = () => { return (
@@ -45,24 +58,7 @@ export const Home: React.FC = () => { {/* Code Preview */}
-
- - - -
-
-              {`// vite.config.ts
-import { funstackStatic } from "@funstack/static";
-
-export default {
-  plugins: [
-    funstackStatic({
-      root: "./src/root.tsx",
-      app: "./src/App.tsx",
-    }),
-  ],
-};`}
-            
+ {heroCode}
diff --git a/packages/docs/src/styles/globals.css b/packages/docs/src/styles/globals.css index ab3578c..0d3be21 100644 --- a/packages/docs/src/styles/globals.css +++ b/packages/docs/src/styles/globals.css @@ -192,6 +192,30 @@ pre code { padding: 0; } +/* Shiki Code Blocks (MDX) */ +pre.shiki { + padding: var(--spacing-lg); + border-radius: var(--radius-lg); + font-size: var(--text-sm); + line-height: 1.6; + margin: var(--spacing-lg) 0; +} + +/* Shiki dual theme switching */ +.shiki, +.shiki span { + color: var(--shiki-light); + background-color: var(--shiki-light-bg); +} + +@media (prefers-color-scheme: dark) { + .shiki, + .shiki span { + color: var(--shiki-dark); + background-color: var(--shiki-dark-bg); + } +} + /* Selection */ ::selection { background-color: var(--color-accent); diff --git a/packages/docs/vite.config.ts b/packages/docs/vite.config.ts index d708f0f..c3f27ae 100644 --- a/packages/docs/vite.config.ts +++ b/packages/docs/vite.config.ts @@ -1,5 +1,6 @@ import funstackStatic from "@funstack/static"; import mdx from "@mdx-js/rollup"; +import rehypeShiki from "@shikijs/rehype"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; @@ -9,7 +10,22 @@ export default defineConfig({ root: "./src/root.tsx", app: "./src/App.tsx", }), - { enforce: "pre", ...mdx() }, + { + enforce: "pre", + ...mdx({ + rehypePlugins: [ + [ + rehypeShiki, + { + themes: { + light: "github-light", + dark: "github-dark", + }, + }, + ], + ], + }), + }, react({ include: /\.(jsx|js|mdx|md|tsx|ts)$/ }), ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 791250b..8b22584 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@funstack/static': specifier: workspace:* version: link:../static + '@shikijs/rehype': + specifier: ^3.21.0 + version: 3.21.0 '@types/node': specifier: 'catalog:' version: 25.0.6 @@ -50,6 +53,9 @@ importers: react-dom: specifier: 'catalog:' version: 19.2.3(react@19.2.3) + shiki: + specifier: ^3.21.0 + version: 3.21.0 devDependencies: '@mdx-js/rollup': specifier: ^3.1.0 @@ -711,6 +717,30 @@ packages: cpu: [x64] os: [win32] + '@shikijs/core@3.21.0': + resolution: {integrity: sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==} + + '@shikijs/engine-javascript@3.21.0': + resolution: {integrity: sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==} + + '@shikijs/engine-oniguruma@3.21.0': + resolution: {integrity: sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==} + + '@shikijs/langs@3.21.0': + resolution: {integrity: sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==} + + '@shikijs/rehype@3.21.0': + resolution: {integrity: sha512-fTQvwsZL67QdosMFdTgQ5SNjW3nxaPplRy//312hqOctRbIwviTV0nAbhv3NfnztHXvFli2zLYNKsTz/f9tbpQ==} + + '@shikijs/themes@3.21.0': + resolution: {integrity: sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==} + + '@shikijs/types@3.21.0': + resolution: {integrity: sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1177,9 +1207,15 @@ packages: hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -1190,6 +1226,9 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1425,6 +1464,12 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -1506,6 +1551,15 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} @@ -1583,6 +1637,9 @@ packages: serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + shiki@3.21.0: + resolution: {integrity: sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2429,6 +2486,48 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.55.1': optional: true + '@shikijs/core@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/rehype@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + '@types/hast': 3.0.4 + hast-util-to-string: 3.0.1 + shiki: 3.21.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + '@shikijs/themes@3.21.0': + dependencies: + '@shikijs/types': 3.21.0 + + '@shikijs/types@3.21.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@standard-schema/spec@1.1.0': {} '@tybys/wasm-util@0.10.1': @@ -2988,6 +3087,20 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -3008,6 +3121,10 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -3020,6 +3137,8 @@ snapshots: transitivePeerDependencies: - '@exodus/crypto' + html-void-elements@3.0.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -3452,6 +3571,14 @@ snapshots: obug@2.1.1: {} + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -3549,6 +3676,16 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + rehype-recma@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -3679,6 +3816,17 @@ snapshots: randombytes: 2.1.0 optional: true + shiki@3.21.0: + dependencies: + '@shikijs/core': 3.21.0 + '@shikijs/engine-javascript': 3.21.0 + '@shikijs/engine-oniguruma': 3.21.0 + '@shikijs/langs': 3.21.0 + '@shikijs/themes': 3.21.0 + '@shikijs/types': 3.21.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} source-map-js@1.2.1: {} From d1f9200f175a33034128310c5f82d2fc449a1ebf Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 15:55:11 +0900 Subject: [PATCH 5/7] docs: fix syntax highlighting --- .../components/CodeBlock/CodeBlock.module.css | 17 +++++++++-------- packages/docs/src/styles/globals.css | 16 ++++++++-------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/docs/src/components/CodeBlock/CodeBlock.module.css b/packages/docs/src/components/CodeBlock/CodeBlock.module.css index 5076552..0ef9e4c 100644 --- a/packages/docs/src/components/CodeBlock/CodeBlock.module.css +++ b/packages/docs/src/components/CodeBlock/CodeBlock.module.css @@ -22,18 +22,19 @@ font-family: var(--font-mono); } -/* Light/dark theme switching for Shiki dual themes */ -.codeContent :global(.shiki), -.codeContent :global(.shiki span) { - color: var(--shiki-light); - background-color: var(--shiki-light-bg); +/* Shiki code styling */ +.codeContent :global(.shiki code) { + background: transparent; } +/* Dark mode: override Shiki's default light colors */ @media (prefers-color-scheme: dark) { - .codeContent :global(.shiki), + .codeContent :global(.shiki) { + background-color: var(--shiki-dark-bg) !important; + } + .codeContent :global(.shiki span) { - color: var(--shiki-dark); - background-color: var(--shiki-dark-bg); + color: var(--shiki-dark) !important; } } diff --git a/packages/docs/src/styles/globals.css b/packages/docs/src/styles/globals.css index 0d3be21..8fb12cc 100644 --- a/packages/docs/src/styles/globals.css +++ b/packages/docs/src/styles/globals.css @@ -201,18 +201,18 @@ pre.shiki { margin: var(--spacing-lg) 0; } -/* Shiki dual theme switching */ -.shiki, -.shiki span { - color: var(--shiki-light); - background-color: var(--shiki-light-bg); +.shiki code { + background: transparent; } +/* Dark mode: override Shiki's default light colors */ @media (prefers-color-scheme: dark) { - .shiki, + pre.shiki { + background-color: var(--shiki-dark-bg) !important; + } + .shiki span { - color: var(--shiki-dark); - background-color: var(--shiki-dark-bg); + color: var(--shiki-dark) !important; } } From 1c04017ecff6e9d66952f43597bbb8cde87b14df Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 16:09:23 +0900 Subject: [PATCH 6/7] docs: update Getting Started --- packages/docs/src/pages/GettingStarted.mdx | 34 ++++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/docs/src/pages/GettingStarted.mdx b/packages/docs/src/pages/GettingStarted.mdx index 4776b18..639a517 100644 --- a/packages/docs/src/pages/GettingStarted.mdx +++ b/packages/docs/src/pages/GettingStarted.mdx @@ -24,24 +24,26 @@ Create or update your `vite.config.ts`: ```typescript import { funstackStatic } from "@funstack/static"; +import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ funstackStatic({ - root: "./src/root.tsx", + root: "./src/Root.tsx", app: "./src/App.tsx", }), + react(), ], }); ``` ### 2. Create Your Root Component -The root component wraps your entire application and defines the HTML structure: +The Root component wraps your entire application and defines the HTML structure. ```tsx -// src/root.tsx +// src/Root.tsx import type React from "react"; export default function Root({ children }: { children: React.ReactNode }) { @@ -58,9 +60,17 @@ export default function Root({ children }: { children: React.ReactNode }) { } ``` +The Root component: + +- is responsible for defining the shell HTML structure of your app +- is a server component +- **CANNOT** import client components; you could, but they are fully rendered into static HTML and never hydrated + ### 3. Create Your App Component -The app component defines your routes and page content: +The App component defines what comes into the `children` of the Root. + +This is the entrypoint of your SPA and you can do anything you want. If you want routing, you can bring your favorite SPA router. Here, we use `@funstack/router` as an example: ```tsx // src/App.tsx @@ -83,6 +93,11 @@ export default function App() { } ``` +The App component: + +- is a server component +- **CAN** import client components and use them within the app + ### 4. Start Development Server ```bash @@ -97,7 +112,7 @@ Your site is now running at `http://localhost:5173`! npm run build ``` -Your SPA is pre-rendered to static files in the `dist/public` directory, ready to deploy to any static hosting provider. The app hydrates on the client for full SPA interactivity. +Your SPA is pre-rendered to static files in the `dist/public` directory, ready to deploy to any static hosting provider. ## Project Structure @@ -106,15 +121,16 @@ A typical FUNSTACK Static project looks like this: ``` my-site/ ├── src/ -│ ├── root.tsx # HTML wrapper component -│ ├── App.tsx # Routes and main app -│ ├── components/ # Your React components -│ └── pages/ # Page components +│ ├── ... +│ ├── Root.tsx # HTML wrapper component +│ └── App.tsx # Routes and main app ├── public/ # Static assets ├── vite.config.ts # Vite configuration └── package.json ``` +Only two files are required: `Root.tsx` and `App.tsx`. The paths to these files can be customized in the `funstackStatic()` configuration. + ## What's Next? - Learn about the [funstackStatic() API](/api/funstack-static) for configuration options From be49e5c3cdb05f55b32898c3c80901085beec594 Mon Sep 17 00:00:00 2001 From: uhyo Date: Sun, 18 Jan 2026 16:12:16 +0900 Subject: [PATCH 7/7] chore: fix formatting --- .claude/format-hook.sh | 2 +- packages/docs/src/pages/api/Defer.mdx | 10 ++++++--- packages/docs/src/pages/concepts/RSC.mdx | 23 +++++++++---------- packages/docs/src/styles/globals.css | 28 ++++++++++++------------ 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/.claude/format-hook.sh b/.claude/format-hook.sh index bcf2dd8..aa6bdcb 100755 --- a/.claude/format-hook.sh +++ b/.claude/format-hook.sh @@ -3,6 +3,6 @@ file_path=$(jq -r '.tool_input.file_path') # Check if file matches JS/TS extensions -if [[ "$file_path" =~ \.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$ ]]; then +if [[ "$file_path" =~ \.(ts|tsx|js|jsx|mjs|cjs|mts|cts|css|mdx)$ ]]; then pnpm exec prettier --write "$file_path" fi diff --git a/packages/docs/src/pages/api/Defer.mdx b/packages/docs/src/pages/api/Defer.mdx index a88b8dc..9b985ab 100644 --- a/packages/docs/src/pages/api/Defer.mdx +++ b/packages/docs/src/pages/api/Defer.mdx @@ -27,7 +27,7 @@ const routes = [ ```typescript function defer>( - Component: T + Component: T, ): React.ReactNode; ``` @@ -55,7 +55,7 @@ async function BlogPosts() { return (
    - {posts.map(post => ( + {posts.map((post) => (
  • {post.title}
  • ))}
@@ -86,7 +86,11 @@ import { defer } from "@funstack/static/server"; // Fast, static header function Header() { - return

My Blog

; + return ( +
+

My Blog

+
+ ); } // Slow, data-fetching component diff --git a/packages/docs/src/pages/concepts/RSC.mdx b/packages/docs/src/pages/concepts/RSC.mdx index 3580f2a..670944b 100644 --- a/packages/docs/src/pages/concepts/RSC.mdx +++ b/packages/docs/src/pages/concepts/RSC.mdx @@ -20,7 +20,7 @@ async function UserList() { return (
    - {users.map(user => ( + {users.map((user) => (
  • {user.name}
  • ))}
@@ -30,14 +30,14 @@ async function UserList() { ## Server Components vs Client Components -| Feature | Server Components | Client Components | -|---------|------------------|-------------------| -| Runs on | Server/Build time | Browser | -| Can use hooks | No | Yes | -| Can use browser APIs | No | Yes | -| Can be async | Yes | No | -| Bundle size impact | Zero | Included | -| Data fetching | Direct | Via APIs | +| Feature | Server Components | Client Components | +| -------------------- | ----------------- | ----------------- | +| Runs on | Server/Build time | Browser | +| Can use hooks | No | Yes | +| Can use browser APIs | No | Yes | +| Can be async | Yes | No | +| Bundle size impact | Zero | Included | +| Data fetching | Direct | Via APIs | ## FUNSTACK Static and RSC @@ -69,8 +69,9 @@ One of the most powerful features of Server Components is native async support: ```tsx // No useEffect, no loading states - just async/await async function WeatherWidget() { - const weather = await fetch("https://api.weather.com/current") - .then(r => r.json()); + const weather = await fetch("https://api.weather.com/current").then((r) => + r.json(), + ); return (
diff --git a/packages/docs/src/styles/globals.css b/packages/docs/src/styles/globals.css index 8fb12cc..83e2474 100644 --- a/packages/docs/src/styles/globals.css +++ b/packages/docs/src/styles/globals.css @@ -31,8 +31,8 @@ --color-code-function: #82aaff; /* Typography */ - --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - sans-serif; + --font-sans: + "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; --font-mono: "JetBrains Mono", "Fira Code", Consolas, monospace; /* Font Sizes */ @@ -63,12 +63,12 @@ /* Shadows */ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), - 0 2px 4px -1px rgba(0, 0, 0, 0.04); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), - 0 4px 6px -2px rgba(0, 0, 0, 0.04); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), - 0 10px 10px -5px rgba(0, 0, 0, 0.04); + --shadow-md: + 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -1px rgba(0, 0, 0, 0.04); + --shadow-lg: + 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04); + --shadow-xl: + 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); /* Border Radius */ --radius-sm: 4px; @@ -98,12 +98,12 @@ --color-accent-bg: rgba(99, 102, 241, 0.15); --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2); - --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3), - 0 2px 4px -1px rgba(0, 0, 0, 0.2); - --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), - 0 4px 6px -2px rgba(0, 0, 0, 0.2); - --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), - 0 10px 10px -5px rgba(0, 0, 0, 0.3); + --shadow-md: + 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); + --shadow-lg: + 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -2px rgba(0, 0, 0, 0.2); + --shadow-xl: + 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.3); } }