Skills로 돌아가기
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"
}
clerk-astro-patterns · 인기 상승 중인 Agent Skills | Mengbi