Add a custom Markdown component
Register a global Vue component, map its MDC tag, declare a narrow content policy, and optionally serialize it for agents.
A custom MDC tag crosses three explicit boundaries: Vue renders it, the Markdown tag map resolves it, and the component policy decides which authored input is safe. Add an agent serializer only when the visual component needs a different text representation.
This example adds ::api-playground with a required HTTP method and path.
Create the Vue component
<script setup lang="ts">
defineProps<{
method: string;
path: string;
}>();
</script>
<template>
<section class="not-prose rounded-lg border border-border p-5">
<p class="font-mono text-sm font-semibold">{{ method }} {{ path }}</p>
<div class="mt-3 text-sm text-muted-foreground">
<slot />
</div>
</section>
</template>MDC resolves tag targets dynamically. Register the component globally in a small Nuxt plugin:
import MdcApiPlayground from "~/components/mdc/MdcApiPlayground.vue";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component("MdcApiPlayground", MdcApiPlayground);
});A .global.vue component is an alternative. Do not add a second Docs-specific component registry.
Extend the tag map and policy
Keep the layer's built-in mappings, then add the authored tag. Declare only the props and slots this component needs:
import { ginkoDocsComponentTags } from "@lupinum/ginko-docs/components";
export default defineNuxtConfig({
extends: ["@lupinum/ginko-docs"],
content: {
componentPolicy: {
components: {
"api-playground": {
kind: "block",
props: {
method: { type: "string", required: true },
path: { type: "string", required: true },
},
slots: ["default"],
media: null,
},
},
},
markdown: {
tags: {
...ginkoDocsComponentTags,
"api-playground": "MdcApiPlayground",
},
},
},
});The policy is the public authoring boundary, not a convenience schema. Dynamic Vue bindings remain rejected in public Markdown. Avoid broad prop bags, arbitrary slots, or media access unless the component has a concrete use for them.
Authors can now use the tag:
::api-playground{method="GET" path="/v1/projects"}
Returns the projects visible to the current token.
::Add agent Markdown when needed
The default agent renderer keeps unknown component structure explicit. Register a serializer only when a compact text form is more useful. The serializer runs on the server and must return deterministic public content.
import {
registerAgentMarkdownSerializers,
type AgentMarkdownSerializer,
} from "@lupinum/ginko-content/agent-registry";
import { defineNitroPlugin } from "nitropack/runtime";
const apiPlaygroundMarkdown: AgentMarkdownSerializer = (node, context) => {
const props = context.cleanProps(node);
const method = String(props.method || "GET");
const path = String(props.path || "/");
const details = context.renderChildren(node).trim();
return [`**${method} ${path}**`, details].filter(Boolean).join("\n\n");
};
export default defineNitroPlugin(() => {
registerAgentMarkdownSerializers({
"api-playground": apiPlaygroundMarkdown,
MdcApiPlayground: apiPlaygroundMarkdown,
});
});Register both the authored tag and component alias because the parsed tree can contain either name. Test the rendered component and its /raw/**.md output before publishing. The content configuration reference documents the policy shape.