├── pnpm-workspace.yaml ├── demo ├── src │ ├── env.d.ts │ ├── pages │ │ ├── prices │ │ │ └── [id].astro │ │ ├── products │ │ │ └── [id].astro │ │ ├── plans │ │ │ └── [id].astro │ │ └── index.astro │ └── content │ │ └── config.ts ├── tsconfig.json ├── astro.config.mjs ├── .gitignore ├── package.json └── README.md ├── prettier.config.cjs ├── release.config.cjs ├── renovate.json ├── src ├── index.ts ├── price-loader.ts ├── plan-loader.ts ├── product-loader.ts └── utils.ts ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── release.yml ├── package.json └── README.md /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | }; 4 | -------------------------------------------------------------------------------- /release.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ["main"], 3 | }; 4 | -------------------------------------------------------------------------------- /demo/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { type StripeLoaderOptions } from "./utils"; 2 | export { stripeProductLoader } from "./product-loader"; 3 | export { stripePriceLoader } from "./price-loader"; 4 | export { stripePlanLoader } from "./plan-loader"; 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /demo/src/pages/prices/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | 4 | export async function getStaticPaths() { 5 | const prices = await getCollection('prices'); 6 | 7 | return prices.map((price: any) => ({ 8 | params: { id: price.id }, props: { price }, 9 | })); 10 | } 11 | 12 | const { price } = Astro.props; 13 | --- 14 | 15 |

{price.data.id}

16 | 17 |
{JSON.stringify({price}, null, 2)}
18 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro check && astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "astro": "^5.0.5", 15 | "@astrojs/check": "^0.9.4", 16 | "typescript": "^5.5.4", 17 | "stripe-astro-loader": "workspace:*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { 3 | stripePriceLoader, 4 | stripeProductLoader, 5 | stripePlanLoader, 6 | } from "stripe-astro-loader"; 7 | import Stripe from "stripe"; 8 | 9 | const stripe = new Stripe(""); 10 | 11 | const products = defineCollection({ 12 | loader: stripeProductLoader(stripe), 13 | }); 14 | 15 | const prices = defineCollection({ 16 | loader: stripePriceLoader(stripe), 17 | }); 18 | 19 | const plans = defineCollection({ 20 | loader: stripePlanLoader(stripe), 21 | }); 22 | 23 | export const collections = { products, prices, plans }; 24 | -------------------------------------------------------------------------------- /demo/src/pages/products/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, render } from 'astro:content'; 3 | 4 | export async function getStaticPaths() { 5 | const products = await getCollection('products'); 6 | 7 | const prices = await getCollection('prices'); 8 | 9 | return products.map((product: any) => ({ 10 | params: { id: product.id }, 11 | props: { 12 | product, 13 | prices: prices.filter((price: any) => price.data.product === product.id) 14 | }, 15 | })); 16 | } 17 | 18 | const { product, prices } = Astro.props; 19 | const { Content } = await render(product); 20 | --- 21 | 22 |

{product.data.name}

23 |

{product.data.created}

24 | 25 |
{JSON.stringify({product, prices}, null, 2)}
26 | 27 | 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | "verbatimModuleSyntax": true, 12 | /* Strictness */ 13 | "strict": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noImplicitOverride": true, 16 | /* AND if you're building for a library: */ 17 | "declaration": true, 18 | "declarationMap": true, 19 | /* If NOT transpiling with TypeScript: */ 20 | "module": "preserve", 21 | "noEmit": true, 22 | /* If your code doesn't run in the DOM: */ 23 | "lib": ["es2022"] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | issues: write 11 | pull-requests: write 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup pnpm 19 | uses: pnpm/action-setup@v3 20 | with: 21 | version: 8 22 | - uses: actions/setup-node@v4 23 | with: 24 | node-version: "lts/*" 25 | cache: "pnpm" 26 | - run: pnpm install 27 | - run: pnpm build 28 | - name: Release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | run: npx semantic-release 33 | -------------------------------------------------------------------------------- /demo/src/pages/plans/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection, render } from 'astro:content'; 3 | 4 | export async function getStaticPaths() { 5 | const plans = await getCollection('plans'); 6 | 7 | return plans.map((plan: any) => ({ 8 | params: { id: plan.id }, 9 | props: { 10 | plan, 11 | }, 12 | })); 13 | } 14 | 15 | const { plan } = Astro.props; 16 | --- 17 | 18 |

{plan.data.nickname || plan.id}

19 |

Created: {new Date(plan.data.created * 1000).toLocaleDateString()}

20 |

Amount: ${(plan.data.amount / 100).toFixed(2)} {plan.data.currency.toUpperCase()}

21 |

Interval: {plan.data.interval}

22 |

Active: {plan.data.active ? 'Yes' : 'No'}

23 | 24 |
{JSON.stringify(plan, null, 2)}
25 | -------------------------------------------------------------------------------- /src/price-loader.ts: -------------------------------------------------------------------------------- 1 | import type { Loader } from "astro/loaders"; 2 | import Stripe from "stripe"; 3 | import { 4 | stripeTsToZod, 5 | paginateStripeAPI, 6 | type StripeLoaderOptions, 7 | } from "./utils"; 8 | 9 | export type StripePriceLoaderOptions = 10 | StripeLoaderOptions; 11 | 12 | export const zodSchemaFromStripePrices = stripeTsToZod(); 13 | 14 | export function stripePriceLoader( 15 | stripe: Stripe, 16 | options: StripePriceLoaderOptions = {} 17 | ): Loader { 18 | return { 19 | name: "stripe-price-loader", 20 | load: async (context) => { 21 | await paginateStripeAPI( 22 | stripe.prices.list.bind(stripe.prices), 23 | options, 24 | context, 25 | "stripe-prices-last-updated" 26 | ); 27 | }, 28 | schema: zodSchemaFromStripePrices, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/plan-loader.ts: -------------------------------------------------------------------------------- 1 | import type { Loader } from "astro/loaders"; 2 | import Stripe from "stripe"; 3 | import { 4 | stripeTsToZod, 5 | paginateStripeAPI, 6 | type StripeLoaderOptions, 7 | } from "./utils"; 8 | 9 | export type StripePlanLoaderOptions = 10 | StripeLoaderOptions; 11 | 12 | export const zodSchemaFromStripePlans = stripeTsToZod(); 13 | 14 | export function stripePlanLoader( 15 | stripe: Stripe, 16 | options: StripePlanLoaderOptions = {} 17 | ): Loader { 18 | return { 19 | name: "stripe-plan-loader", 20 | load: async (context) => { 21 | await paginateStripeAPI( 22 | stripe.plans.list.bind(stripe.plans), 23 | options, 24 | context, 25 | "stripe-plans-last-updated", 26 | (plan) => plan.nickname || null 27 | ); 28 | }, 29 | schema: zodSchemaFromStripePlans, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/product-loader.ts: -------------------------------------------------------------------------------- 1 | import type { Loader } from "astro/loaders"; 2 | import Stripe from "stripe"; 3 | import { 4 | stripeTsToZod, 5 | paginateStripeAPI, 6 | type StripeLoaderOptions, 7 | } from "./utils"; 8 | 9 | export type StripeProductLoaderOptions = 10 | StripeLoaderOptions; 11 | 12 | export const zodSchemaFromStripeProducts = stripeTsToZod(); 13 | 14 | export function stripeProductLoader( 15 | stripe: Stripe, 16 | options: StripeProductLoaderOptions = {} 17 | ): Loader { 18 | return { 19 | name: "stripe-product-loader", 20 | load: async (context) => { 21 | await paginateStripeAPI( 22 | stripe.products.list.bind(stripe.products), 23 | options, 24 | context, 25 | "stripe-products-last-updated", 26 | (product) => product.description || null 27 | ); 28 | }, 29 | schema: zodSchemaFromStripeProducts, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stripe-astro-loader", 3 | "version": "0.0.0-development", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "repository": "notrab/stripe-astro-loader", 7 | "exports": { 8 | ".": "./dist/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsup src/index.ts --format esm --dts --clean", 12 | "dev": "tsup src/index.ts --format esm --dts --watch" 13 | }, 14 | "keywords": [ 15 | "astro", 16 | "astro-loader", 17 | "astro-integration", 18 | "content-layer", 19 | "stripe", 20 | "withastro" 21 | ], 22 | "author": "Jamie Barton ", 23 | "license": "MIT", 24 | "description": "Load Stripe data in Astro.", 25 | "devDependencies": { 26 | "astro": "^5.10.1", 27 | "tsup": "^8.5.0", 28 | "typescript": "^5.8.3", 29 | "zod": "^3.25.67", 30 | "stripe": "^18.2.1" 31 | }, 32 | "peerDependencies": { 33 | "astro": "^4.14.0 || ^5.0.0", 34 | "stripe": "^16.7.0 || ^17.0.0" 35 | }, 36 | "files": [ 37 | "dist", 38 | "README.md" 39 | ], 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /demo/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | 4 | const products = await getCollection('products'); 5 | const prices = await getCollection('prices'); 6 | const plans = await getCollection('plans'); 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Astro 16 | 17 | 18 |

Stripe Astro Data Loader

19 |

Fetch and cache from Stripe using Astro's Content Layer

20 |

Products

21 | 26 | 27 |

Prices

28 |
    29 | {prices.map((price: any) => ( 30 |
  • {price.id}
  • 31 | ))} 32 |
33 | 34 |

Plans

35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stripe-astro-loader 2 | 3 | Fetch data from the Stripe API and use it in Astro collections — only has **products** for now. 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm i stripe stripe-astro-loader 9 | ``` 10 | 11 | ## Configure 12 | 13 | ```ts 14 | import { defineCollection } from "astro:content"; 15 | import { stripePriceLoader, stripeProductLoader } from "stripe-astro-loader"; 16 | import Stripe from "stripe"; 17 | 18 | const stripe = new Stripe("SECRET_KEY"); 19 | 20 | const products = defineCollection({ 21 | loader: stripeProductLoader(stripe), 22 | }); 23 | 24 | const prices = defineCollection({ 25 | loader: stripePriceLoader(stripe), 26 | }); 27 | 28 | export const collections = { products, prices }; 29 | ``` 30 | 31 | Make sure to enable the experimental content layer in your Astro config: 32 | 33 | ```ts 34 | import { defineConfig } from "astro/config"; 35 | 36 | export default defineConfig({ 37 | experimental: { 38 | contentLayer: true, 39 | }, 40 | }); 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```astro 46 | // pages/index.astro 47 | --- 48 | import { getCollection } from 'astro:content'; 49 | 50 | const products = await getCollection('products'); 51 | --- 52 | ``` 53 | 54 | ```astro 55 | // pages/products/[id].astro 56 | --- 57 | import { getCollection,render } from 'astro:content'; 58 | 59 | export async function getStaticPaths() { 60 | const products = await getCollection('products'); 61 | 62 | return products.map(product => ({ 63 | params: { id: product.id }, props: { product }, 64 | })); 65 | } 66 | 67 | const { product } = Astro.props; 68 | const { Content, headings } = await render(product); 69 | --- 70 | 71 |

{product.data.name}

72 | 73 |
{JSON.stringify({product, headings}, null, 2)}
74 | 75 | 76 | ``` 77 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Astro Starter Kit: Minimal 2 | 3 | ```sh 4 | npm create astro@latest -- --template minimal 5 | ``` 6 | 7 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal) 8 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal) 9 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/minimal/devcontainer.json) 10 | 11 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 12 | 13 | ## 🚀 Project Structure 14 | 15 | Inside of your Astro project, you'll see the following folders and files: 16 | 17 | ```text 18 | / 19 | ├── public/ 20 | ├── src/ 21 | │ └── pages/ 22 | │ └── index.astro 23 | └── package.json 24 | ``` 25 | 26 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 27 | 28 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 29 | 30 | Any static assets, like images, can be placed in the `public/` directory. 31 | 32 | ## 🧞 Commands 33 | 34 | All commands are run from the root of the project, from a terminal: 35 | 36 | | Command | Action | 37 | | :------------------------ | :----------------------------------------------- | 38 | | `npm install` | Installs dependencies | 39 | | `npm run dev` | Starts local dev server at `localhost:4321` | 40 | | `npm run build` | Build your production site to `./dist/` | 41 | | `npm run preview` | Preview your build locally, before deploying | 42 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 43 | | `npm run astro -- --help` | Get help using the Astro CLI | 44 | 45 | ## 👀 Want to learn more? 46 | 47 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 48 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { z } from "astro/zod"; 2 | import { AstroError } from "astro/errors"; 3 | import type { Loader } from "astro/loaders"; 4 | import Stripe from "stripe"; 5 | 6 | export function stripeTsToZod() { 7 | return z.custom(() => true) as z.ZodType; 8 | } 9 | 10 | export type StripeLoaderOptions = T & { 11 | limit?: number; 12 | }; 13 | 14 | export async function paginateStripeAPI< 15 | T extends { id: string; created: number }, 16 | P 17 | >( 18 | listFunction: (params: P) => Promise>, 19 | options: StripeLoaderOptions

, 20 | context: Parameters[0], 21 | metaKey: string, 22 | renderItem?: (item: T) => string | null 23 | ): Promise { 24 | const { logger, parseData, store, meta, generateDigest } = context; 25 | const { limit = Infinity, ...queryParams } = options; 26 | 27 | let allItems: T[] = []; 28 | let hasMore = true; 29 | let lastId: string | undefined; 30 | 31 | const lastUpdated = meta.get(metaKey); 32 | 33 | if (lastUpdated) { 34 | // @ts-expect-error 35 | queryParams.created = { gt: parseInt(lastUpdated) }; 36 | } 37 | 38 | while (hasMore && allItems.length < limit) { 39 | const params = { 40 | ...queryParams, 41 | limit: Math.min(100, limit - allItems.length), 42 | starting_after: lastId, 43 | } as P; 44 | 45 | try { 46 | const response = await listFunction(params); 47 | 48 | for (const item of response.data) { 49 | if (allItems.length >= limit) break; 50 | 51 | const data = await parseData({ id: item.id, data: item }); 52 | const digest = generateDigest(data); 53 | 54 | const storeItem: { 55 | id: string; 56 | data: any; 57 | digest: string | number; 58 | rendered?: { html: string }; 59 | } = { 60 | id: item.id, 61 | data, 62 | digest, 63 | }; 64 | 65 | if (renderItem) { 66 | const renderedHtml = renderItem(item); 67 | if (renderedHtml) { 68 | storeItem.rendered = { html: renderedHtml }; 69 | } 70 | } 71 | 72 | const changed = store.set(storeItem); 73 | 74 | if (changed) { 75 | logger.debug(`Updated item: ${item.id}`); 76 | } 77 | 78 | allItems.push(item); 79 | } 80 | 81 | hasMore = response.has_more && allItems.length < limit; 82 | lastId = response.data[response.data.length - 1]?.id; 83 | 84 | if (response.data.length > 0) { 85 | const latestUpdated = Math.max( 86 | ...response.data.map((item) => item.created) 87 | ); 88 | meta.set(metaKey, latestUpdated.toString()); 89 | } 90 | } catch (error) { 91 | if (error instanceof Stripe.errors.StripeError) { 92 | throw new AstroError(`Stripe API error: ${error.message}`); 93 | } 94 | throw error; 95 | } 96 | 97 | logger.info(`Loaded ${allItems.length} items from Stripe so far`); 98 | } 99 | 100 | logger.info(`Finished loading ${allItems.length} items from Stripe`); 101 | 102 | return allItems; 103 | } 104 | --------------------------------------------------------------------------------