diff --git a/src/_components/providers-page-shell.webc b/src/_components/providers-page-shell.webc
new file mode 100644
index 0000000..357a1bf
--- /dev/null
+++ b/src/_components/providers-page-shell.webc
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/_layouts/main.webc b/src/_layouts/main.webc
index 4373460..ea15633 100644
--- a/src/_layouts/main.webc
+++ b/src/_layouts/main.webc
@@ -4,7 +4,7 @@
-
+
@@ -15,7 +15,7 @@
-
+
diff --git a/src/assets/css/main.css b/src/assets/css/main.css
index ea48b62..f195909 100644
--- a/src/assets/css/main.css
+++ b/src/assets/css/main.css
@@ -387,4 +387,4 @@
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
-}
\ No newline at end of file
+}
diff --git a/src/lib/provider-pages.js b/src/lib/provider-pages.js
new file mode 100644
index 0000000..b633651
--- /dev/null
+++ b/src/lib/provider-pages.js
@@ -0,0 +1,62 @@
+export function stripTrailingSlash(value = "") {
+ return String(value).replace(/\/$/, "");
+}
+
+export function getProviderId(provider) {
+ return String(provider?.slug ?? "").split("/")[0] ?? "";
+}
+
+export function getServiceId(provider) {
+ return String(provider?.slug ?? "").split("/")[1] ?? "";
+}
+
+export function getProviderPageSlug(provider) {
+ return getProviderId(provider)
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "");
+}
+
+export function getProviderPagePath(provider) {
+ return `/providers/${getProviderPageSlug(provider)}/`;
+}
+
+export function getProviderPageUrl(provider, siteOrigin = "https://projects.dev") {
+ return `${stripTrailingSlash(siteOrigin)}${getProviderPagePath(provider)}`;
+}
+
+export function formatToken(token = "") {
+ const upper = token.toUpperCase();
+ if (["AI", "API", "CI", "CD", "TTS"].includes(upper)) return upper;
+ return token.charAt(0).toUpperCase() + token.slice(1);
+}
+
+export function formatIdentifier(value = "") {
+ return String(value)
+ .split(/[^a-zA-Z0-9]+/)
+ .filter(Boolean)
+ .map(formatToken)
+ .join(" ");
+}
+
+export function formatCategory(category = "") {
+ if (category === "ci-cd") return "CI/CD";
+ if (category === "ai") return "AI";
+ return formatIdentifier(category);
+}
+
+export function getDisplayUrl(url = "") {
+ return stripTrailingSlash(url).replace(/^https?:\/\//, "");
+}
+
+export function getInstallCommand(provider) {
+ return `stripe projects add ${provider.slug}`;
+}
+
+export function getProviderSummary(provider) {
+ return provider?.longDescription || provider?.description || "";
+}
+
+export function getProviderSeoDescription(provider) {
+ return `Provision ${provider.name} with Stripe Projects. Add ${provider.name} via ${getInstallCommand(provider)}. ${provider.description}.`;
+}
diff --git a/src/provider-detail.11tydata.js b/src/provider-detail.11tydata.js
new file mode 100644
index 0000000..5ab5ee5
--- /dev/null
+++ b/src/provider-detail.11tydata.js
@@ -0,0 +1,41 @@
+import {
+ formatCategory,
+ formatIdentifier,
+ getDisplayUrl,
+ getInstallCommand,
+ getProviderPagePath,
+ getProviderPageUrl,
+ getProviderSeoDescription,
+ getProviderSummary,
+ getProviderId,
+ getServiceId,
+} from "./lib/provider-pages.js";
+
+export default {
+ layout: "main.webc",
+ pagination: {
+ data: "providers",
+ size: 1,
+ alias: "provider",
+ },
+ permalink: ({ provider }) => `${getProviderPagePath(provider)}index.html`,
+ changefreq: "weekly",
+ wave: false,
+ eleventyComputed: {
+ title: ({ provider }) => provider.name,
+ seoTitle: ({ provider }) => `${provider.name} provider | Stripe Projects`,
+ ogTitle: ({ provider }) => `${provider.name} provider | Stripe Projects`,
+ seoDesc: ({ provider }) => getProviderSeoDescription(provider),
+ ogDesc: ({ provider }) => getProviderSeoDescription(provider),
+ ogImage: "/assets/images/og/default.jpg",
+ ogImageAlt: ({ provider }) => `${provider.name} on Stripe Projects`,
+ canonicalUrl: ({ base, provider }) => getProviderPageUrl(provider, base?.siteOrigin),
+ providerCategoryLabel: ({ provider }) => formatCategory(provider.category),
+ providerServiceId: ({ provider }) => getServiceId(provider),
+ providerServiceLabel: ({ provider }) => formatIdentifier(getServiceId(provider)),
+ providerId: ({ provider }) => getProviderId(provider),
+ providerInstallCommand: ({ provider }) => getInstallCommand(provider),
+ providerSummary: ({ provider }) => getProviderSummary(provider),
+ providerWebsiteLabel: ({ provider }) => getDisplayUrl(provider.url),
+ },
+};
diff --git a/src/provider-detail.webc b/src/provider-detail.webc
new file mode 100644
index 0000000..97e626e
--- /dev/null
+++ b/src/provider-detail.webc
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
Install command
+
+
+
+
+
+
+
+
+
+
+
+
+
Catalog entry
+
This provider is shown here in the same table format used on the full catalog page.
+
+
+
+
+
+ | Provider |
+ Category |
+ Description |
+ Install command |
+
+
+
+
+ |
+
+ |
+ |
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/providers.webc b/src/providers.webc
index 69b792d..f9a5803 100644
--- a/src/providers.webc
+++ b/src/providers.webc
@@ -11,118 +11,102 @@ changefreq: "weekly"
wave: false
---
-
+
-
-
-
-
-
-
-
- Stripe Projects
-
-
-
+
+
+
+ Providers
+ Browse and provision services from supported providers. Click any install command to copy it.
-
+
-
+
+
+
+
+
+
Install command
+
+ $ stripe projects add <provider>/<service>
+
+
+
+
+
+
+
+
-
-
-
- Providers
- Browse and provision services from supported providers. Click any install command to copy it.
-
-
+
+
+
-
-
-
-
-
-
Install command
-
- $ stripe projects add <provider>/<service>
-
+
+
+
+
-
-
-
-
-
-
- | Provider |
- Category |
- Description |
- Install command |
-
-
-
+
+
+
+
+ | Provider |
+ Category |
+ Description |
+ Install command |
+
+
+
| AgentMail |
Communications |
@@ -423,162 +407,148 @@ wave: false
No providers match your search.
-
-
-
-
+
+
-
-
-
+
-
+ })();
+
+
diff --git a/src/sitemap.xml.11ty.js b/src/sitemap.xml.11ty.js
index 2e84df4..451008e 100644
--- a/src/sitemap.xml.11ty.js
+++ b/src/sitemap.xml.11ty.js
@@ -1,3 +1,5 @@
+import { getProviderPagePath } from "./lib/provider-pages.js";
+
function escapeXml(value = "") {
return String(value)
.replaceAll("&", "&")
@@ -43,14 +45,24 @@ export default class SitemapTemplate {
render(data) {
const siteOrigin = (data.base?.siteOrigin ?? "https://projects.dev").replace(/\/$/, "");
- const pages = Array.from(
+ const collectionPages = Array.from(
new Map(
(data.collections.all ?? [])
.filter(shouldIncludePage)
.map((item) => [item.url, item]),
).values(),
- )
- .sort((a, b) => a.url.localeCompare(b.url));
+ );
+ const providerPages = (data.providers ?? []).map((provider) => ({
+ url: getProviderPagePath(provider),
+ data: {
+ changefreq: "weekly",
+ },
+ }));
+ const pages = Array.from(
+ new Map(
+ [...collectionPages, ...providerPages].map((item) => [item.url, item]),
+ ).values(),
+ ).sort((a, b) => a.url.localeCompare(b.url));
const body = pages
.map((item) => {