[Docs][Content] Github install, UI changes, platform selection (#1098)

## Summary

This PR improves the documentation for GitHub authentication setup and
self-hosting.

## Changes

### GitHub OAuth/App Setup Guide
- Updated
[github.mdx](cci:7://file:///Users/madison/source/stack-auth/docs/content/docs/%28guides%29/concepts/auth-providers/github.mdx:0:0-0:0)
with clearer instructions differentiating between **GitHub OAuth App**
and **GitHub App** setup
- Added better explanations for when to use each option

### Self-Hosting Documentation
- Added prominent danger warning about self-hosting responsibilities
- Migrated inline shell commands to structured code examples using
[PlatformCodeblock](cci:1://file:///Users/madison/source/stack-auth/docs/src/components/mdx/platform-codeblock.tsx:242:0-673:1)
component
- Created
[docs/code-examples/self-host.ts](cci:7://file:///Users/madison/source/stack-auth/docs/code-examples/self-host.ts:0:0-0:0)
with all self-hosting commands

### Info Component
- Added new `danger` type for critical warnings with red accent styling
- Updated component styling with modern left accent bar and gradient
backgrounds

### PlatformCodeblock Component
- Added `hidePlatformSelector` prop to hide platform dropdown for
single-platform code examples
- Added Shell platform support for terminal commands (Docker, Git, pnpm)
- Filtered Shell platform from user-selectable options in both the
codeblock and header selectors

### Platform Config
- Added Shell platform with Docker, Git, and pnpm frameworks

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added comprehensive self-hosting and authentication customization
example collections for copy-paste use.
  * New "danger" info style with visual accent for important warnings.

* **Documentation**
* GitHub integration guide now centers on GitHub App with an alternate
OAuth path retained.
* Replaced many inline snippets with platform-driven code blocks and
improved platform/framework selector behavior (single-platform
optimization; option to hide selector).
  * Pages now surface "Last updated" above descriptions.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Madison 2026-01-20 11:49:08 -06:00 committed by GitHub
parent 4d42f9aa66
commit d0173af691
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 526 additions and 307 deletions

View File

@ -0,0 +1,207 @@
import { CodeExample } from '../lib/code-examples';
export const customizationExamples = {
'sign-in': {
'default': [
{
language: 'JavaScript',
framework: 'Next.js',
code: `'use client';
import { SignIn } from "@stackframe/stack";
export default function DefaultSignIn() {
// optionally redirect to some other page if the user is already signed in
// const user = useUser();
// if (user) { redirect to some other page }
return <SignIn fullPage />;
}`,
highlightLanguage: 'tsx',
filename: 'app/handler/sign-in/page.tsx',
},
] as CodeExample[],
'custom-oauth': [
{
language: 'JavaScript',
framework: 'Next.js',
code: `'use client';
import { useStackApp } from "@stackframe/stack";
export default function CustomOAuthSignIn() {
const app = useStackApp();
return (
<div>
<h1>My Custom Sign In page</h1>
<button onClick={async () => {
// This will redirect to the OAuth provider's login page.
await app.signInWithOAuth('google');
}}>
Sign In with Google
</button>
</div>
);
}`,
highlightLanguage: 'tsx',
filename: 'app/handler/sign-in/page.tsx',
},
] as CodeExample[],
'custom-credential': [
{
language: 'JavaScript',
framework: 'Next.js',
code: `'use client';
import { useStackApp } from "@stackframe/stack";
import { useState } from "react";
export default function CustomCredentialSignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const app = useStackApp();
const onSubmit = async () => {
if (!password) {
setError('Please enter your password');
return;
}
// This will redirect to app.urls.afterSignIn if successful.
// You can customize the redirect URL in the StackServerApp constructor.
const result = await app.signInWithCredential({ email, password });
// It's better to handle each error code separately, but for simplicity,
// we'll just display the error message directly here.
if (result.status === 'error') {
setError(result.error.message);
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
{error}
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type='submit'>Sign In</button>
</form>
);
}`,
highlightLanguage: 'tsx',
filename: 'app/handler/sign-in/page.tsx',
},
] as CodeExample[],
'custom-magic-link': [
{
language: 'JavaScript',
framework: 'Next.js',
code: `'use client';
import { useStackApp } from "@stackframe/stack";
import { useState } from "react";
export default function CustomMagicLinkSignIn() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const app = useStackApp();
const onSubmit = async () => {
if (!email) {
setError('Please enter your email address');
return;
}
// This will send a magic link email to the user's email address.
// When they click the link, they will be redirected to your application.
const result = await app.sendMagicLinkEmail(email);
// It's better to handle each error code separately, but for simplicity,
// we'll just display the error message directly here.
if (result.status === 'error') {
setError(result.error.message);
} else {
setMessage('Magic link sent! Please check your email.');
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
{error}
{message ?
<div>{message}</div> :
<>
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
<button type='submit'>Send Magic Link</button>
</>}
</form>
);
}`,
highlightLanguage: 'tsx',
filename: 'app/handler/sign-in/page.tsx',
},
] as CodeExample[],
},
'sign-up': {
'default': [
{
language: 'JavaScript',
framework: 'Next.js',
code: `'use client';
import { SignUp } from "@stackframe/stack";
export default function DefaultSignUp() {
// optionally redirect to some other page if the user is already signed in
// const user = useUser();
// if (user) { redirect to some other page }
return <SignUp fullPage />;
}`,
highlightLanguage: 'tsx',
filename: 'app/handler/sign-up/page.tsx',
},
] as CodeExample[],
'custom-credential': [
{
language: 'JavaScript',
framework: 'Next.js',
code: `'use client';
import { useStackApp } from "@stackframe/stack";
import { useState } from "react";
export default function CustomCredentialSignUp() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const app = useStackApp();
const onSubmit = async () => {
if (!password) {
setError('Please enter your password');
return;
}
// This will redirect to app.urls.afterSignUp if successful.
// You can customize the redirect URL in the StackServerApp constructor.
const result = await app.signUpWithCredential({ email, password });
// It's better to handle each error code separately, but for simplicity,
// we'll just display the error message directly here.
if (result.status === 'error') {
setError(result.error.message);
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
{error}
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type='submit'>Sign Up</button>
</form>
);
}`,
highlightLanguage: 'tsx',
filename: 'app/handler/sign-up/page.tsx',
},
] as CodeExample[],
},
};

View File

@ -1,6 +1,8 @@
import { CodeExample } from '../lib/code-examples';
import { apiKeysExamples } from './api-keys';
import { customizationExamples } from './customization';
import { paymentsExamples } from './payments';
import { selfHostExamples } from './self-host';
import { setupExamples } from './setup';
import { viteExamples } from './vite-example';
@ -8,9 +10,8 @@ const allExamples: Record<string, Record<string, Record<string, CodeExample[]>>>
'setup': setupExamples,
'apps': {...apiKeysExamples, ...paymentsExamples },
'getting-started': viteExamples,
// Add more sections here as needed:
// 'auth': authExamples,
// 'customization': customizationExamples,
'others': selfHostExamples,
'customization': customizationExamples,
};
export function getExample(documentPath: string, exampleName: string): CodeExample[] | undefined {

View File

@ -0,0 +1,106 @@
import { CodeExample } from '../lib/code-examples';
export const selfHostExamples = {
'self-host': {
'docker-postgres': [
{
language: 'Shell',
framework: 'Docker',
code: `docker run -d --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=stackframe -p 5432:5432 postgres:latest`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'docker-run': [
{
language: 'Shell',
framework: 'Docker',
code: `docker run --env-file <your-env-file.env> -p 8101:8101 -p 8102:8102 stackauth/server:latest`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'git-clone': [
{
language: 'Shell',
framework: 'Git',
code: `git clone git@github.com:stack-auth/stack-auth.git
cd stack-auth`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'local-dev-setup': [
{
language: 'Shell',
framework: 'pnpm',
code: `pnpm install
# Run build to build everything once
pnpm run build:dev
# reset & start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema
pnpm run start-deps
# pnpm run restart-deps
# pnpm run stop-deps
# Start the dev server
pnpm run dev
# For systems with limited resources, you can run a minimal development setup with just the backend and dashboard
# pnpm run dev:basic
# In a different terminal, run tests in watch mode
pnpm run test`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'prisma-studio': [
{
language: 'Shell',
framework: 'pnpm',
code: `pnpm run prisma studio`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'backend-build': [
{
language: 'Shell',
framework: 'pnpm',
code: `pnpm install
pnpm build:backend
pnpm start:backend`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'dashboard-build': [
{
language: 'Shell',
framework: 'pnpm',
code: `pnpm install
pnpm build:dashboard
pnpm start:dashboard`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
'db-init': [
{
language: 'Shell',
framework: 'pnpm',
code: `pnpm db:init`,
highlightLanguage: 'bash',
filename: 'Terminal'
}
] as CodeExample[],
}
};

View File

@ -1,8 +1,9 @@
---
title: "GitHub"
lastModified: "2026-01-13"
---
This guide explains how to set up GitHub as an authentication provider with Stack Auth. GitHub OAuth allows users to sign in to your application using their GitHub account.
This guide explains how to set up GitHub as an authentication provider with Stack Auth. GitHub allows users to sign in to your Stack Auth-enabled app using their GitHub account.
<Info>
For Development purposes, Stack Auth uses shared keys for this provider. Shared keys are automatically created by Stack, but show Stack's logo on the OAuth sign-in page.
@ -11,9 +12,40 @@ You should replace these before you go into production.
## Integration Steps
<Info>
If you are unsure if you need to create a GitHub App, or a GitHub OAuth App, check the [Differences On GitHub](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/differences-between-github-apps-and-oauth-apps).
More than likely, you will want to create a GitHub App. The installation process is the same for both.
</Info>
<Steps>
<Step>
### Create a GitHub OAuth App
### Create a GitHub App
1. Navigate to your [GitHub Developer App Settings](https://github.com/settings/apps).
2. Click the **New GitHub App** button.
3. Enter a name for your application, homepage URL, and a description.
4. For **Authorization callback URL**, add `https://api.stack-auth.com/api/v1/auth/oauth/callback/github`
5. For permissions, at a **minimum**, you will need **Account Permissions > Email Addresses** set to **Read Only**. Your sign-in flow will not work without this permission.
6. Select **Any Account** under the **_Where can this GitHub App be installed_** section.
7. Click **Create GitHub App**
8. Save the **Client ID** and click **Generate a new client secret** to create your **Client Secret**.
</Step>
<Step>
### Enable GitHub Provider in Stack Auth
1. On the Stack Auth dashboard, select **Auth Methods** in the left sidebar.
2. Click **Add SSO Providers** and select **GitHub** as the provider.
3. Set the **Client ID** and **Client Secret** you obtained from your GitHub App earlier.
</Step>
</Steps>
---
<Accordion title="GitHub OAuth App installation">
<Steps>
<Step>
### Create an OAuth App
1. Navigate to your [GitHub Developer Settings](https://github.com/settings/developers).
2. Click the **New OAuth App** button.
@ -31,6 +63,7 @@ You should replace these before you go into production.
3. Set the **Client ID** and **Client Secret** you obtained from GitHub earlier.
</Step>
</Steps>
</Accordion>
### Need More Help?

View File

@ -1,24 +1,16 @@
---
title: Sign-In Page Examples
lastModified: "2026-01-12"
---
# Sign-In Page Examples
This page provides examples of how to create custom sign-in pages for your application using Stack Auth components and functions.
## Custom page with `SignIn` component
```tsx
'use client';
import { SignIn } from "@stackframe/stack";
export default function DefaultSignIn() {
// optionally redirect to some other page if the user is already signed in
// const user = useUser();
// if (user) { redirect to some other page }
return <SignIn fullPage />;
}
```
<PlatformCodeblock
document="customization/sign-in"
examples={["default"]}
/>
You can also use `useUser` at the beginning of the sign-in page to check whether the user is already signed in and redirect them to another page if they are.
@ -34,108 +26,21 @@ You can also use `useUser` at the beginning of the sign-in page to check whether
## Custom OAuth Sign In
```tsx
'use client';
import { useStackApp } from "@stackframe/stack";
export default function CustomOAuthSignIn() {
const app = useStackApp();
return (
<div>
<h1>My Custom Sign In page</h1>
<button onClick={async () => {
// This will redirect to the OAuth provider's login page.
await app.signInWithOAuth('google');
}}>
Sign In with Google
</button>
</div>
);
}
```
<PlatformCodeblock
document="customization/sign-in"
examples={["custom-oauth"]}
/>
## Custom Credential Sign In
```tsx
'use client';
import { useStackApp } from "@stackframe/stack";
import { useState } from "react";
export default function CustomCredentialSignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const app = useStackApp();
const onSubmit = async () => {
if (!password) {
setError('Please enter your password');
return;
}
// This will redirect to app.urls.afterSignIn if successful.
// You can customize the redirect URL in the StackServerApp constructor.
const result = await app.signInWithCredential({ email, password });
// It's better to handle each error code separately, but for simplicity,
// we'll just display the error message directly here.
if (result.status === 'error') {
setError(result.error.message);
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
{error}
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type='submit'>Sign In</button>
</form>
);
}
```
<PlatformCodeblock
document="customization/sign-in"
examples={["custom-credential"]}
/>
## Custom Magic Link Sign In
```tsx
'use client';
import { useStackApp } from "@stackframe/stack";
import { useState } from "react";
export default function CustomMagicLinkSignIn() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const app = useStackApp();
const onSubmit = async () => {
if (!email) {
setError('Please enter your email address');
return;
}
// This will send a magic link email to the user's email address.
// When they click the link, they will be redirected to your application.
const result = await app.sendMagicLinkEmail(email);
// It's better to handle each error code separately, but for simplicity,
// we'll just display the error message directly here.
if (result.status === 'error') {
setError(result.error.message);
} else {
setMessage('Magic link sent! Please check your email.');
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
{error}
{message ?
<div>{message}</div> :
<>
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
<button type='submit'>Send Magic Link</button>
</>}
</form>
);
}
```
<PlatformCodeblock
document="customization/sign-in"
examples={["custom-magic-link"]}
/>

View File

@ -1,24 +1,17 @@
---
title: Sign-Up Page Examples
lastModified: "2026-01-12"
---
# Custom Sign-Up Page Examples
This page provides examples of how to create custom sign-up pages for your application using Stack Auth components and functions.
## Custom page with `SignUp` component
```tsx
'use client';
import { SignUp } from "@stackframe/stack";
export default function DefaultSignUp() {
// optionally redirect to some other page if the user is already signed in
// const user = useUser();
// if (user) { redirect to some other page }
return <SignUp fullPage />;
}
```
<PlatformCodeblock
document="customization/sign-up"
examples={["default"]}
/>
You can also use `useUser` at the beginning of the sign-up page to check whether the user is already signed in and redirect them to another page if they are.
@ -36,43 +29,10 @@ OAuth sign-in and sign-up share the same function. Check out the [Sign In exampl
## Custom Credential Sign Up
```tsx
'use client';
import { useStackApp } from "@stackframe/stack";
import { useState } from "react";
export default function CustomCredentialSignUp() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const app = useStackApp();
const onSubmit = async () => {
if (!password) {
setError('Please enter your password');
return;
}
// This will redirect to app.urls.afterSignUp if successful.
// You can customize the redirect URL in the StackServerApp constructor.
const result = await app.signUpWithCredential({ email, password });
// It's better to handle each error code separately, but for simplicity,
// we'll just display the error message directly here.
if (result.status === 'error') {
setError(result.error.message);
}
};
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(); } }>
{error}
<input type='email' placeholder="email@example.com" value={email} onChange={(e) => setEmail(e.target.value)} />
<input type='password' placeholder="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type='submit'>Sign Up</button>
</form>
);
}
```
<PlatformCodeblock
document="customization/sign-up"
examples={["custom-credential"]}
/>
## Custom Magic Link Sign Up

View File

@ -1,7 +1,12 @@
---
title: Self-host
lastModified: "2026-01-10"
---
<Info type="danger">
**If you self-host, YOU will be responsible for updating Stack Auth and its dependencies.** Security patches, bug fixes, and new features require manual updates on your infrastructure. If you would like premium support to help with this, [contact us](mailto:team@stack-auth.com).
</Info>
Stack Auth is fully open-source and can be self-hosted on your own infrastructure. This guide will introduce each component of the project and how to set them up.
<Info>
@ -31,16 +36,14 @@ On a high level, Stack Auth is composed of the following services:
Stack Auth provides a [pre-configured Docker](https://hub.docker.com/r/stackauth/server) image that bundles the dashboard and API backend into a single container. To complete the setup, you'll need to provide your own PostgreSQL database, and optionally configure an email server and Svix instance for webhooks.
1. Use a cloud hosted Postgres or start a example Postgres database. Don't use this setting in production:
```sh
docker run -d --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=stackframe -p 5432:5432 postgres:latest
```
<PlatformCodeblock document="others/self-host" examples={["docker-postgres"]} hidePlatformSelector />
2. Get the [example environment file](https://github.com/stack-auth/stack-auth/tree/main/docker/server/.env.example) and modify it to your needs (for security, you MUST edit at least the `STACK_SERVER_SECRET` value). You must also supply a `STACK_FREESTYLE_API_KEY` in order to send emails with Stack Auth (can be generated on [freestyle](https://freestyle.sh)). See the [full template here](https://github.com/stack-auth/stack-auth/blob/dev/docker/server/.env).
3. Run the Docker container:
```sh
docker run --env-file <your-env-file.env> -p 8101:8101 -p 8102:8102 stackauth/server:latest
```
<PlatformCodeblock document="others/self-host" examples={["docker-run"]} hidePlatformSelector />
<Info>
For M-series Mac users, you might need to add `--platform linux/x86_64` to the `docker run` command.
@ -59,34 +62,13 @@ Now, login with your admin account on the dashboard and follow the [normal setup
Clone the repository and check out the directory:
```sh
git clone git@github.com:stack-auth/stack-auth.git
cd stack
```
<PlatformCodeblock document="others/self-host" examples={["git-clone"]} hidePlatformSelector />
Pre-populated .env files for the setup below are available and used by default in `.env.development` in each of the packages. (Note: If you're creating a production build (eg. with `pnpm run build`), you must supply the environment variables manually.)
In a new terminal:
```sh
pnpm install
# Run build to build everything once
pnpm run build:dev
# reset & start the dependencies (DB, Inbucket, etc.) as Docker containers, seeding the DB with the Prisma schema
pnpm run start-deps
# pnpm run restart-deps
# pnpm run stop-deps
# Start the dev server
pnpm run dev
# For systems with limited resources, you can run a minimal development setup with just the backend and dashboard
# pnpm run dev:basic
# In a different terminal, run tests in watch mode
pnpm run test
```
<PlatformCodeblock document="others/self-host" examples={["local-dev-setup"]} hidePlatformSelector />
You can now open the dev launchpad at [http://localhost:8100](http://localhost:8100). From there, you can navigate to the dashboard at [http://localhost:8101](http://localhost:8101), API on port 8102, demo on port 8103, docs on port 8104, Inbucket (e-mails) on port 8105, and Prisma Studio on port 8106. See the dev launchpad for a list of all running services.
@ -94,9 +76,7 @@ Your IDE may show an error on all `@stackframe/XYZ` imports. To fix this, simply
You can also open Prisma Studio to see the database interface and edit data directly:
```sh
pnpm run prisma studio
```
<PlatformCodeblock document="others/self-host" examples={["prisma-studio"]} hidePlatformSelector />
## Run individual services
@ -108,47 +88,31 @@ Deploy these services with your preferred platform. Copy the URLs/API keys—you
Clone the repository and check out the root directory:
```sh
git clone git@github.com:stack-auth/stack-auth.git
cd stack
```
<PlatformCodeblock document="others/self-host" examples={["git-clone"]} hidePlatformSelector />
Set all the necessary environment variables (you can check out `apps/backend/.env`). Note that `NEXT_PUBLIC_STACK_API_URL` should be the URL of your deployed domain (e.g., https://your-backend-url.com).
Build and start the server:
```sh
pnpm install
pnpm build:backend
pnpm start:backend
```
<PlatformCodeblock document="others/self-host" examples={["backend-build"]} hidePlatformSelector />
### Dashboard
Clone the repository (if you are running it on a separate server, or skip this step if you are using the same server as the API backend) and check out the dashboard directory:
```sh
git clone git@github.com:stack-auth/stack-auth.git
cd stack
```
<PlatformCodeblock document="others/self-host" examples={["git-clone"]} hidePlatformSelector />
Set all the necessary environment variables (you can check out `apps/dashboard/.env`). Note that `NEXT_PUBLIC_STACK_API_URL` should be the URL of your deployed backend (e.g., https://your-backend-url.com).
Build and start the server:
```sh
pnpm install
pnpm build:dashboard
pnpm start:dashboard
```
<PlatformCodeblock document="others/self-host" examples={["dashboard-build"]} hidePlatformSelector />
### Initialize the database
You need to initialize the database with the following command with the backend environment variables set:
```sh
pnpm db:init
```
<PlatformCodeblock document="others/self-host" examples={["db-init"]} hidePlatformSelector />
Now you can go to the dashboard (e.g., https://your-dashboard-url.com) and sign up for an account.

View File

@ -5,7 +5,7 @@
* This ensures consistency between code examples, platform selectors, and the header indicator.
*/
export type PlatformName = 'JavaScript' | 'Python';
export type PlatformName = 'JavaScript' | 'Python' | 'Shell';
export type FrameworkName = string;
export type PlatformConfig = {
@ -29,6 +29,11 @@ export const PLATFORMS: PlatformConfig[] = [
frameworks: ['Django', 'FastAPI', 'Flask'],
defaultFramework: 'Django',
},
{
name: 'Shell',
frameworks: ['Docker', 'Git', 'pnpm'],
defaultFramework: 'Docker',
},
];
/**

View File

@ -24,6 +24,18 @@ function getDefaultDocsRedirectUrl(): string | null {
return firstDocsPage?.url ?? null;
}
function formatDate(dateString: string): string {
// Parse date parts directly to avoid timezone issues
// (new Date("2026-01-12") is interpreted as UTC, which can shift the day in local time)
const [year, month, day] = dateString.split('-').map(Number);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const suffix = day === 1 || day === 21 || day === 31 ? 'st'
: day === 2 || day === 22 ? 'nd'
: day === 3 || day === 23 ? 'rd'
: 'th';
return `${months[month - 1]} ${day}${suffix}, ${year}`;
}
export default async function Page(props: {
params: Promise<{ slug?: string[] }>,
}) {
@ -46,7 +58,7 @@ export default async function Page(props: {
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<div className="flex flex-row items-center justify-between gap-4 mb-4">
<div className="flex flex-row items-center justify-between gap-4 mb-2">
<DocsTitle>{page.data.title}</DocsTitle>
<div className="flex flex-row gap-2 items-center">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
@ -55,6 +67,11 @@ export default async function Page(props: {
/>
</div>
</div>
{page.data.lastModified && (
<p className="text-xs text-fd-muted-foreground/60 mb-4">
Last updated {formatDate(page.data.lastModified)}
</p>
)}
{/* Only show description if it exists and is not empty */}
{page.data.description && page.data.description.trim() && (
<DocsDescription>{page.data.description}</DocsDescription>
@ -67,12 +84,6 @@ export default async function Page(props: {
})}
/>
</DocsBody>
{/* Show last modified date at the bottom, tucked away */}
{page.data.lastModified && (
<p className="text-xs text-fd-muted-foreground/50 mt-6">
Last updated {page.data.lastModified}
</p>
)}
</DocsPage>
);
}

View File

@ -163,7 +163,8 @@ export function PlatformIndicator({ className }: { className?: string }) {
<div className="px-3 py-2 text-[10px] font-semibold uppercase tracking-wide text-fd-muted-foreground">
Platform
</div>
{PLATFORMS.map((platformConfig) => (
{/* Filter out Shell platform - it's only used internally for shell commands */}
{PLATFORMS.filter(p => p.name !== 'Shell').map((platformConfig) => (
<button
key={platformConfig.name}
onClick={(e) => {

View File

@ -5,29 +5,35 @@ import { cn } from '../../lib/cn';
export type InfoProps = {
children: React.ReactNode,
type?: 'info' | 'warning' | 'success',
type?: 'info' | 'warning' | 'success' | 'danger',
size?: 'default' | 'small',
}
export function Info({ children, type = 'info', size = 'default' }: InfoProps) {
const colorVariants = {
info: {
border: 'border-blue-400/30 dark:border-blue-400/20',
bg: 'bg-blue-50/50 dark:bg-blue-900/10',
accent: 'bg-gradient-to-b from-blue-400 to-blue-600',
bg: 'bg-gradient-to-r from-blue-50/80 to-transparent dark:from-blue-950/30 dark:to-transparent',
icon: 'text-blue-500 dark:text-blue-400',
title: 'text-blue-700 dark:text-blue-300'
title: 'text-blue-800 dark:text-blue-200'
},
warning: {
border: 'border-amber-400/30 dark:border-amber-400/20',
bg: 'bg-amber-50/50 dark:bg-amber-900/10',
accent: 'bg-gradient-to-b from-amber-400 to-amber-600',
bg: 'bg-gradient-to-r from-amber-50/80 to-transparent dark:from-amber-950/30 dark:to-transparent',
icon: 'text-amber-500 dark:text-amber-400',
title: 'text-amber-700 dark:text-amber-300'
title: 'text-amber-800 dark:text-amber-200'
},
success: {
border: 'border-emerald-400/30 dark:border-emerald-400/20',
bg: 'bg-emerald-50/50 dark:bg-emerald-900/10',
accent: 'bg-gradient-to-b from-emerald-400 to-emerald-600',
bg: 'bg-gradient-to-r from-emerald-50/80 to-transparent dark:from-emerald-950/30 dark:to-transparent',
icon: 'text-emerald-500 dark:text-emerald-400',
title: 'text-emerald-700 dark:text-emerald-300'
title: 'text-emerald-800 dark:text-emerald-200'
},
danger: {
accent: 'bg-gradient-to-b from-red-500 to-red-700',
bg: 'bg-gradient-to-r from-red-50/90 to-transparent dark:from-red-950/40 dark:to-transparent',
icon: 'text-red-600 dark:text-red-400',
title: 'text-red-900 dark:text-red-200'
}
};
@ -51,12 +57,14 @@ export function Info({ children, type = 'info', size = 'default' }: InfoProps) {
return (
<div className={cn(
'relative overflow-hidden rounded-lg',
'border border-dashed',
'shadow-sm',
sizes.container,
colors.border,
colors.bg
)}>
{/* Left accent bar */}
<div className={cn(
'absolute left-0 top-0 bottom-0 w-1 rounded-l-lg',
colors.accent
)} />
<div className={cn("flex items-baseline", sizes.content)}>
<div className={cn("flex-shrink-0", colors.icon)}>
{type === 'info' && (
@ -74,6 +82,11 @@ export function Info({ children, type = 'info', size = 'default' }: InfoProps) {
<path fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clipRule="evenodd" />
</svg>
)}
{type === 'danger' && (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className={sizes.icon}>
<path fillRule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z" clipRule="evenodd" />
</svg>
)}
</div>
<div className={cn("flex-1", colors.title)}>
{children}

View File

@ -124,6 +124,11 @@ export type PlatformCodeblockProps = {
* Additional CSS classes
*/
className?: string,
/**
* Hide the platform/framework selector dropdown
* Useful for single-platform examples like shell commands
*/
hidePlatformSelector?: boolean,
}
type VariantConfig = {
@ -239,7 +244,8 @@ export function PlatformCodeblock({
document: documentPath,
examples: exampleNames,
title,
className
className,
hidePlatformSelector = false
}: PlatformCodeblockProps) {
// Load and convert examples from the centralized code-examples.ts file
const allExamples: CodeExample[] = [];
@ -258,8 +264,11 @@ export function PlatformCodeblock({
? convertExamplesToPlatforms(allExamples)
: { platforms: {}, defaultPlatform: '', defaultFrameworks: {}, defaultVariants: {}, variantKeys: {} };
const platformNames = Object.keys(platforms);
const firstPlatform = defaultPlatform || platformNames[0];
// Get all platform names but filter out Shell - it's only used internally for shell commands
// and shouldn't be shown as a user-selectable option in the platform dropdown
const allPlatformNames = Object.keys(platforms);
const platformNames = allPlatformNames.filter(p => p !== 'Shell');
const firstPlatform = defaultPlatform || platformNames[0] || allPlatformNames[0];
// Initialize with global platform or default
// Important: This must return the same value on server and client for hydration
@ -526,7 +535,8 @@ export function PlatformCodeblock({
}));
};
if (platformNames.length === 0) {
// Check if there are ANY platforms (including Shell) - not just user-selectable ones
if (allPlatformNames.length === 0) {
return <div className="text-fd-muted-foreground">No platforms configured</div>;
}
@ -540,92 +550,95 @@ export function PlatformCodeblock({
title={title}
filename={currentCodeConfig?.filename}
headerContent={
<div className="relative ml-auto">
<button
onClick={toggleDropdown}
className={cn(
// Hide selector when explicitly requested, when there's only one platform with one framework, or when there are no user-selectable platforms (Shell-only)
hidePlatformSelector || platformNames.length === 0 || (platformNames.length === 1 && currentFrameworks.length === 1) ? undefined : (
<div className="relative ml-auto">
<button
onClick={toggleDropdown}
className={cn(
"inline-flex items-center gap-2 rounded-lg border border-fd-border/70 bg-fd-background/80 px-3 py-1.5 text-sm font-medium text-fd-foreground shadow-sm transition-all duration-150",
"hover:border-fd-primary/50 hover:bg-fd-primary/5 hover:shadow-md"
)}
>
<span className="flex items-center gap-1">
{selectedPlatform} / {currentFramework}
</span>
<ChevronDown
className={cn(
>
<span className="flex items-center gap-1">
{selectedPlatform} / {currentFramework}
</span>
<ChevronDown
className={cn(
"h-3 w-3 text-fd-muted-foreground transition-transform duration-200",
isDropdownOpen && "rotate-180"
)}
/>
</button>
/>
</button>
{isDropdownOpen && (
<div className="absolute right-0 top-full z-[200] mt-1 min-w-[220px] rounded-lg border border-fd-border/70 bg-fd-background shadow-lg">
{dropdownView === 'platform' ? (
<div className="py-1">
<div className="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
Choose Platform
</div>
{platformNames.map((platform) => (
<button
key={platform}
onClick={(e) => {
{isDropdownOpen && (
<div className="absolute right-0 top-full z-[200] mt-1 min-w-[220px] rounded-lg border border-fd-border/70 bg-fd-background shadow-lg">
{dropdownView === 'platform' ? (
<div className="py-1">
<div className="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
Choose Platform
</div>
{platformNames.map((platform) => (
<button
key={platform}
onClick={(e) => {
e.stopPropagation();
handlePlatformSelect(platform);
}}
className={cn(
}}
className={cn(
"flex w-full items-center justify-between px-3 py-1.5 text-sm transition-all duration-150",
"hover:bg-fd-primary/10 hover:text-fd-primary hover:font-medium",
selectedPlatform === platform
? "bg-fd-primary/15 text-fd-primary font-semibold"
: "text-fd-muted-foreground"
)}
>
<span>{platform}</span>
<ChevronDown className="h-3 w-3 -rotate-90 text-fd-muted-foreground/80" />
</button>
))}
</div>
) : (
<div className="py-1">
<div className="flex items-center px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
<button
onClick={() => setDropdownView('platform')}
className="mr-2 flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground hover:text-fd-primary"
>
<ChevronDown className="h-3 w-3 rotate-90" />
Back
</button>
<span>Select {selectedPlatform} framework</span>
>
<span>{platform}</span>
<ChevronDown className="h-3 w-3 -rotate-90 text-fd-muted-foreground/80" />
</button>
))}
</div>
{currentFrameworks.map((framework) => (
<button
key={framework}
onClick={(e) => {
) : (
<div className="py-1">
<div className="flex items-center px-3 py-2 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground">
<button
onClick={() => setDropdownView('platform')}
className="mr-2 flex items-center gap-1 text-xs font-semibold uppercase tracking-wide text-fd-muted-foreground hover:text-fd-primary"
>
<ChevronDown className="h-3 w-3 rotate-90" />
Back
</button>
<span>Select {selectedPlatform} framework</span>
</div>
{currentFrameworks.map((framework) => (
<button
key={framework}
onClick={(e) => {
e.stopPropagation();
handleFrameworkSelect(framework);
}}
className={cn(
}}
className={cn(
"w-full px-3 py-1.5 text-sm text-left",
"hover:bg-fd-primary/10 hover:text-fd-primary hover:font-medium",
currentFramework === framework
? "bg-fd-primary/15 text-fd-primary font-semibold"
: "text-fd-muted-foreground"
)}
>
<span className="flex items-center gap-2">
{framework}
{currentFramework === framework && (
<span className="text-[10px] text-fd-primary/70 font-medium">current</span>
)}
</span>
</button>
))}
</div>
)}
</div>
)}
</div>
>
<span className="flex items-center gap-2">
{framework}
{currentFramework === framework && (
<span className="text-[10px] text-fd-primary/70 font-medium">current</span>
)}
</span>
</button>
))}
</div>
)}
</div>
)}
</div>
)
}
beforeCodeContent={
hasVariants(selectedPlatform, currentFramework) ? (