Project Routing Collision Fix
Complete Project Routing Fix Implementation
Executive Summary
This document provides the complete, line-by-line implementation of the project routing fix that resolves 404 errors and preserves nested directory structures in project URLs. The solution transforms URLs from broken
/projects/filename to working /projects/full/nested/path/filename format.Problem Statement
Before Fix:
- File:
/content/projects/Astro-Turf/Specs/Maintain-a-UI-for-JSON-Canvas.md - Broken URL:
/projects/maintain-a-ui-for-json-canvas(404 error) - Issue: Directory structure
Astro-Turf/Specs/was lost in slug generation
After Fix:
- File:
/content/projects/Astro-Turf/Specs/Maintain-a-UI-for-JSON-Canvas.md - Working URL:
/projects/astro-turf/specs/maintain-a-ui-for-json-canvas(HTTP 200) - Result: Full directory structure preserved in URL
Complete Implementation
1. Content Collection Configuration (src/content.config.ts)
Key Change: Use
generateId function in glob loader to preserve directory structure. typescript
// CRITICAL: Projects collection definition (lines 558-614)
const projects = defineCollection({
loader: glob({
pattern: '**/*.md',
base: '/Users/mpstaton/code/lossless-monorepo/content/projects',
// THIS IS THE KEY FIX: generateId preserves full directory path
generateId: ({ entry, data }) => {
console.log(`[PROJECTS COLLECTION] Processing entry: ${entry}`);
// Remove .md extension and convert to slug format
const withoutExtension = entry.replace(/\.md$/, '');
console.log(`[PROJECTS COLLECTION] Without extension: ${withoutExtension}`);
// Use getReferenceSlug to slugify while preserving directory structure
const slug = getReferenceSlug(withoutExtension);
console.log(`[PROJECTS COLLECTION] Generated slug: ${slug}`);
return slug;
}
}),
schema: z.object({
title: z.string().optional(),
lede: z.string().optional(),
date_authored_initial_draft: z.date().optional(),
date_authored_current_draft: z.date().optional(),
date_authored_final_draft: z.date().optional(),
date_first_published: z.date().optional(),
date_last_updated: z.date().optional(),
at_semantic_version: z.string().optional(),
status: z.string().optional(),
augmented_with: z.string().optional(),
category: z.string().optional(),
date_created: z.date().optional(),
date_modified: z.date().optional(),
tags: z.array(z.string()).optional(),
authors: z.array(z.string()).optional(),
image_prompt: z.string().optional(),
site_uuid: z.string().optional(),
slug: z.string().optional(),
banner_image: z.string().optional(),
portrait_image: z.string().optional(),
square_image: z.string().optional(),
}),
}); Supporting Function: The
getReferenceSlug function (already existed): typescript
export function getReferenceSlug(filename: string): string {
if (!filename) {
throw new Error("Blank or improper filename passed to the getReferenceSlug function. Work backwards from where this function is being called")
}
// Split by directory separators
const parts = filename.split('/');
// Slugify each part individually to preserve directory structure
const slugifiedParts = parts.map(p => slugify(p));
// Rejoin with slashes to maintain directory hierarchy
return slugifiedParts.join('/');
} 2. Dynamic Route Handler (src/pages/projects/[...slug].astro)
Key Change: Use
entry.id (generated by generateId) instead of entry.data.slug. astro
---
// CRITICAL: getStaticPaths implementation (lines 8-40)
export async function getStaticPaths() {
// Get all projects from the collection
const projects = await getCollection('projects');
console.log(`[PROJECTS ROUTE] Found ${projects.length} projects`);
// Generate static paths using entry.id (the slug from generateId)
const paths = projects.map((entry) => {
// THIS IS THE KEY FIX: Use entry.id instead of entry.data.slug
const slug = entry.id;
console.log(`[PROJECTS ROUTE] Entry ID: ${entry.id}, using as slug: ${slug}`);
return {
params: {
slug: slug // This becomes the [...slug] parameter
},
props: {
entry // Pass the entire entry as props
}
};
});
console.log(`[PROJECTS ROUTE] Generated ${paths.length} project paths`);
return paths;
}
// Get the entry from props
const { entry } = Astro.props;
---
<!-- Render the project content -->
<Layout>
<OneArticleOnPage
entry={entry}
collection="projects"
/>
</Layout> 3. Collection Export (src/content.config.ts)
Critical: Ensure projects collection is properly exported:
typescript
// Export collections object
export const collections = {
// ... other collections ...
projects, // Make sure projects collection is exported
// ... other collections ...
}; How It Works - Complete Flow
1. File Processing
flowchart TD
A["File: |content|projects|Astro-Turf|Specs|Maintain-a-UI-for-JSON-Canvas.md"] --> B["Glob loader finds file with pattern **|*.md"]
B --> C["generateId receives: 'Astro-Turf|Specs|Maintain-a-UI-for-JSON-Canvas.md'"]
C --> D["Remove .md: 'Astro-Turf|Specs|Maintain-a-UI-for-JSON-Canvas'"]
D --> E["getReferenceSlug processes:<br/>- Split by '/': 'Astro-Turf', 'Specs', 'Maintain-a-UI-for-JSON-Canvas'<br/>- Slugify each: ['astro-turf', 'specs', 'maintain-a-ui-for-json-canvas']<br/>- Join with '/': 'astro-turf/specs/maintain-a-ui-for-json-canvas'"]
E --> F["entry.id = 'astro-turf|specs|maintain-a-ui-for-json-canvas'"]
2. Route Generation
flowchart TD
A["getStaticPaths() runs"] --> B["getCollection('projects') returns all entries"]
B --> C["For each entry, create path object:<br|>{<br|> params: { slug: 'astro-turf|specs|maintain-a-ui-for-json-canvas' },<br|> props: { entry: <full entry object> }<br|>}"]
C --> D["Astro creates route: |projects|astro-turf|specs|maintain-a-ui-for-json-canvas"]
3. URL Resolution
flowchart TD
A["User visits: |projects|astro-turf|specs|maintain-a-ui-for-json-canvas"] --> B["Astro matches [...slug] pattern"]
B --> C["slug parameter = 'astro-turf|specs|maintain-a-ui-for-json-canvas'"]
C --> D["Finds matching static path"]
D --> E["Returns entry props to component"]
E --> F["OneArticleOnPage renders the content"]
F --> G["HTTP 200 success"]
Debug Logging Output
When working correctly, you should see this in the console:
bash
[PROJECTS COLLECTION] Processing entry: Astro-Turf/Specs/Maintain-a-UI-for-JSON-Canvas.md
[PROJECTS COLLECTION] Without extension: Astro-Turf/Specs/Maintain-a-UI-for-JSON-Canvas
[PROJECTS COLLECTION] Generated slug: astro-turf/specs/maintain-a-ui-for-json-canvas
[PROJECTS ROUTE] Found 91 projects
[PROJECTS ROUTE] Entry ID: astro-turf/specs/maintain-a-ui-for-json-canvas, using as slug: astro-turf/specs/maintain-a-ui-for-json-canvas
[PROJECTS ROUTE] Generated 91 project paths Verification Steps
- Check Collection Loading:bash
# Should show all 91 projects with proper slugs pnpm dev # Look for "[PROJECTS COLLECTION] Generated slug:" logs - Check Route Generation:bash
# Should show "Generated 91 project paths" # Look for "[PROJECTS ROUTE] Entry ID:" logs - Test URL Access:bash
curl -I http://localhost:4321/projects/astro-turf/specs/maintain-a-ui-for-json-canvas # Should return HTTP 200, not 404
Files Modified
src/content.config.ts- Lines 558-614: Projects collection with generateIdsrc/pages/projects/[...slug].astro- Lines 8-40: getStaticPaths using entry.id
Critical Success Factors
- Use
generateIdin glob loader - This is what preserves directory structure - Use
entry.idin getStaticPaths - This uses the slug generated by generateId - Don't use
entry.data.slug- This was the source of 404 errors - Preserve directory separators - getReferenceSlug maintains '/' between path segments
- Slugify individual path segments - Each directory/filename is slugified separately
Result
- 91 project paths generated successfully
- All URLs preserve nested directory structure
- No more 404 errors on project pages
- Canonical URLs match file system hierarchy
Example transformations:
text
Content-Farm/Specs/file.md → /projects/content-farm/specs/file
Augment-It/Apps/app.md → /projects/augment-it/apps/app
Astro-Turf/Specs/spec.md → /projects/astro-turf/specs/spec This implementation successfully resolves the project routing issues while maintaining clean, SEO-friendly URLs that reflect the actual content organization.
Phase 2: Enhanced Implementation (August 2025)
Overview of Additional Changes
Building on the core routing fix, we implemented several enhancements to improve the project system and resolve conflicts between client-specific and canonical project routing.
Branch Comparison and Integration
Branches Analyzed:
clean/jsoncanvas(base branch with core routing fix)save/jsoncanvas(enhanced branch with additional features)
Integration Strategy:
gitGraph
commit id: "Core routing fix"
branch save/jsoncanvas
checkout save/jsoncanvas
commit id: "JSONCanvas enhancements"
commit id: "Project components"
commit id: "Client routing cleanup"
checkout clean/jsoncanvas
merge save/jsoncanvas
commit id: "Integrated implementation"
Key Files Modified in Integration
bash
# Files pulled from save/jsoncanvas to clean/jsoncanvas
src/components/projects/ProjectShowcase.astro # 91 lines added
src/components/projects/Section__Project-Container.astro # 79 lines added
src/pages/projects/[...slug].astro # 53 lines added
src/pages/projects/index.astro # 128 lines modified
src/utils/simpleMarkdownRenderer.ts # 2 lines changed
src/generated-content # Submodule updated
src/components/jsoncanvas/JSONCanvasFile.svelte # 295 lines added Enhanced JSONCanvas Implementation
File Path to Site URL Conversion
New Feature: Smart routing from JSONCanvas files to actual site URLs.
typescript
// src/components/jsoncanvas/JSONCanvasFile.svelte
function convertFilePathToSiteUrl(filePath: string): string {
console.log('🔄 Converting file path to site URL:', filePath);
let siteUrl = '';
const contentPath = filePath.replace(/^.*\/content\//, '');
if (contentPath.startsWith('client-content/')) {
// Handle client-content paths: client-content/Laerdal/Projects/file.md
const pathParts = contentPath.split('/');
if (pathParts.length >= 4 && pathParts[2] === 'Projects') {
const clientName = pathParts[1].toLowerCase();
const projectPathParts = pathParts.slice(3);
// Slugify function for consistent URL generation
const slugify = (str: string) => str
.replace(/\.[a-z0-9]+$/, '') // Remove file extension
.replace(/[^a-z0-9\s\-_]/g, '') // Remove special chars
.replace(/[\s_]+/g, '-') // Replace spaces/underscores with dashes
.replace(/-+/g, '-') // Collapse multiple dashes
.replace(/^-+|-+$/g, ''); // Trim leading/trailing dashes
const projectSlug = projectPathParts
.map(part => slugify(part))
.join('/');
siteUrl = `/client/${clientName}/projects/${projectSlug}`;
}
} else if (contentPath.startsWith('projects/')) {
// Handle regular projects directory
const projectPath = contentPath.replace(/\.md$/, '');
const slugifiedPath = projectPath
.toLowerCase()
.replace(/[^a-z0-9\/]/g, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/\/-+/g, '/')
.replace(/-+\//g, '/');
siteUrl = `/${slugifiedPath}`;
}
console.log('🎯 Converted to site URL:', siteUrl);
return siteUrl;
} Interactive "Open in New Tab" Feature
Implementation: Click-to-navigate functionality for JSONCanvas file nodes.
svelte
<!-- Open in new tab icon (appears when selected) -->
<g
class="open-tab-icon"
class:visible={isSelected}
transform="translate({width - 20}, 6)"
on:click={handleOpenInNewTab}
on:keydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const syntheticEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
handleOpenInNewTab(syntheticEvent);
}
}}
tabindex={isSelected ? 0 : -1}
role="button"
aria-label="Open file in new tab"
>
<!-- External link icon with hover states -->
<circle cx="10" cy="10" r="10"
fill="rgba(255, 255, 255, 0.1)"
stroke="var(--clr-lossless-accent--brightest, #4a9eff)"
stroke-width="1" />
<path d="M6 6h2v2M8 4v2l-4 4"
stroke="var(--clr-lossless-accent--brightest, #4a9eff)"
stroke-width="1.2" />
</g> Enhanced Code Block Styling
Feature: Complete code block system matching site-wide
BaseCodeblock.astro structure. css
/* Code block styling for JSON Canvas */
.content-text :global(.codeblock-container) {
position: relative;
margin: 1.5rem 0;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
background: var(--clr-code-bg, #1e1e1e);
}
.content-text :global(.codeblock-header) {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background-color: rgba(0, 0, 0, 0.2);
font-family: var(--ff-monospace, monospace);
font-size: 0.8rem;
}
.content-text :global(.copy-button) {
background: transparent;
border: none;
color: var(--clr-code-lang, #8a8a8a);
cursor: pointer;
transition: all 0.2s ease;
} Client Routing Cleanup
Problem Resolution
Issue: Conflicting routes between client projects (
/client/[client]/projects/) and canonical projects (/projects/).Solution: Disabled interfering client projects route to prioritize canonical routing.
bash
# Disabled problematic route file
mv "src/pages/client/[client]/projects/index.astro" \
"src/pages/client/[client]/projects/index.astro.disabled" Route Configuration Updates
Updated Route Paths:
typescript
// src/utils/routePaths.ts
export const ROUTE_PATHS = {
CLIENT: {
BASE: '/client',
PORTFOLIO: '/client/[client]/portfolio',
// PROJECTS: '/client/[client]/projects', // Removed conflicting route
RECOMMENDATIONS: '/client/[client]/recommendations',
},
PROJECTS: {
BASE: '/projects', // Added canonical projects route
},
// ... other routes
}; Route Manager Integration:
typescript
// src/utils/routing/routeManager.ts
export const ROUTE_MAPPINGS = [
// ... existing mappings ...
{
contentPath: 'projects',
routePath: 'projects'
},
// ... other mappings ...
]; Content Configuration Enhancements
Projects Collection Definition
Enhanced Collection: Added from
save/jsoncanvas branch with improved logging and slug generation. typescript
// src/content.config.ts
const projectsCollection = defineCollection({
loader: glob({
pattern: "**/*.md",
base: "../content/projects",
generateId: ({ entry }) => {
console.log(`[PROJECTS] Processing entry: "${entry}"`);
// Remove .md extension from entry path
const pathWithoutExt = entry.replace(/\.md$/, '');
console.log(`[PROJECTS] Path without extension: "${pathWithoutExt}"`);
// Extract project root directory and internal path
const pathParts = pathWithoutExt.split('/');
if (pathParts.length === 0) {
console.log(`[PROJECTS] ERROR: Empty path parts`);
return 'unknown';
}
// First part is the project root directory (e.g., "Augment-It")
const projectRoot = pathParts[0];
// Generate slug: project-root/internal/path
let slug;
if (pathParts.length === 1) {
// Just the project root file
slug = getReferenceSlug(projectRoot);
} else {
// Project root + internal path
const internalPath = pathParts.slice(1).join('/');
slug = `${getReferenceSlug(projectRoot)}/${getReferenceSlug(internalPath)}`;
}
console.log(`[PROJECTS] Generated slug: "${slug}"`);
return slug;
}
}),
schema: z.object({
title: z.string().optional(),
tags: z.array(z.string()).optional(),
slug: z.string().optional(),
publish: z.boolean().default(true),
}),
}); Collection Publishing Defaults
Added Configuration:
typescript
export const collectionPublishingDefaults = {
'issue-resolution': {
publishByDefault: true,
},
'talks': {
publishByDefault: true,
},
'projects': {
publishByDefault: true, // Added projects publishing default
},
}; Project Components Integration
ProjectShowcase Component
New Component:
src/components/projects/ProjectShowcase.astro (91 lines) astro
---
// Project showcase component for displaying project information
export interface Props {
project: any;
showDescription?: boolean;
compact?: boolean;
}
const { project, showDescription = true, compact = false } = Astro.props;
---
<div class="project-showcase" class:list={{ compact }}>
<h3 class="project-title">{project.data.title || project.id}</h3>
{showDescription && project.data.lede && (
<p class="project-description">{project.data.lede}</p>
)}
<div class="project-meta">
{project.data.tags && (
<div class="project-tags">
{project.data.tags.map(tag => (
<span class="tag">{tag}</span>
))}
</div>
)}
</div>
</div> Section Project Container
New Component:
src/components/projects/Section__Project-Container.astro (79 lines) astro
---
// Container component for project sections
export interface Props {
title?: string;
projects: any[];
layout?: 'grid' | 'list';
}
const { title, projects, layout = 'grid' } = Astro.props;
---
<section class="project-container">
{title && <h2 class="section-title">{title}</h2>}
<div class="projects-grid" class:list={[`layout-${layout}`]}>
{projects.map(project => (
<ProjectShowcase project={project} />
))}
</div>
</section> Build and Development Verification
Successful Build Output
bash
$ pnpm build
# ... build process ...
[PROJECTS] Processing entry: "augment-it/specs/apps/prompttemplatemanager.md"
[PROJECTS] Generated slug: "augment-it/specs/apps/prompttemplatemanager"
# ... 91 projects processed ...
Generated 91 project paths
✓ Completed in 67.95s
[build] Complete! Development Server Verification
bash
$ pnpm dev
# ... dev server starts ...
[PROJECTS ROUTE] Found 91 projects
[PROJECTS ROUTE] Entry ID: augment-it/specs/shared-ui-elements/shareduploadbutton,
using as slug: augment-it/specs/shared-ui-elements/shareduploadbutton
# ... all 91 projects loaded successfully ...
Generated 91 project paths Architecture Flow Diagram
flowchart TB
subgraph "Content Layer"
A["|content|projects|Augment-It|Specs|file.md"]
B["|content|client-content|Laerdal|Projects|file.md"]
end
subgraph "Collection Processing"
C["projectsCollection<br|>generateId()"]
D["clientProjectsCollection<br|>(disabled)"]
end
subgraph "Route Generation"
E["/projects/[...slug].astro<br/>getStaticPaths()"]
F["/client/[client]/projects/<br/>(disabled)"]
end
subgraph "URL Resolution"
G["|projects|augment-it|specs|file"]
H["JSONCanvas Integration<br|>convertFilePathToSiteUrl()"]
end
subgraph "User Interface"
I["ProjectShowcase Component"]
J["JSONCanvas with Navigation"]
K["Interactive File Nodes"]
end
A --> C
B --> D
C --> E
D -.-> F
E --> G
G --> H
H --> I
H --> J
J --> K
style D fill:#ffcccc
style F fill:#ffcccc
style C fill:#ccffcc
style E fill:#ccffcc
Critical Success Metrics
- ✅ 91 project paths generated successfully
- ✅ Zero 404 errors on project routes
- ✅ JSONCanvas navigation functional
- ✅ Client routing conflicts resolved
- ✅ Build completes without errors
- ✅ Development server runs cleanly
Files Modified Summary
Core Implementation:
src/content.config.ts- Enhanced projects collectionsrc/pages/projects/[...slug].astro- Dynamic route handlersrc/pages/projects/index.astro- Projects index page
New Components:
src/components/projects/ProjectShowcase.astrosrc/components/projects/Section__Project-Container.astro
Enhanced Features:
src/components/jsoncanvas/JSONCanvasFile.svelte- Interactive navigationsrc/utils/routePaths.ts- Route configurationsrc/utils/routing/routeManager.ts- Route mappingsrc/utils/simpleMarkdownRenderer.ts- Rendering improvements
Disabled/Cleaned:
src/pages/client/[client]/projects/index.astro.disabled- Conflicting route
This comprehensive implementation provides a robust, scalable project routing system with enhanced user experience through JSONCanvas integration and clean separation of concerns between client-specific and canonical project routing.