clerk/skills검사 통과
SKILL DETAIL
clerk-astro-patterns
clerk/skills/clerk-astro-patterns
Astro patterns with Clerk — middleware, SSR pages, island components,
설치 수 · 92출처 보기
Installation
npx skills add https://github.com/clerk/skills --skill clerk-astro-patterns
스킬 파일
SKILL.md
최근 동기화 · 2026. 8. 30.
evals/evals.json›
{
"skill_name": "clerk-astro-patterns",
"evals": [
{
"id": 1,
"prompt": "i'm adding clerk to my astro app. set up the middleware so the /dashboard and /settings routes are protected and redirect unauthenticated users to /sign-in.",
"expected_output": "Creates src/middleware.ts with clerkMiddleware, createRouteMatcher matching /dashboard and /settings, redirects via auth().redirectToSignIn()",
"scaffold": "astro-basic-auth",
"expectations": [
"Creates src/middleware.ts that exports onRequest",
"Uses clerkMiddleware from @clerk/astro/server",
"Uses createRouteMatcher to match /dashboard and /settings routes",
"Calls auth().redirectToSignIn() when route is protected and user is not signed in",
"Calls next() for unprotected routes"
]
},
{
"id": 2,
"prompt": "i have an astro SSR page at /dashboard. i need to get the current user's id and redirect to /sign-in if they're not authenticated.",
"expected_output": "Uses Astro.locals.auth() to get userId, calls Astro.redirect('/sign-in') when userId is falsy",
"scaffold": "astro-basic-auth",
"expectations": [
"Calls Astro.locals.auth() to get auth data in the frontmatter",
"Destructures userId from the auth result",
"Calls return Astro.redirect('/sign-in') when userId is falsy",
"Page has output: server or is not marked as prerender",
"Does NOT import auth from @clerk/nextjs/server"
]
},
{
"id": 3,
"prompt": "i need to protect an Astro API route at /api/data.ts so only authenticated users can call it. return 401 if not authenticated.",
"expected_output": "API route reads Astro.locals.auth() to get userId, returns 401 Response when not authenticated",
"scaffold": "astro-basic-auth",
"expectations": [
"Reads Astro.locals.auth() in the GET or POST handler",
"Returns new Response('Unauthorized', { status: 401 }) when userId is falsy",
"Uses userId in the data fetching logic",
"Does NOT require auth in the static prerender path"
]
},
{
"id": 4,
"prompt": "i want to show a sign-in button in my astro site's header when the user is not signed in, and a user menu when they are. this needs to work client-side.",
"expected_output": "Creates a React island component with client:load using useAuth and SignInButton/UserButton from @clerk/astro/react",
"scaffold": "astro-basic-auth",
"expectations": [
"Creates a React island component (Header.tsx or similar)",
"Imports useAuth from @clerk/astro/react",
"Shows UserButton when isSignedIn is true",
"Shows SignInButton when isSignedIn is false",
"Mounts the component with client:load directive in the .astro file"
]
},
{
"id": 5,
"prompt": "i have a mix of static and dynamic pages in my astro app. how do i handle auth on pages that are statically prerendered?",
"expected_output": "Explains that static prerendered pages skip middleware, suggests using client-side auth in islands or converting the page to SSR with export const prerender = false",
"scaffold": "astro-basic-auth",
"expectations": [
"Notes that clerkMiddleware is skipped for prerendered pages",
"Suggests removing export const prerender = true for pages that need auth",
"OR suggests using client-side useAuth in island components for partially static pages",
"Does NOT suggest that Astro.locals.auth() works on prerendered pages",
"Explains the hybrid rendering approach (output: hybrid in astro.config.mjs)"
]
},
{
"id": 6,
"prompt": "i need to show org-aware content in an astro SSR page. load the current org name from clerk and show it, or redirect to /select-org if no org is active.",
"expected_output": "Uses Astro.locals.auth() to get orgId, redirects to /select-org if orgId is missing, shows org data",
"scaffold": "astro-basic-auth",
"expectations": [
"Calls Astro.locals.auth() and destructures both userId and orgId",
"Returns Astro.redirect('/sign-in') when userId is falsy",
"Returns Astro.redirect('/select-org') when orgId is falsy",
"Uses orgId to fetch or display org-specific data",
"Handles both unauthenticated and no-org-selected states"
]
},
{
"id": 7,
"prompt": "i want to use React components with Clerk in my Astro app. set up the React integration and create a header component that shows sign-in/sign-up for signed-out users and a user button for signed-in users. use it in an Astro layout.",
"expected_output": "Adds @astrojs/react, updates astro.config with react(), creates React header using Show/UserButton/SignInButton from @clerk/astro/react, mounts with client:load in Astro layout",
"scaffold": "astro-basic-auth",
"expectations": [
"Updates astro.config to include react() in integrations array alongside clerk()",
"Creates a React component (.tsx) importing from @clerk/astro/react (NOT @clerk/astro/components)",
"Uses Show when='signed-in' and Show when='signed-out' (or SignedIn/SignedOut) for conditional rendering",
"Includes UserButton and SignInButton from @clerk/astro/react",
"Mounts the React component in an .astro file with client:load directive",
"Does NOT forget client:load on the React component (would render as static HTML without it)"
]
}
]
}
references/api-routes.md›
# API Routes
## Basic Auth Check
```ts
// src/pages/api/data.ts
import type { APIRoute } from 'astro'
export const GET: APIRoute = async (context) => {
const { userId } = context.locals.auth()
if (!userId) {
return new Response('Unauthorized', { status: 401 })
}
const data = await fetchData(userId)
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
})
}
```
## POST with Org Check
```ts
export const POST: APIRoute = async (context) => {
const { userId, orgId } = context.locals.auth()
if (!userId) return new Response('Unauthorized', { status: 401 })
if (!orgId) return new Response('No active org', { status: 403 })
const body = await context.request.json()
await saveOrgData(orgId, body)
return new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
})
}
```
## Permission Check
```ts
export const DELETE: APIRoute = async (context) => {
const auth = context.locals.auth()
if (!auth.userId) return new Response('Unauthorized', { status: 401 })
const canDelete = auth.has({ permission: 'org:items:delete' })
if (!canDelete) return new Response('Forbidden', { status: 403 })
await deleteItem(context.params.id!)
return new Response(null, { status: 204 })
}
```
## Using clerkClient in API Routes
```ts
import { clerkClient } from '@clerk/astro/server'
export const GET: APIRoute = async (context) => {
const { userId } = context.locals.auth()
if (!userId) return new Response('Unauthorized', { status: 401 })
const client = clerkClient(context)
const user = await client.users.getUser(userId)
return new Response(JSON.stringify({ name: user.fullName }), {
headers: { 'Content-Type': 'application/json' },
})
}
```
## CRITICAL
- API routes are always SSR — `prerender` does not apply
- Use `context.locals.auth()` (not `Astro.locals.auth()`) in API routes
- Return proper HTTP status codes: 401 = not authenticated, 403 = not authorized
references/astro-react.md›
# Astro + React Integration
Use Clerk components in React islands within Astro pages.
## Setup
Install the React integration:
```bash
npx astro add react
```
Update config:
```ts
// astro.config.mjs
import { defineConfig } from "astro/config"
import node from "@astrojs/node"
import react from "@astrojs/react"
import clerk from "@clerk/astro"
export default defineConfig({
integrations: [clerk(), react()],
output: "server",
adapter: node({ mode: "standalone" }),
})
```
## Clerk Components in Astro Pages
Import from `@clerk/astro/react` (NOT `@clerk/astro/components`). Add `client:load` to hydrate:
```astro
---
// src/layouts/SiteLayout.astro
import { Show, UserButton, SignInButton } from "@clerk/astro/react"
---
<header>
<nav>
<Show when="signed-out" client:load>
<SignInButton client:load mode="modal" />
</Show>
<Show when="signed-in" client:load>
<UserButton client:load />
</Show>
</nav>
</header>
<slot />
```
`client:load` is required on every Clerk React component used in `.astro` files.
## Clerk Components in React Components
Standard React imports from `@clerk/astro/react`:
```tsx
// src/components/Header.tsx
import { SignInButton, Show, UserButton } from "@clerk/astro/react"
export default function Header() {
return (
<>
<Show when="signed-out">
<SignInButton />
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</>
)
}
```
Use the component in an Astro page with `client:load`:
```astro
---
import Header from "../components/Header"
---
<Header client:load />
```
## Stores in React Components
Access user data with `$userStore` from `@clerk/astro/client`:
```tsx
// src/components/Username.tsx
import { useSyncExternalStore } from "react"
import { $userStore } from "@clerk/astro/client"
export default function Username() {
const user = useSyncExternalStore($userStore.listen, $userStore.get, $userStore.get)
return <>{user?.firstName}</>
}
```
## Key Differences
| Import | From | Use In |
|--------|------|--------|
| `SignedIn`, `SignedOut` | `@clerk/astro/components` | `.astro` files (SSR) |
| `Show`, `UserButton` | `@clerk/astro/react` | `.astro` files (with `client:load`) or `.tsx` files |
| `$userStore` | `@clerk/astro/client` | React components (via `useSyncExternalStore`) |
references/island-components.md›
# Island Components
## React Island with Clerk Hooks
```tsx
// src/components/UserNav.tsx
import { useAuth, useUser, UserButton, SignInButton } from '@clerk/astro/react'
export function UserNav() {
const { isSignedIn, isLoaded } = useAuth()
const { user } = useUser()
if (!isLoaded) return null
if (!isSignedIn) {
return <SignInButton mode="modal" />
}
return (
<div>
<span>{user?.firstName}</span>
<UserButton />
</div>
)
}
```
```astro
---
// src/pages/index.astro
import { UserNav } from '../components/UserNav'
---
<UserNav client:load />
```
## Client Directives
| Directive | When to Use |
|-----------|-------------|
| `client:load` | Hydrate immediately on page load |
| `client:idle` | Hydrate when browser is idle |
| `client:visible` | Hydrate when component scrolls into view |
| `client:only="react"` | No SSR, render only on client |
## Prebuilt UI Components
```tsx
import {
SignIn,
SignUp,
UserButton,
UserProfile,
OrganizationSwitcher,
SignInButton,
SignOutButton,
} from '@clerk/astro/react'
```
Use these in islands (`.tsx` files) — not directly in `.astro` files without a client directive.
## CRITICAL
- Always add a `client:*` directive — without it the island is server-rendered only and Clerk hooks return undefined
- Import from `@clerk/astro/react` — not `@clerk/react`
- Islands that need real-time auth state (e.g. sign-out button) must use `client:load`
references/middleware.md›
# Middleware
## Basic Setup
```ts
// src/middleware.ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server'
const isProtectedRoute = createRouteMatcher([
'/dashboard(.*)',
'/settings(.*)',
'/api/private(.*)',
])
export const onRequest = clerkMiddleware((auth, context, next) => {
if (isProtectedRoute(context.request) && !auth().userId) {
return auth().redirectToSignIn()
}
return next()
})
```
## With Custom Handler
```ts
export const onRequest = clerkMiddleware((auth, context, next) => {
const { userId, orgId } = auth()
if (isProtectedRoute(context.request)) {
if (!userId) return auth().redirectToSignIn()
if (!orgId) return context.redirect('/select-org')
}
return next()
})
```
## No Handler (Pass-Through)
```ts
export const onRequest = clerkMiddleware()
```
The middleware still populates `Astro.locals.auth` — you just do the redirect check per-page.
## createRouteMatcher Patterns
```ts
createRouteMatcher([
'/dashboard', // exact
'/dashboard(.*)', // prefix match
'/api/private/(.*)', // nested
/^\/admin/, // regex
])
```
## clerkMiddleware Signature
```ts
clerkMiddleware(handler?, options?)
// handler: (auth, context, next) => Response | Promise<Response>
// auth: () => AuthObject (call it to get { userId, orgId, ... })
// context: Astro APIContext
// next: () => Promise<Response>
```
## CRITICAL
- Middleware is skipped for pages with `export const prerender = true`
- `auth()` is a function — call it to get the auth object: `auth().userId` not `auth.userId`
- Always return `next()` for non-protected routes
references/ssr-pages.md›
# SSR Pages
## Basic Auth Check
```astro
---
// src/pages/dashboard.astro
const { userId } = Astro.locals.auth()
if (!userId) return Astro.redirect('/sign-in')
const data = await fetchData(userId)
---
<h1>Dashboard</h1>
<pre>{JSON.stringify(data)}</pre>
```
## Org-Scoped Page
```astro
---
const { userId, orgId, orgRole } = Astro.locals.auth()
if (!userId) return Astro.redirect('/sign-in')
if (!orgId) return Astro.redirect('/select-org')
if (orgRole !== 'org:admin') return Astro.redirect('/dashboard')
const settings = await fetchOrgSettings(orgId)
---
<h1>Org Settings</h1>
```
## Fetch Current User Data
```astro
---
import { clerkClient } from '@clerk/astro/server'
const { userId } = Astro.locals.auth()
if (!userId) return Astro.redirect('/sign-in')
const client = clerkClient(Astro)
const user = await client.users.getUser(userId)
---
<img src={user.imageUrl} alt={user.fullName ?? ''} />
```
## getToken for External APIs
```astro
---
const auth = Astro.locals.auth()
if (!auth.userId) return Astro.redirect('/sign-in')
const token = await auth.getToken({ template: 'supabase' })
const data = await fetchFromSupabase(token)
---
```
## Auth Object Fields
| Field | Type | Description |
|-------|------|-------------|
| `userId` | `string \| null` | Current user ID |
| `orgId` | `string \| null` | Active org ID |
| `orgRole` | `string \| null` | User's role in active org |
| `sessionId` | `string \| null` | Current session ID |
| `has()` | `function` | Check permissions |
| `getToken()` | `async function` | Get JWT for external APIs |
## CRITICAL
- `Astro.locals.auth()` returns an auth object — note the `()` call
- Pages must NOT have `export const prerender = true` for server auth to work
- To opt a single page out of static rendering: `export const prerender = false`
SKILL.md›
---
name: clerk-astro-patterns
description: 'Astro patterns with Clerk — middleware, SSR pages, island components,
API routes, static vs SSR rendering. Triggers on: astro clerk, clerk astro middleware,
astro protected page, clerk island component, astro API route auth, clerk astro
SSR.'
license: MIT
allowed-tools: WebFetch
metadata:
author: clerk
version: 1.0.0
---
# Astro Patterns
SDK: `@clerk/astro` v3+. Requires Astro 4.15+.
## What Do You Need?
| Task | Reference |
|------|-----------|
| Configure middleware | references/middleware.md |
| Protect SSR pages | references/ssr-pages.md |
| Use Clerk in island components | references/island-components.md |
| Auth in API routes | references/api-routes.md |
| Use Clerk with React in Astro | references/astro-react.md |
## Mental Model
Astro has two rendering modes per page: **SSR** and **static prerender**. Clerk works differently in each:
- **SSR pages** — use `Astro.locals.auth()` which is populated by the middleware
- **Static pages** (`export const prerender = true`) — Clerk middleware skips them; use client-side hooks in islands
- **Islands** — React/Vue/Svelte components; use `useAuth()` and other hooks from `@clerk/astro/react`
```
Request → clerkMiddleware() → SSR page → Astro.locals.auth()
↓
Island (.client) → useAuth() hook
```
## Setup
### astro.config.mjs
```ts
import { defineConfig } from 'astro/config'
import clerk from '@clerk/astro'
export default defineConfig({
integrations: [clerk()],
output: 'server',
})
```
### src/middleware.ts
```ts
import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server'
const isProtectedRoute = createRouteMatcher(['/dashboard(.*)'])
export const onRequest = clerkMiddleware((auth, context, next) => {
if (isProtectedRoute(context.request) && !auth().userId) {
return auth().redirectToSignIn()
}
return next()
})
```
## SSR Page Auth
```astro
---
const { userId, orgId } = Astro.locals.auth()
if (!userId) return Astro.redirect('/sign-in')
---
<h1>Dashboard</h1>
```
## Common Pitfalls
| Symptom | Cause | Fix |
|---------|-------|-----|
| `Astro.locals.auth` is undefined | Missing middleware | Add `clerkMiddleware` to `src/middleware.ts` |
| Auth works in dev but not production | `output: 'static'` globally | Set `output: 'server'` or `hybrid` for protected pages |
| Static page has no auth | Prerendered pages skip middleware | Use `export const prerender = false` or move to island |
| Island not reactive to sign-in | Missing `client:load` directive | Add `client:load` to the island component |
## Import Map
| What | Import From |
|------|-------------|
| `clerkMiddleware`, `createRouteMatcher` | `@clerk/astro/server` |
| `useAuth`, `useUser`, `UserButton` | `@clerk/astro/react` |
| Astro components (`<SignIn>`, etc.) | `@clerk/astro/components` |
## Env Variables
```
# .env
PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...
CLERK_SECRET_KEY=sk_...
```
Astro uses `PUBLIC_` prefix for client-exposed variables (not `NEXT_PUBLIC_`).
## See Also
- `clerk-setup` - Initial Clerk install
- `clerk-custom-ui` - Custom flows & appearance
- `clerk-orgs` - B2B organizations
## Docs
[Astro SDK](https://clerk.com/docs/astro/getting-started/quickstart)
templates/astro-basic-auth/astro.config.mjs›
import { defineConfig } from "astro/config"
import node from "@astrojs/node"
import clerk from "@clerk/astro"
export default defineConfig({
integrations: [clerk()],
adapter: node({ mode: "standalone" }),
output: "server",
})
templates/astro-basic-auth/package.json›
{
"name": "clerk-astro",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build"
},
"dependencies": {
"astro": "^5.0.0",
"@clerk/astro": "^2.0.0",
"@astrojs/node": "^9.0.0"
}
}
templates/astro-basic-auth/src/layouts/Layout.astro›
---
import { SignedIn, SignedOut, SignInButton, SignUpButton } from "@clerk/astro/components"
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Clerk + Astro</title>
</head>
<body>
<header>
<SignedOut>
<SignInButton mode="modal" />
<SignUpButton mode="modal" />
</SignedOut>
<SignedIn>
<p>You are signed in!</p>
</SignedIn>
</header>
<slot />
</body>
</html>
templates/astro-basic-auth/src/middleware.ts›
import { clerkMiddleware } from "@clerk/astro/server"
export const onRequest = clerkMiddleware()
templates/astro-basic-auth/src/pages/index.astro›
---
import Layout from "../layouts/Layout.astro"
import { SignedIn, SignedOut } from "@clerk/astro/components"
---
<Layout>
<SignedOut>
<p>Sign in to try Clerk out!</p>
</SignedOut>
<SignedIn>
<p>You are signed in!</p>
</SignedIn>
</Layout>
templates/astro-basic-auth/tsconfig.json›
{
"extends": "astro/tsconfigs/strict"
}