Recur
Recur
Recur 文件中心
開發者指南
框架整合指南Vanilla JS 整合指南Next.js 整合指南Astro 整合指南
開發者指南框架整合

Astro 整合指南

在 Astro 專案中整合 Recur 訂閱功能

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

前置需求

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

安裝

官方 Astro adapter 提供最簡潔的整合方式:

npm install @recur-tw/astro

@recur-tw/astro 提供預封裝的 handler 函數,大幅減少 boilerplate 程式碼。

如果你需要更多自訂彈性,可以直接使用核心 SDK:

npm install recur-tw

設定輸出模式

在 astro.config.mjs 中設定:

全伺服器模式,所有頁面和 API 都在伺服器執行:

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

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

如果想讓部分頁面預先渲染(例如首頁),在該頁面加入 export const prerender = true;

預設靜態,只有 API routes 需要伺服器執行:

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

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

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

export const prerender = false;

純靜態網站,只能使用前端功能:

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

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

靜態模式無法使用 Server SDK。如需 Customer Portal 或 Webhook,需要另外建立後端。

設定環境變數

在 .env 中加入:

# 前端使用(可公開)
PUBLIC_RECUR_PUBLISHABLE_KEY=pk_test_xxx

# 後端使用(保密)
RECUR_SECRET_KEY=sk_test_xxx
RECUR_WEBHOOK_SECRET=whsec_xxx

結帳功能

方法 1:使用 Adapter(推薦)

建立 Checkout Endpoint

// 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',
});

在頁面中使用

---
// 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="checkout(this)"
    >
      立即訂閱
    </button>

    <script is:inline>
      function checkout(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>

customerEmail 是必填參數。如果沒有提供,API 會返回錯誤。

支援的 Query Parameters:

參數說明必填
productId產品 ID✓*
productSlug產品 slug(替代 productId)✓*
customerEmail客戶 Email✓
customerName客戶姓名
customerId現有客戶 ID
metadataURL-encoded JSON 字串
* productId 或 productSlug 至少需提供一個

方法 2:自訂 Resolver

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

// 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)

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

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

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

    <script>
      import Recur from 'recur-tw';

      const recur = Recur.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;

        await recur.checkout({
          productId,
          customerEmail: 'user@example.com',
          customerName: '王小明',
          mode: 'modal',
          onPaymentComplete: (result) => {
            alert('訂閱成功!');
            window.location.href = '/success';
          },
          onError: (error) => {
            alert('錯誤:' + error.message);
          },
        });
      });
    </script>
  </body>
</html>

Customer Portal

方法 1:使用 Adapter(推薦)

// 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

整合認證系統

// 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:手動實作

---
// 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 組合

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

方法 1:使用 RecurClient(最彈性)

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

// 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 頁面中使用:

---
// 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.currentPeriodEnd).toLocaleDateString('zh-TW')}</p>

RecurClient 方法

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

查詢參數(至少需提供一個):

參數說明
email顧客 Email
externalId你系統中的 User ID
customerIdRecur 的 Customer ID
planSlug篩選特定方案
planId篩選特定方案 ID
activeOnly只查詢有效訂閱(預設 false)

方法 2:使用 withSubscription Middleware

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

// 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 };
  },
});

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

---
// 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 支援

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

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

declare namespace App {
  interface Locals extends SubscriptionLocals {}
}

方法 3:使用 requireSubscription Middleware

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

// 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:

// 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();
};

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

與多個 Middleware 組合(使用 sequence)

如果你選擇使用我們的 middleware,可以用 sequence 與其他 middleware 組合:

// 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 處理

使用 Adapter(推薦)

// 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.createdonSubscriptionCreated新訂閱建立
subscription.updatedonSubscriptionUpdated訂閱更新
subscription.canceledonSubscriptionCanceled訂閱取消
subscription.expiredonSubscriptionExpired訂閱過期
subscription.pausedonSubscriptionPaused訂閱暫停
subscription.resumedonSubscriptionResumed訂閱恢復
charge.succeededonChargeSucceeded付款成功
charge.failedonChargeFailed付款失敗
charge.refundedonChargeRefunded已退款
customer.createdonCustomerCreated客戶建立
customer.updatedonCustomerUpdated客戶更新
customer.deletedonCustomerDeleted客戶刪除
invoice.createdonInvoiceCreated發票建立
invoice.paidonInvoicePaid發票已付
invoice.payment_failedonInvoicePaymentFailed發票付款失敗

取得產品列表

在伺服器端取得(推薦)

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

const recur = new Recur(import.meta.env.RECUR_SECRET_KEY);
const { products } = await recur.products.list({ type: 'SUBSCRIPTION' });
---

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

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

React Island

如果你偏好 React 組件:

npx astro add react
// src/components/SubscribeButton.tsx
import { useState } from 'react';
import Recur from 'recur-tw';

export function SubscribeButton({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false);

  const handleSubscribe = async () => {
    setLoading(true);

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

    await recur.checkout({
      productId,
      mode: 'modal',
      onPaymentComplete: () => {
        window.location.href = '/success';
      },
      onError: (error) => {
        alert(error.message);
        setLoading(false);
      },
    });
  };

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

在 Astro 頁面中使用(注意 client:load 指令):

---
import { SubscribeButton } from '../components/SubscribeButton';
---

<SubscribeButton productId="prod_xxx" client:load />

client:load 讓組件在頁面載入後立即 hydrate。其他選項包括 client:visible(進入視窗時)和 client:idle(瀏覽器閒置時)。

輸出模式比較

功能StaticServer
結帳(Modal/Redirect)✅✅
Checkout Adapter❌✅
Customer Portal Adapter❌✅
Webhooks Adapter❌✅
取得產品列表(前端)✅✅
取得產品列表(伺服器)❌✅

建議:大多數情況使用 Server 模式搭配 prerender = true 來靜態化特定頁面。這讓你享受靜態頁面的效能,同時保留 API routes 的彈性。

常見問題

Q: 為什麼我的 API endpoint 返回 404?

確保你已經:

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

Q: React 組件為什麼沒有互動功能?

確保你加了 client:* 指令:

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

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

Q: 如何在靜態模式下使用 Customer Portal?

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

部署到 Vercel

安裝 Vercel Adapter

npx astro add vercel

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

設定環境變數

在 Vercel Dashboard → Settings → Environment Variables 加入:

RECUR_SECRET_KEY=sk_live_xxx
RECUR_WEBHOOK_SECRET=whsec_xxx
PUBLIC_RECUR_PUBLISHABLE_KEY=pk_live_xxx

部署

# 使用 Vercel CLI
vercel

# 或連結 GitHub 自動部署

Vercel 會自動偵測 Astro 專案並正確設定。如果使用 output: 'server',Vercel 會自動建立 Serverless Functions。

下一步

  • Webhook 整合 - 深入了解事件處理
  • Customer Portal - 完整的 Portal 功能
  • API 參考 - 完整的 API 文件

Next.js 整合指南

在 Next.js App Router 中整合 Recur 訂閱功能

結帳整合

選擇最適合您的結帳整合方式

On this page

前置需求安裝設定輸出模式設定環境變數結帳功能方法 1:使用 Adapter(推薦)建立 Checkout Endpoint在頁面中使用方法 2:自訂 Resolver方法 3:純 Astro(無 Adapter)Customer Portal方法 1:使用 Adapter(推薦)整合認證系統方法 2:手動實作查詢訂閱狀態選擇適合的方法方法 1:使用 RecurClient(最彈性)RecurClient 方法方法 2:使用 withSubscription MiddlewareTypeScript 支援方法 3:使用 requireSubscription Middleware在現有 Middleware 中整合 RecurClient與多個 Middleware 組合(使用 sequence)Webhook 處理使用 Adapter(推薦)支援的事件類型取得產品列表在伺服器端取得(推薦)React Island輸出模式比較常見問題Q: 為什麼我的 API endpoint 返回 404?Q: React 組件為什麼沒有互動功能?Q: 如何在靜態模式下使用 Customer Portal?部署到 Vercel安裝 Vercel Adapter設定環境變數部署下一步