From eb23dbe0198d37746f3019c584456160ab2abc60 Mon Sep 17 00:00:00 2001 From: Gordon Diggs Date: Wed, 1 Jul 2026 11:33:35 -0400 Subject: [PATCH] Add provider-specific pages for SEO --- src/_components/providers-page-shell.webc | 37 ++ src/_layouts/main.webc | 4 +- src/assets/css/main.css | 2 +- src/lib/provider-pages.js | 62 +++ src/provider-detail.11tydata.js | 41 ++ src/provider-detail.webc | 152 ++++++++ src/providers.webc | 444 ++++++++++------------ src/sitemap.xml.11ty.js | 18 +- 8 files changed, 517 insertions(+), 243 deletions(-) create mode 100644 src/_components/providers-page-shell.webc create mode 100644 src/lib/provider-pages.js create mode 100644 src/provider-detail.11tydata.js create mode 100644 src/provider-detail.webc 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 @@ +
+ +
+ +
+ +
+ + Stripe Projects +
+
+
+
+
+ +
+ + + +
+ + + + + +
+ + + 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 @@ + + + +
+ Providers + / + +
+

+

+
+
+ + + +
+

Install command

+ +
+ + + +
+

Catalog details

+
+

Category:

+

Provider ID:

+

Service ID:

+
+ + View all providers + + +
+
+
+ + + +
+

Catalog entry

+

This provider is shown here in the same table format used on the full catalog page.

+
+ + + + + + + + + + + + + + + + + + +
ProviderCategoryDescriptionInstall 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> +
+
+ + + +
+

Missing a provider?

+

Let us know what services we should add next.

+ + Suggest a provider + + +
+
+
- - - -

Providers

-

Browse and provision services from supported providers. Click any install command to copy it.

-
-
+ + + - - - - -
-

Install command

-
- $ stripe projects add <provider>/<service> -
+ +
+
+
- -
- -
-
llms.txt
-
Service discovery for agents
-
- -
- -
-
Projects docs
-
Full documentation
-
- -
- -
-
Stripe CLI
-
Install and setup guide
-
- -
+
+ + + + + + + + + + + + + + + +
- -
-

Missing a provider?

-

Let us know what services we should add next.

- - Suggest a provider - - -
- - - - - - - - -
-
- -
-
- - - - - - - - - - - - - - - - -
-
+
- - - - - - - - - - - + +
ProviderCategoryDescriptionInstall command
+ + + + + + + + + @@ -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) => {
ProviderCategoryDescriptionInstall command
AgentMail Communications