-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsource.config.ts
More file actions
170 lines (161 loc) · 7.19 KB
/
Copy pathsource.config.ts
File metadata and controls
170 lines (161 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
import type { ShikiTransformer } from "@shikijs/types";
import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
import { z } from "zod";
import { cipherstashDark, cipherstashLight } from "./src/lib/shiki-themes";
// You can customise Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.dev/docs/mdx/collections
export const docs = defineDocs({
dir: "content/stack",
docs: {
// `seoTitle` overrides the <title>/OG title for pages whose visual `title`
// is brand-styled (e.g. "/ENCRYPTION") and not ideal for search results.
schema: pageSchema.extend({
seoTitle: z.string().optional(),
}),
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
// V2 information architecture (CIP-3325). New content lives in content/docs
// and is served from the site root (e.g. /docs/get-started/...). The legacy
// `docs` collection above (content/stack) is served alongside it during the
// migration and is deleted once the last section moves. See IA.md.
export const v2docs = defineDocs({
dir: "content/docs",
docs: {
schema: pageSchema.extend({
seoTitle: z.string().optional(),
// Sidebar label, when it should differ from the page's `title` (which is
// also the H1). Mainly for section index pages: the folder already names
// the section, so repeating it on the index item is noise. Applied to the
// page tree in `src/lib/source.ts`; never affects the URL or the H1.
navTitle: z.string().optional(),
// Diátaxis page type. Every page should declare one; enforced by the
// docs lint (CIP-3337) rather than the schema so stubs can land first.
type: z.enum(["tutorial", "guide", "concept", "reference"]).optional(),
// Facets powering index pages, filtered views, and the future
// tailored-quickstart picker (CIP-3339). Nav position never depends on
// these — the sidebar tree comes from meta.json alone.
components: z
.array(z.enum(["encryption", "platform", "eql", "proxy", "cli"]))
.optional(),
audience: z.array(z.enum(["developer", "cto", "ciso"])).optional(),
integration: z
.object({
category: z.enum([
"platform",
"orm",
"framework",
"auth-provider",
"language",
"runtime",
]),
setup: z.enum(["code-only", "dashboard-required"]),
pairsWith: z.array(z.string()).optional(),
})
.optional(),
// Review tracking (CIP-3337): API pages pin the releases they were
// verified against (e.g. { stack: "1.2.0", eql: "3.0.0" }); claims pages
// (compliance, pricing, comparisons) carry a review-by date instead.
verifiedAgainst: z.record(z.string(), z.string()).optional(),
reviewBy: z.string().optional(),
}),
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
// ── Syntax highlighting: the CipherStash code theme ──────────────────────────
// Parse the leftover code-fence meta string (what remains after Fumadocs
// extracts `title`, `tab`, and line-number directives) for the analytics
// attributes documented for authors: `example-id`, `cta`, and `cta-type`.
// Attribute names may contain hyphens, e.g. `example-id="drizzle-basic-query"`.
function parseTrackingAttributes(raw: string): Record<string, string | true> {
const attributes: Record<string, string | true> = {};
// Accept quoted ("…"/'…') and bare unquoted values, so a forgotten quote
// (`example-id=foo`) still parses instead of being silently dropped. A bare
// key with no value (`cta`) is recorded as `true`.
const pattern = /(?<=^|\s)([\w-]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s"']+)))?/g;
for (const match of raw.matchAll(pattern)) {
const [, name, double, single, unquoted] = match;
attributes[name] = double ?? single ?? unquoted ?? true;
}
return attributes;
}
// Emit code-fence metadata as `data-*` attributes on the rendered `<pre>` so the
// client-side copy button (see `src/components/code-block.tsx`) can report it to
// PostHog. This runs for every code block, so `data-language` is always present
// even when the fence has no meta string (e.g. a plain ```bash block).
const codeCopyTrackingTransformer: ShikiTransformer = {
name: "cipherstash:code-copy-tracking",
pre(node) {
node.properties["data-language"] = this.options.lang ?? "plaintext";
// A ```mermaid fence stays a code fence in the mdast, so the processed
// markdown we serve at `.mdx` and in llms.txt keeps the diagram as
// readable source. Carry the raw source through to the client, where
// `TrackedCodeBlock` swaps the highlighted block for a rendered diagram.
if (this.options.lang === "mermaid") {
node.properties["data-mermaid"] = this.source;
}
const raw =
typeof this.options.meta?.__raw === "string"
? this.options.meta.__raw
: "";
if (!raw) return;
const attributes = parseTrackingAttributes(raw);
// Only emit attributes for non-empty string values, so `example-id=""`
// falls back to the client-derived id rather than reporting an empty slug.
const exampleId = attributes["example-id"];
if (typeof exampleId === "string" && exampleId !== "") {
node.properties["data-example-id"] = exampleId;
}
// Surface the `filename` so the client can derive a readable fallback
// `example_id` for blocks that lack an explicit `example-id`.
const filename = attributes.filename;
if (typeof filename === "string" && filename !== "") {
node.properties["data-filename"] = filename;
}
const ctaType = attributes["cta-type"];
const hasCtaType = typeof ctaType === "string" && ctaType !== "";
// `cta` is a flag: a bare `cta` (or `cta="true"`) opts in. An explicit
// `cta="false"`/`cta=""` is an opt-out that wins even when a `cta-type` is
// present. A lone `cta-type` (no `cta`) still implies a CTA so the category
// isn't silently dropped.
const ctaFlag = attributes.cta;
const ctaOptOut = ctaFlag === "false" || ctaFlag === "";
const isCta =
!ctaOptOut && (ctaFlag === true || ctaFlag === "true" || hasCtaType);
if (isCta) {
node.properties["data-cta"] = "true";
if (hasCtaType) {
node.properties["data-cta-type"] = ctaType;
}
}
},
};
export default defineConfig({
mdxOptions: {
rehypeCodeOptions: {
// Preserve Fumadocs' default Shiki config (dual-theme mode,
// parseMetaString) and its default transformers (notation highlight, diff,
// focus, word highlight) — passing `transformers` alone would replace them
// entirely — then append our copy-tracking transformer.
...rehypeCodeDefaultOptions,
// Swap GitHub's themes for the CipherStash palette (defined above).
themes: { light: cipherstashLight, dark: cipherstashDark },
transformers: [
...(rehypeCodeDefaultOptions.transformers ?? []),
codeCopyTrackingTransformer,
],
},
},
});