diff --git a/astro.config.ts b/astro.config.ts
index 5e55e256..d4693bcf 100644
--- a/astro.config.ts
+++ b/astro.config.ts
@@ -9,6 +9,7 @@ import starlightLlmsTxt from "starlight-llms-txt";
import { loadEnv } from "vite";
import { readFile } from "node:fs/promises";
import satteriExternalLinks from "./src/plugins/satteri/external-links";
+import satteriCallouts from "./src/plugins/satteri/callouts";
import { defineConfig } from "astro/config";
import type { HeadUserConfig } from "node_modules/@astrojs/starlight/schemas/head";
@@ -194,9 +195,11 @@ export default defineConfig({
markdown: {
processor: satteri({
features: {
+ directive: true,
gfm: true,
smartPunctuation: true,
},
+ mdastPlugins: [satteriCallouts],
hastPlugins: [satteriExternalLinks],
}),
},
@@ -334,6 +337,7 @@ export default defineConfig({
"./src/styles/theme-light.css",
"./src/styles/starlight-vars.css",
"./src/styles/utilities.css",
+ "./src/styles/callout.css",
],
pagination: false,
lastUpdated: true,
diff --git a/ec.config.mjs b/ec.config.mjs
index 8d7c9805..d381f41f 100644
--- a/ec.config.mjs
+++ b/ec.config.mjs
@@ -4,7 +4,7 @@ import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
export default defineEcConfig({
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
- themes: ["github-dark-dimmed", "github-light-default"],
+ themes: ["github-dark-default", "github-light-default"],
styleOverrides: {
codeFontSize: "0.8rem",
borderColor: "var(--cui-border-subtle)",
diff --git a/src/components/ApiDocs/Operation.astro b/src/components/ApiDocs/Operation.astro
index d5d76b0f..0226d9df 100644
--- a/src/components/ApiDocs/Operation.astro
+++ b/src/components/ApiDocs/Operation.astro
@@ -2,10 +2,13 @@
import { Badge, Tabs, TabItem } from "@astrojs/starlight/components";
import Callout from "@components/content/Callout";
import Markdown from "@components/Markdown.astro";
+import MarkdownContent from "@components/MarkdownContent.astro";
import { formatSlug } from "@lib/helpers";
import { resolveReference } from "@lib/openapi";
+import { getOperationDescriptionId } from "@lib/openapiContent";
import { Headline, Body, Anchor } from "@sumup-oss/circuit-ui";
import type { HTMLAttributes } from "astro/types";
+import { getEntry, render } from "astro:content";
import type { OpenAPIV3_1 } from "openapi-types";
import type {
OperationObject,
@@ -26,6 +29,24 @@ interface Props extends HTMLAttributes<"section"> {
}
const { operation, coreObjects = [], ...props } = Astro.props;
+// OpenAPI descriptions are content entries so they use the same configured
+// Markdown pipeline as authored docs instead of a component-local parser.
+const descriptionEntry = operation.operationId
+ ? await getEntry(
+ "apiDescriptions",
+ getOperationDescriptionId(operation.operationId),
+ )
+ : undefined;
+
+if (operation.description && !descriptionEntry) {
+ throw new Error(
+ `Missing Markdown content entry for operation ${operation.operationId ?? "without an operationId"}`,
+ );
+}
+
+const Description = descriptionEntry
+ ? (await render(descriptionEntry)).Content
+ : undefined;
const isParameterObject = (
object: OpenAPIV3_1.ParameterObject | OpenAPIV3_1.ReferenceObject,
@@ -260,10 +281,10 @@ const formattedPermissions = (operation["x-permissions"] || [])
)
}
{
- operation.description && (
-
- {operation.description}
-
+ operation.description && Description && (
+
+
+
)
}
{
diff --git a/src/components/ApiDocs/TagSection.astro b/src/components/ApiDocs/TagSection.astro
index c1b56988..6250a570 100644
--- a/src/components/ApiDocs/TagSection.astro
+++ b/src/components/ApiDocs/TagSection.astro
@@ -1,8 +1,11 @@
---
import Callout from "@components/content/Callout";
import Markdown from "@components/Markdown.astro";
+import MarkdownContent from "@components/MarkdownContent.astro";
import { formatSlug } from "@lib/helpers";
+import { getTagDescriptionId } from "@lib/openapiContent";
import type { HTMLAttributes } from "astro/types";
+import { getEntry, render } from "astro:content";
import type { OperationObject, TagObject } from "src/types/openapi";
import EndpointList from "./EndpointList";
import LRGrid from "./LRGrid.astro";
@@ -28,6 +31,20 @@ const {
} = Astro.props;
const slug = formatSlug(name);
+// Tag descriptions are content entries so they use the configured Satteri
+// pipeline, matching operation descriptions and authored documentation.
+const descriptionEntry = await getEntry(
+ "apiDescriptions",
+ getTagDescriptionId(name),
+);
+
+if (description && !descriptionEntry) {
+ throw new Error(`Missing Markdown content entry for tag ${name}`);
+}
+
+const Description = descriptionEntry
+ ? (await render(descriptionEntry)).Content
+ : undefined;
const [targetTag, targetOp] = (target || "").split("/");
const tagIsTarget = !targetOp && targetTag === slug;
const objectPagefindAttributes = tagIsTarget
@@ -80,7 +97,13 @@ const coreObjectSections = coreObjects.map((schema, index) => {
)
}
- {description}
+ {
+ description && Description && (
+
+
+
+ )
+ }
{
operations.length > 0 && (
diff --git a/src/components/ApiDocs/TopSections.astro b/src/components/ApiDocs/TopSections.astro
index 391a9030..dabe474d 100644
--- a/src/components/ApiDocs/TopSections.astro
+++ b/src/components/ApiDocs/TopSections.astro
@@ -1,4 +1,5 @@
---
+import MarkdownContent from "@components/MarkdownContent.astro";
import { CodeBlock, MultiCode } from "@components/Code";
import Markdown from "@components/Markdown.astro";
import type { ApiTopSection } from "@lib/openapi/routes";
@@ -72,14 +73,14 @@ const sectionAttrs = (id: ApiTopSection) =>
-
+
SDKs
The SumUp SDKs reduce the amount of work required to use our REST
- APIs. SumUp maintains SDKs for PHP, JavaScript, Python, Java, Go, Rust,
- and .NET.
+ APIs. SumUp maintains SDKs for PHP, JavaScript, Python, Java, Go,
+ Rust, and .NET.
-
+
@@ -167,7 +168,7 @@ uv add sumup`,
-
+
Authentication
@@ -185,7 +186,7 @@ uv add sumup`,
All API requests must be made over HTTPS and authenticated. Calls made
over plain HTTP or calls without authentication will fail.
-
+
-
+
Errors
Newer APIs use{" "}
@@ -381,7 +382,7 @@ let client = Client::default().with_authorization("sup_sk_MvxmLOl0...");`,
)
}
-
+
+
-
+
diff --git a/src/components/MarkdownContent.astro b/src/components/MarkdownContent.astro
new file mode 100644
index 00000000..5d474380
--- /dev/null
+++ b/src/components/MarkdownContent.astro
@@ -0,0 +1,12 @@
+---
+import type { HTMLAttributes } from "astro/types";
+import "@astrojs/starlight/style/markdown.css";
+
+type Props = HTMLAttributes<"div">;
+
+const { class: className, ...attrs } = Astro.props;
+---
+
+
+
+
diff --git a/src/components/content/Callout.module.css b/src/components/content/Callout.module.css
deleted file mode 100644
index ad366479..00000000
--- a/src/components/content/Callout.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.callout {
- max-width: 100%;
-}
-
-.callout :global(.cui-callout-content-kcln) {
- min-width: 0;
- max-width: 100%;
-}
-
-.callout :global(.expressive-code),
-.callout :global(.expressive-code .frame),
-.callout :global(.expressive-code .frame pre),
-.callout :global(pre) {
- max-width: 100%;
-}
-
-.callout :global(.expressive-code .frame pre),
-.callout :global(pre) {
- overflow-x: auto;
-}
diff --git a/src/components/content/Callout.tsx b/src/components/content/Callout.tsx
index 94df6807..89fe1f20 100644
--- a/src/components/content/Callout.tsx
+++ b/src/components/content/Callout.tsx
@@ -1,7 +1,3 @@
-import {
- Callout as CircuitCallout,
- type CalloutColor,
-} from "@sumup-oss/circuit-ui";
import {
Confirm,
Info,
@@ -9,12 +5,10 @@ import {
Sparkles,
type IconComponentType,
} from "@sumup-oss/icons";
-import type { ComponentProps, ReactNode } from "react";
-import styles from "./Callout.module.css";
-
-type CalloutType = "note" | "tip" | "caution" | "success" | "promo";
+import type { HTMLAttributes, ReactNode } from "react";
+import type { CalloutType } from "./calloutTypes";
-type Props = Omit, "body" | "color"> & {
+type Props = HTMLAttributes & {
children: ReactNode;
type?: CalloutType;
};
@@ -22,28 +16,36 @@ type Props = Omit, "body" | "color"> & {
const calloutConfig: Record<
CalloutType,
{
- color: CalloutColor;
icon: IconComponentType<"24">;
iconLabel: string;
}
> = {
- note: { color: "neutral", icon: Info, iconLabel: "Note" },
- tip: { color: "promo", icon: Sparkles, iconLabel: "Tip" },
- caution: { color: "alert", icon: Notify, iconLabel: "Caution" },
- success: { color: "confirm", icon: Confirm, iconLabel: "Success" },
- promo: { color: "promo", icon: Sparkles, iconLabel: "Promo" },
+ note: { icon: Info, iconLabel: "Note" },
+ tip: { icon: Sparkles, iconLabel: "Tip" },
+ caution: { icon: Notify, iconLabel: "Caution" },
+ success: { icon: Confirm, iconLabel: "Success" },
+ promo: { icon: Sparkles, iconLabel: "Promo" },
};
-export default function Callout({ children, type = "note", ...props }: Props) {
+export default function Callout({
+ children,
+ className,
+ type = "note",
+ ...props
+}: Props) {
const config = calloutConfig[type];
- const className = [styles.callout, props.className].filter(Boolean).join(" ");
+ const classes = ["sumup-callout", `sumup-callout--${type}`, className]
+ .filter(Boolean)
+ .join(" ");
+ const Icon = config.icon;
return (
-
+
+
+
+
+
{config.iconLabel}
+
{children}
+
);
}
diff --git a/src/components/content/calloutTypes.ts b/src/components/content/calloutTypes.ts
new file mode 100644
index 00000000..7ee07dfd
--- /dev/null
+++ b/src/components/content/calloutTypes.ts
@@ -0,0 +1,12 @@
+export const CALLOUT_TYPES = [
+ "note",
+ "tip",
+ "caution",
+ "success",
+ "promo",
+] as const;
+
+export type CalloutType = (typeof CALLOUT_TYPES)[number];
+
+export const isCalloutType = (value: string): value is CalloutType =>
+ CALLOUT_TYPES.includes(value as CalloutType);
diff --git a/src/content.config.ts b/src/content.config.ts
index c4dff19c..c626a5b6 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -2,6 +2,7 @@ import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
import { glob } from "astro/loaders";
import { defineCollection, z } from "astro:content";
+import { openapiDescriptionsLoader } from "./loaders/openapiDescriptions";
const help = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/help" }),
@@ -19,6 +20,22 @@ const changelog = defineCollection({
}),
});
+const apiDescriptions = defineCollection({
+ loader: openapiDescriptionsLoader(),
+ schema: z.discriminatedUnion("kind", [
+ z.object({
+ kind: z.literal("tag"),
+ name: z.string(),
+ }),
+ z.object({
+ kind: z.literal("operation"),
+ name: z.string(),
+ method: z.string(),
+ path: z.string(),
+ }),
+ ]),
+});
+
export const collections = {
docs: defineCollection({
schema: docsSchema({
@@ -38,4 +55,5 @@ export const collections = {
}),
help,
changelog,
+ apiDescriptions,
};
diff --git a/src/content/changelog/android-tap-to-pay-1.1.6.md b/src/content/changelog/android-tap-to-pay-1.1.6.md
index 89a936cf..264dfe0f 100644
--- a/src/content/changelog/android-tap-to-pay-1.1.6.md
+++ b/src/content/changelog/android-tap-to-pay-1.1.6.md
@@ -4,25 +4,25 @@ tags: ["Android Tap-to-pay SDK", "Android", "SDK"]
publishedDate: 2026-08-25
---
-### New Features
-
-- Redesigned the payment UI (success, error, warning, loading, and tap-card screens), including updated animations and PIN-pad styling.
-- When a presented card cannot be used, the SDK now prompts the cardholder to try another card instead of failing the payment immediately.
-- After repeated unsuccessful “try another card” attempts, the payment fails with `PaymentException.CardErrorNotAccepted` (code `1029`).
-
-### Fixes
-
-- Fixed an issue that could send the charge request twice after a network interruption.
-- Fixed an issue where a transaction that had already failed on the backend might not notify the integrator with `TransactionFailed`.
-- Fixed a PIN-pad issue on some devices where digit 0 was not visible.
-
-### Integration Notes
-
-- The **Send receipt** button has been removed from the SDK success screen. Receipts must be handled by the host app if required.
-- `PaymentFlowClosedSuccessfully.shouldDisplayReceipt` is deprecated, always emitted as `false`, and will be removed in **1.1.7**. Existing two-argument constructors still compile.
-- `skipSuccessScreen` is unchanged. Hosts that already show their own success UI are unaffected.
-- If you use an exhaustive `when` on `PaymentException`, add a branch for `PaymentException.CardErrorNotAccepted`.
-
-### Important — dependency and toolchain
-
+### New Features
+
+- Redesigned the payment UI (success, error, warning, loading, and tap-card screens), including updated animations and PIN-pad styling.
+- When a presented card cannot be used, the SDK now prompts the cardholder to try another card instead of failing the payment immediately.
+- After repeated unsuccessful “try another card” attempts, the payment fails with `PaymentException.CardErrorNotAccepted` (code `1029`).
+
+### Fixes
+
+- Fixed an issue that could send the charge request twice after a network interruption.
+- Fixed an issue where a transaction that had already failed on the backend might not notify the integrator with `TransactionFailed`.
+- Fixed a PIN-pad issue on some devices where digit 0 was not visible.
+
+### Integration Notes
+
+- The **Send receipt** button has been removed from the SDK success screen. Receipts must be handled by the host app if required.
+- `PaymentFlowClosedSuccessfully.shouldDisplayReceipt` is deprecated, always emitted as `false`, and will be removed in **1.1.7**. Existing two-argument constructors still compile.
+- `skipSuccessScreen` is unchanged. Hosts that already show their own success UI are unaffected.
+- If you use an exhaustive `when` on `PaymentException`, add a branch for `PaymentException.CardErrorNotAccepted`.
+
+### Important — dependency and toolchain
+
- The SDK is now built with **Kotlin 2.2.21**. Host apps compiling with Kotlin **1.9.x** are likely to fail with a Kotlin metadata version mismatch. Upgrade the host Kotlin Gradle plugin to **2.0+** (2.2.x recommended) before integrating 1.1.6.
diff --git a/src/lib/openapiContent.ts b/src/lib/openapiContent.ts
new file mode 100644
index 00000000..c4cfac3d
--- /dev/null
+++ b/src/lib/openapiContent.ts
@@ -0,0 +1,4 @@
+export const getOperationDescriptionId = (operationId: string) =>
+ `operation:${operationId}`;
+
+export const getTagDescriptionId = (tagName: string) => `tag:${tagName}`;
diff --git a/src/loaders/openapiDescriptions.ts b/src/loaders/openapiDescriptions.ts
new file mode 100644
index 00000000..ca7b8b7f
--- /dev/null
+++ b/src/loaders/openapiDescriptions.ts
@@ -0,0 +1,122 @@
+import type { Loader, LoaderContext } from "astro/loaders";
+import { readFile } from "node:fs/promises";
+import { fileURLToPath } from "node:url";
+import { OpenAPIV3, type OpenAPIV3_1 } from "openapi-types";
+import {
+ getOperationDescriptionId,
+ getTagDescriptionId,
+} from "@lib/openapiContent";
+
+const openapiFileUrl = new URL("../../openapi.json", import.meta.url);
+const openapiFilePath = fileURLToPath(openapiFileUrl);
+
+const methods = [
+ OpenAPIV3.HttpMethods.GET,
+ OpenAPIV3.HttpMethods.POST,
+ OpenAPIV3.HttpMethods.PATCH,
+ OpenAPIV3.HttpMethods.PUT,
+ OpenAPIV3.HttpMethods.DELETE,
+ OpenAPIV3.HttpMethods.OPTIONS,
+ OpenAPIV3.HttpMethods.HEAD,
+ OpenAPIV3.HttpMethods.TRACE,
+];
+
+export function openapiDescriptionsLoader(): Loader {
+ return {
+ name: "openapi-descriptions",
+ async load(context) {
+ await syncDescriptions(context);
+
+ if (!context.watcher) {
+ return;
+ }
+
+ // Imported JSON is cached by the module graph. Watch and reread the file
+ // explicitly so a synchronized OpenAPI document updates the collection
+ // without requiring the Astro dev server to be restarted.
+ context.watcher.add(openapiFilePath);
+ context.watcher.on("change", async (changedPath) => {
+ if (changedPath !== openapiFilePath) {
+ return;
+ }
+
+ try {
+ await syncDescriptions(context);
+ context.logger.info("Reloaded descriptions from openapi.json");
+ } catch (error) {
+ context.logger.error(
+ `Failed to reload openapi.json: ${error instanceof Error ? error.message : String(error)}`,
+ );
+ }
+ });
+ },
+ };
+}
+
+async function syncDescriptions({
+ generateDigest,
+ parseData,
+ renderMarkdown,
+ store,
+}: LoaderContext) {
+ const document = JSON.parse(
+ await readFile(openapiFileUrl, "utf8"),
+ ) as OpenAPIV3_1.Document;
+
+ // Parse the document before clearing the store, preserving the previous
+ // collection if a file watcher observes an incomplete write.
+ store.clear();
+
+ for (const tag of document.tags ?? []) {
+ const id = getTagDescriptionId(tag.name);
+ const body = tag.description ?? "";
+ const data = await parseData({
+ id,
+ data: { kind: "tag", name: tag.name },
+ });
+
+ store.set({
+ id,
+ data,
+ body,
+ digest: generateDigest(body),
+ rendered: await renderMarkdown(body),
+ });
+ }
+
+ for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
+ if (!pathItem) {
+ continue;
+ }
+
+ for (const method of methods) {
+ const operation = pathItem[method] as
+ OpenAPIV3_1.OperationObject | undefined;
+ if (!operation?.operationId) {
+ continue;
+ }
+
+ const id = getOperationDescriptionId(operation.operationId);
+ const body = operation.description ?? "";
+ const data = await parseData({
+ id,
+ data: {
+ kind: "operation",
+ name: operation.operationId,
+ method,
+ path,
+ },
+ });
+
+ store.set({
+ id,
+ data,
+ body,
+ digest: generateDigest(body),
+ // Route OpenAPI descriptions through Astro's configured Satteri
+ // processor, including the callout directive plugin.
+ rendered: await renderMarkdown(body),
+ });
+ }
+ }
+}
diff --git a/src/overrides/MarkdownContent.astro b/src/overrides/MarkdownContent.astro
index 52b7cb29..e06ee1bf 100644
--- a/src/overrides/MarkdownContent.astro
+++ b/src/overrides/MarkdownContent.astro
@@ -1,13 +1,13 @@
---
-import Default from "@astrojs/starlight/components/MarkdownContent.astro";
+import MarkdownContent from "@components/MarkdownContent.astro";
---
{
Astro.locals.starlightRoute.custom ? (
) : (
-
+
-
+
)
}
diff --git a/src/pages/changelog/[slug].astro b/src/pages/changelog/[slug].astro
index ac72af36..8e7f3aac 100644
--- a/src/pages/changelog/[slug].astro
+++ b/src/pages/changelog/[slug].astro
@@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content";
import slugify from "@sindresorhus/slugify";
import { Anchor, Body } from "@sumup-oss/circuit-ui";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
-import MarkdownContent from "@astrojs/starlight/components/MarkdownContent.astro";
+import MarkdownContent from "@components/MarkdownContent.astro";
import type { StarlightPageProps } from "@astrojs/starlight/props";
export async function getStaticPaths() {
diff --git a/src/pages/changelog/index.astro b/src/pages/changelog/index.astro
index 8ab6fe91..191c6849 100644
--- a/src/pages/changelog/index.astro
+++ b/src/pages/changelog/index.astro
@@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content";
import slugify from "@sindresorhus/slugify";
import { Status, Body, Button, Anchor, Headline } from "@sumup-oss/circuit-ui";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
-import MarkdownContent from "@astrojs/starlight/components/MarkdownContent.astro";
+import MarkdownContent from "@components/MarkdownContent.astro";
import type { StarlightPageProps } from "@astrojs/starlight/props";
const changelogDocs = await getCollection("changelog");
diff --git a/src/pages/changelog/tags/[tag].astro b/src/pages/changelog/tags/[tag].astro
index d1264834..11c3a97e 100644
--- a/src/pages/changelog/tags/[tag].astro
+++ b/src/pages/changelog/tags/[tag].astro
@@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content";
import slugify from "@sindresorhus/slugify";
import { Status, Body, Button, Anchor, Headline } from "@sumup-oss/circuit-ui";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
-import MarkdownContent from "@astrojs/starlight/components/MarkdownContent.astro";
+import MarkdownContent from "@components/MarkdownContent.astro";
import type { StarlightPageProps } from "@astrojs/starlight/props";
export async function getStaticPaths() {
diff --git a/src/pages/help.astro b/src/pages/help.astro
index 671755a9..203b22e8 100644
--- a/src/pages/help.astro
+++ b/src/pages/help.astro
@@ -1,5 +1,6 @@
---
import type { StarlightPageProps } from "@astrojs/starlight/props";
+import MarkdownContent from "@components/MarkdownContent.astro";
import { getCollection, render } from "astro:content";
import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";
@@ -23,7 +24,9 @@ const posts = await getCollection("help");
{post.data.title}
-
+
+
+
);
diff --git a/src/plugins/satteri/callouts.test.ts b/src/plugins/satteri/callouts.test.ts
new file mode 100644
index 00000000..e792554f
--- /dev/null
+++ b/src/plugins/satteri/callouts.test.ts
@@ -0,0 +1,39 @@
+import { describe, expect, it } from "vitest";
+import { markdownToHtml } from "satteri";
+import callouts from "./callouts";
+
+describe("callouts Markdown plugin", () => {
+ it("renders a titled callout with nested Markdown", () => {
+ const source = `:::caution[PCI DSS compliance required]
+Use the [official requirements](https://example.com).
+:::`;
+
+ const { html } = markdownToHtml(source, {
+ features: { directive: true },
+ mdastPlugins: [callouts],
+ });
+
+ expect(html).toContain(
+ '