Docs Site Path Resolution Plan
Issue: The navigation.ts file references package and service READMEs (packages/api-client/README.md, services/api-gateway/README.md) that exist at the project root, but loadDoc() only resolves paths relative to the docs/ directory.
Current Behavior: Pages show "Content coming soon" for these navigation items.
Problem Analysis
Current Architecture
VoiceAssist/
├── docs/ # DOCS_DIR - where loadDoc() looks
│ ├── archive/
│ ├── api-reference/
│ └── *.md files
├── packages/ # Package READMEs live here
│ ├── api-client/README.md
│ ├── config/README.md
│ └── ...
├── services/ # Service READMEs live here
│ └── api-gateway/README.md
└── apps/
└── docs-site/ # The Next.js documentation site
└── src/lib/
├── docs.ts # loadDoc() function
└── navigation.ts # References all paths
Root Cause
loadDoc(relativePath)indocs.tsjoins paths withDOCS_DIR(thedocs/folder)navigation.tsspecifiesdocPathslikepackages/api-client/README.md- This resolves to
docs/packages/api-client/README.mdwhich doesn't exist - Security checks in
loadDoc()intentionally block..traversal
Solution Options
Option A: Path Prefix Convention (Recommended)
Approach: Use a prefix convention to indicate the base directory for each path.
// navigation.ts - Updated docPaths format docPaths: [ "API_REFERENCE.md", // Resolves to docs/API_REFERENCE.md "@root/services/api-gateway/README.md", // Resolves to project root "@root/packages/api-client/README.md", // Resolves to project root ];
Implementation:
- Define
PROJECT_ROOTindocs.tsalongsideDOCS_DIR - Create
loadDocWithPrefix()that parses the prefix and routes appropriately - Update
DocPage.tsxto use the new loader - Update
navigation.tspaths with@root/prefix where needed
Pros:
- Explicit and self-documenting
- No magic behavior
- Maintains security (whitelist approach)
- Easy to extend for future base directories
Cons:
- Requires updating existing navigation entries
- Slightly more verbose paths
Option B: Multi-Directory Auto-Resolution
Approach: Try multiple directories in order until a file is found.
const SEARCH_PATHS = [ DOCS_DIR, // Try docs/ first PROJECT_ROOT, // Then try project root ]; function loadDoc(relativePath: string): DocContent | null { for (const basePath of SEARCH_PATHS) { const fullPath = path.join(basePath, relativePath); if (fs.existsSync(fullPath)) { return loadFromPath(fullPath, basePath); } } return null; }
Pros:
- No changes to navigation.ts needed
- "It just works" behavior
Cons:
- Implicit behavior can be confusing
- Potential for unexpected file resolution
- Harder to debug which file was loaded
- Security implications if not carefully implemented
Option C: Build-Time Sync Script
Approach: Copy/symlink READMEs to docs/ during build.
# prebuild script cp packages/*/README.md docs/packages/ cp services/*/README.md docs/services/
Pros:
- No code changes to docs.ts
- Simple file system approach
Cons:
- Files can get out of sync
- Clutters docs/ directory
- Symlinks don't work well across all environments
- Need to update .gitignore
Option D: Extended NavItem Interface
Approach: Add baseDir property to docPaths entries.
interface DocPath { path: string; baseDir?: "docs" | "root"; // default: 'docs' } // navigation.ts docPaths: [{ path: "API_REFERENCE.md" }, { path: "services/api-gateway/README.md", baseDir: "root" }];
Pros:
- Type-safe
- Flexible
Cons:
- Breaking change to navigation.ts structure
- More verbose than prefix convention
- Requires updating DocPage component
Recommended Solution: Option A (Path Prefix Convention)
Implementation Plan
Phase 1: Update docs.ts (30 min)
// docs.ts additions // Project root (monorepo root, parent of docs/) export const PROJECT_ROOT = path.join(DOCS_DIR, ".."); // Allowed prefixes and their base directories const PATH_PREFIXES: Record<string, string> = { "@root/": PROJECT_ROOT, "@docs/": DOCS_DIR, // explicit docs prefix (optional) }; /** * Load a document with prefix support * - No prefix or @docs/ prefix: loads from docs/ * - @root/ prefix: loads from project root */ export function loadDocWithPrefix(docPath: string): DocContent | null { let basePath = DOCS_DIR; let relativePath = docPath; // Check for prefixes for (const [prefix, baseDir] of Object.entries(PATH_PREFIXES)) { if (docPath.startsWith(prefix)) { basePath = baseDir; relativePath = docPath.slice(prefix.length); break; } } // Security: reject path traversal if (relativePath.includes("..") || path.isAbsolute(relativePath)) { console.warn(`Invalid path rejected: ${docPath}`); return null; } const fullPath = path.join(basePath, relativePath); // Security: ensure resolved path stays within base if (!isPathWithinBase(basePath, fullPath)) { console.warn(`Path traversal blocked: ${docPath}`); return null; } // Load the file try { if (!fs.existsSync(fullPath)) { return null; // Silent fail - DocPage handles missing docs gracefully } const fileContents = fs.readFileSync(fullPath, "utf8"); const { data, content } = matter(fileContents); return { content, frontmatter: data }; } catch (error) { console.error(`Error loading ${docPath}:`, error); return null; } } /** * Get GitHub URL for a document (handles prefixes) */ export function getGitHubEditUrlWithPrefix(docPath: string): string { let repoPath = `docs/${docPath}`; if (docPath.startsWith("@root/")) { repoPath = docPath.slice(6); // Remove @root/ prefix } else if (docPath.startsWith("@docs/")) { repoPath = `docs/${docPath.slice(6)}`; } return `https://github.com/${GITHUB_REPO}/edit/${GITHUB_BRANCH}/${repoPath}`; }
Phase 2: Update DocPage.tsx (10 min)
// DocPage.tsx - Update import and usage import { loadDocWithPrefix } from "@/lib/docs"; // Change line 17 from: .map((path) => ({ path, doc: loadDoc(path) })) // To: .map((path) => ({ path, doc: loadDocWithPrefix(path) }))
Phase 3: Update navigation.ts (15 min)
Update the package and service paths to use the @root/ prefix:
{ title: "API Reference", href: "/reference/api", description: "REST endpoints, gateway routing, and OpenAPI details", docPaths: [ "API_REFERENCE.md", "api-reference/rest-api.md", "@root/services/api-gateway/README.md", // Updated ], }, { title: "Packages", href: "/reference/all-docs", description: "Monorepo package READMEs and shared utilities", docPaths: [ "@root/packages/api-client/README.md", // Updated "@root/packages/config/README.md", // Updated "@root/packages/design-tokens/README.md", // Updated "@root/packages/telemetry/README.md", // Updated "@root/packages/types/README.md", // Updated "@root/packages/ui/README.md", // Updated "@root/packages/utils/README.md", // Updated ], },
Phase 4: Update Tests (if any) (15 min)
Add unit tests for the new loadDocWithPrefix() function:
describe("loadDocWithPrefix", () => { it("loads docs from docs/ by default", () => { const result = loadDocWithPrefix("START_HERE.md"); expect(result).not.toBeNull(); }); it("loads docs from project root with @root/ prefix", () => { const result = loadDocWithPrefix("@root/packages/api-client/README.md"); expect(result).not.toBeNull(); }); it("blocks path traversal attempts", () => { const result = loadDocWithPrefix("@root/../../../etc/passwd"); expect(result).toBeNull(); }); });
Phase 5: Documentation (10 min)
Add a comment block in navigation.ts explaining the convention:
/** * Documentation paths can use these prefixes: * - No prefix: Relative to docs/ directory (e.g., "START_HERE.md") * - @root/: Relative to project root (e.g., "@root/packages/ui/README.md") * * Example: * docPaths: [ * "ARCHITECTURE.md", // → docs/ARCHITECTURE.md * "@root/services/api-gateway/README.md" // → services/api-gateway/README.md * ] */
Security Considerations
- Path Traversal Prevention: The implementation maintains the existing
..rejection andisPathWithinBase()check - Whitelist Approach: Only explicitly defined prefixes are allowed
- Base Directory Validation: Each prefix maps to a specific, validated base directory
- No Arbitrary File Access: Cannot escape the defined base directories
Testing Checklist
- Existing doc pages still work (no regression)
- Package README pages now load content
- Service README pages now load content
- Path traversal attempts are blocked
- GitHub edit links work correctly
- Build succeeds without errors
- Dev server works correctly
Estimated Effort
| Phase | Task | Time |
|---|---|---|
| 1 | Update docs.ts | 30 min |
| 2 | Update DocPage.tsx | 10 min |
| 3 | Update navigation.ts | 15 min |
| 4 | Add tests | 15 min |
| 5 | Documentation | 10 min |
| Total | ~1.5 hours |
Future Enhancements
- Add more prefixes: Could add
@apps/for loading from apps directory - Caching: Add file content caching for production builds
- Hot reload: Ensure dev server picks up changes to external docs
- Search index: Update search index generation to include
@root/docs
Rollback Plan
If issues arise:
- Revert the navigation.ts changes (remove
@root/prefixes) - The system will gracefully fall back to "Content coming soon"
- No data loss or breaking changes to other parts of the system