├── 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 | [](https://stackblitz.com/github/withastro/astro/tree/latest/examples/minimal)
8 | [](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/minimal)
9 | [](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 |
--------------------------------------------------------------------------------