# Astro 整合指南 (/guides/frameworks/astro)





Astro 是一個內容優先的現代框架，支援 Islands Architecture 和多種渲染模式。Recur 提供官方 Astro adapter，讓你只需幾行程式碼就能整合訂閱功能。

前置需求 [#前置需求]

Astro 預設是靜態網站生成（SSG），如果需要使用 Server SDK 功能（如 Customer Portal、Webhook），你需要啟用 Server 模式並安裝 adapter。

安裝 [#安裝]

<Tabs items="['使用 Adapter（推薦）', '使用核心 SDK']">
  <Tab value="使用 Adapter（推薦）">
    官方 Astro adapter 提供最簡潔的整合方式：

    ```bash
    npm install @recur-tw/astro
    ```

    <Callout type="info">
      `@recur-tw/astro` 提供預封裝的 handler 函數，大幅減少 boilerplate 程式碼。
    </Callout>
  </Tab>

  <Tab value="使用核心 SDK">
    如果你需要更多自訂彈性，可以直接使用核心 SDK：

    ```bash
    npm install recur-tw
    ```
  </Tab>
</Tabs>

設定輸出模式 [#設定輸出模式]

在 `astro.config.mjs` 中設定：

<Tabs items="['Server（推薦）', 'Static + API Routes', 'Static']">
  <Tab value="Server（推薦）">
    全伺服器模式，所有頁面和 API 都在伺服器執行：

    ```javascript
    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    import vercel from '@astrojs/vercel'; // 或其他 adapter

    export default defineConfig({
      output: 'server',
      adapter: vercel(),
    });
    ```

    <Callout type="info">
      如果想讓部分頁面預先渲染（例如首頁），在該頁面加入 `export const prerender = true;`
    </Callout>
  </Tab>

  <Tab value="Static + API Routes">
    預設靜態，只有 API routes 需要伺服器執行：

    ```javascript
    // astro.config.mjs
    import { defineConfig } from 'astro/config';
    import node from '@astrojs/node';

    export default defineConfig({
      adapter: node({ mode: 'standalone' }),
    });
    ```

    在需要伺服器執行的 API 檔案中加入：

    ```typescript
    export const prerender = false;
    ```
  </Tab>

  <Tab value="Static">
    純靜態網站，只能使用前端功能：

    ```javascript
    // astro.config.mjs
    import { defineConfig } from 'astro/config';

    export default defineConfig({
      // 不需要設定 output，預設就是 static
    });
    ```

    <Callout type="warn">
      靜態模式無法使用 Server SDK。如需 Customer Portal 或 Webhook，需要另外建立後端。
    </Callout>
  </Tab>
</Tabs>

設定環境變數 [#設定環境變數]

在 `.env` 中加入：

```bash
# 前端使用（可公開）
PUBLIC_RECUR_PUBLISHABLE_KEY=pk_test_xxx

# 後端使用（保密）
RECUR_SECRET_KEY=sk_test_xxx
RECUR_WEBHOOK_SECRET=whsec_xxx
```

結帳功能 [#結帳功能]

方法 1：使用 Adapter（推薦） [#方法-1使用-adapter推薦]

<Steps>
  <Step>
    建立 Checkout Endpoint [#建立-checkout-endpoint]

    ```typescript
    // src/pages/api/checkout.ts
    import { Checkout } from '@recur-tw/astro';

    export const prerender = false;

    // 使用 process.env 確保在 Vercel 等平台上正確讀取環境變數
    const secretKey = process.env.RECUR_SECRET_KEY || import.meta.env.RECUR_SECRET_KEY;

    export const GET = Checkout({
      secretKey,
      successUrl: '/success?session_id={CHECKOUT_SESSION_ID}',
      cancelUrl: '/pricing',
      locale: 'zh-TW',
    });
    ```
  </Step>

  <Step>
    在頁面中使用 [#在頁面中使用]

    ```astro
    ---
    // src/pages/pricing.astro
    ---

    <html>
      <body>
        <h1>選擇你的方案</h1>

        <div class="email-input">
          <label for="email">您的 Email</label>
          <input type="email" id="email" placeholder="you@example.com" />
        </div>

        <button
          data-product-id="prod_xxx"
          onclick="handleCheckout(this)"
        >
          立即訂閱
        </button>

        <script is:inline>
          function handleCheckout(btn) {
            const email = document.getElementById('email').value;
            if (!email) {
              alert('請輸入 Email');
              return;
            }
            const productId = btn.dataset.productId;
            window.location.href = `/api/checkout?productId=${productId}&customerEmail=${encodeURIComponent(email)}`;
          }
        </script>
      </body>
    </html>
    ```
  </Step>
</Steps>

<Callout type="info">
  `customerEmail` 和 `customerName` 都是選填。如果未預填，用戶會在結帳頁面輸入（Email 在結帳時為必填，用於寄送收據）。
</Callout>

支援的 Query Parameters：

| 參數              | 說明                    |  必填 |
| --------------- | --------------------- | :-: |
| `productId`     | 產品 ID                 | ✓\* |
| `productSlug`   | 產品 slug（替代 productId） | ✓\* |
| `customerEmail` | 預填客戶 Email            |     |
| `customerName`  | 預填客戶姓名                |     |
| `customerId`    | 現有客戶 ID               |     |
| `metadata`      | URL-encoded JSON 字串   |     |

<small>
  \* 

  `productId`

   或 

  `productSlug`

   至少需提供一個
</small>

方法 2：自訂 Resolver [#方法-2自訂-resolver]

如果你需要從認證系統取得客戶資訊：

```typescript
// src/pages/api/checkout.ts
import { Checkout } from '@recur-tw/astro';
import { getSession } from '@/lib/auth';

export const prerender = false;

export const GET = Checkout({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
  successUrl: '/success',
  resolver: async (context) => {
    const session = await getSession(context.request);
    return {
      customerEmail: session?.user?.email,
      customerName: session?.user?.name,
      customerId: session?.user?.recurCustomerId,
    };
  },
});
```

方法 3：純 Astro（無 Adapter） [#方法-3純-astro無-adapter]

使用 `<script>` 標籤直接引入 SDK：

```astro
---
// src/pages/pricing.astro
---

<html>
  <head>
    <title>定價方案</title>
  </head>
  <body>
    <h1>選擇你的方案</h1>
    <button id="subscribe-btn" data-product-id="prod_xxx">
      立即訂閱
    </button>

    <script>
      import { RecurCheckout } from 'recur-tw/vanilla';

      const recur = RecurCheckout.init({
        publishableKey: import.meta.env.PUBLIC_RECUR_PUBLISHABLE_KEY,
      });

      document.getElementById('subscribe-btn')?.addEventListener('click', async (e) => {
        const productId = (e.target as HTMLElement).dataset.productId;

        // 推薦：使用 Hosted Checkout（支援 localhost 開發）
        await recur.redirectToCheckout({
          productId,
          successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
          cancelUrl: `${window.location.origin}/pricing`,
        });
      });
    </script>
  </body>
</html>
```

Customer Portal [#customer-portal]

方法 1：使用 Adapter（推薦） [#方法-1使用-adapter推薦-1]

```typescript
// src/pages/api/portal.ts
import { CustomerPortal } from '@recur-tw/astro';

export const prerender = false;

const secretKey = process.env.RECUR_SECRET_KEY || import.meta.env.RECUR_SECRET_KEY;

export const GET = CustomerPortal({
  secretKey,
  returnUrl: '/account',
});
```

使用方式：`/api/portal?customerId=cus_xxx`

整合認證系統 [#整合認證系統]

```typescript
// src/pages/api/portal.ts
import { CustomerPortal } from '@recur-tw/astro';
import { getSession } from '@/lib/auth';

export const prerender = false;

const secretKey = process.env.RECUR_SECRET_KEY || import.meta.env.RECUR_SECRET_KEY;

export const GET = CustomerPortal({
  secretKey,
  returnUrl: '/account',
  resolver: async (context) => {
    const session = await getSession(context.request);
    if (!session?.user?.recurCustomerId) {
      throw new Error('Unauthorized');
    }
    return { customerId: session.user.recurCustomerId };
  },
});
```

方法 2：手動實作 [#方法-2手動實作]

```astro
---
// src/pages/portal.astro
import { Recur } from 'recur-tw/server';

export const prerender = false;

const recur = new Recur(import.meta.env.RECUR_SECRET_KEY);

const customerId = Astro.url.searchParams.get('customerId');

if (!customerId) {
  return Astro.redirect('/login');
}

const session = await recur.portal.sessions.create({
  customer: customerId,
  returnUrl: new URL('/account', Astro.url).toString(),
});

return Astro.redirect(session.url);
---
```

查詢訂閱狀態 [#查詢訂閱狀態]

查詢已登入使用者的訂閱狀態是常見需求，例如：判斷是否顯示付費功能、控制存取權限等。

選擇適合的方法 [#選擇適合的方法]

根據你的需求選擇最適合的整合方式：

| 使用情境                    | 推薦方法                           | 說明                           |
| ----------------------- | ------------------------------ | ---------------------------- |
| **只有少數頁面需要檢查**          | RecurClient                    | 直接在需要的頁面呼叫 API，簡單直覺          |
| **多個頁面都需要訂閱狀態**         | withSubscription Middleware    | 自動注入到 `Astro.locals`，避免重複程式碼 |
| **整個區塊需要付費才能存取**        | requireSubscription Middleware | 自動重導向未訂閱的使用者                 |
| **已有自己的權限/快取機制**        | RecurClient                    | 完全掌控查詢時機和快取策略                |
| **需要跨 middleware 共享資料** | withSubscription + sequence    | 與你現有的 auth middleware 組合     |

<Callout type="info">
  **如果你已經有自己的 middleware**：不一定需要使用我們的 middleware。你可以在現有的 middleware 或頁面邏輯中直接使用 `RecurClient` 查詢訂閱狀態，這樣可以更好地整合你現有的快取、session 或權限管理機制。
</Callout>

方法 1：使用 RecurClient（最彈性） [#方法-1使用-recurclient最彈性]

在任何伺服器端程式碼中直接查詢：

```typescript
// src/pages/api/check-subscription.ts
import { RecurClient } from '@recur-tw/astro';
import { getSession } from '@/lib/auth';

export const prerender = false;

const recur = new RecurClient({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
});

export async function GET({ request }) {
  const session = await getSession(request);
  if (!session?.user) {
    return new Response('Unauthorized', { status: 401 });
  }

  // 使用 email 查詢
  const result = await recur.getSubscription({
    email: session.user.email,
  });

  // 或使用你系統的 user ID（需先在建立顧客時設定 externalId）
  // const result = await recur.getSubscription({
  //   externalId: session.user.id,
  // });

  return new Response(JSON.stringify({
    hasAccess: result.hasActiveSubscription,
    plan: result.subscriptions[0]?.planName,
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
}
```

在 Astro 頁面中使用：

```astro
---
// src/pages/dashboard.astro
import { RecurClient } from '@recur-tw/astro';
import { getSession } from '@/lib/auth';

export const prerender = false;

const session = await getSession(Astro.request);
if (!session?.user) {
  return Astro.redirect('/login');
}

const recur = new RecurClient({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
});

const { hasActiveSubscription, subscriptions } = await recur.getSubscription({
  email: session.user.email,
});

if (!hasActiveSubscription) {
  return Astro.redirect('/pricing');
}

const currentPlan = subscriptions[0];
---

<h1>歡迎回來！</h1>
<p>你的方案：{currentPlan.planName}</p>
<p>到期日：{new Date(currentPlan.current_period_end).toLocaleDateString('zh-TW')}</p>
```

RecurClient 方法 [#recurclient-方法]

| 方法                               | 說明          | 回傳值                        |
| -------------------------------- | ----------- | -------------------------- |
| `getSubscription(options)`       | 取得完整訂閱資訊    | `SubscriptionResponse`     |
| `hasActiveSubscription(options)` | 快速檢查是否有有效訂閱 | `boolean`                  |
| `getActiveSubscription(options)` | 取得第一個有效訂閱   | `SubscriptionInfo \| null` |

查詢參數（至少需提供一個）：

| 參數            | 說明                  |
| ------------- | ------------------- |
| `email`       | 顧客 Email            |
| `externalId`  | 你系統中的 User ID       |
| `customerId`  | Recur 的 Customer ID |
| `productSlug` | 篩選特定商品              |
| `productId`   | 篩選特定商品 ID           |
| `activeOnly`  | 只查詢有效訂閱（預設 `false`） |

方法 2：使用 withSubscription Middleware [#方法-2使用-withsubscription-middleware]

適合多個頁面都需要存取訂閱狀態的情境。Middleware 會在每個請求自動查詢並注入到 `Astro.locals`：

```typescript
// src/middleware.ts
import { withSubscription } from '@recur-tw/astro';
import { getSession } from './lib/auth';

export const onRequest = withSubscription({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
  getCustomer: async (context) => {
    const session = await getSession(context.request);
    if (!session?.user) return null;

    // 使用你系統的 user ID
    return { externalId: session.user.id };

    // 或使用 email
    // return { email: session.user.email };
  },
});
```

然後在任何頁面中直接使用：

```astro
---
// src/pages/dashboard.astro
const { subscription } = Astro.locals;

if (!subscription?.hasActiveSubscription) {
  return Astro.redirect('/pricing');
}

const plan = subscription.subscriptions[0];
---

<h1>Dashboard</h1>
<p>方案：{plan.planName}</p>
```

TypeScript 支援 [#typescript-支援]

為了取得 `Astro.locals.subscription` 的型別提示，在 `src/env.d.ts` 加入：

```typescript
/// <reference types="astro/client" />
import type { SubscriptionLocals } from '@recur-tw/astro';

declare namespace App {
  interface Locals extends SubscriptionLocals {}
}
```

方法 3：使用 requireSubscription Middleware [#方法-3使用-requiresubscription-middleware]

適合需要整個區塊都限制為付費會員存取的情境，自動重導向未訂閱的使用者：

```typescript
// src/middleware.ts
import { requireSubscription } from '@recur-tw/astro';
import { getSession } from './lib/auth';

export const onRequest = requireSubscription({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
  getCustomer: async (context) => {
    const session = await getSession(context.request);
    if (!session?.user) return null;
    return { externalId: session.user.id };
  },
  // 無訂閱時重導向到這裡
  redirectTo: '/pricing',
  // 排除這些路徑（公開頁面）
  exclude: [
    '/',
    '/pricing',
    '/login',
    '/signup',
    '/api/*',
    '/public/**',
  ],
});
```

在現有 Middleware 中整合 RecurClient [#在現有-middleware-中整合-recurclient]

如果你已經有自己的 middleware 處理認證和權限，可以直接在裡面使用 `RecurClient`：

```typescript
// src/middleware.ts
import type { MiddlewareHandler } from 'astro';
import { RecurClient } from '@recur-tw/astro';
import { getSession } from './lib/auth';

const recur = new RecurClient({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
});

export const onRequest: MiddlewareHandler = async (context, next) => {
  // 你現有的認證邏輯
  const session = await getSession(context.request);
  context.locals.user = session?.user || null;

  // 只有登入的使用者才查詢訂閱
  if (context.locals.user) {
    try {
      // 你可以在這裡加入快取邏輯
      const result = await recur.getSubscription({
        externalId: context.locals.user.id,
      });
      context.locals.subscription = result;
      context.locals.isPremium = result.hasActiveSubscription;
    } catch (error) {
      console.error('Failed to fetch subscription:', error);
      context.locals.subscription = null;
      context.locals.isPremium = false;
    }
  }

  return next();
};
```

<Callout type="info">
  這個方式讓你完全掌控查詢時機、錯誤處理和快取策略。例如你可以將結果存入 Redis 快取，避免每個請求都呼叫 API。
</Callout>

與多個 Middleware 組合（使用 sequence） [#與多個-middleware-組合使用-sequence]

如果你選擇使用我們的 middleware，可以用 `sequence` 與其他 middleware 組合：

```typescript
// src/middleware.ts
import { sequence } from 'astro:middleware';
import { withSubscription } from '@recur-tw/astro';

// 你的認證 middleware
const auth = async (context, next) => {
  const session = await getSession(context.request);
  context.locals.user = session?.user || null;
  return next();
};

// 訂閱狀態 middleware
const subscription = withSubscription({
  secretKey: import.meta.env.RECUR_SECRET_KEY,
  getCustomer: (context) => {
    // 從前一個 middleware 取得 user
    if (!context.locals.user) return null;
    return { externalId: context.locals.user.id };
  },
});

export const onRequest = sequence(auth, subscription);
```

Webhook 處理 [#webhook-處理]

使用 Adapter（推薦） [#使用-adapter推薦]

```typescript
// src/pages/api/webhooks/recur.ts
import { Webhooks } from '@recur-tw/astro';

export const prerender = false;

const webhookSecret = process.env.RECUR_WEBHOOK_SECRET || import.meta.env.RECUR_WEBHOOK_SECRET;

export const POST = Webhooks({
  webhookSecret,

  // 通用 handler（可選）
  onPayload: async (event) => {
    console.log('收到事件:', event.type);
  },

  // 訂閱事件
  onSubscriptionCreated: async (subscription) => {
    console.log('新訂閱:', subscription.id);
    // 發送歡迎郵件、開通權限等
  },

  onSubscriptionCanceled: async (subscription) => {
    console.log('訂閱取消:', subscription.id);
    // 關閉權限、發送挽留郵件等
  },

  // 付款事件
  onChargeSucceeded: async (charge) => {
    console.log('付款成功:', charge.id, charge.amount);
  },

  onChargeFailed: async (charge) => {
    console.log('付款失敗:', charge.id, charge.failureMessage);
  },

  // 客戶事件
  onCustomerCreated: async (customer) => {
    console.log('新客戶:', customer.id, customer.email);
  },
});
```

支援的事件類型 [#支援的事件類型]

| 事件                       | Handler                  | 說明     |
| ------------------------ | ------------------------ | ------ |
| `subscription.created`   | `onSubscriptionCreated`  | 新訂閱建立  |
| `subscription.updated`   | `onSubscriptionUpdated`  | 訂閱更新   |
| `subscription.canceled`  | `onSubscriptionCanceled` | 訂閱取消   |
| `subscription.expired`   | `onSubscriptionExpired`  | 訂閱過期   |
| `subscription.paused`    | `onSubscriptionPaused`   | 訂閱暫停   |
| `subscription.resumed`   | `onSubscriptionResumed`  | 訂閱恢復   |
| `charge.succeeded`       | `onChargeSucceeded`      | 付款成功   |
| `charge.failed`          | `onChargeFailed`         | 付款失敗   |
| `charge.refunded`        | `onChargeRefunded`       | 已退款    |
| `customer.created`       | `onCustomerCreated`      | 客戶建立   |
| `customer.updated`       | `onCustomerUpdated`      | 客戶更新   |
| `customer.deleted`       | `onCustomerDeleted`      | 客戶刪除   |
| `invoice.created`        | `onInvoiceCreated`       | 發票建立   |
| `invoice.paid`           | `onInvoicePaid`          | 發票已付   |
| `invoice.payment_failed` | `onInvoicePaymentFailed` | 發票付款失敗 |

取得產品列表 [#取得產品列表]

在伺服器端取得（推薦） [#在伺服器端取得推薦]

```astro
---
// src/pages/pricing.astro
// 使用 Publishable Key 從 API 取得產品列表
const response = await fetch('https://api.recur.tw/v1/products?type=SUBSCRIPTION', {
  headers: {
    'X-Recur-Publishable-Key': import.meta.env.PUBLIC_RECUR_PUBLISHABLE_KEY,
  },
});
const { products } = await response.json();
---

<html>
  <body>
    <h1>選擇你的方案</h1>

    <div class="grid">
      {products.map((product) => (
        <div class="card">
          <h2>{product.name}</h2>
          <p class="price">
            NT$ {product.price} / {product.billing_period === 'MONTHLY' ? '月' : '年'}
          </p>
          <p>{product.description}</p>
          <a href={`/api/checkout?productId=${product.id}`}>
            立即訂閱
          </a>
        </div>
      ))}
    </div>
  </body>
</html>
```

React Island [#react-island]

如果你偏好 React 組件：

```bash
npx astro add react
```

```tsx
// src/components/SubscribeButton.tsx
'use client';

import { useRecur } from 'recur-tw';

export function SubscribeButton({ productId }: { productId: string }) {
  const { redirectToCheckout, isCheckingOut } = useRecur();

  const handleSubscribe = async () => {
    await redirectToCheckout({
      productId,
      successUrl: `${window.location.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancelUrl: `${window.location.origin}/pricing`,
    });
  };

  return (
    <button onClick={handleSubscribe} disabled={isCheckingOut}>
      {isCheckingOut ? '處理中...' : '立即訂閱'}
    </button>
  );
```

在 Astro 頁面中使用（注意需要 `RecurProvider` 和 `client:load` 指令）：

```astro
---
import { SubscribeIsland } from '../components/SubscribeIsland';
---

<SubscribeIsland productId="prod_xxx" client:load />
```

```tsx
// src/components/SubscribeIsland.tsx — 包含 Provider 的 Island wrapper
'use client';

import { RecurProvider } from 'recur-tw';
import { SubscribeButton } from './SubscribeButton';

export function SubscribeIsland({ productId }: { productId: string }) {
  return (
    <RecurProvider config={{ publishableKey: import.meta.env.PUBLIC_RECUR_PUBLISHABLE_KEY }}>
      <SubscribeButton productId={productId} />
    </RecurProvider>
  );
}
```

<Callout type="info">
  `client:load` 讓組件在頁面載入後立即 hydrate。其他選項包括 `client:visible`（進入視窗時）和 `client:idle`（瀏覽器閒置時）。
</Callout>

輸出模式比較 [#輸出模式比較]

| 功能                      | Static | Server |
| ----------------------- | :----: | :----: |
| 結帳（Modal/Redirect）      |    ✅   |    ✅   |
| Checkout Adapter        |    ❌   |    ✅   |
| Customer Portal Adapter |    ❌   |    ✅   |
| Webhooks Adapter        |    ❌   |    ✅   |
| 取得產品列表（前端）              |    ✅   |    ✅   |
| 取得產品列表（伺服器）             |    ❌   |    ✅   |

<Callout type="info">
  **建議**：大多數情況使用 **Server** 模式搭配 `prerender = true` 來靜態化特定頁面。這讓你享受靜態頁面的效能，同時保留 API routes 的彈性。
</Callout>

常見問題 [#常見問題]

Q: 為什麼我的 API endpoint 返回 404？ [#q-為什麼我的-api-endpoint-返回-404]

確保你已經：

1. 設定了 adapter（如 `@astrojs/vercel`）
2. 在 `astro.config.mjs` 設定 `output: 'server'`
3. 或在使用 static output 時，API 檔案中加入 `export const prerender = false;`

Q: React 組件為什麼沒有互動功能？ [#q-react-組件為什麼沒有互動功能]

確保你加了 `client:*` 指令：

```astro
<!-- 錯誤：組件不會 hydrate -->
<SubscribeButton productId="prod_xxx" />

<!-- 正確：組件會在載入後 hydrate -->
<SubscribeButton productId="prod_xxx" client:load />
```

Q: 如何在靜態模式下使用 Customer Portal？ [#q-如何在靜態模式下使用-customer-portal]

你需要自行建立後端（如 Cloudflare Workers、Vercel Functions），或使用第三方服務。Recur 的 Portal 連結需要 Secret Key，無法在純前端生成。

部署到 Vercel [#部署到-vercel]

<Steps>
  <Step>
    安裝 Vercel Adapter [#安裝-vercel-adapter]

    ```bash
    npx astro add vercel
    ```

    這會自動安裝 `@astrojs/vercel` 並更新 `astro.config.mjs`。
  </Step>

  <Step>
    設定環境變數 [#設定環境變數-1]

    在 Vercel Dashboard → Settings → Environment Variables 加入：

    ```bash
    RECUR_SECRET_KEY=sk_live_xxx
    RECUR_WEBHOOK_SECRET=whsec_xxx
    PUBLIC_RECUR_PUBLISHABLE_KEY=pk_live_xxx
    ```
  </Step>

  <Step>
    部署 [#部署]

    ```bash
    # 使用 Vercel CLI
    vercel

    # 或連結 GitHub 自動部署
    ```
  </Step>
</Steps>

<Callout type="info">
  Vercel 會自動偵測 Astro 專案並正確設定。如果使用 `output: 'server'`，Vercel 會自動建立 Serverless Functions。
</Callout>

下一步 [#下一步]

* [Webhook 整合](/guides/webhooks) - 深入了解事件處理
* [Customer Portal](/guides/portal) - 完整的 Portal 功能
* [API 參考](/api) - 完整的 API 文件
