@eventcatalog/core 3.14.6 → 3.15.0-beta.0

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.
Files changed (35) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-6NRRWRLT.js → chunk-4JTLFCQ7.js} +1 -1
  6. package/dist/{chunk-TERDXO46.js → chunk-7RPTQBIY.js} +1 -1
  7. package/dist/{chunk-LNIPDPVB.js → chunk-CVH4LGYA.js} +1 -1
  8. package/dist/{chunk-NYVQSLA5.js → chunk-FRILQLHV.js} +1 -1
  9. package/dist/{chunk-KWCAGR52.js → chunk-VUARMJ2E.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +5 -5
  14. package/dist/generate.cjs +1 -1
  15. package/dist/generate.js +3 -3
  16. package/dist/utils/cli-logger.cjs +1 -1
  17. package/dist/utils/cli-logger.js +2 -2
  18. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +56 -10
  19. package/eventcatalog/src/content.config.ts +53 -0
  20. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/_index.data.ts +85 -0
  21. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +195 -0
  22. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/_index.data.ts +86 -0
  23. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +195 -0
  24. package/eventcatalog/src/stores/sidebar-store/builders/container.ts +9 -0
  25. package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +15 -9
  26. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +9 -0
  27. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +13 -4
  28. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +9 -0
  29. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +9 -0
  30. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +82 -0
  31. package/eventcatalog/src/stores/sidebar-store/state.ts +27 -2
  32. package/eventcatalog/src/utils/collections/data-products.ts +6 -2
  33. package/eventcatalog/src/utils/collections/resource-docs.ts +601 -0
  34. package/eventcatalog/src/utils/feature.ts +1 -0
  35. package/package.json +3 -3
@@ -0,0 +1,86 @@
1
+ import { isSSR, isResourceDocsEnabled } from '@utils/feature';
2
+ import { HybridPage } from '@utils/page-loaders/hybrid-page';
3
+ import { getResourceDocs, getResourceDocsForResource, type ResourceCollection } from '@utils/collections/resource-docs';
4
+
5
+ const supportedResourceCollections = new Set<ResourceCollection>([
6
+ 'domains',
7
+ 'services',
8
+ 'events',
9
+ 'commands',
10
+ 'queries',
11
+ 'flows',
12
+ 'containers',
13
+ 'channels',
14
+ 'entities',
15
+ 'data-products',
16
+ ]);
17
+
18
+ export class Page extends HybridPage {
19
+ static async getStaticPaths() {
20
+ if (isSSR() || !isResourceDocsEnabled()) {
21
+ return [];
22
+ }
23
+
24
+ const docs = await getResourceDocs();
25
+ const latestDocs = docs.filter((doc) => doc.data.version === doc.data.latestVersion);
26
+
27
+ return latestDocs.map((doc) => ({
28
+ params: {
29
+ type: doc.data.resourceCollection,
30
+ id: doc.data.resourceId,
31
+ version: doc.data.resourceVersion,
32
+ docType: doc.data.type,
33
+ docId: doc.data.id,
34
+ },
35
+ props: {},
36
+ }));
37
+ }
38
+
39
+ protected static async fetchData(params: any) {
40
+ if (!isResourceDocsEnabled()) {
41
+ return null;
42
+ }
43
+
44
+ const decodeParam = (value: string) => {
45
+ try {
46
+ return decodeURIComponent(value);
47
+ } catch {
48
+ return value;
49
+ }
50
+ };
51
+
52
+ const type = decodeParam(params.type);
53
+ const id = decodeParam(params.id);
54
+ const version = decodeParam(params.version);
55
+ const docType = decodeParam(params.docType);
56
+ const docId = decodeParam(params.docId);
57
+ if (!type || !id || !version || !docType || !docId) {
58
+ return null;
59
+ }
60
+
61
+ if (!supportedResourceCollections.has(type as ResourceCollection)) {
62
+ return null;
63
+ }
64
+
65
+ const docsForResource = await getResourceDocsForResource(type as ResourceCollection, id, version);
66
+ const doc = docsForResource.find(
67
+ (resourceDoc) =>
68
+ resourceDoc.data.type === docType &&
69
+ resourceDoc.data.id === docId &&
70
+ resourceDoc.data.version === resourceDoc.data.latestVersion
71
+ );
72
+
73
+ if (!doc) {
74
+ return null;
75
+ }
76
+
77
+ return doc;
78
+ }
79
+
80
+ protected static createNotFoundResponse(): Response {
81
+ return new Response(null, {
82
+ status: 404,
83
+ statusText: 'Resource documentation not found',
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,195 @@
1
+ ---
2
+ import { render } from 'astro:content';
3
+
4
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
5
+ import components from '@components/MDX/components';
6
+ import { buildUrl } from '@utils/url-builder';
7
+ import { AlignLeftIcon, HistoryIcon } from 'lucide-react';
8
+ import { isResourceDocsEnabled } from '@utils/feature';
9
+ import { getIcon } from '@utils/badges';
10
+ import { collectionToResourceMap } from '@utils/collections/util';
11
+
12
+ import { Page } from './_index.data';
13
+
14
+ export const prerender = Page.prerender;
15
+ export const getStaticPaths = Page.getStaticPaths;
16
+
17
+ if (!isResourceDocsEnabled()) {
18
+ return Astro.redirect(buildUrl('/docs/custom/feature'));
19
+ }
20
+
21
+ const props = await Page.getData(Astro);
22
+ const { Content, headings } = await render(props);
23
+
24
+ const title = props.data.title || props.data.id;
25
+ const pageTitle = `${title} | ${props.data.resourceId}`;
26
+ const versions = props.data.versions || [props.data.version];
27
+ const badges = (props.data.badges || []).map((badge: any) => ({
28
+ ...badge,
29
+ iconComponent: badge.icon ? getIcon(badge.icon) : null,
30
+ }));
31
+
32
+ const docsBasePath = `/docs/${props.data.resourceCollection}/${props.data.resourceId}/${props.data.resourceVersion}/${encodeURIComponent(props.data.type)}/${encodeURIComponent(props.data.id)}`;
33
+ const singularResourceName =
34
+ collectionToResourceMap[props.data.resourceCollection as keyof typeof collectionToResourceMap] ??
35
+ props.data.resourceCollection.slice(0, props.data.resourceCollection.length - 1);
36
+
37
+ const pagefindAttributes =
38
+ props.data.version === props.data.latestVersion
39
+ ? {
40
+ 'data-pagefind-body': '',
41
+ 'data-pagefind-meta': `title:${pageTitle}`,
42
+ }
43
+ : {};
44
+ ---
45
+
46
+ <VerticalSideBarLayout title={pageTitle} description={props.data.summary}>
47
+ <main class="flex docs-layout h-full bg-[rgb(var(--ec-page-bg))]" {...pagefindAttributes}>
48
+ <div class="flex docs-layout w-full pl-16">
49
+ <div class="w-full lg:mr-2 pr-8 overflow-y-auto py-8 bg-[rgb(var(--ec-page-bg))]">
50
+ <div class="border-b border-[rgb(var(--ec-page-border))] pb-4">
51
+ <p class="text-xs uppercase tracking-wide text-[rgb(var(--ec-page-text-muted))]">
52
+ <a
53
+ href={buildUrl(`/docs/${props.data.resourceCollection}/${props.data.resourceId}/${props.data.resourceVersion}`)}
54
+ class="hover:underline"
55
+ >
56
+ {singularResourceName}: {props.data.resourceId}
57
+ </a>
58
+ </p>
59
+ <div class="flex items-center gap-2 pt-1">
60
+ <h2 id="doc-page-header" class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{title}</h2>
61
+ <span
62
+ class="text-xs rounded-md px-2 py-1 bg-[rgb(var(--ec-content-hover))] text-[rgb(var(--ec-page-text-muted))] border border-[rgb(var(--ec-page-border))]"
63
+ >
64
+ v{props.data.version}
65
+ {props.data.version === props.data.latestVersion && ' (latest)'}
66
+ </span>
67
+ </div>
68
+ {
69
+ props.data.summary && (
70
+ <p class="text-lg pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{props.data.summary}</p>
71
+ )
72
+ }
73
+ {
74
+ badges.length > 0 && (
75
+ <div class="flex flex-wrap gap-3 pt-4">
76
+ {badges.map((badge: any) => (
77
+ <span
78
+ class={`
79
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
80
+ bg-[rgb(var(--ec-content-hover))] border border-[rgb(var(--ec-page-border))]
81
+ text-[rgb(var(--ec-page-text))]
82
+ `}
83
+ >
84
+ {badge.iconComponent && (
85
+ <badge.iconComponent className="w-4 h-4 flex-shrink-0 text-[rgb(var(--ec-icon-color))]" />
86
+ )}
87
+ <span>{badge.content}</span>
88
+ </span>
89
+ ))}
90
+ </div>
91
+ )
92
+ }
93
+ </div>
94
+ <div data-pagefind-ignore>
95
+ {
96
+ props.data.version !== props.data.latestVersion && (
97
+ <div class="rounded-md bg-[rgb(var(--ec-accent-subtle))] p-4 not-prose my-4">
98
+ <div class="flex">
99
+ <div class="flex-shrink-0">
100
+ <svg class="h-5 w-5 text-[rgb(var(--ec-accent))]" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
101
+ <path
102
+ fill-rule="evenodd"
103
+ d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
104
+ clip-rule="evenodd"
105
+ />
106
+ </svg>
107
+ </div>
108
+ <div class="ml-3">
109
+ <h3 class="text-sm font-medium text-[rgb(var(--ec-accent-text))]">New version found</h3>
110
+ <div class="mt-2 text-sm text-[rgb(var(--ec-accent-text))]">
111
+ <p>
112
+ You are looking at a previous version of the {singularResourceName} doc <strong>{title}</strong>.{' '}
113
+ <a
114
+ class="underline hover:text-primary block pt-2"
115
+ href={buildUrl(`${docsBasePath}/${props.data.latestVersion}`)}
116
+ >
117
+ The latest version of this doc is <span>v{props.data.latestVersion}</span> &rarr;
118
+ </a>
119
+ </p>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ )
125
+ }
126
+ </div>
127
+ <div class="prose py-8 max-w-none">
128
+ <Content components={components(props)} />
129
+ </div>
130
+ </div>
131
+ <aside class="hidden lg:block sticky top-0 pb-10 w-80 overflow-y-auto border-l border-[rgb(var(--ec-page-border))] py-2">
132
+ <div class="sticky top-28 left-0 h-full overflow-y-auto px-6 py-4">
133
+ <h3 class="text-sm text-[rgb(var(--ec-page-text))] font-semibold capitalize flex items-center gap-2">
134
+ <AlignLeftIcon className="w-4 h-4" />
135
+ On this page
136
+ </h3>
137
+ <nav class="space-y-1 text-sm py-4">
138
+ {
139
+ headings.map((heading) => {
140
+ if (heading.depth > 3) {
141
+ return null;
142
+ }
143
+ return (
144
+ <a
145
+ href={`#${heading.slug}`}
146
+ class={`block text-[12px] py-0.5 ${heading.depth === 1 ? 'font-light' : ''} text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]`}
147
+ style={`padding-left: ${(heading.depth - 1) * 8}px`}
148
+ >
149
+ {heading.text}
150
+ </a>
151
+ );
152
+ })
153
+ }
154
+ </nav>
155
+ <div class="space-y-2 pb-4">
156
+ <span class="text-xs text-[rgb(var(--ec-page-text))] font-semibold capitalize">Versions ({versions.length})</span>
157
+ <ul role="list" class="space-y-2">
158
+ {
159
+ versions.map((version: string) => (
160
+ <li class="version-item rounded-md px-1 group w-full">
161
+ <a class="flex items-center space-x-2" href={buildUrl(`${docsBasePath}/${encodeURIComponent(version)}`)}>
162
+ <HistoryIcon
163
+ className="h-4 w-4 text-[rgb(var(--ec-page-text-muted))] group-hover:text-[rgb(var(--ec-accent-text))]"
164
+ strokeWidth={1}
165
+ />
166
+ <span
167
+ class={`font-light text-xs text-[rgb(var(--ec-page-text))] group-hover:text-[rgb(var(--ec-accent-text))] ${
168
+ version === props.data.version ? 'underline' : ''
169
+ }`}
170
+ >
171
+ {version === props.data.latestVersion ? `v${version} (latest)` : `v${version}`}
172
+ </span>
173
+ </a>
174
+ </li>
175
+ ))
176
+ }
177
+ </ul>
178
+ <div class="border-b border-[rgb(var(--ec-page-border))] pt-2"></div>
179
+ </div>
180
+ </div>
181
+ </aside>
182
+ </div>
183
+ </main>
184
+ </VerticalSideBarLayout>
185
+
186
+ <style is:global>
187
+ .docs-layout .prose {
188
+ max-width: none;
189
+ overflow: auto;
190
+ }
191
+
192
+ .version-item:hover {
193
+ background: linear-gradient(to left, rgb(var(--ec-accent-gradient-from)), rgb(var(--ec-accent-gradient-to)));
194
+ }
195
+ </style>
@@ -8,6 +8,7 @@ import {
8
8
  buildRepositorySection,
9
9
  buildAttachmentsSection,
10
10
  buildDiagramNavItems,
11
+ buildResourceDocsSection,
11
12
  } from './shared';
12
13
  import { isVisualiserEnabled } from '@utils/feature';
13
14
 
@@ -42,6 +43,13 @@ export const buildContainerNode = (
42
43
  const hasAttachments = container.data.attachments && container.data.attachments.length > 0;
43
44
 
44
45
  const renderRepository = container.data.repository && shouldRenderSideBarSection(container, 'repository');
46
+ const docsSection = buildResourceDocsSection(
47
+ 'containers',
48
+ container.data.id,
49
+ container.data.version,
50
+ context.resourceDocs,
51
+ context.resourceDocCategories
52
+ );
45
53
 
46
54
  // Diagrams
47
55
  const containerDiagrams = container.data.diagrams || [];
@@ -60,6 +68,7 @@ export const buildContainerNode = (
60
68
  href: buildUrl(`/docs/containers/${container.data.id}/${container.data.version}`),
61
69
  },
62
70
  ]),
71
+ docsSection,
63
72
  renderVisualiser && {
64
73
  type: 'group',
65
74
  title: 'Architecture',
@@ -1,19 +1,17 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
2
  import { buildUrl } from '@utils/url-builder';
3
- import type { NavNode, ChildRef } from './shared';
4
- import { buildQuickReferenceSection, buildOwnersSection, shouldRenderSideBarSection } from './shared';
3
+ import type { NavNode, ChildRef, ResourceGroupContext } from './shared';
4
+ import { buildQuickReferenceSection, buildOwnersSection, shouldRenderSideBarSection, buildResourceDocsSection } from './shared';
5
5
  import { isVisualiserEnabled } from '@utils/feature';
6
6
  import { getItemsFromCollectionByIdAndSemverOrLatest, sortVersioned } from '@utils/collections/util';
7
7
  import { getSchemaFormatFromURL } from '@utils/collections/schemas';
8
8
 
9
- interface DataProductContext {
10
- events: CollectionEntry<'events'>[];
11
- commands: CollectionEntry<'commands'>[];
12
- queries: CollectionEntry<'queries'>[];
13
- services: CollectionEntry<'services'>[];
14
- containers: CollectionEntry<'containers'>[];
9
+ type DataProductContext = Pick<
10
+ ResourceGroupContext,
11
+ 'events' | 'commands' | 'queries' | 'services' | 'containers' | 'resourceDocs' | 'resourceDocCategories'
12
+ > & {
15
13
  channels: CollectionEntry<'channels'>[];
16
- }
14
+ };
17
15
 
18
16
  // Get highest version from matched items (semver ranges may return multiple matches)
19
17
  const getHighestVersion = <T extends { data: { version: string } }>(items: T[]): T | undefined => {
@@ -68,6 +66,13 @@ export const buildDataProductNode = (
68
66
 
69
67
  const renderVisualiser = isVisualiserEnabled();
70
68
  const renderOwners = owners.length > 0 && shouldRenderSideBarSection(dataProduct, 'owners');
69
+ const docsSection = buildResourceDocsSection(
70
+ 'data-products',
71
+ dataProduct.data.id,
72
+ dataProduct.data.version,
73
+ context.resourceDocs,
74
+ context.resourceDocCategories
75
+ );
71
76
 
72
77
  // Resolve inputs and outputs to their proper sidebar references
73
78
  const resolvedInputs = inputs.map((input) => resolvePointerToRef(input, context)).filter(Boolean) as string[];
@@ -94,6 +99,7 @@ export const buildDataProductNode = (
94
99
  buildQuickReferenceSection([
95
100
  { title: 'Overview', href: buildUrl(`/docs/data-products/${dataProduct.data.id}/${dataProduct.data.version}`) },
96
101
  ]),
102
+ docsSection,
97
103
  renderVisualiser && {
98
104
  type: 'group',
99
105
  title: 'Architecture',
@@ -9,6 +9,7 @@ import {
9
9
  buildRepositorySection,
10
10
  buildAttachmentsSection,
11
11
  buildDiagramNavItems,
12
+ buildResourceDocsSection,
12
13
  } from './shared';
13
14
  import { isVisualiserEnabled } from '@utils/feature';
14
15
  import { pluralizeMessageType } from '@utils/collections/messages';
@@ -41,6 +42,13 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
41
42
  const hasAttachments = domain.data.attachments && domain.data.attachments.length > 0;
42
43
 
43
44
  const renderRepository = domain.data.repository && shouldRenderSideBarSection(domain, 'repository');
45
+ const docsSection = buildResourceDocsSection(
46
+ 'domains',
47
+ domain.data.id,
48
+ domain.data.version,
49
+ context.resourceDocs,
50
+ context.resourceDocCategories
51
+ );
44
52
 
45
53
  // Domain-level messages (sends/receives)
46
54
  const sendsMessages = domain.data.sends || [];
@@ -70,6 +78,7 @@ export const buildDomainNode = (domain: CollectionEntry<'domains'>, owners: any[
70
78
  { title: 'Overview', href: buildUrl(`/docs/domains/${domain.data.id}/${domain.data.version}`) },
71
79
  renderUbiquitousLanguage && { title: 'Ubiquitous Language', href: buildUrl(`/docs/domains/${domain.data.id}/language`) },
72
80
  ]),
81
+ docsSection,
73
82
  {
74
83
  type: 'group',
75
84
  title: 'Architecture',
@@ -1,9 +1,17 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
2
  import { buildUrl } from '@utils/url-builder';
3
- import type { NavNode, ChildRef } from './shared';
4
- import { buildQuickReferenceSection } from './shared';
3
+ import type { NavNode, ChildRef, ResourceGroupContext } from './shared';
4
+ import { buildQuickReferenceSection, buildResourceDocsSection } from './shared';
5
+
6
+ export const buildFlowNode = (flow: CollectionEntry<'flows'>, context: ResourceGroupContext): NavNode => {
7
+ const docsSection = buildResourceDocsSection(
8
+ 'flows',
9
+ flow.data.id,
10
+ flow.data.version,
11
+ context.resourceDocs,
12
+ context.resourceDocCategories
13
+ );
5
14
 
6
- export const buildFlowNode = (flow: CollectionEntry<'flows'>): NavNode => {
7
15
  return {
8
16
  type: 'item',
9
17
  title: flow.data.name,
@@ -12,6 +20,7 @@ export const buildFlowNode = (flow: CollectionEntry<'flows'>): NavNode => {
12
20
  summary: flow.data.summary,
13
21
  pages: [
14
22
  buildQuickReferenceSection([{ title: 'Overview', href: buildUrl(`/docs/flows/${flow.data.id}/${flow.data.version}`) }]),
23
+ docsSection,
15
24
  {
16
25
  type: 'group',
17
26
  title: 'Architecture',
@@ -24,6 +33,6 @@ export const buildFlowNode = (flow: CollectionEntry<'flows'>): NavNode => {
24
33
  },
25
34
  ].filter(Boolean) as ChildRef[],
26
35
  },
27
- ],
36
+ ].filter(Boolean) as ChildRef[],
28
37
  };
29
38
  };
@@ -9,6 +9,7 @@ import {
9
9
  buildRepositorySection,
10
10
  buildAttachmentsSection,
11
11
  buildDiagramNavItems,
12
+ buildResourceDocsSection,
12
13
  } from './shared';
13
14
  import { isVisualiserEnabled } from '@utils/feature';
14
15
 
@@ -35,6 +36,13 @@ export const buildMessageNode = (
35
36
 
36
37
  const hasSchema = message.data.schemaPath !== undefined;
37
38
  const renderVisualiser = isVisualiserEnabled();
39
+ const docsSection = buildResourceDocsSection(
40
+ collection as 'events' | 'commands' | 'queries',
41
+ message.data.id,
42
+ message.data.version,
43
+ context.resourceDocs,
44
+ context.resourceDocCategories
45
+ );
38
46
 
39
47
  const hasAttachments = message.data.attachments && message.data.attachments.length > 0;
40
48
 
@@ -57,6 +65,7 @@ export const buildMessageNode = (
57
65
  href: buildUrl(`/docs/${collection}/${message.data.id}/${message.data.version}`),
58
66
  },
59
67
  ]),
68
+ docsSection,
60
69
  renderVisualiser && {
61
70
  type: 'group',
62
71
  title: 'Architecture',
@@ -11,6 +11,7 @@ import {
11
11
  buildRepositorySection,
12
12
  buildAttachmentsSection,
13
13
  buildDiagramNavItems,
14
+ buildResourceDocsSection,
14
15
  } from './shared';
15
16
  import { isVisualiserEnabled } from '@utils/feature';
16
17
  import { pluralizeMessageType } from '@utils/collections/messages';
@@ -44,6 +45,13 @@ export const buildServiceNode = (service: CollectionEntry<'services'>, owners: a
44
45
  const renderEntities = serviceEntities.length > 0 && shouldRenderSideBarSection(service, 'entities');
45
46
  const renderOwners = owners.length > 0 && shouldRenderSideBarSection(service, 'owners');
46
47
  const renderRepository = service.data.repository && shouldRenderSideBarSection(service, 'repository');
48
+ const docsSection = buildResourceDocsSection(
49
+ 'services',
50
+ service.data.id,
51
+ service.data.version,
52
+ context.resourceDocs,
53
+ context.resourceDocCategories
54
+ );
47
55
 
48
56
  // Diagrams
49
57
  const serviceDiagrams = service.data.diagrams || [];
@@ -59,6 +67,7 @@ export const buildServiceNode = (service: CollectionEntry<'services'>, owners: a
59
67
  buildQuickReferenceSection([
60
68
  { title: 'Overview', href: buildUrl(`/docs/services/${service.data.id}/${service.data.version}`) },
61
69
  ]),
70
+ docsSection,
62
71
  {
63
72
  type: 'group',
64
73
  title: 'Architecture',
@@ -2,6 +2,12 @@ import type { ResourceGroup } from '@eventcatalog/sdk';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import { getLatestVersionInCollectionById } from '@utils/collections/util';
4
4
  import { buildUrl } from '@utils/url-builder';
5
+ import {
6
+ getGroupedResourceDocsByType,
7
+ type ResourceCollection,
8
+ type ResourceDocEntry,
9
+ type ResourceDocCategoryEntry,
10
+ } from '@utils/collections/resource-docs';
5
11
 
6
12
  /**
7
13
  * A child reference can be:
@@ -17,6 +23,7 @@ export type NavNode = {
17
23
  type: 'group' | 'item';
18
24
  title: string;
19
25
  icon?: string; // Lucide icon name
26
+ subtle?: boolean; // Render lightweight styling for nested subgroup headers
20
27
  leftIcon?: string; // Path to SVG icon shown on the left of the label
21
28
  href?: string; // URL (for leaf items)
22
29
  external?: boolean; // If true, the item will open in a new tab
@@ -55,6 +62,8 @@ export type ResourceGroupContext = {
55
62
  flows: CollectionEntry<'flows'>[];
56
63
  containers: CollectionEntry<'containers'>[];
57
64
  diagrams: CollectionEntry<'diagrams'>[];
65
+ resourceDocs: ResourceDocEntry[];
66
+ resourceDocCategories: ResourceDocCategoryEntry[];
58
67
  };
59
68
 
60
69
  export const buildQuickReferenceSection = (items: { title: string; href: string }[]): NavNode => ({
@@ -112,6 +121,79 @@ export const buildAttachmentsSection = (attachments: any[]): NavNode | null => {
112
121
  })),
113
122
  };
114
123
  };
124
+
125
+ export const buildResourceDocsSection = (
126
+ collection: ResourceCollection,
127
+ id: string,
128
+ version: string,
129
+ resourceDocs: ResourceDocEntry[],
130
+ resourceDocCategories: ResourceDocCategoryEntry[]
131
+ ): NavNode | null => {
132
+ const docsForResource = resourceDocs.filter(
133
+ (doc) => doc.data.resourceCollection === collection && doc.data.resourceId === id && doc.data.resourceVersion === version
134
+ );
135
+
136
+ if (docsForResource.length === 0) {
137
+ return null;
138
+ }
139
+
140
+ const categoriesForResource = resourceDocCategories.filter(
141
+ (category) =>
142
+ category.data.resourceCollection === collection &&
143
+ category.data.resourceId === id &&
144
+ category.data.resourceVersion === version
145
+ );
146
+
147
+ const groupedDocs = getGroupedResourceDocsByType(docsForResource, {
148
+ latestOnly: true,
149
+ categories: categoriesForResource,
150
+ });
151
+
152
+ if (groupedDocs.length === 0) {
153
+ return null;
154
+ }
155
+
156
+ const typeLabelMap: Record<string, string> = {
157
+ adrs: 'ADR',
158
+ runbooks: 'Runbook',
159
+ contracts: 'Contract',
160
+ troubleshooting: 'Troubleshooting',
161
+ guides: 'Guide',
162
+ };
163
+
164
+ const toTypeLabel = (value: string) => {
165
+ if (typeLabelMap[value]) {
166
+ return typeLabelMap[value];
167
+ }
168
+
169
+ const normalized = value
170
+ .split(/[-_\s]+/)
171
+ .filter(Boolean)
172
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
173
+ .join(' ');
174
+
175
+ return normalized || 'Doc';
176
+ };
177
+
178
+ return {
179
+ type: 'group',
180
+ title: 'Documentation',
181
+ icon: 'BookText',
182
+ pages: groupedDocs.map((group) => ({
183
+ type: 'group',
184
+ title: group.label || toTypeLabel(group.type),
185
+ subtle: true,
186
+ pages: group.docs.map((doc) => ({
187
+ type: 'item',
188
+ title: doc.data.title || doc.data.id,
189
+ href: buildUrl(
190
+ `/docs/${collection}/${id}/${version}/${encodeURIComponent(doc.data.type)}/${encodeURIComponent(doc.data.id)}`
191
+ ),
192
+ })),
193
+ })),
194
+ };
195
+ };
196
+
115
197
  export const buildResourceGroupSections = (resourceGroups: ResourceGroup[], context: ResourceGroupContext) => {
116
198
  return resourceGroups.map((resourceGroup) => buildResourceGroupSection(resourceGroup, context));
117
199
  };
@@ -8,6 +8,7 @@ import { getUsers } from '@utils/collections/users';
8
8
  import { getTeams } from '@utils/collections/teams';
9
9
  import { getDiagrams } from '@utils/collections/diagrams';
10
10
  import { getDataProducts } from '@utils/collections/data-products';
11
+ import { getResourceDocCategories, getResourceDocs } from '@utils/collections/resource-docs';
11
12
  import { buildUrl } from '@utils/url-builder';
12
13
  import type { NavigationData, NavNode, ChildRef } from './builders/shared';
13
14
  import { buildDomainNode } from './builders/domain';
@@ -19,6 +20,7 @@ import { buildDataProductNode } from './builders/data-product';
19
20
  import config from '@config';
20
21
  import { getDesigns } from '@utils/collections/designs';
21
22
  import { getChannels } from '@utils/collections/channels';
23
+ import { buildQuickReferenceSection, buildResourceDocsSection } from './builders/shared';
22
24
 
23
25
  export type { NavigationData, NavNode, ChildRef };
24
26
 
@@ -45,6 +47,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
45
47
  channels,
46
48
  diagrams,
47
49
  dataProducts,
50
+ resourceDocs,
51
+ resourceDocCategories,
48
52
  ] = await Promise.all([
49
53
  getDomains({ getAllVersions: false, includeServicesInSubdomains: false }),
50
54
  getServices({ getAllVersions: false }),
@@ -57,6 +61,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
57
61
  getChannels({ getAllVersions: false }),
58
62
  getDiagrams({ getAllVersions: false }),
59
63
  getDataProducts({ getAllVersions: false }),
64
+ getResourceDocs(),
65
+ getResourceDocCategories(),
60
66
  ]);
61
67
 
62
68
  // Calculate derived lists to avoid extra fetches
@@ -75,6 +81,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
75
81
  containers,
76
82
  diagrams,
77
83
  dataProducts,
84
+ resourceDocs,
85
+ resourceDocCategories,
78
86
  };
79
87
 
80
88
  // Process all domains with their owners first (async)
@@ -122,7 +130,7 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
122
130
 
123
131
  const flowNodes = flows.reduce(
124
132
  (acc, flow) => {
125
- acc[`flow:${flow.data.id}:${flow.data.version}`] = buildFlowNode(flow);
133
+ acc[`flow:${flow.data.id}:${flow.data.version}`] = buildFlowNode(flow, context);
126
134
  return acc;
127
135
  },
128
136
  {} as Record<string, NavNode>
@@ -196,6 +204,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
196
204
  services,
197
205
  containers,
198
206
  channels,
207
+ resourceDocs,
208
+ resourceDocCategories,
199
209
  };
200
210
 
201
211
  const dataProductNodes = dataProductWithOwners.reduce(
@@ -238,12 +248,27 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
238
248
  const channelNodes = channels.reduce(
239
249
  (acc, channel) => {
240
250
  const versionedKey = `channel:${channel.data.id}:${channel.data.version}`;
251
+ const docsSection = buildResourceDocsSection(
252
+ 'channels',
253
+ channel.data.id,
254
+ channel.data.version,
255
+ resourceDocs,
256
+ resourceDocCategories
257
+ );
241
258
  acc[versionedKey] = {
242
259
  type: 'item',
243
260
  title: channel.data.name,
244
261
  badge: 'Channel',
245
262
  summary: channel.data.summary,
246
- href: buildUrl(`/docs/${channel.collection}/${channel.data.id}/${channel.data.version}`),
263
+ pages: [
264
+ buildQuickReferenceSection([
265
+ {
266
+ title: 'Overview',
267
+ href: buildUrl(`/docs/${channel.collection}/${channel.data.id}/${channel.data.version}`),
268
+ },
269
+ ]),
270
+ docsSection,
271
+ ].filter(Boolean) as ChildRef[],
247
272
  };
248
273
 
249
274
  if (channel.data.latestVersion === channel.data.version) {