Skip to content

Commit 55972fa

Browse files
authored
Implement Markdown reference renderer (#19)
* Implement Markdown reference renderer * Add missing whitespace to articles * Run `npm audit fix` * Refactor Index component to improve navigation structure and rendering of reference items * Add scrolling and focus behavior to navbar * Fix bug with page highlight * Update sample Markdown files * Update YAML validation * Update schemas * Remove content that will now be served from `documentdb/docs` * Update documentation landing page * Update gitignore * Update package versions with `npm audit` * Add content compilation logic script * Update Markdown renderer for external files * Update `articleService` to transform hyperlink files * Update `referenceService` to serve descriptions from Markdown partial files * Update engine to allow Markdown for HTML partials * Update reference landing page to use partial * Update content configuration to use correct repository * Update NPM serve configuration * Remove duplicate header * Fix errant URL in `postgres-api` * Simplify workflow to infer and use NPM * Update `tsx` package * Update package versions to latest * Add site build validation * Change title casing for sections in navigation bar * Update CI workflow title
1 parent 3a2123c commit 55972fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3696
-4411
lines changed

.github/workflows/continuous-deployment.yml

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,11 @@ jobs:
5050
# Configure DocumentDB version (can be overridden by repository variables)
5151
echo "DOCUMENTDB_VERSION=${{ vars.DOCUMENTDB_VERSION || 'latest' }}" >> $GITHUB_ENV
5252
echo "MULTI_VERSION=${{ vars.MULTI_VERSION || 'true' }}" >> $GITHUB_ENV
53-
- name: Detect package manager
54-
id: detect-package-manager
55-
run: |
56-
if [ -f "${{ github.workspace }}/yarn.lock" ]; then
57-
echo "manager=yarn" >> $GITHUB_OUTPUT
58-
echo "command=install" >> $GITHUB_OUTPUT
59-
echo "runner=yarn" >> $GITHUB_OUTPUT
60-
exit 0
61-
elif [ -f "${{ github.workspace }}/package.json" ]; then
62-
echo "manager=npm" >> $GITHUB_OUTPUT
63-
echo "command=ci" >> $GITHUB_OUTPUT
64-
echo "runner=npx --no-install" >> $GITHUB_OUTPUT
65-
exit 0
66-
else
67-
echo "Unable to determine package manager"
68-
exit 1
69-
fi
7053
- name: Setup Node.js
7154
uses: actions/setup-node@v5
7255
with:
7356
node-version: 24
74-
cache: ${{ steps.detect-package-manager.outputs.manager }}
57+
cache: npm
7558
- name: Restore cache
7659
uses: actions/cache@v4
7760
with:
@@ -83,11 +66,11 @@ jobs:
8366
restore-keys: |
8467
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
8568
- name: Install dependencies
86-
run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
69+
run: npm ci
8770
- name: Build with Next.js
8871
env:
8972
NEXT_BASE_PATH: ${{ github.event.repository.name }}
90-
run: ${{ steps.detect-package-manager.outputs.runner }} next build
73+
run: npm run build
9174
- name: Download DocumentDB packages from latest release
9275
run: .github/scripts/download_packages.sh
9376
- name: Upload artifact

.github/workflows/continuous-integration.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
name: Validate structured content
1+
# Workflow for validating content structure and build integrity
2+
name: Validate site and content
23
on:
4+
# Runs on pushes targeting the default branch
35
push:
46
branches:
57
- main
8+
# Runs on pull requests targeting the default branch
69
pull_request:
710
branches:
811
- main
912
jobs:
13+
# YAML schema validation job
1014
yaml-schema-validation:
1115
name: Validate YAML files against schemas
1216
runs-on: ubuntu-latest
@@ -25,3 +29,22 @@ jobs:
2529
npx yaml-ls-check .\articles
2630
npx yaml-ls-check .\blogs
2731
npx yaml-ls-check .\reference
32+
# Build validation job
33+
build-validation:
34+
name: Validate Next.js build
35+
runs-on: ubuntu-latest
36+
steps:
37+
- name: Checkout repository
38+
uses: actions/checkout@v5
39+
- name: Setup Node.js
40+
uses: actions/setup-node@v5
41+
with:
42+
node-version: 24
43+
cache: npm
44+
- name: Install dependencies
45+
run: npm ci
46+
- name: Build Next.js site
47+
# Validate that the site builds successfully
48+
#
49+
# Use the built-in NPM script to build the site
50+
run: npm run build

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
# Sourced from https://github.com/github/gitignore/blob/main/Nextjs.gitignore
22
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
33

4+
# Temporary content cloning directory
5+
_tmp/
6+
7+
# Reference files
8+
api-reference/
9+
reference/
10+
11+
# Documentation articles
12+
articles/*/
13+
articles/**/*.md
14+
articles/**/*.yml
15+
!articles/content.yml
16+
!articles/demo.yml
17+
418
# dependencies
519
/node_modules
620
/.pnp

.vscode/settings.json

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,8 @@
66
"./schema/reference.content.schema.json": [
77
"reference/content.{yml,yaml}"
88
],
9-
"./schema/reference.data.schema.json": [
10-
"reference/*/**/*.{yml,yaml}"
11-
],
129
"./schema/articles.content.schema.json": [
1310
"articles/**/content.{yml,yaml}"
14-
],
15-
"./schema/articles.navigation.schema.json": [
16-
"articles/**/navigation.{yml,yaml}"
1711
]
1812
},
1913
"editor.tabSize": 2,

app/components/Breadcrumb.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function Breadcrumb({ type, category, name }: {
1616
<>
1717
<span className="mx-2">/</span>
1818
<Link href={`/docs/reference/${type}`} className="hover:text-blue-400 transition-colors capitalize">
19-
{capitalCase(pluralize(type))}
19+
{capitalCase(type)}
2020
</Link>
2121
</>
2222
)}

app/components/Index.tsx

Lines changed: 165 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,193 @@
1-
"use client";
1+
'use client'
22

33
import Link from 'next/link';
44
import { usePathname } from 'next/navigation';
5-
import type { ReferencePage } from '../services/referenceService';
65
import { capitalCase } from 'change-case';
6+
import pluralize from 'pluralize';
7+
import React, { useEffect, useRef } from 'react';
8+
9+
type ReferencePage = {
10+
name: string;
11+
description: string;
12+
reference: string;
13+
type: string;
14+
category?: string;
15+
};
16+
17+
type NavigationStructure = {
18+
type: string;
19+
displayName: string;
20+
categories: {
21+
category: string;
22+
displayName: string;
23+
items: ReferencePage[];
24+
}[];
25+
}[];
26+
27+
const icons = {
28+
chevronRight: (
29+
<path
30+
strokeLinecap="round"
31+
strokeLinejoin="round"
32+
strokeWidth={2}
33+
d="M9 5l7 7-7 7"
34+
/>
35+
),
36+
};
737

838
export default function Index({
939
groupedReferences
1040
}: {
1141
groupedReferences: Record<string, Record<string, ReferencePage[]>>;
1242
}) {
1343
const pathname = usePathname();
44+
const activeItemRef = useRef<HTMLAnchorElement>(null);
45+
const containerRef = useRef<HTMLElement>(null);
46+
47+
// Auto-scroll to active item when pathname changes
48+
useEffect(() => {
49+
if (activeItemRef.current && containerRef.current) {
50+
const container = containerRef.current;
51+
const activeItem = activeItemRef.current;
52+
53+
// Calculate position to scroll within the container
54+
const containerRect = container.getBoundingClientRect();
55+
const itemRect = activeItem.getBoundingClientRect();
56+
const scrollOffset = itemRect.top - containerRect.top - (containerRect.height / 2) + (itemRect.height / 2);
57+
58+
// Instant scroll without animation
59+
container.scrollBy({
60+
top: scrollOffset,
61+
behavior: 'instant'
62+
});
63+
}
64+
}, [pathname]);
1465

15-
// Get all types and sort them
16-
const types = Object.keys(groupedReferences).sort();
66+
// Convert old structure to new structure
67+
const navigationStructure: NavigationStructure = Object.entries(groupedReferences)
68+
.sort(([a], [b]) => a.localeCompare(b))
69+
.map(([type, categories]) => ({
70+
type,
71+
displayName: type,
72+
categories: Object.entries(categories)
73+
.sort(([a], [b]) => a.localeCompare(b))
74+
.map(([category, items]) => ({
75+
category,
76+
displayName: category,
77+
items: items.sort((a, b) => a.name.localeCompare(b.name))
78+
}))
79+
}));
80+
81+
// Parse pathname to determine current location
82+
const pathParts = pathname.split('/').filter(Boolean);
83+
const currentType = pathParts[2]; // /docs/reference/[type]
84+
const currentCategory = pathParts[3]; // /docs/reference/[type]/[category]
85+
const currentName = pathParts[4] ? decodeURIComponent(pathParts[4]) : undefined; // /docs/reference/[type]/[category]/[name]
1786

1887
return (
19-
<section className="flex-1 p-4 overflow-y-auto">
88+
<section ref={containerRef} className="flex-1 overflow-y-auto p-4 min-h-0">
2089
<nav className="space-y-1">
21-
{types.map((type, index) => {
22-
const categories = Object.keys(groupedReferences[type]).sort();
90+
{navigationStructure.map((typeSection, typeIndex) => {
91+
// Types are always expanded
92+
const isTypeExpanded = true;
93+
const isTypeActive = pathname === `/docs/reference/${typeSection.type}`;
2394

2495
return (
25-
<div key={type}>
26-
{/* Add horizontal rule between sections (not before the first one) */}
27-
{index > 0 && (
96+
<div key={typeSection.type}>
97+
{/* Separator between type sections (not before first) */}
98+
{typeIndex > 0 && (
2899
<div className="my-4 border-t border-neutral-700/50"></div>
29100
)}
30101

31-
{/* Type header */}
32-
{categories.length > 0 && (
33-
<>
34-
<Option
35-
key={type}
36-
target={`/docs/reference/${type}`}
37-
display={capitalCase(type) + 's'}
38-
className="uppercase"
39-
currentPath={pathname}
40-
/>
102+
{/* Type row */}
103+
<div className="flex items-center gap-1">
104+
<div className="p-1 flex-shrink-0">
105+
<svg
106+
className="w-4 h-4 text-gray-500 rotate-90"
107+
fill="none"
108+
stroke="currentColor"
109+
viewBox="0 0 24 24"
110+
>
111+
{icons.chevronRight}
112+
</svg>
113+
</div>
114+
<Link
115+
href={`/docs/reference/${typeSection.type}`}
116+
ref={isTypeActive ? activeItemRef : null}
117+
className={`flex-1 px-3 py-3 rounded-lg text-sm font-semibold uppercase transition-all duration-200 ${
118+
isTypeActive
119+
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
120+
: "text-gray-300 hover:text-white hover:bg-neutral-700/50"
121+
}`}
122+
>
123+
{pluralize(capitalCase(typeSection.displayName))}
124+
</Link>
125+
</div>
41126

42-
{/* Category links */}
43-
{categories.map((category) => (
44-
<Option
45-
key={`${type}-${category}`}
46-
target={`/docs/reference/${type}/${category}`}
47-
display={capitalCase(category)}
48-
currentPath={pathname}
49-
/>
50-
))}
51-
</>
52-
)}
127+
{/* Categories */}
128+
{isTypeExpanded && typeSection.categories.map((categorySection) => {
129+
// Category is expanded if we're on that category page or on one of its item pages
130+
const isCategoryExpanded = currentType === typeSection.type && currentCategory === categorySection.category;
131+
const isCategoryActive = pathname === `/docs/reference/${typeSection.type}/${categorySection.category}`;
132+
133+
return (
134+
<div key={`${typeSection.type}-${categorySection.category}`}>
135+
{/* Category row */}
136+
<div className="flex items-center gap-1 pl-4">
137+
<div className="p-1 flex-shrink-0">
138+
<svg
139+
className={`w-3 h-3 text-gray-500 transition-transform ${isCategoryExpanded ? 'rotate-90' : ''}`}
140+
fill="none"
141+
stroke="currentColor"
142+
viewBox="0 0 24 24"
143+
>
144+
{icons.chevronRight}
145+
</svg>
146+
</div>
147+
<Link
148+
href={`/docs/reference/${typeSection.type}/${categorySection.category}`}
149+
ref={isCategoryActive ? activeItemRef : null}
150+
className={`flex-1 px-3 py-3 rounded-lg text-sm transition-all duration-200 ${
151+
isCategoryActive
152+
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
153+
: "text-gray-300 hover:text-white hover:bg-neutral-700/50"
154+
}`}
155+
>
156+
{capitalCase(categorySection.displayName)}
157+
</Link>
158+
</div>
159+
160+
{/* Individual reference items */}
161+
{isCategoryExpanded && categorySection.items.map((item) => {
162+
const itemFilename = item.reference.split('/').pop();
163+
const itemPath = `/docs/reference/${typeSection.type}/${categorySection.category}/${itemFilename}`;
164+
// Decode pathname to match against the constructed path
165+
const decodedPathname = decodeURIComponent(pathname);
166+
const isItemActive = decodedPathname === itemPath || pathname === itemPath;
167+
168+
return (
169+
<div key={item.reference} className="flex items-center pl-10 ml-4">
170+
<Link
171+
href={itemPath}
172+
ref={isItemActive ? activeItemRef : null}
173+
className={`flex-1 px-3 py-3 rounded-lg text-sm transition-all duration-200 ${
174+
isItemActive
175+
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
176+
: "text-gray-400 hover:text-white hover:bg-neutral-700/50"
177+
}`}
178+
>
179+
{item.name}
180+
</Link>
181+
</div>
182+
);
183+
})}
184+
</div>
185+
);
186+
})}
53187
</div>
54188
);
55189
})}
56190
</nav>
57191
</section>
58192
);
59193
}
60-
61-
function Option({
62-
target,
63-
display,
64-
className,
65-
currentPath
66-
}: {
67-
target: string;
68-
display: string;
69-
className?: string;
70-
currentPath: string;
71-
}) {
72-
// Determine if this option should be highlighted
73-
// Highlight if:
74-
// 1. Exact match (e.g., on /docs/reference/operator, highlight "Operators")
75-
// 2. Category match (e.g., on /docs/reference/operator/accumulator or /docs/reference/operator/accumulator/avg, highlight "Operators" and "Accumulator")
76-
const isExactMatch = currentPath === target;
77-
const isCategoryMatch = currentPath.startsWith(target + '/');
78-
const isActive = isExactMatch || isCategoryMatch;
79-
80-
return (
81-
<Link
82-
href={target}
83-
className={`block w-full text-left px-4 py-3 rounded-lg text-sm transition-all duration-200 ${
84-
isActive
85-
? "bg-blue-500/20 text-blue-300 border border-blue-500/30"
86-
: "text-gray-300 hover:text-white hover:bg-neutral-700/50"
87-
}${className ? ' ' + className : ''}`}
88-
>
89-
{display}
90-
</Link>
91-
);
92-
}

0 commit comments

Comments
 (0)