@valkyrianlabs/payload-markdown-docs 0.5.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,10 @@ to a matching docs set from the configured branch.
73
73
 
74
74
  ## Render In Next
75
75
 
76
+ The plugin does not mutate your Pages collection and does not register public
77
+ frontend routes. Add route handlers in your Next app where you want docs to
78
+ render.
79
+
76
80
  ```tsx
77
81
  import config from '@payload-config'
78
82
  import {
@@ -82,11 +86,7 @@ import {
82
86
  import { notFound } from 'next/navigation'
83
87
  import { getPayload } from 'payload'
84
88
 
85
- export default async function Page({
86
- params,
87
- }: {
88
- params: Promise<{ slug?: string[] }>
89
- }) {
89
+ export default async function Page({ params }: { params: Promise<{ slug?: string[] }> }) {
90
90
  const { slug } = await params
91
91
  const payload = await getPayload({ config })
92
92
  const resolved = await resolvePayloadMarkdownDocsRoute({ payload, slug })
@@ -99,7 +99,54 @@ export default async function Page({
99
99
  }
100
100
  ```
101
101
 
102
- For header navigation, use the link helper:
102
+ For docs navigation, use the drop-in navbar when you want the plugin to own the
103
+ docs menu UI:
104
+
105
+ ```tsx
106
+ import { PayloadMarkdownDocsNavbar } from '@valkyrianlabs/payload-markdown-docs/next'
107
+ import type { Payload } from 'payload'
108
+
109
+ export async function HeaderDocsNav({ payload }: { payload: Payload }) {
110
+ return (
111
+ <PayloadMarkdownDocsNavbar currentPath="/plugins/payload-markdown-docs" payload={payload} />
112
+ )
113
+ }
114
+ ```
115
+
116
+ The navbar reads docs groups and docs sets, renders nested docs navigation, and
117
+ accepts `classNames` and `renderLink` overrides for app-specific Tailwind,
118
+ routing, and analytics.
119
+
120
+ If you already have a site header, use the Header adapter to append top-level
121
+ docs groups and top-level ungrouped docs sets without exceeding your existing
122
+ menu cap:
123
+
124
+ ```ts
125
+ import { appendPayloadMarkdownDocsHeaderNavItems } from '@valkyrianlabs/payload-markdown-docs/next'
126
+
127
+ const navItems = await appendPayloadMarkdownDocsHeaderNavItems({
128
+ existingItems: header.navItems ?? [],
129
+ maxItems: headerNavItemsMaxRows,
130
+ payload,
131
+ })
132
+ ```
133
+
134
+ The adapter defaults to custom URL links so it does not require CMSLink changes.
135
+ Use `mode: 'relationship'` only when your renderer understands `docs-groups`
136
+ and `docs-sets` relationships.
137
+
138
+ For fully custom navigation, use the headless nav builder:
139
+
140
+ ```ts
141
+ import { getPayloadMarkdownDocsNavItems } from '@valkyrianlabs/payload-markdown-docs/next'
142
+
143
+ const docsNav = await getPayloadMarkdownDocsNavItems({
144
+ availableSlots: 4,
145
+ payload,
146
+ })
147
+ ```
148
+
149
+ For simple flat header links, use the compatibility link helper:
103
150
 
104
151
  ```ts
105
152
  import { getPayloadMarkdownDocsLinks } from '@valkyrianlabs/payload-markdown-docs/next'
@@ -108,6 +155,37 @@ const docsLinks = await getPayloadMarkdownDocsLinks({ payload })
108
155
  // [{ label: 'Payload Markdown Docs', url: '/plugins/payload-markdown-docs' }]
109
156
  ```
110
157
 
158
+ ## Serve Raw Markdown
159
+
160
+ The AI-facing raw Markdown export is a route-handler response, not a generated
161
+ Payload Page. Add a `route.ts` at the exported path, usually the value from
162
+ `docs/index.ai.yml`:
163
+
164
+ ```ts
165
+ // app/(frontend)/plugins/payload-markdown-docs.md/route.ts
166
+ import config from '@payload-config'
167
+ import { createPayloadMarkdownDocsMarkdownResponse } from '@valkyrianlabs/payload-markdown-docs/next'
168
+ import { notFound } from 'next/navigation'
169
+ import { getPayload } from 'payload'
170
+
171
+ export async function GET() {
172
+ const payload = await getPayload({ config })
173
+ const response = await createPayloadMarkdownDocsMarkdownResponse({
174
+ payload,
175
+ path: '/plugins/payload-markdown-docs.md',
176
+ })
177
+
178
+ if (response) {
179
+ return response
180
+ }
181
+
182
+ notFound()
183
+ }
184
+ ```
185
+
186
+ The response is `text/markdown; charset=utf-8` and is assembled from synced
187
+ docs records using `docs/index.ai.yml` when present.
188
+
111
189
  ## Validate Locally
112
190
 
113
191
  In an app or docs repository that has installed this package:
@@ -127,6 +205,19 @@ pnpm cli validate ./docs --source payload-markdown-docs
127
205
  In GitHub Actions, `--source` can be omitted when the docs set slug matches the
128
206
  repository name. The CLI infers it from `GITHUB_REPOSITORY`.
129
207
 
208
+ ## Maintain Docs With Codex
209
+
210
+ In a docs set target application, install the local Codex skill so agents have
211
+ repo-local guidance for maintaining Markdown docs, frontmatter, `index.ai.yml`,
212
+ validation, and sync safety rules.
213
+
214
+ ```bash
215
+ pnpm exec payload-markdown-docs install skill --codex
216
+ ```
217
+
218
+ The installer writes `.agents/skills/payload-markdown-docs/`. It does not sync
219
+ docs, call Payload, or publish content.
220
+
130
221
  ## Publish From GitHub Actions
131
222
 
132
223
  ```yaml
@@ -186,5 +277,6 @@ empty list rejects all workflow publishing for that docs set.
186
277
  - [Quick Start](docs/getting-started/quick-start.md)
187
278
  - [Plugin Config](docs/configuration/plugin-config.md)
188
279
  - [GitHub Actions](docs/workflow/ci-github-actions.md)
280
+ - [Docs Navbar](docs/frontend/navbar.md)
189
281
  - [CLI](docs/reference/cli.md)
190
282
  - [Migration Notes](docs/reference/migration.md)
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { GetPayloadMarkdownDocsNavItemsOptions, PayloadMarkdownDocsNavItem } from './links.js';
3
+ export type PayloadMarkdownDocsNavbarClassNames = {
4
+ activeLink?: string;
5
+ childrenList?: string;
6
+ item?: string;
7
+ link?: string;
8
+ list?: string;
9
+ panel?: string;
10
+ root?: string;
11
+ trigger?: string;
12
+ };
13
+ export type PayloadMarkdownDocsNavbarRenderLinkOptions = {
14
+ active: boolean;
15
+ children: ReactNode;
16
+ className: string;
17
+ current: boolean;
18
+ href: string;
19
+ item: PayloadMarkdownDocsNavItem;
20
+ };
21
+ export type PayloadMarkdownDocsNavbarProps = {
22
+ ariaLabel?: string;
23
+ classNames?: PayloadMarkdownDocsNavbarClassNames;
24
+ currentPath?: string;
25
+ items?: PayloadMarkdownDocsNavItem[];
26
+ renderLink?: (options: PayloadMarkdownDocsNavbarRenderLinkOptions) => ReactNode;
27
+ } & Partial<GetPayloadMarkdownDocsNavItemsOptions>;
28
+ export declare const PayloadMarkdownDocsNavbar: ({ ariaLabel, classNames, currentPath, items, renderLink, ...options }: PayloadMarkdownDocsNavbarProps) => Promise<import("react/jsx-runtime").JSX.Element | null>;
@@ -0,0 +1,107 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { isRouteDescendant, normalizeRoutePath } from '../routing/index.js';
3
+ import { getPayloadMarkdownDocsNavItems } from './links.js';
4
+ const cx = (...values)=>values.filter(Boolean).join(' ');
5
+ const defaultClassNames = {
6
+ activeLink: 'bg-cyan-400/10 text-cyan-200',
7
+ childrenList: 'space-y-1',
8
+ item: 'relative list-none',
9
+ link: 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/70 transition-colors hover:bg-white/[0.04] hover:text-foreground',
10
+ list: 'm-0 flex list-none items-center gap-1 p-0',
11
+ panel: 'absolute left-0 top-full z-50 hidden min-w-60 rounded-xl border border-border bg-background p-2 shadow-xl group-hover:block group-focus-within:block',
12
+ root: 'relative',
13
+ trigger: 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/60 transition-colors group-hover:bg-white/[0.04] group-hover:text-foreground'
14
+ };
15
+ const isItemActive = ({ currentPath, item })=>{
16
+ if (!currentPath) {
17
+ return false;
18
+ }
19
+ if (item.url) {
20
+ const normalizedUrl = normalizeRoutePath(item.url);
21
+ if (currentPath === normalizedUrl || isRouteDescendant(normalizedUrl, currentPath)) {
22
+ return true;
23
+ }
24
+ }
25
+ return (item.children ?? []).some((child)=>isItemActive({
26
+ currentPath,
27
+ item: child
28
+ }));
29
+ };
30
+ const isItemCurrent = ({ currentPath, item })=>Boolean(currentPath && item.url && currentPath === normalizeRoutePath(item.url));
31
+ const renderDefaultLink = ({ children, className, current, href })=>/*#__PURE__*/ _jsx("a", {
32
+ "aria-current": current ? 'page' : undefined,
33
+ className: className,
34
+ href: href,
35
+ children: children
36
+ });
37
+ const renderNavItems = ({ classNames, currentPath, depth = 0, items, renderLink = renderDefaultLink })=>{
38
+ if (items.length === 0) {
39
+ return null;
40
+ }
41
+ return /*#__PURE__*/ _jsx("ul", {
42
+ className: cx(depth === 0 ? classNames.list : classNames.childrenList),
43
+ children: items.map((item)=>{
44
+ const active = isItemActive({
45
+ currentPath,
46
+ item
47
+ });
48
+ const current = isItemCurrent({
49
+ currentPath,
50
+ item
51
+ });
52
+ const linkClassName = cx(classNames.link, active && classNames.activeLink);
53
+ const children = item.children?.length ? renderNavItems({
54
+ classNames,
55
+ currentPath,
56
+ depth: depth + 1,
57
+ items: item.children,
58
+ renderLink
59
+ }) : null;
60
+ return /*#__PURE__*/ _jsxs("li", {
61
+ className: cx('group', classNames.item),
62
+ children: [
63
+ item.url ? renderLink({
64
+ active,
65
+ children: item.label,
66
+ className: linkClassName,
67
+ current,
68
+ href: item.url,
69
+ item
70
+ }) : /*#__PURE__*/ _jsx("span", {
71
+ className: cx(classNames.trigger, active && classNames.activeLink),
72
+ children: item.label
73
+ }),
74
+ children ? /*#__PURE__*/ _jsx("div", {
75
+ className: classNames.panel,
76
+ children: children
77
+ }) : null
78
+ ]
79
+ }, `${item.collection}:${item.id}`);
80
+ })
81
+ });
82
+ };
83
+ export const PayloadMarkdownDocsNavbar = async ({ ariaLabel = 'Docs navigation', classNames, currentPath, items, renderLink, ...options })=>{
84
+ const navItems = items ?? (options.payload ? await getPayloadMarkdownDocsNavItems({
85
+ ...options,
86
+ payload: options.payload
87
+ }) : []);
88
+ if (navItems.length === 0) {
89
+ return null;
90
+ }
91
+ const mergedClassNames = {
92
+ ...defaultClassNames,
93
+ ...classNames
94
+ };
95
+ return /*#__PURE__*/ _jsx("nav", {
96
+ "aria-label": ariaLabel,
97
+ className: mergedClassNames.root,
98
+ children: renderNavItems({
99
+ classNames: mergedClassNames,
100
+ currentPath: currentPath ? normalizeRoutePath(currentPath) : undefined,
101
+ items: navItems,
102
+ renderLink
103
+ })
104
+ });
105
+ };
106
+
107
+ //# sourceMappingURL=PayloadMarkdownDocsNavbar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/next/PayloadMarkdownDocsNavbar.tsx"],"sourcesContent":["import type { ReactNode } from 'react'\n\nimport type { GetPayloadMarkdownDocsNavItemsOptions, PayloadMarkdownDocsNavItem } from './links.js'\n\nimport { isRouteDescendant, normalizeRoutePath } from '../routing/index.js'\nimport { getPayloadMarkdownDocsNavItems } from './links.js'\n\nexport type PayloadMarkdownDocsNavbarClassNames = {\n activeLink?: string\n childrenList?: string\n item?: string\n link?: string\n list?: string\n panel?: string\n root?: string\n trigger?: string\n}\n\nexport type PayloadMarkdownDocsNavbarRenderLinkOptions = {\n active: boolean\n children: ReactNode\n className: string\n current: boolean\n href: string\n item: PayloadMarkdownDocsNavItem\n}\n\nexport type PayloadMarkdownDocsNavbarProps = {\n ariaLabel?: string\n classNames?: PayloadMarkdownDocsNavbarClassNames\n currentPath?: string\n items?: PayloadMarkdownDocsNavItem[]\n renderLink?: (options: PayloadMarkdownDocsNavbarRenderLinkOptions) => ReactNode\n} & Partial<GetPayloadMarkdownDocsNavItemsOptions>\n\nconst cx = (...values: (false | null | string | undefined)[]): string =>\n values.filter(Boolean).join(' ')\n\nconst defaultClassNames = {\n activeLink: 'bg-cyan-400/10 text-cyan-200',\n childrenList: 'space-y-1',\n item: 'relative list-none',\n link: 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/70 transition-colors hover:bg-white/[0.04] hover:text-foreground',\n list: 'm-0 flex list-none items-center gap-1 p-0',\n panel:\n 'absolute left-0 top-full z-50 hidden min-w-60 rounded-xl border border-border bg-background p-2 shadow-xl group-hover:block group-focus-within:block',\n root: 'relative',\n trigger:\n 'block rounded-lg px-3 py-2 text-sm leading-5 text-foreground/60 transition-colors group-hover:bg-white/[0.04] group-hover:text-foreground',\n} satisfies Required<PayloadMarkdownDocsNavbarClassNames>\n\nconst isItemActive = ({\n currentPath,\n item,\n}: {\n currentPath?: string\n item: PayloadMarkdownDocsNavItem\n}): boolean => {\n if (!currentPath) {\n return false\n }\n\n if (item.url) {\n const normalizedUrl = normalizeRoutePath(item.url)\n\n if (currentPath === normalizedUrl || isRouteDescendant(normalizedUrl, currentPath)) {\n return true\n }\n }\n\n return (item.children ?? []).some((child) =>\n isItemActive({\n currentPath,\n item: child,\n }),\n )\n}\n\nconst isItemCurrent = ({\n currentPath,\n item,\n}: {\n currentPath?: string\n item: PayloadMarkdownDocsNavItem\n}): boolean => Boolean(currentPath && item.url && currentPath === normalizeRoutePath(item.url))\n\nconst renderDefaultLink = ({\n children,\n className,\n current,\n href,\n}: PayloadMarkdownDocsNavbarRenderLinkOptions) => (\n <a aria-current={current ? 'page' : undefined} className={className} href={href}>\n {children}\n </a>\n)\n\nconst renderNavItems = ({\n classNames,\n currentPath,\n depth = 0,\n items,\n renderLink = renderDefaultLink,\n}: {\n classNames: PayloadMarkdownDocsNavbarClassNames\n currentPath?: string\n depth?: number\n items: PayloadMarkdownDocsNavItem[]\n renderLink?: (options: PayloadMarkdownDocsNavbarRenderLinkOptions) => ReactNode\n}): ReactNode => {\n if (items.length === 0) {\n return null\n }\n\n return (\n <ul className={cx(depth === 0 ? classNames.list : classNames.childrenList)}>\n {items.map((item) => {\n const active = isItemActive({\n currentPath,\n item,\n })\n const current = isItemCurrent({\n currentPath,\n item,\n })\n const linkClassName = cx(classNames.link, active && classNames.activeLink)\n const children = item.children?.length\n ? renderNavItems({\n classNames,\n currentPath,\n depth: depth + 1,\n items: item.children,\n renderLink,\n })\n : null\n\n return (\n <li className={cx('group', classNames.item)} key={`${item.collection}:${item.id}`}>\n {item.url ? (\n renderLink({\n active,\n children: item.label,\n className: linkClassName,\n current,\n href: item.url,\n item,\n })\n ) : (\n <span className={cx(classNames.trigger, active && classNames.activeLink)}>\n {item.label}\n </span>\n )}\n {children ? <div className={classNames.panel}>{children}</div> : null}\n </li>\n )\n })}\n </ul>\n )\n}\n\nexport const PayloadMarkdownDocsNavbar = async ({\n ariaLabel = 'Docs navigation',\n classNames,\n currentPath,\n items,\n renderLink,\n ...options\n}: PayloadMarkdownDocsNavbarProps) => {\n const navItems =\n items ??\n (options.payload\n ? await getPayloadMarkdownDocsNavItems({\n ...options,\n payload: options.payload,\n })\n : [])\n\n if (navItems.length === 0) {\n return null\n }\n\n const mergedClassNames = {\n ...defaultClassNames,\n ...classNames,\n }\n\n return (\n <nav aria-label={ariaLabel} className={mergedClassNames.root}>\n {renderNavItems({\n classNames: mergedClassNames,\n currentPath: currentPath ? normalizeRoutePath(currentPath) : undefined,\n items: navItems,\n renderLink,\n })}\n </nav>\n )\n}\n"],"names":["isRouteDescendant","normalizeRoutePath","getPayloadMarkdownDocsNavItems","cx","values","filter","Boolean","join","defaultClassNames","activeLink","childrenList","item","link","list","panel","root","trigger","isItemActive","currentPath","url","normalizedUrl","children","some","child","isItemCurrent","renderDefaultLink","className","current","href","a","aria-current","undefined","renderNavItems","classNames","depth","items","renderLink","length","ul","map","active","linkClassName","li","label","span","div","collection","id","PayloadMarkdownDocsNavbar","ariaLabel","options","navItems","payload","mergedClassNames","nav","aria-label"],"mappings":"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"}
@@ -1,8 +1,10 @@
1
- export { getPayloadMarkdownDocsLinks } from './links.js';
2
- export type { GetPayloadMarkdownDocsLinksOptions, PayloadMarkdownDocsLink } from './links.js';
1
+ export { appendPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsLinks, getPayloadMarkdownDocsNavItems, } from './links.js';
2
+ export type { AppendPayloadMarkdownDocsHeaderNavItemsOptions, GetPayloadMarkdownDocsHeaderNavItemsOptions, GetPayloadMarkdownDocsLinksOptions, GetPayloadMarkdownDocsNavItemsOptions, PayloadMarkdownDocsHeaderNavItem, PayloadMarkdownDocsHeaderNavLink, PayloadMarkdownDocsLink, PayloadMarkdownDocsNavCapacityOptions, PayloadMarkdownDocsNavItem, PayloadMarkdownDocsNavItemType, } from './links.js';
3
3
  export { createPayloadMarkdownDocsMarkdownResponse, resolvePayloadMarkdownDocsMarkdownRoute, } from './markdown.js';
4
4
  export type { ResolvedPayloadMarkdownDocsMarkdownRoute, ResolvePayloadMarkdownDocsMarkdownRouteOptions, } from './markdown.js';
5
5
  export { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js';
6
+ export { PayloadMarkdownDocsNavbar } from './PayloadMarkdownDocsNavbar.js';
7
+ export type { PayloadMarkdownDocsNavbarClassNames, PayloadMarkdownDocsNavbarProps, PayloadMarkdownDocsNavbarRenderLinkOptions, } from './PayloadMarkdownDocsNavbar.js';
6
8
  export { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js';
7
9
  export type { PayloadMarkdownDocsPageProps } from './PayloadMarkdownDocsPage.js';
8
10
  export { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js';
@@ -1,6 +1,7 @@
1
- export { getPayloadMarkdownDocsLinks } from './links.js';
1
+ export { appendPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsHeaderNavItems, getPayloadMarkdownDocsLinks, getPayloadMarkdownDocsNavItems } from './links.js';
2
2
  export { createPayloadMarkdownDocsMarkdownResponse, resolvePayloadMarkdownDocsMarkdownRoute } from './markdown.js';
3
3
  export { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js';
4
+ export { PayloadMarkdownDocsNavbar } from './PayloadMarkdownDocsNavbar.js';
4
5
  export { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js';
5
6
  export { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js';
6
7
  export { buildPayloadMarkdownDocsSidebar, getPayloadMarkdownDocsSidebar } from './sidebar.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["export { getPayloadMarkdownDocsLinks } from './links.js'\nexport type { GetPayloadMarkdownDocsLinksOptions, PayloadMarkdownDocsLink } from './links.js'\nexport {\n createPayloadMarkdownDocsMarkdownResponse,\n resolvePayloadMarkdownDocsMarkdownRoute,\n} from './markdown.js'\nexport type {\n ResolvedPayloadMarkdownDocsMarkdownRoute,\n ResolvePayloadMarkdownDocsMarkdownRouteOptions,\n} from './markdown.js'\nexport { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js'\nexport { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js'\nexport type { PayloadMarkdownDocsPageProps } from './PayloadMarkdownDocsPage.js'\nexport { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js'\nexport { buildPayloadMarkdownDocsSidebar, getPayloadMarkdownDocsSidebar } from './sidebar.js'\nexport type {\n BuildPayloadMarkdownDocsSidebarOptions,\n GetPayloadMarkdownDocsSidebarOptions,\n} from './sidebar.js'\nexport type {\n PayloadMarkdownDocsCollectionSlugs,\n PayloadMarkdownDocsDefaults,\n PayloadMarkdownDocsFindArgs,\n PayloadMarkdownDocsHeroImage,\n PayloadMarkdownDocsMetadata,\n PayloadMarkdownDocsOverrides,\n PayloadMarkdownDocsReadPayload,\n PayloadMarkdownDocsSidebarItem,\n ResolvedPayloadMarkdownDocsGroup,\n ResolvedPayloadMarkdownDocsRecord,\n ResolvedPayloadMarkdownDocsRoute,\n ResolvedPayloadMarkdownDocsSet,\n ResolvePayloadMarkdownDocsRouteOptions,\n} from './types.js'\n"],"names":["getPayloadMarkdownDocsLinks","createPayloadMarkdownDocsMarkdownResponse","resolvePayloadMarkdownDocsMarkdownRoute","generatePayloadMarkdownDocsMetadata","getPayloadMarkdownDocsMetadata","PayloadMarkdownDocsPage","getPayloadMarkdownDocsRoutePath","resolvePayloadMarkdownDocsRoute","buildPayloadMarkdownDocsSidebar","getPayloadMarkdownDocsSidebar"],"mappings":"AAAA,SAASA,2BAA2B,QAAQ,aAAY;AAExD,SACEC,yCAAyC,EACzCC,uCAAuC,QAClC,gBAAe;AAKtB,SAASC,mCAAmC,EAAEC,8BAA8B,QAAQ,gBAAe;AACnG,SAASC,uBAAuB,QAAQ,+BAA8B;AAEtE,SAASC,+BAA+B,EAAEC,+BAA+B,QAAQ,aAAY;AAC7F,SAASC,+BAA+B,EAAEC,6BAA6B,QAAQ,eAAc"}
1
+ {"version":3,"sources":["../../src/next/index.ts"],"sourcesContent":["export {\n appendPayloadMarkdownDocsHeaderNavItems,\n getPayloadMarkdownDocsHeaderNavItems,\n getPayloadMarkdownDocsLinks,\n getPayloadMarkdownDocsNavItems,\n} from './links.js'\nexport type {\n AppendPayloadMarkdownDocsHeaderNavItemsOptions,\n GetPayloadMarkdownDocsHeaderNavItemsOptions,\n GetPayloadMarkdownDocsLinksOptions,\n GetPayloadMarkdownDocsNavItemsOptions,\n PayloadMarkdownDocsHeaderNavItem,\n PayloadMarkdownDocsHeaderNavLink,\n PayloadMarkdownDocsLink,\n PayloadMarkdownDocsNavCapacityOptions,\n PayloadMarkdownDocsNavItem,\n PayloadMarkdownDocsNavItemType,\n} from './links.js'\nexport {\n createPayloadMarkdownDocsMarkdownResponse,\n resolvePayloadMarkdownDocsMarkdownRoute,\n} from './markdown.js'\nexport type {\n ResolvedPayloadMarkdownDocsMarkdownRoute,\n ResolvePayloadMarkdownDocsMarkdownRouteOptions,\n} from './markdown.js'\nexport { generatePayloadMarkdownDocsMetadata, getPayloadMarkdownDocsMetadata } from './metadata.js'\nexport { PayloadMarkdownDocsNavbar } from './PayloadMarkdownDocsNavbar.js'\nexport type {\n PayloadMarkdownDocsNavbarClassNames,\n PayloadMarkdownDocsNavbarProps,\n PayloadMarkdownDocsNavbarRenderLinkOptions,\n} from './PayloadMarkdownDocsNavbar.js'\nexport { PayloadMarkdownDocsPage } from './PayloadMarkdownDocsPage.js'\nexport type { PayloadMarkdownDocsPageProps } from './PayloadMarkdownDocsPage.js'\nexport { getPayloadMarkdownDocsRoutePath, resolvePayloadMarkdownDocsRoute } from './route.js'\nexport { buildPayloadMarkdownDocsSidebar, getPayloadMarkdownDocsSidebar } from './sidebar.js'\nexport type {\n BuildPayloadMarkdownDocsSidebarOptions,\n GetPayloadMarkdownDocsSidebarOptions,\n} from './sidebar.js'\nexport type {\n PayloadMarkdownDocsCollectionSlugs,\n PayloadMarkdownDocsDefaults,\n PayloadMarkdownDocsFindArgs,\n PayloadMarkdownDocsHeroImage,\n PayloadMarkdownDocsMetadata,\n PayloadMarkdownDocsOverrides,\n PayloadMarkdownDocsReadPayload,\n PayloadMarkdownDocsSidebarItem,\n ResolvedPayloadMarkdownDocsGroup,\n ResolvedPayloadMarkdownDocsRecord,\n ResolvedPayloadMarkdownDocsRoute,\n ResolvedPayloadMarkdownDocsSet,\n ResolvePayloadMarkdownDocsRouteOptions,\n} from './types.js'\n"],"names":["appendPayloadMarkdownDocsHeaderNavItems","getPayloadMarkdownDocsHeaderNavItems","getPayloadMarkdownDocsLinks","getPayloadMarkdownDocsNavItems","createPayloadMarkdownDocsMarkdownResponse","resolvePayloadMarkdownDocsMarkdownRoute","generatePayloadMarkdownDocsMetadata","getPayloadMarkdownDocsMetadata","PayloadMarkdownDocsNavbar","PayloadMarkdownDocsPage","getPayloadMarkdownDocsRoutePath","resolvePayloadMarkdownDocsRoute","buildPayloadMarkdownDocsSidebar","getPayloadMarkdownDocsSidebar"],"mappings":"AAAA,SACEA,uCAAuC,EACvCC,oCAAoC,EACpCC,2BAA2B,EAC3BC,8BAA8B,QACzB,aAAY;AAanB,SACEC,yCAAyC,EACzCC,uCAAuC,QAClC,gBAAe;AAKtB,SAASC,mCAAmC,EAAEC,8BAA8B,QAAQ,gBAAe;AACnG,SAASC,yBAAyB,QAAQ,iCAAgC;AAM1E,SAASC,uBAAuB,QAAQ,+BAA8B;AAEtE,SAASC,+BAA+B,EAAEC,+BAA+B,QAAQ,aAAY;AAC7F,SAASC,+BAA+B,EAAEC,6BAA6B,QAAQ,eAAc"}
@@ -3,9 +3,62 @@ export type PayloadMarkdownDocsLink = {
3
3
  label: string;
4
4
  url: string;
5
5
  };
6
+ export type PayloadMarkdownDocsNavItemType = 'docsGroup' | 'docsSet';
7
+ export type PayloadMarkdownDocsNavItem = {
8
+ children?: PayloadMarkdownDocsNavItem[];
9
+ collection: string;
10
+ id: string;
11
+ label: string;
12
+ order: number;
13
+ route: string;
14
+ type: PayloadMarkdownDocsNavItemType;
15
+ url?: string;
16
+ };
17
+ export type PayloadMarkdownDocsNavCapacityOptions = {
18
+ availableSlots?: number;
19
+ existingItemsCount?: number;
20
+ maxItems?: number;
21
+ };
22
+ export type GetPayloadMarkdownDocsNavItemsOptions = {
23
+ collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>;
24
+ fetchLimit?: number;
25
+ includeDrafts?: boolean;
26
+ overrideAccess?: boolean;
27
+ payload: PayloadMarkdownDocsReadPayload;
28
+ } & PayloadMarkdownDocsNavCapacityOptions;
6
29
  export type GetPayloadMarkdownDocsLinksOptions = {
7
30
  collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>;
31
+ includeDrafts?: boolean;
8
32
  overrideAccess?: boolean;
9
33
  payload: PayloadMarkdownDocsReadPayload;
10
34
  };
11
- export declare const getPayloadMarkdownDocsLinks: ({ collections, overrideAccess, payload, }: GetPayloadMarkdownDocsLinksOptions) => Promise<PayloadMarkdownDocsLink[]>;
35
+ export type PayloadMarkdownDocsHeaderNavLink = {
36
+ label: string;
37
+ newTab?: false;
38
+ reference: {
39
+ relationTo: string;
40
+ value: string;
41
+ };
42
+ type: 'reference';
43
+ } | {
44
+ label: string;
45
+ newTab?: false;
46
+ type: 'custom';
47
+ url: string;
48
+ };
49
+ export type PayloadMarkdownDocsHeaderNavItem = {
50
+ childItems?: PayloadMarkdownDocsHeaderNavItem[];
51
+ link: PayloadMarkdownDocsHeaderNavLink;
52
+ subItems?: PayloadMarkdownDocsHeaderNavItem[];
53
+ };
54
+ export type GetPayloadMarkdownDocsHeaderNavItemsOptions = {
55
+ existingItems?: unknown[];
56
+ mode?: 'relationship' | 'url';
57
+ } & GetPayloadMarkdownDocsNavItemsOptions;
58
+ export type AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem> = {
59
+ existingItems: TExistingItem[];
60
+ } & GetPayloadMarkdownDocsHeaderNavItemsOptions;
61
+ export declare const getPayloadMarkdownDocsNavItems: ({ collections, fetchLimit, includeDrafts, overrideAccess, payload, ...capacityOptions }: GetPayloadMarkdownDocsNavItemsOptions) => Promise<PayloadMarkdownDocsNavItem[]>;
62
+ export declare const getPayloadMarkdownDocsLinks: ({ collections, includeDrafts, overrideAccess, payload, }: GetPayloadMarkdownDocsLinksOptions) => Promise<PayloadMarkdownDocsLink[]>;
63
+ export declare const getPayloadMarkdownDocsHeaderNavItems: ({ existingItems, existingItemsCount, mode, ...options }: GetPayloadMarkdownDocsHeaderNavItemsOptions) => Promise<PayloadMarkdownDocsHeaderNavItem[]>;
64
+ export declare const appendPayloadMarkdownDocsHeaderNavItems: <TExistingItem>({ existingItems, ...options }: AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem>) => Promise<(PayloadMarkdownDocsHeaderNavItem | TExistingItem)[]>;
@@ -1,6 +1,19 @@
1
1
  import { DEFAULT_DOCS_GROUPS_COLLECTION_SLUG, DEFAULT_DOCS_SETS_COLLECTION_SLUG } from '../constants.js';
2
2
  import { deriveDocsSetRouteBase, joinRouteSegments } from '../routing/index.js';
3
- import { getRelationshipId, isRecord, isVisibleDocsSet, toResolvedDocsSet } from './records.js';
3
+ import { getRelationshipId, isRecord, isVisibleDocsSet, toResolvedDocsGroup, toResolvedDocsSet } from './records.js';
4
+ const getAvailableSlots = ({ availableSlots, existingItemsCount = 0, maxItems })=>{
5
+ if (availableSlots !== undefined) {
6
+ return Math.max(0, availableSlots);
7
+ }
8
+ if (maxItems === undefined) {
9
+ return undefined;
10
+ }
11
+ return Math.max(0, maxItems - existingItemsCount);
12
+ };
13
+ const applyTopLevelCapacity = (items, options)=>{
14
+ const availableSlots = getAvailableSlots(options);
15
+ return availableSlots === undefined ? items : items.slice(0, availableSlots);
16
+ };
4
17
  const getGroupRoutePath = ({ groupId, groupsById, seen = new Set() })=>{
5
18
  if (!groupId || seen.has(groupId)) {
6
19
  return undefined;
@@ -18,14 +31,172 @@ const getGroupRoutePath = ({ groupId, groupsById, seen = new Set() })=>{
18
31
  ])
19
32
  }), group.slug);
20
33
  };
21
- export const getPayloadMarkdownDocsLinks = async ({ collections, overrideAccess = true, payload })=>{
34
+ const sortByOrderThenLabel = (items)=>[
35
+ ...items
36
+ ].sort((first, second)=>{
37
+ if (first.order !== second.order) {
38
+ return first.order - second.order;
39
+ }
40
+ return first.label.localeCompare(second.label);
41
+ });
42
+ const getFirstLinkableUrl = (item)=>{
43
+ if (item.url) {
44
+ return item.url;
45
+ }
46
+ for (const child of item.children ?? []){
47
+ const url = getFirstLinkableUrl(child);
48
+ if (url) {
49
+ return url;
50
+ }
51
+ }
52
+ return undefined;
53
+ };
54
+ export const getPayloadMarkdownDocsNavItems = async ({ collections, fetchLimit = 1000, includeDrafts = false, overrideAccess = true, payload, ...capacityOptions })=>{
55
+ const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG;
56
+ const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG;
57
+ const [docsSetsResult, docsGroupsResult] = await Promise.all([
58
+ payload.find({
59
+ collection: docsSetsCollectionSlug,
60
+ depth: 0,
61
+ draft: includeDrafts,
62
+ limit: fetchLimit,
63
+ overrideAccess
64
+ }),
65
+ payload.find({
66
+ collection: docsGroupsCollectionSlug,
67
+ depth: 0,
68
+ limit: fetchLimit,
69
+ overrideAccess
70
+ })
71
+ ]);
72
+ const groupsById = new Map(docsGroupsResult.docs.flatMap((group)=>{
73
+ if (!isRecord(group)) {
74
+ return [];
75
+ }
76
+ const id = getRelationshipId(group);
77
+ return id ? [
78
+ [
79
+ id,
80
+ group
81
+ ]
82
+ ] : [];
83
+ }));
84
+ const childGroupIdsByParentId = new Map();
85
+ const topLevelGroupIds = [];
86
+ for (const [groupId, group] of groupsById){
87
+ const parentId = getRelationshipId(group.parent);
88
+ if (parentId) {
89
+ childGroupIdsByParentId.set(parentId, [
90
+ ...childGroupIdsByParentId.get(parentId) ?? [],
91
+ groupId
92
+ ]);
93
+ } else {
94
+ topLevelGroupIds.push(groupId);
95
+ }
96
+ }
97
+ const docsSetItemsByGroupId = new Map();
98
+ const topLevelDocsSetItems = [];
99
+ for (const doc of docsSetsResult.docs){
100
+ const docsSet = toResolvedDocsSet(doc);
101
+ if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({
102
+ docsSet,
103
+ includeDrafts
104
+ })) {
105
+ continue;
106
+ }
107
+ const groupId = getRelationshipId(doc.group);
108
+ const groupRoutePath = groupId ? getGroupRoutePath({
109
+ groupId,
110
+ groupsById
111
+ }) : undefined;
112
+ if (groupId && !groupRoutePath) {
113
+ continue;
114
+ }
115
+ const item = {
116
+ id: docsSet.id,
117
+ type: 'docsSet',
118
+ collection: docsSetsCollectionSlug,
119
+ label: docsSet.navTitle ?? docsSet.title,
120
+ order: docsSet.order,
121
+ route: deriveDocsSetRouteBase({
122
+ docsSetSlug: docsSet.slug,
123
+ groupRoutePath
124
+ }),
125
+ url: deriveDocsSetRouteBase({
126
+ docsSetSlug: docsSet.slug,
127
+ groupRoutePath
128
+ })
129
+ };
130
+ if (groupId) {
131
+ docsSetItemsByGroupId.set(groupId, [
132
+ ...docsSetItemsByGroupId.get(groupId) ?? [],
133
+ item
134
+ ]);
135
+ } else {
136
+ topLevelDocsSetItems.push(item);
137
+ }
138
+ }
139
+ const buildGroupItem = (groupId, seen = new Set())=>{
140
+ if (seen.has(groupId)) {
141
+ return undefined;
142
+ }
143
+ const doc = groupsById.get(groupId);
144
+ const group = toResolvedDocsGroup(doc);
145
+ const routePath = getGroupRoutePath({
146
+ groupId,
147
+ groupsById
148
+ });
149
+ if (!group || !routePath) {
150
+ return undefined;
151
+ }
152
+ const nextSeen = new Set([
153
+ groupId,
154
+ ...seen
155
+ ]);
156
+ const childGroups = (childGroupIdsByParentId.get(groupId) ?? []).flatMap((childGroupId)=>{
157
+ const item = buildGroupItem(childGroupId, nextSeen);
158
+ return item ? [
159
+ item
160
+ ] : [];
161
+ });
162
+ const childDocsSets = docsSetItemsByGroupId.get(groupId) ?? [];
163
+ const children = sortByOrderThenLabel([
164
+ ...childGroups,
165
+ ...childDocsSets
166
+ ]);
167
+ return {
168
+ ...children.length > 0 ? {
169
+ children
170
+ } : {},
171
+ id: group.id,
172
+ type: 'docsGroup',
173
+ collection: docsGroupsCollectionSlug,
174
+ label: group.navTitle ?? group.title,
175
+ order: group.order,
176
+ route: routePath,
177
+ ...group.serveIndex ? {
178
+ url: routePath
179
+ } : {}
180
+ };
181
+ };
182
+ return applyTopLevelCapacity(sortByOrderThenLabel([
183
+ ...topLevelGroupIds.flatMap((groupId)=>{
184
+ const item = buildGroupItem(groupId);
185
+ return item ? [
186
+ item
187
+ ] : [];
188
+ }),
189
+ ...topLevelDocsSetItems
190
+ ]), capacityOptions);
191
+ };
192
+ export const getPayloadMarkdownDocsLinks = async ({ collections, includeDrafts = false, overrideAccess = true, payload })=>{
22
193
  const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG;
23
194
  const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG;
24
195
  const [docsSetsResult, docsGroupsResult] = await Promise.all([
25
196
  payload.find({
26
197
  collection: docsSetsCollectionSlug,
27
198
  depth: 0,
28
- draft: false,
199
+ draft: includeDrafts,
29
200
  limit: 1000,
30
201
  overrideAccess
31
202
  }),
@@ -51,7 +222,8 @@ export const getPayloadMarkdownDocsLinks = async ({ collections, overrideAccess
51
222
  return docsSetsResult.docs.flatMap((doc)=>{
52
223
  const docsSet = toResolvedDocsSet(doc);
53
224
  if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({
54
- docsSet
225
+ docsSet,
226
+ includeDrafts
55
227
  })) {
56
228
  return [];
57
229
  }
@@ -78,5 +250,65 @@ export const getPayloadMarkdownDocsLinks = async ({ collections, overrideAccess
78
250
  url
79
251
  }));
80
252
  };
253
+ const toHeaderLink = ({ item, mode })=>{
254
+ if (mode === 'relationship') {
255
+ return {
256
+ type: 'reference',
257
+ label: item.label,
258
+ reference: {
259
+ relationTo: item.collection,
260
+ value: item.id
261
+ }
262
+ };
263
+ }
264
+ const url = getFirstLinkableUrl(item);
265
+ return url ? {
266
+ type: 'custom',
267
+ label: item.label,
268
+ url
269
+ } : undefined;
270
+ };
271
+ const toHeaderNavItem = (item, mode, depth = 0)=>{
272
+ const link = toHeaderLink({
273
+ item,
274
+ mode
275
+ });
276
+ if (!link) {
277
+ return undefined;
278
+ }
279
+ const children = (item.children ?? []).flatMap((child)=>{
280
+ const childItem = toHeaderNavItem(child, mode, depth + 1);
281
+ return childItem ? [
282
+ childItem
283
+ ] : [];
284
+ });
285
+ return {
286
+ link,
287
+ ...children.length > 0 ? depth === 0 ? {
288
+ subItems: children
289
+ } : {
290
+ childItems: children
291
+ } : {}
292
+ };
293
+ };
294
+ export const getPayloadMarkdownDocsHeaderNavItems = async ({ existingItems, existingItemsCount = existingItems?.length ?? 0, mode = 'url', ...options })=>{
295
+ const items = await getPayloadMarkdownDocsNavItems({
296
+ ...options,
297
+ existingItemsCount
298
+ });
299
+ return items.flatMap((item)=>{
300
+ const headerItem = toHeaderNavItem(item, mode);
301
+ return headerItem ? [
302
+ headerItem
303
+ ] : [];
304
+ });
305
+ };
306
+ export const appendPayloadMarkdownDocsHeaderNavItems = async ({ existingItems, ...options })=>[
307
+ ...existingItems,
308
+ ...await getPayloadMarkdownDocsHeaderNavItems({
309
+ ...options,
310
+ existingItems
311
+ })
312
+ ];
81
313
 
82
314
  //# sourceMappingURL=links.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/links.ts"],"sourcesContent":["import type { PayloadMarkdownDocsCollectionSlugs, PayloadMarkdownDocsReadPayload } from './types.js'\n\nimport {\n DEFAULT_DOCS_GROUPS_COLLECTION_SLUG,\n DEFAULT_DOCS_SETS_COLLECTION_SLUG,\n} from '../constants.js'\nimport { deriveDocsSetRouteBase, joinRouteSegments } from '../routing/index.js'\nimport { getRelationshipId, isRecord, isVisibleDocsSet, toResolvedDocsSet } from './records.js'\n\nexport type PayloadMarkdownDocsLink = {\n label: string\n url: string\n}\n\nexport type GetPayloadMarkdownDocsLinksOptions = {\n collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>\n overrideAccess?: boolean\n payload: PayloadMarkdownDocsReadPayload\n}\n\nconst getGroupRoutePath = ({\n groupId,\n groupsById,\n seen = new Set<string>(),\n}: {\n groupId?: string\n groupsById: Map<string, unknown>\n seen?: Set<string>\n}): string | undefined => {\n if (!groupId || seen.has(groupId)) {\n return undefined\n }\n\n const group = groupsById.get(groupId)\n\n if (!isRecord(group) || typeof group.slug !== 'string') {\n return undefined\n }\n\n return joinRouteSegments(\n getGroupRoutePath({\n groupId: getRelationshipId(group.parent),\n groupsById,\n seen: new Set([groupId, ...seen]),\n }),\n group.slug,\n )\n}\n\nexport const getPayloadMarkdownDocsLinks = async ({\n collections,\n overrideAccess = true,\n payload,\n}: GetPayloadMarkdownDocsLinksOptions): Promise<PayloadMarkdownDocsLink[]> => {\n const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG\n const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG\n const [docsSetsResult, docsGroupsResult] = await Promise.all([\n payload.find({\n collection: docsSetsCollectionSlug,\n depth: 0,\n draft: false,\n limit: 1000,\n overrideAccess,\n }),\n payload.find({\n collection: docsGroupsCollectionSlug,\n depth: 0,\n limit: 1000,\n overrideAccess,\n }),\n ])\n const groupsById = new Map(\n docsGroupsResult.docs.flatMap((group) => {\n if (!isRecord(group)) {\n return []\n }\n\n const id = getRelationshipId(group)\n\n return id ? [[id, group]] : []\n }),\n )\n\n return docsSetsResult.docs\n .flatMap((doc) => {\n const docsSet = toResolvedDocsSet(doc)\n\n if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({ docsSet })) {\n return []\n }\n\n return [\n {\n label: docsSet.navTitle ?? docsSet.title,\n order: docsSet.order,\n url: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath: getGroupRoutePath({\n groupId: getRelationshipId(doc.group),\n groupsById,\n }),\n }),\n },\n ]\n })\n .sort((first, second) => {\n if (first.order !== second.order) {\n return first.order - second.order\n }\n\n return first.label.localeCompare(second.label)\n })\n .map(({ label, url }) => ({\n label,\n url,\n }))\n}\n"],"names":["DEFAULT_DOCS_GROUPS_COLLECTION_SLUG","DEFAULT_DOCS_SETS_COLLECTION_SLUG","deriveDocsSetRouteBase","joinRouteSegments","getRelationshipId","isRecord","isVisibleDocsSet","toResolvedDocsSet","getGroupRoutePath","groupId","groupsById","seen","Set","has","undefined","group","get","slug","parent","getPayloadMarkdownDocsLinks","collections","overrideAccess","payload","docsGroupsCollectionSlug","docsGroups","docsSetsCollectionSlug","docsSets","docsSetsResult","docsGroupsResult","Promise","all","find","collection","depth","draft","limit","Map","docs","flatMap","id","doc","docsSet","label","navTitle","title","order","url","docsSetSlug","groupRoutePath","sort","first","second","localeCompare","map"],"mappings":"AAEA,SACEA,mCAAmC,EACnCC,iCAAiC,QAC5B,kBAAiB;AACxB,SAASC,sBAAsB,EAAEC,iBAAiB,QAAQ,sBAAqB;AAC/E,SAASC,iBAAiB,EAAEC,QAAQ,EAAEC,gBAAgB,EAAEC,iBAAiB,QAAQ,eAAc;AAa/F,MAAMC,oBAAoB,CAAC,EACzBC,OAAO,EACPC,UAAU,EACVC,OAAO,IAAIC,KAAa,EAKzB;IACC,IAAI,CAACH,WAAWE,KAAKE,GAAG,CAACJ,UAAU;QACjC,OAAOK;IACT;IAEA,MAAMC,QAAQL,WAAWM,GAAG,CAACP;IAE7B,IAAI,CAACJ,SAASU,UAAU,OAAOA,MAAME,IAAI,KAAK,UAAU;QACtD,OAAOH;IACT;IAEA,OAAOX,kBACLK,kBAAkB;QAChBC,SAASL,kBAAkBW,MAAMG,MAAM;QACvCR;QACAC,MAAM,IAAIC,IAAI;YAACH;eAAYE;SAAK;IAClC,IACAI,MAAME,IAAI;AAEd;AAEA,OAAO,MAAME,8BAA8B,OAAO,EAChDC,WAAW,EACXC,iBAAiB,IAAI,EACrBC,OAAO,EAC4B;IACnC,MAAMC,2BAA2BH,aAAaI,cAAcxB;IAC5D,MAAMyB,yBAAyBL,aAAaM,YAAYzB;IACxD,MAAM,CAAC0B,gBAAgBC,iBAAiB,GAAG,MAAMC,QAAQC,GAAG,CAAC;QAC3DR,QAAQS,IAAI,CAAC;YACXC,YAAYP;YACZQ,OAAO;YACPC,OAAO;YACPC,OAAO;YACPd;QACF;QACAC,QAAQS,IAAI,CAAC;YACXC,YAAYT;YACZU,OAAO;YACPE,OAAO;YACPd;QACF;KACD;IACD,MAAMX,aAAa,IAAI0B,IACrBR,iBAAiBS,IAAI,CAACC,OAAO,CAAC,CAACvB;QAC7B,IAAI,CAACV,SAASU,QAAQ;YACpB,OAAO,EAAE;QACX;QAEA,MAAMwB,KAAKnC,kBAAkBW;QAE7B,OAAOwB,KAAK;YAAC;gBAACA;gBAAIxB;aAAM;SAAC,GAAG,EAAE;IAChC;IAGF,OAAOY,eAAeU,IAAI,CACvBC,OAAO,CAAC,CAACE;QACR,MAAMC,UAAUlC,kBAAkBiC;QAElC,IAAI,CAACC,SAASxB,QAAQ,CAACZ,SAASmC,QAAQ,CAAClC,iBAAiB;YAAEmC;QAAQ,IAAI;YACtE,OAAO,EAAE;QACX;QAEA,OAAO;YACL;gBACEC,OAAOD,QAAQE,QAAQ,IAAIF,QAAQG,KAAK;gBACxCC,OAAOJ,QAAQI,KAAK;gBACpBC,KAAK5C,uBAAuB;oBAC1B6C,aAAaN,QAAQxB,IAAI;oBACzB+B,gBAAgBxC,kBAAkB;wBAChCC,SAASL,kBAAkBoC,IAAIzB,KAAK;wBACpCL;oBACF;gBACF;YACF;SACD;IACH,GACCuC,IAAI,CAAC,CAACC,OAAOC;QACZ,IAAID,MAAML,KAAK,KAAKM,OAAON,KAAK,EAAE;YAChC,OAAOK,MAAML,KAAK,GAAGM,OAAON,KAAK;QACnC;QAEA,OAAOK,MAAMR,KAAK,CAACU,aAAa,CAACD,OAAOT,KAAK;IAC/C,GACCW,GAAG,CAAC,CAAC,EAAEX,KAAK,EAAEI,GAAG,EAAE,GAAM,CAAA;YACxBJ;YACAI;QACF,CAAA;AACJ,EAAC"}
1
+ {"version":3,"sources":["../../src/next/links.ts"],"sourcesContent":["import type { PayloadMarkdownDocsCollectionSlugs, PayloadMarkdownDocsReadPayload } from './types.js'\n\nimport {\n DEFAULT_DOCS_GROUPS_COLLECTION_SLUG,\n DEFAULT_DOCS_SETS_COLLECTION_SLUG,\n} from '../constants.js'\nimport { deriveDocsSetRouteBase, joinRouteSegments } from '../routing/index.js'\nimport {\n getRelationshipId,\n isRecord,\n isVisibleDocsSet,\n toResolvedDocsGroup,\n toResolvedDocsSet,\n} from './records.js'\n\nexport type PayloadMarkdownDocsLink = {\n label: string\n url: string\n}\n\nexport type PayloadMarkdownDocsNavItemType = 'docsGroup' | 'docsSet'\n\nexport type PayloadMarkdownDocsNavItem = {\n children?: PayloadMarkdownDocsNavItem[]\n collection: string\n id: string\n label: string\n order: number\n route: string\n type: PayloadMarkdownDocsNavItemType\n url?: string\n}\n\nexport type PayloadMarkdownDocsNavCapacityOptions = {\n availableSlots?: number\n existingItemsCount?: number\n maxItems?: number\n}\n\nexport type GetPayloadMarkdownDocsNavItemsOptions = {\n collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>\n fetchLimit?: number\n includeDrafts?: boolean\n overrideAccess?: boolean\n payload: PayloadMarkdownDocsReadPayload\n} & PayloadMarkdownDocsNavCapacityOptions\n\nexport type GetPayloadMarkdownDocsLinksOptions = {\n collections?: Pick<PayloadMarkdownDocsCollectionSlugs, 'docsGroups' | 'docsSets'>\n includeDrafts?: boolean\n overrideAccess?: boolean\n payload: PayloadMarkdownDocsReadPayload\n}\n\nexport type PayloadMarkdownDocsHeaderNavLink =\n | {\n label: string\n newTab?: false\n reference: {\n relationTo: string\n value: string\n }\n type: 'reference'\n }\n | {\n label: string\n newTab?: false\n type: 'custom'\n url: string\n }\n\nexport type PayloadMarkdownDocsHeaderNavItem = {\n childItems?: PayloadMarkdownDocsHeaderNavItem[]\n link: PayloadMarkdownDocsHeaderNavLink\n subItems?: PayloadMarkdownDocsHeaderNavItem[]\n}\n\nexport type GetPayloadMarkdownDocsHeaderNavItemsOptions = {\n existingItems?: unknown[]\n mode?: 'relationship' | 'url'\n} & GetPayloadMarkdownDocsNavItemsOptions\n\nexport type AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem> = {\n existingItems: TExistingItem[]\n} & GetPayloadMarkdownDocsHeaderNavItemsOptions\n\nconst getAvailableSlots = ({\n availableSlots,\n existingItemsCount = 0,\n maxItems,\n}: PayloadMarkdownDocsNavCapacityOptions): number | undefined => {\n if (availableSlots !== undefined) {\n return Math.max(0, availableSlots)\n }\n\n if (maxItems === undefined) {\n return undefined\n }\n\n return Math.max(0, maxItems - existingItemsCount)\n}\n\nconst applyTopLevelCapacity = (\n items: PayloadMarkdownDocsNavItem[],\n options: PayloadMarkdownDocsNavCapacityOptions,\n): PayloadMarkdownDocsNavItem[] => {\n const availableSlots = getAvailableSlots(options)\n\n return availableSlots === undefined ? items : items.slice(0, availableSlots)\n}\n\nconst getGroupRoutePath = ({\n groupId,\n groupsById,\n seen = new Set<string>(),\n}: {\n groupId?: string\n groupsById: Map<string, unknown>\n seen?: Set<string>\n}): string | undefined => {\n if (!groupId || seen.has(groupId)) {\n return undefined\n }\n\n const group = groupsById.get(groupId)\n\n if (!isRecord(group) || typeof group.slug !== 'string') {\n return undefined\n }\n\n return joinRouteSegments(\n getGroupRoutePath({\n groupId: getRelationshipId(group.parent),\n groupsById,\n seen: new Set([groupId, ...seen]),\n }),\n group.slug,\n )\n}\n\nconst sortByOrderThenLabel = <T extends { label: string; order: number }>(items: T[]): T[] =>\n [...items].sort((first, second) => {\n if (first.order !== second.order) {\n return first.order - second.order\n }\n\n return first.label.localeCompare(second.label)\n })\n\nconst getFirstLinkableUrl = (item: PayloadMarkdownDocsNavItem): string | undefined => {\n if (item.url) {\n return item.url\n }\n\n for (const child of item.children ?? []) {\n const url = getFirstLinkableUrl(child)\n\n if (url) {\n return url\n }\n }\n\n return undefined\n}\n\nexport const getPayloadMarkdownDocsNavItems = async ({\n collections,\n fetchLimit = 1000,\n includeDrafts = false,\n overrideAccess = true,\n payload,\n ...capacityOptions\n}: GetPayloadMarkdownDocsNavItemsOptions): Promise<PayloadMarkdownDocsNavItem[]> => {\n const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG\n const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG\n const [docsSetsResult, docsGroupsResult] = await Promise.all([\n payload.find({\n collection: docsSetsCollectionSlug,\n depth: 0,\n draft: includeDrafts,\n limit: fetchLimit,\n overrideAccess,\n }),\n payload.find({\n collection: docsGroupsCollectionSlug,\n depth: 0,\n limit: fetchLimit,\n overrideAccess,\n }),\n ])\n const groupsById = new Map(\n docsGroupsResult.docs.flatMap((group) => {\n if (!isRecord(group)) {\n return []\n }\n\n const id = getRelationshipId(group)\n\n return id ? [[id, group]] : []\n }),\n )\n const childGroupIdsByParentId = new Map<string, string[]>()\n const topLevelGroupIds: string[] = []\n\n for (const [groupId, group] of groupsById) {\n const parentId = getRelationshipId(group.parent)\n\n if (parentId) {\n childGroupIdsByParentId.set(parentId, [\n ...(childGroupIdsByParentId.get(parentId) ?? []),\n groupId,\n ])\n } else {\n topLevelGroupIds.push(groupId)\n }\n }\n\n const docsSetItemsByGroupId = new Map<string, PayloadMarkdownDocsNavItem[]>()\n const topLevelDocsSetItems: PayloadMarkdownDocsNavItem[] = []\n\n for (const doc of docsSetsResult.docs) {\n const docsSet = toResolvedDocsSet(doc)\n\n if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({ docsSet, includeDrafts })) {\n continue\n }\n\n const groupId = getRelationshipId(doc.group)\n const groupRoutePath = groupId\n ? getGroupRoutePath({\n groupId,\n groupsById,\n })\n : undefined\n\n if (groupId && !groupRoutePath) {\n continue\n }\n\n const item: PayloadMarkdownDocsNavItem = {\n id: docsSet.id,\n type: 'docsSet',\n collection: docsSetsCollectionSlug,\n label: docsSet.navTitle ?? docsSet.title,\n order: docsSet.order,\n route: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath,\n }),\n url: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath,\n }),\n }\n\n if (groupId) {\n docsSetItemsByGroupId.set(groupId, [...(docsSetItemsByGroupId.get(groupId) ?? []), item])\n } else {\n topLevelDocsSetItems.push(item)\n }\n }\n\n const buildGroupItem = (\n groupId: string,\n seen = new Set<string>(),\n ): PayloadMarkdownDocsNavItem | undefined => {\n if (seen.has(groupId)) {\n return undefined\n }\n\n const doc = groupsById.get(groupId)\n const group = toResolvedDocsGroup(doc)\n const routePath = getGroupRoutePath({\n groupId,\n groupsById,\n })\n\n if (!group || !routePath) {\n return undefined\n }\n\n const nextSeen = new Set([groupId, ...seen])\n const childGroups = (childGroupIdsByParentId.get(groupId) ?? []).flatMap((childGroupId) => {\n const item = buildGroupItem(childGroupId, nextSeen)\n\n return item ? [item] : []\n })\n const childDocsSets = docsSetItemsByGroupId.get(groupId) ?? []\n const children = sortByOrderThenLabel([...childGroups, ...childDocsSets])\n\n return {\n ...(children.length > 0 ? { children } : {}),\n id: group.id,\n type: 'docsGroup',\n collection: docsGroupsCollectionSlug,\n label: group.navTitle ?? group.title,\n order: group.order,\n route: routePath,\n ...(group.serveIndex ? { url: routePath } : {}),\n }\n }\n\n return applyTopLevelCapacity(\n sortByOrderThenLabel([\n ...topLevelGroupIds.flatMap((groupId) => {\n const item = buildGroupItem(groupId)\n\n return item ? [item] : []\n }),\n ...topLevelDocsSetItems,\n ]),\n capacityOptions,\n )\n}\n\nexport const getPayloadMarkdownDocsLinks = async ({\n collections,\n includeDrafts = false,\n overrideAccess = true,\n payload,\n}: GetPayloadMarkdownDocsLinksOptions): Promise<PayloadMarkdownDocsLink[]> => {\n const docsGroupsCollectionSlug = collections?.docsGroups ?? DEFAULT_DOCS_GROUPS_COLLECTION_SLUG\n const docsSetsCollectionSlug = collections?.docsSets ?? DEFAULT_DOCS_SETS_COLLECTION_SLUG\n const [docsSetsResult, docsGroupsResult] = await Promise.all([\n payload.find({\n collection: docsSetsCollectionSlug,\n depth: 0,\n draft: includeDrafts,\n limit: 1000,\n overrideAccess,\n }),\n payload.find({\n collection: docsGroupsCollectionSlug,\n depth: 0,\n limit: 1000,\n overrideAccess,\n }),\n ])\n const groupsById = new Map(\n docsGroupsResult.docs.flatMap((group) => {\n if (!isRecord(group)) {\n return []\n }\n\n const id = getRelationshipId(group)\n\n return id ? [[id, group]] : []\n }),\n )\n\n return docsSetsResult.docs\n .flatMap((doc) => {\n const docsSet = toResolvedDocsSet(doc)\n\n if (!docsSet?.slug || !isRecord(doc) || !isVisibleDocsSet({ docsSet, includeDrafts })) {\n return []\n }\n\n return [\n {\n label: docsSet.navTitle ?? docsSet.title,\n order: docsSet.order,\n url: deriveDocsSetRouteBase({\n docsSetSlug: docsSet.slug,\n groupRoutePath: getGroupRoutePath({\n groupId: getRelationshipId(doc.group),\n groupsById,\n }),\n }),\n },\n ]\n })\n .sort((first, second) => {\n if (first.order !== second.order) {\n return first.order - second.order\n }\n\n return first.label.localeCompare(second.label)\n })\n .map(({ label, url }) => ({\n label,\n url,\n }))\n}\n\nconst toHeaderLink = ({\n item,\n mode,\n}: {\n item: PayloadMarkdownDocsNavItem\n mode: 'relationship' | 'url'\n}): PayloadMarkdownDocsHeaderNavLink | undefined => {\n if (mode === 'relationship') {\n return {\n type: 'reference',\n label: item.label,\n reference: {\n relationTo: item.collection,\n value: item.id,\n },\n }\n }\n\n const url = getFirstLinkableUrl(item)\n\n return url\n ? {\n type: 'custom',\n label: item.label,\n url,\n }\n : undefined\n}\n\nconst toHeaderNavItem = (\n item: PayloadMarkdownDocsNavItem,\n mode: 'relationship' | 'url',\n depth = 0,\n): PayloadMarkdownDocsHeaderNavItem | undefined => {\n const link = toHeaderLink({\n item,\n mode,\n })\n\n if (!link) {\n return undefined\n }\n\n const children = (item.children ?? []).flatMap((child) => {\n const childItem = toHeaderNavItem(child, mode, depth + 1)\n\n return childItem ? [childItem] : []\n })\n\n return {\n link,\n ...(children.length > 0\n ? depth === 0\n ? { subItems: children }\n : { childItems: children }\n : {}),\n }\n}\n\nexport const getPayloadMarkdownDocsHeaderNavItems = async ({\n existingItems,\n existingItemsCount = existingItems?.length ?? 0,\n mode = 'url',\n ...options\n}: GetPayloadMarkdownDocsHeaderNavItemsOptions): Promise<PayloadMarkdownDocsHeaderNavItem[]> => {\n const items = await getPayloadMarkdownDocsNavItems({\n ...options,\n existingItemsCount,\n })\n\n return items.flatMap((item) => {\n const headerItem = toHeaderNavItem(item, mode)\n\n return headerItem ? [headerItem] : []\n })\n}\n\nexport const appendPayloadMarkdownDocsHeaderNavItems = async <TExistingItem>({\n existingItems,\n ...options\n}: AppendPayloadMarkdownDocsHeaderNavItemsOptions<TExistingItem>): Promise<\n (PayloadMarkdownDocsHeaderNavItem | TExistingItem)[]\n> => [\n ...existingItems,\n ...(await getPayloadMarkdownDocsHeaderNavItems({\n ...options,\n existingItems,\n })),\n]\n"],"names":["DEFAULT_DOCS_GROUPS_COLLECTION_SLUG","DEFAULT_DOCS_SETS_COLLECTION_SLUG","deriveDocsSetRouteBase","joinRouteSegments","getRelationshipId","isRecord","isVisibleDocsSet","toResolvedDocsGroup","toResolvedDocsSet","getAvailableSlots","availableSlots","existingItemsCount","maxItems","undefined","Math","max","applyTopLevelCapacity","items","options","slice","getGroupRoutePath","groupId","groupsById","seen","Set","has","group","get","slug","parent","sortByOrderThenLabel","sort","first","second","order","label","localeCompare","getFirstLinkableUrl","item","url","child","children","getPayloadMarkdownDocsNavItems","collections","fetchLimit","includeDrafts","overrideAccess","payload","capacityOptions","docsGroupsCollectionSlug","docsGroups","docsSetsCollectionSlug","docsSets","docsSetsResult","docsGroupsResult","Promise","all","find","collection","depth","draft","limit","Map","docs","flatMap","id","childGroupIdsByParentId","topLevelGroupIds","parentId","set","push","docsSetItemsByGroupId","topLevelDocsSetItems","doc","docsSet","groupRoutePath","type","navTitle","title","route","docsSetSlug","buildGroupItem","routePath","nextSeen","childGroups","childGroupId","childDocsSets","length","serveIndex","getPayloadMarkdownDocsLinks","map","toHeaderLink","mode","reference","relationTo","value","toHeaderNavItem","link","childItem","subItems","childItems","getPayloadMarkdownDocsHeaderNavItems","existingItems","headerItem","appendPayloadMarkdownDocsHeaderNavItems"],"mappings":"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"}
@@ -69,6 +69,7 @@ const getRepositoryName = (repository)=>{
69
69
  const [, name] = repository.split('/', 2);
70
70
  return name ?? repository;
71
71
  };
72
+ const isReleaseTagRef = (claims)=>claims.event_name === 'release' && claims.ref.startsWith('refs/tags/');
72
73
  const repositoryMatches = ({ allowed, owner, repository })=>{
73
74
  const normalized = allowed.trim();
74
75
  if (!normalized) {
@@ -179,7 +180,7 @@ export const verifyGitHubOidcToken = async ({ config, fetchJson, now = new Date(
179
180
  return issue('oidc_owner_not_allowed', `GitHub OIDC token repository owner "${claims.repository_owner}" is not trusted.`);
180
181
  }
181
182
  const repositoryName = getRepositoryName(claims.repository);
182
- if (!includesIfConfigured(config.allowedRefs, claims.ref)) {
183
+ if (!includesIfConfigured(config.allowedRefs, claims.ref) && !isReleaseTagRef(claims)) {
183
184
  return issue('oidc_ref_not_allowed', `GitHub OIDC token ref "${claims.ref}" is not allowed for "${repositoryName}".`);
184
185
  }
185
186
  const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/security/githubOidc.ts"],"sourcesContent":["import {\n createPublicKey,\n type JsonWebKey,\n verify,\n} from 'node:crypto'\n\nimport type { FetchJson } from './jwks.js'\n\nimport {\n DEFAULT_GITHUB_OIDC_ISSUER,\n DEFAULT_MAX_SKEW_SECONDS,\n} from '../constants.js'\nimport {\n fetchJwks,\n findJwkByKid,\n getGithubOidcJwksUrl,\n} from './jwks.js'\nimport { decodeJwt } from './jwt.js'\n\nexport type GitHubOidcErrorCode =\n | 'oidc_expired'\n | 'oidc_invalid_audience'\n | 'oidc_invalid_issuer'\n | 'oidc_invalid_token'\n | 'oidc_jwks_unavailable'\n | 'oidc_missing_claim'\n | 'oidc_missing_jti'\n | 'oidc_not_yet_valid'\n | 'oidc_owner_not_allowed'\n | 'oidc_pull_request_not_allowed'\n | 'oidc_ref_not_allowed'\n | 'oidc_repository_not_allowed'\n | 'oidc_workflow_not_allowed'\n\nexport type GitHubOidcClaims = {\n actor?: string\n aud: string | string[]\n environment?: string\n event_name?: string\n exp: number\n iat: number\n iss: string\n job_workflow_ref?: string\n jti: string\n nbf?: number\n ref: string\n repository: string\n repository_owner: string\n sha?: string\n sub: string\n workflow?: string\n workflow_ref?: string\n}\n\nexport type GitHubOidcTrustedSource = {\n limitRepos?: boolean\n owner: string\n repositories?: string[]\n}\n\nexport type GitHubOidcVerifyConfig = {\n allowedRefs?: string[]\n allowedWorkflowRefs?: string[]\n allowPullRequests?: boolean\n audience: string\n enforceWorkflowRefs?: boolean\n issuer?: string\n jwksUrl?: string\n maxSkewSeconds?: number\n trustedSources: GitHubOidcTrustedSource[]\n}\n\nexport type VerifiedGitHubOidcToken = {\n claims: GitHubOidcClaims\n expiresAt: Date\n keyId: string\n}\n\nexport type VerifyGitHubOidcTokenResult =\n | {\n code: GitHubOidcErrorCode\n message: string\n ok: false\n }\n | {\n ok: true\n token: VerifiedGitHubOidcToken\n }\n\nconst isString = (value: unknown): value is string =>\n typeof value === 'string' && value.trim() !== ''\n\nconst isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every(isString)\n\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\nconst getStringClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): string | undefined => {\n const value = payload[claim]\n\n return isString(value) ? value : undefined\n}\n\nconst getNumberClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): number | undefined => {\n const value = payload[claim]\n\n return isNumber(value) ? value : undefined\n}\n\nconst getAudienceClaim = (\n payload: Record<string, unknown>,\n): string | string[] | undefined => {\n const value = payload.aud\n\n if (isString(value) || isStringArray(value)) {\n return value\n }\n\n return undefined\n}\n\nconst toClaims = (\n payload: Record<string, unknown>,\n): GitHubOidcClaims | undefined => {\n const aud = getAudienceClaim(payload)\n const exp = getNumberClaim(payload, 'exp')\n const iat = getNumberClaim(payload, 'iat')\n const iss = getStringClaim(payload, 'iss')\n const jti = getStringClaim(payload, 'jti')\n const ref = getStringClaim(payload, 'ref')\n const repository = getStringClaim(payload, 'repository')\n const repositoryOwner = getStringClaim(payload, 'repository_owner')\n const sub = getStringClaim(payload, 'sub')\n\n if (\n !aud ||\n exp === undefined ||\n iat === undefined ||\n !iss ||\n !jti ||\n !ref ||\n !repository ||\n !repositoryOwner ||\n !sub\n ) {\n return undefined\n }\n\n return {\n actor: getStringClaim(payload, 'actor'),\n aud,\n environment: getStringClaim(payload, 'environment'),\n event_name: getStringClaim(payload, 'event_name'),\n exp,\n iat,\n iss,\n job_workflow_ref: getStringClaim(payload, 'job_workflow_ref'),\n jti,\n nbf: getNumberClaim(payload, 'nbf'),\n ref,\n repository,\n repository_owner: repositoryOwner,\n sha: getStringClaim(payload, 'sha'),\n sub,\n workflow: getStringClaim(payload, 'workflow'),\n workflow_ref: getStringClaim(payload, 'workflow_ref'),\n }\n}\n\nconst issue = (\n code: GitHubOidcErrorCode,\n message: string,\n): VerifyGitHubOidcTokenResult => ({\n code,\n message,\n ok: false,\n})\n\nconst includesIfConfigured = (\n allowed: string[] | undefined,\n value: string | undefined,\n): boolean => {\n if (!allowed || allowed.length === 0) {\n return true\n }\n\n return value !== undefined && allowed.includes(value)\n}\n\nconst audienceMatches = (\n audience: string | string[],\n expected: string,\n): boolean =>\n Array.isArray(audience) ? audience.includes(expected) : audience === expected\n\nconst getRepositoryName = (repository: string): string => {\n const [, name] = repository.split('/', 2)\n\n return name ?? repository\n}\n\nconst repositoryMatches = ({\n allowed,\n owner,\n repository,\n}: {\n allowed: string\n owner: string\n repository: string\n}): boolean => {\n const normalized = allowed.trim()\n\n if (!normalized) {\n return false\n }\n\n return normalized.includes('/')\n ? normalized.toLowerCase() === repository.toLowerCase()\n : `${owner}/${normalized}`.toLowerCase() === repository.toLowerCase()\n}\n\nconst findTrustedSource = ({\n repository,\n repositoryOwner,\n trustedSources,\n}: {\n repository: string\n repositoryOwner: string\n trustedSources: GitHubOidcTrustedSource[]\n}): GitHubOidcTrustedSource | undefined =>\n trustedSources.find((source) => {\n if (source.owner.toLowerCase() !== repositoryOwner.toLowerCase()) {\n return false\n }\n\n if (source.limitRepos !== true) {\n return true\n }\n\n return (source.repositories ?? []).some((allowedRepository) =>\n repositoryMatches({\n allowed: allowedRepository,\n owner: source.owner,\n repository,\n }),\n )\n })\n\nconst verifyJwtSignature = ({\n jwk,\n signature,\n signingInput,\n}: {\n jwk: Record<string, unknown>\n signature: Buffer\n signingInput: string\n}): boolean => {\n try {\n const publicKey = createPublicKey({\n format: 'jwk',\n key: jwk as JsonWebKey,\n })\n\n return verify(\n 'RSA-SHA256',\n Buffer.from(signingInput, 'utf8'),\n publicKey,\n signature,\n )\n } catch {\n return false\n }\n}\n\nexport const verifyGitHubOidcToken = async ({\n config,\n fetchJson,\n now = new Date(),\n token,\n}: {\n config: GitHubOidcVerifyConfig\n fetchJson?: FetchJson\n now?: Date\n token: string\n}): Promise<VerifyGitHubOidcTokenResult> => {\n const decoded = decodeJwt(token)\n\n if (!decoded) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is malformed.')\n }\n\n if (decoded.header.alg !== 'RS256') {\n return issue('oidc_invalid_token', 'GitHub OIDC token must use RS256.')\n }\n\n if (!isString(decoded.header.kid)) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is missing kid.')\n }\n\n const issuer = config.issuer ?? DEFAULT_GITHUB_OIDC_ISSUER\n let jwksUrl: string\n\n try {\n jwksUrl = await getGithubOidcJwksUrl({\n fetchJson,\n issuer,\n jwksUrl: config.jwksUrl,\n })\n const jwks = await fetchJwks({\n fetchJson,\n now,\n url: jwksUrl,\n })\n const jwk = findJwkByKid({\n jwks,\n kid: decoded.header.kid,\n })\n\n if (\n !jwk ||\n !verifyJwtSignature({\n jwk,\n signature: decoded.signature,\n signingInput: decoded.signingInput,\n })\n ) {\n return issue('oidc_invalid_token', 'GitHub OIDC token signature is invalid.')\n }\n } catch {\n return issue('oidc_jwks_unavailable', 'GitHub OIDC signing keys are unavailable.')\n }\n\n if (!isString(decoded.payload.jti)) {\n return issue('oidc_missing_jti', 'GitHub OIDC token is missing jti.')\n }\n\n const claims = toClaims(decoded.payload)\n\n if (!claims) {\n return issue('oidc_missing_claim', 'GitHub OIDC token is missing a required claim.')\n }\n\n if (claims.iss !== issuer) {\n return issue('oidc_invalid_issuer', 'GitHub OIDC token issuer is not allowed.')\n }\n\n if (!audienceMatches(claims.aud, config.audience)) {\n return issue('oidc_invalid_audience', 'GitHub OIDC token audience is not allowed.')\n }\n\n const maxSkewSeconds = config.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS\n const nowSeconds = now.getTime() / 1000\n\n if (claims.exp + maxSkewSeconds < nowSeconds) {\n return issue('oidc_expired', 'GitHub OIDC token has expired.')\n }\n\n if (claims.nbf !== undefined && claims.nbf - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token is not valid yet.')\n }\n\n if (claims.iat - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token was issued in the future.')\n }\n\n const trustedSources = config.trustedSources ?? []\n\n if (trustedSources.length === 0) {\n return issue(\n 'oidc_repository_not_allowed',\n 'GitHub OIDC auth requires a trusted GitHub owner.',\n )\n }\n\n const trustedSource = findTrustedSource({\n repository: claims.repository,\n repositoryOwner: claims.repository_owner,\n trustedSources,\n })\n\n if (!trustedSource) {\n const matchingOwner = trustedSources.find(\n (source) =>\n source.owner.toLowerCase() === claims.repository_owner.toLowerCase(),\n )\n\n if (matchingOwner) {\n return issue(\n 'oidc_repository_not_allowed',\n `GitHub OIDC token repository \"${claims.repository}\" is not trusted for owner \"${claims.repository_owner}\".`,\n )\n }\n\n return issue(\n 'oidc_owner_not_allowed',\n `GitHub OIDC token repository owner \"${claims.repository_owner}\" is not trusted.`,\n )\n }\n\n const repositoryName = getRepositoryName(claims.repository)\n\n if (!includesIfConfigured(config.allowedRefs, claims.ref)) {\n return issue(\n 'oidc_ref_not_allowed',\n `GitHub OIDC token ref \"${claims.ref}\" is not allowed for \"${repositoryName}\".`,\n )\n }\n\n const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref\n\n if (\n config.enforceWorkflowRefs === true &&\n (config.allowedWorkflowRefs?.length ?? 0) === 0\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'Advanced workflow security is enabled but no workflow refs are trusted.',\n )\n }\n\n if (\n config.enforceWorkflowRefs === true &&\n !includesIfConfigured(config.allowedWorkflowRefs, workflowRef)\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'GitHub OIDC token workflow ref is not allowed.',\n )\n }\n\n if (claims.event_name === 'pull_request' && config.allowPullRequests !== true) {\n return issue(\n 'oidc_pull_request_not_allowed',\n 'GitHub OIDC pull request events are not allowed.',\n )\n }\n\n return {\n ok: true,\n token: {\n claims,\n expiresAt: new Date(claims.exp * 1000),\n keyId: `github-oidc:${claims.repository}`,\n },\n }\n}\n"],"names":["createPublicKey","verify","DEFAULT_GITHUB_OIDC_ISSUER","DEFAULT_MAX_SKEW_SECONDS","fetchJwks","findJwkByKid","getGithubOidcJwksUrl","decodeJwt","isString","value","trim","isStringArray","Array","isArray","every","isNumber","Number","isFinite","getStringClaim","payload","claim","undefined","getNumberClaim","getAudienceClaim","aud","toClaims","exp","iat","iss","jti","ref","repository","repositoryOwner","sub","actor","environment","event_name","job_workflow_ref","nbf","repository_owner","sha","workflow","workflow_ref","issue","code","message","ok","includesIfConfigured","allowed","length","includes","audienceMatches","audience","expected","getRepositoryName","name","split","repositoryMatches","owner","normalized","toLowerCase","findTrustedSource","trustedSources","find","source","limitRepos","repositories","some","allowedRepository","verifyJwtSignature","jwk","signature","signingInput","publicKey","format","key","Buffer","from","verifyGitHubOidcToken","config","fetchJson","now","Date","token","decoded","header","alg","kid","issuer","jwksUrl","jwks","url","claims","maxSkewSeconds","nowSeconds","getTime","trustedSource","matchingOwner","repositoryName","allowedRefs","workflowRef","enforceWorkflowRefs","allowedWorkflowRefs","allowPullRequests","expiresAt","keyId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/security/githubOidc.ts"],"sourcesContent":["import {\n createPublicKey,\n type JsonWebKey,\n verify,\n} from 'node:crypto'\n\nimport type { FetchJson } from './jwks.js'\n\nimport {\n DEFAULT_GITHUB_OIDC_ISSUER,\n DEFAULT_MAX_SKEW_SECONDS,\n} from '../constants.js'\nimport {\n fetchJwks,\n findJwkByKid,\n getGithubOidcJwksUrl,\n} from './jwks.js'\nimport { decodeJwt } from './jwt.js'\n\nexport type GitHubOidcErrorCode =\n | 'oidc_expired'\n | 'oidc_invalid_audience'\n | 'oidc_invalid_issuer'\n | 'oidc_invalid_token'\n | 'oidc_jwks_unavailable'\n | 'oidc_missing_claim'\n | 'oidc_missing_jti'\n | 'oidc_not_yet_valid'\n | 'oidc_owner_not_allowed'\n | 'oidc_pull_request_not_allowed'\n | 'oidc_ref_not_allowed'\n | 'oidc_repository_not_allowed'\n | 'oidc_workflow_not_allowed'\n\nexport type GitHubOidcClaims = {\n actor?: string\n aud: string | string[]\n environment?: string\n event_name?: string\n exp: number\n iat: number\n iss: string\n job_workflow_ref?: string\n jti: string\n nbf?: number\n ref: string\n repository: string\n repository_owner: string\n sha?: string\n sub: string\n workflow?: string\n workflow_ref?: string\n}\n\nexport type GitHubOidcTrustedSource = {\n limitRepos?: boolean\n owner: string\n repositories?: string[]\n}\n\nexport type GitHubOidcVerifyConfig = {\n allowedRefs?: string[]\n allowedWorkflowRefs?: string[]\n allowPullRequests?: boolean\n audience: string\n enforceWorkflowRefs?: boolean\n issuer?: string\n jwksUrl?: string\n maxSkewSeconds?: number\n trustedSources: GitHubOidcTrustedSource[]\n}\n\nexport type VerifiedGitHubOidcToken = {\n claims: GitHubOidcClaims\n expiresAt: Date\n keyId: string\n}\n\nexport type VerifyGitHubOidcTokenResult =\n | {\n code: GitHubOidcErrorCode\n message: string\n ok: false\n }\n | {\n ok: true\n token: VerifiedGitHubOidcToken\n }\n\nconst isString = (value: unknown): value is string =>\n typeof value === 'string' && value.trim() !== ''\n\nconst isStringArray = (value: unknown): value is string[] =>\n Array.isArray(value) && value.every(isString)\n\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value)\n\nconst getStringClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): string | undefined => {\n const value = payload[claim]\n\n return isString(value) ? value : undefined\n}\n\nconst getNumberClaim = (\n payload: Record<string, unknown>,\n claim: string,\n): number | undefined => {\n const value = payload[claim]\n\n return isNumber(value) ? value : undefined\n}\n\nconst getAudienceClaim = (\n payload: Record<string, unknown>,\n): string | string[] | undefined => {\n const value = payload.aud\n\n if (isString(value) || isStringArray(value)) {\n return value\n }\n\n return undefined\n}\n\nconst toClaims = (\n payload: Record<string, unknown>,\n): GitHubOidcClaims | undefined => {\n const aud = getAudienceClaim(payload)\n const exp = getNumberClaim(payload, 'exp')\n const iat = getNumberClaim(payload, 'iat')\n const iss = getStringClaim(payload, 'iss')\n const jti = getStringClaim(payload, 'jti')\n const ref = getStringClaim(payload, 'ref')\n const repository = getStringClaim(payload, 'repository')\n const repositoryOwner = getStringClaim(payload, 'repository_owner')\n const sub = getStringClaim(payload, 'sub')\n\n if (\n !aud ||\n exp === undefined ||\n iat === undefined ||\n !iss ||\n !jti ||\n !ref ||\n !repository ||\n !repositoryOwner ||\n !sub\n ) {\n return undefined\n }\n\n return {\n actor: getStringClaim(payload, 'actor'),\n aud,\n environment: getStringClaim(payload, 'environment'),\n event_name: getStringClaim(payload, 'event_name'),\n exp,\n iat,\n iss,\n job_workflow_ref: getStringClaim(payload, 'job_workflow_ref'),\n jti,\n nbf: getNumberClaim(payload, 'nbf'),\n ref,\n repository,\n repository_owner: repositoryOwner,\n sha: getStringClaim(payload, 'sha'),\n sub,\n workflow: getStringClaim(payload, 'workflow'),\n workflow_ref: getStringClaim(payload, 'workflow_ref'),\n }\n}\n\nconst issue = (\n code: GitHubOidcErrorCode,\n message: string,\n): VerifyGitHubOidcTokenResult => ({\n code,\n message,\n ok: false,\n})\n\nconst includesIfConfigured = (\n allowed: string[] | undefined,\n value: string | undefined,\n): boolean => {\n if (!allowed || allowed.length === 0) {\n return true\n }\n\n return value !== undefined && allowed.includes(value)\n}\n\nconst audienceMatches = (\n audience: string | string[],\n expected: string,\n): boolean =>\n Array.isArray(audience) ? audience.includes(expected) : audience === expected\n\nconst getRepositoryName = (repository: string): string => {\n const [, name] = repository.split('/', 2)\n\n return name ?? repository\n}\n\nconst isReleaseTagRef = (claims: GitHubOidcClaims): boolean =>\n claims.event_name === 'release' && claims.ref.startsWith('refs/tags/')\n\nconst repositoryMatches = ({\n allowed,\n owner,\n repository,\n}: {\n allowed: string\n owner: string\n repository: string\n}): boolean => {\n const normalized = allowed.trim()\n\n if (!normalized) {\n return false\n }\n\n return normalized.includes('/')\n ? normalized.toLowerCase() === repository.toLowerCase()\n : `${owner}/${normalized}`.toLowerCase() === repository.toLowerCase()\n}\n\nconst findTrustedSource = ({\n repository,\n repositoryOwner,\n trustedSources,\n}: {\n repository: string\n repositoryOwner: string\n trustedSources: GitHubOidcTrustedSource[]\n}): GitHubOidcTrustedSource | undefined =>\n trustedSources.find((source) => {\n if (source.owner.toLowerCase() !== repositoryOwner.toLowerCase()) {\n return false\n }\n\n if (source.limitRepos !== true) {\n return true\n }\n\n return (source.repositories ?? []).some((allowedRepository) =>\n repositoryMatches({\n allowed: allowedRepository,\n owner: source.owner,\n repository,\n }),\n )\n })\n\nconst verifyJwtSignature = ({\n jwk,\n signature,\n signingInput,\n}: {\n jwk: Record<string, unknown>\n signature: Buffer\n signingInput: string\n}): boolean => {\n try {\n const publicKey = createPublicKey({\n format: 'jwk',\n key: jwk as JsonWebKey,\n })\n\n return verify(\n 'RSA-SHA256',\n Buffer.from(signingInput, 'utf8'),\n publicKey,\n signature,\n )\n } catch {\n return false\n }\n}\n\nexport const verifyGitHubOidcToken = async ({\n config,\n fetchJson,\n now = new Date(),\n token,\n}: {\n config: GitHubOidcVerifyConfig\n fetchJson?: FetchJson\n now?: Date\n token: string\n}): Promise<VerifyGitHubOidcTokenResult> => {\n const decoded = decodeJwt(token)\n\n if (!decoded) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is malformed.')\n }\n\n if (decoded.header.alg !== 'RS256') {\n return issue('oidc_invalid_token', 'GitHub OIDC token must use RS256.')\n }\n\n if (!isString(decoded.header.kid)) {\n return issue('oidc_invalid_token', 'GitHub OIDC token is missing kid.')\n }\n\n const issuer = config.issuer ?? DEFAULT_GITHUB_OIDC_ISSUER\n let jwksUrl: string\n\n try {\n jwksUrl = await getGithubOidcJwksUrl({\n fetchJson,\n issuer,\n jwksUrl: config.jwksUrl,\n })\n const jwks = await fetchJwks({\n fetchJson,\n now,\n url: jwksUrl,\n })\n const jwk = findJwkByKid({\n jwks,\n kid: decoded.header.kid,\n })\n\n if (\n !jwk ||\n !verifyJwtSignature({\n jwk,\n signature: decoded.signature,\n signingInput: decoded.signingInput,\n })\n ) {\n return issue('oidc_invalid_token', 'GitHub OIDC token signature is invalid.')\n }\n } catch {\n return issue('oidc_jwks_unavailable', 'GitHub OIDC signing keys are unavailable.')\n }\n\n if (!isString(decoded.payload.jti)) {\n return issue('oidc_missing_jti', 'GitHub OIDC token is missing jti.')\n }\n\n const claims = toClaims(decoded.payload)\n\n if (!claims) {\n return issue('oidc_missing_claim', 'GitHub OIDC token is missing a required claim.')\n }\n\n if (claims.iss !== issuer) {\n return issue('oidc_invalid_issuer', 'GitHub OIDC token issuer is not allowed.')\n }\n\n if (!audienceMatches(claims.aud, config.audience)) {\n return issue('oidc_invalid_audience', 'GitHub OIDC token audience is not allowed.')\n }\n\n const maxSkewSeconds = config.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS\n const nowSeconds = now.getTime() / 1000\n\n if (claims.exp + maxSkewSeconds < nowSeconds) {\n return issue('oidc_expired', 'GitHub OIDC token has expired.')\n }\n\n if (claims.nbf !== undefined && claims.nbf - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token is not valid yet.')\n }\n\n if (claims.iat - maxSkewSeconds > nowSeconds) {\n return issue('oidc_not_yet_valid', 'GitHub OIDC token was issued in the future.')\n }\n\n const trustedSources = config.trustedSources ?? []\n\n if (trustedSources.length === 0) {\n return issue(\n 'oidc_repository_not_allowed',\n 'GitHub OIDC auth requires a trusted GitHub owner.',\n )\n }\n\n const trustedSource = findTrustedSource({\n repository: claims.repository,\n repositoryOwner: claims.repository_owner,\n trustedSources,\n })\n\n if (!trustedSource) {\n const matchingOwner = trustedSources.find(\n (source) =>\n source.owner.toLowerCase() === claims.repository_owner.toLowerCase(),\n )\n\n if (matchingOwner) {\n return issue(\n 'oidc_repository_not_allowed',\n `GitHub OIDC token repository \"${claims.repository}\" is not trusted for owner \"${claims.repository_owner}\".`,\n )\n }\n\n return issue(\n 'oidc_owner_not_allowed',\n `GitHub OIDC token repository owner \"${claims.repository_owner}\" is not trusted.`,\n )\n }\n\n const repositoryName = getRepositoryName(claims.repository)\n\n if (!includesIfConfigured(config.allowedRefs, claims.ref) && !isReleaseTagRef(claims)) {\n return issue(\n 'oidc_ref_not_allowed',\n `GitHub OIDC token ref \"${claims.ref}\" is not allowed for \"${repositoryName}\".`,\n )\n }\n\n const workflowRef = claims.workflow_ref ?? claims.job_workflow_ref\n\n if (\n config.enforceWorkflowRefs === true &&\n (config.allowedWorkflowRefs?.length ?? 0) === 0\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'Advanced workflow security is enabled but no workflow refs are trusted.',\n )\n }\n\n if (\n config.enforceWorkflowRefs === true &&\n !includesIfConfigured(config.allowedWorkflowRefs, workflowRef)\n ) {\n return issue(\n 'oidc_workflow_not_allowed',\n 'GitHub OIDC token workflow ref is not allowed.',\n )\n }\n\n if (claims.event_name === 'pull_request' && config.allowPullRequests !== true) {\n return issue(\n 'oidc_pull_request_not_allowed',\n 'GitHub OIDC pull request events are not allowed.',\n )\n }\n\n return {\n ok: true,\n token: {\n claims,\n expiresAt: new Date(claims.exp * 1000),\n keyId: `github-oidc:${claims.repository}`,\n },\n }\n}\n"],"names":["createPublicKey","verify","DEFAULT_GITHUB_OIDC_ISSUER","DEFAULT_MAX_SKEW_SECONDS","fetchJwks","findJwkByKid","getGithubOidcJwksUrl","decodeJwt","isString","value","trim","isStringArray","Array","isArray","every","isNumber","Number","isFinite","getStringClaim","payload","claim","undefined","getNumberClaim","getAudienceClaim","aud","toClaims","exp","iat","iss","jti","ref","repository","repositoryOwner","sub","actor","environment","event_name","job_workflow_ref","nbf","repository_owner","sha","workflow","workflow_ref","issue","code","message","ok","includesIfConfigured","allowed","length","includes","audienceMatches","audience","expected","getRepositoryName","name","split","isReleaseTagRef","claims","startsWith","repositoryMatches","owner","normalized","toLowerCase","findTrustedSource","trustedSources","find","source","limitRepos","repositories","some","allowedRepository","verifyJwtSignature","jwk","signature","signingInput","publicKey","format","key","Buffer","from","verifyGitHubOidcToken","config","fetchJson","now","Date","token","decoded","header","alg","kid","issuer","jwksUrl","jwks","url","maxSkewSeconds","nowSeconds","getTime","trustedSource","matchingOwner","repositoryName","allowedRefs","workflowRef","enforceWorkflowRefs","allowedWorkflowRefs","allowPullRequests","expiresAt","keyId"],"mappings":"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"}
@@ -1,3 +1,8 @@
1
+ ---
2
+ name: payload-markdown-docs
3
+ description: Use this skill when maintaining Git-backed documentation for a project that uses `@valkyrianlabs/payload-markdown-docs`.
4
+ ---
5
+
1
6
  # Payload Markdown Docs Skill
2
7
 
3
8
  Use this skill when maintaining Git-backed documentation for a project that uses `@valkyrianlabs/payload-markdown-docs`.
@@ -11,8 +16,10 @@ The docs source lives in `{{docsRoot}}` unless the user says otherwise. Edit Mar
11
16
  - Use supported frontmatter only.
12
17
  - Keep internal docs links root-relative inside the docs set, such as `/getting-started/quick-start`.
13
18
  - Use `payload-markdown` directives only when they are supported.
19
+ - Keep formatting plain Markdown: supported frontmatter, one H1, clear H2/H3 sections, root-relative links, and supported directives.
14
20
  - Do not invent directives, frontmatter fields, CLI flags, sync modes, or runtime features.
15
21
  - Do not describe unsupported features as implemented.
22
+ - Do not add MDX, arbitrary YAML objects, inline frontmatter arrays, HTML widgets, or one Payload Page per Markdown file.
16
23
  - Run validation before finishing docs edits.
17
24
  - Treat sync and publishing as CMS/server-owned. The request may ask; Payload
18
25
  docs sets and plugin config decide.
@@ -39,6 +46,7 @@ Rules for `index.ai.yml`:
39
46
  - Do not show the manifest in human docs navigation.
40
47
  - Do not render the manifest as a normal docs page.
41
48
  - Use it as the canonical ordering source for the `.md` export.
49
+ - Remember that the `.md` export is served by a Next route handler, not by a generated Payload Page.
42
50
  - Avoid backend hand-sorting unless the project already has a docs sort field.
43
51
 
44
52
  Ordering source preference:
@@ -154,6 +162,7 @@ Sync writes require `sync.allowWrites: true`. Publishing additionally requires `
154
162
  ## References
155
163
 
156
164
  - `reference/payload-markdown-directives.md`
165
+ - `reference/formatting.md`
157
166
  - `reference/frontmatter.md`
158
167
  - `reference/workflow.md`
159
168
  - `reference/sync.md`
@@ -0,0 +1,24 @@
1
+ # Formatting
2
+
3
+ Keep docs as plain Markdown that validates before sync.
4
+
5
+ Expected shape:
6
+
7
+ - supported frontmatter at the top
8
+ - one H1 for the page title
9
+ - H2 and H3 sections for structure
10
+ - root-relative internal links, such as `/workflow/publishing`
11
+ - supported `payload-markdown` directives only
12
+ - `index.ai.yml` updated when pages are added, moved, renamed, or removed
13
+
14
+ Do not add:
15
+
16
+ - `.mdx` files
17
+ - arbitrary YAML objects
18
+ - inline frontmatter arrays
19
+ - invented directive names or props
20
+ - HTML scripts, iframes, or client-side widgets
21
+ - one Payload Page per Markdown file
22
+ - production docs domains for internal links
23
+
24
+ Prefer short, concrete sections over long pages with deeply nested headings.
@@ -35,5 +35,7 @@ Rules:
35
35
  - `tags` and `redirectFrom` must use list item syntax.
36
36
  - `slug` may contain letters, numbers, and hyphens.
37
37
  - Avoid arbitrary nested YAML objects.
38
+ - Avoid inline arrays such as `tags: [getting-started]`.
38
39
  - Avoid unsupported fields unless the user accepts validation warnings.
39
40
  - Explicit `title` is preferred even though title fallback exists.
41
+ - Use `slug` only to override the final route segment; move files to change route hierarchy.
@@ -34,3 +34,11 @@ Do not hardcode production docs domains for internal navigation.
34
34
  ## Route Adapter
35
35
 
36
36
  The `/next` export can resolve docs routes and let an app fall back to normal Pages rendering when no docs route matches. It does not mutate Pages.
37
+
38
+ ## Raw Markdown
39
+
40
+ The AI-facing `.md` export is served by a Next route handler. It is not a
41
+ generated Payload Page and cannot be returned from a `page.tsx` catch-all.
42
+
43
+ Use `createPayloadMarkdownDocsMarkdownResponse` at the output path from
44
+ `index.ai.yml`, or place AI exports in a dedicated namespace such as `/ai`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valkyrianlabs/payload-markdown-docs",
3
- "version": "0.5.3",
3
+ "version": "0.7.1",
4
4
  "description": "Git-backed Markdown documentation sync for Payload CMS, powered by payload-markdown.",
5
5
  "bin": {
6
6
  "payload-markdown-docs": "./dist/cli/index.js"
@@ -86,10 +86,10 @@
86
86
  "copyfiles": "2.4.1",
87
87
  "cross-env": "^7.0.3",
88
88
  "eslint": "^9.39.4",
89
- "eslint-config-next": "16.2.3",
89
+ "eslint-config-next": "latest",
90
90
  "graphql": "^16.14.0",
91
91
  "mongodb-memory-server": "10.1.4",
92
- "next": "16.2.3",
92
+ "next": "latest",
93
93
  "open": "^10.2.0",
94
94
  "payload": "latest",
95
95
  "postcss": "^8.5.14",