OpenContracts Routing System Documentation¶
Table of Contents¶
- Overview
 - Architecture
 - Route Patterns
 - Core Components
 - Data Hydration
 - Navigation Utilities
 - GraphQL Integration
 - Annotation Handling
 - ID-Based Navigation & Redirection
 - Performance & Caching
 - Error Handling
 - Testing Guide
 - Migration Guide
 - Best Practices
 
Overview¶
The OpenContracts routing system uses explicit, deterministic slug-based routing with clear URL prefixes to eliminate ambiguity. Every route clearly indicates its content type through /c/ (corpus) and /d/ (document) prefixes. The system supports both slug-based and ID-based navigation, with IDs automatically redirecting to canonical slug URLs.
Key Principles¶
- Explicit Routes: Clear patterns with 
/c/and/d/prefixes eliminate ambiguity - Slug-First: Slugs are preferred for SEO and readability, IDs redirect to slug URLs
 - Deterministic: Each URL pattern maps to exactly one route handler
 - Graceful Fallback: ID-based URLs automatically resolve and redirect to canonical URLs
 - No Page Reloads: Navigation uses React Router for smooth SPA transitions
 - Performant: Single GraphQL query per route with efficient caching strategies
 
Design Decisions¶
- Hybrid Support: Both slug and ID-based navigation work seamlessly
 - Canonical URLs: IDs always redirect to slug-based canonical URLs for SEO
 - No Ambiguity: Routes use explicit prefixes to avoid pattern conflicts
 - Smart Resolution: Automatically detects IDs vs slugs and handles appropriately
 - Clear Intent: URLs clearly communicate content type to users and crawlers
 
Architecture¶
```mermaid graph TB subgraph "Route Components" A[App.tsx Routes] → B[CorpusLandingRoute] A → C[DocumentLandingRoute] end
subgraph "Resolution Layer"
    B --> D[useSlugResolver Hook]
    C --> D
    D --> E[Request Tracker]
    D --> F[Slug Cache]
    D --> G[Route Cache]
end
subgraph "GraphQL Queries"
    D --> H[RESOLVE_CORPUS_BY_SLUGS_FULL]
    D --> I[RESOLVE_DOCUMENT_BY_SLUGS_FULL]
    D --> J[RESOLVE_DOCUMENT_IN_CORPUS_BY_SLUGS_FULL]
end
subgraph "State Management"
    D --> K[openedCorpus Reactive Var]
    D --> L[openedDocument Reactive Var]
    D --> M[selectedAnnotationIds Reactive Var]
end
subgraph "Navigation Utilities"
    N[navigateToCorpus]
    O[navigateToDocument]
    P[getCorpusUrl]
    Q[getDocumentUrl]
end
```
Route Patterns¶
Primary URL Patterns¶
| Pattern | Example | Component | Description | 
|---|---|---|---|
| Corpus Routes | |||
/c/:userIdent/:corpusIdent |  /c/john/my-corpus |  CorpusLandingRoute | Slug-based corpus route | 
| Document Routes | |||
/d/:userIdent/:docIdent |  /d/john/my-document |  DocumentLandingRoute | Standalone document | 
/d/:userIdent/:corpusIdent/:docIdent |  /d/john/my-corpus/contract |  DocumentLandingRoute | Document within corpus context | 
| List Views | |||
/corpuses |  /corpuses |  Corpuses | Browse all corpuses | 
/documents |  /documents |  Documents | Browse all documents | 
Query Parameters¶
| Parameter | Purpose | Example | Component Handling | 
|---|---|---|---|
?ann= |  Select specific annotations | /d/john/doc?ann=123 |  DocumentLandingRoute | 
| Multiple annotations | /d/john/doc?ann=123,456,789 |  Comma-separated IDs | 
Route Configuration (App.tsx)¶
// Document routes - explicit /d/ prefix
<Route path="/d/:userIdent/:corpusIdent/:docIdent" element={<DocumentLandingRoute />} />
<Route path="/d/:userIdent/:docIdent" element={<DocumentLandingRoute />} />
// Corpus routes - explicit /c/ prefix
<Route path="/c/:userIdent/:corpusIdent" element={<CorpusLandingRoute />} />
// List views
<Route path="/corpuses" element={<Corpuses />} />
<Route path="/documents" element={<Documents />} />
ID-Based Navigation (Auto-Redirect)¶
These patterns automatically redirect to canonical slug URLs:
- ✅ 
/c/john/123→/c/john/my-corpus(corpus ID redirects to slug) - ✅ 
/d/john/456→/d/john/my-document(document ID redirects to slug) - ✅ 
/d/john/corpus/789→/d/john/corpus/doc(mixed ID/slug supported) - ✅ 
/d/456→/d/john/my-document(single ID redirects with user context) 
Deprecated Patterns¶
These patterns are NO LONGER SUPPORTED:
- ❌ 
/corpuses/:corpusId(use/c/user/corpus) - ❌ 
/documents/:documentId(use/d/user/document) - ❌ 
/corpus/:corpusId/document/:documentId(use new patterns) - ❌ 
/:userIdent/:secondIdent(ambiguous - use explicit prefixes) 
Core Components¶
DocumentLandingRoute¶
Handles all document routes with explicit /d/ prefix.
Route patterns: - /d/:userIdent/:corpusIdent/:docIdent - Document within a corpus - /d/:userIdent/:docIdent - Standalone document
Key Features: - Simple parameter extraction from route - Uses useSlugResolver for data fetching - Handles both standalone and corpus-context documents - Clean error handling and loading states - Automatic annotation selection from query params
CorpusLandingRoute¶
Handles corpus routes with explicit /c/ prefix.
Route pattern: - /c/:userIdent/:corpusIdent
Key Features: - Single route pattern to handle - Uses useSlugResolver for data fetching - Clean, minimal implementation - Proper meta tags for SEO - Automatic stats loading
useSlugResolver Hook¶
Central hub for all slug resolution logic, handling both slug and ID-based navigation.
interface SlugResolverOptions {
  userIdent?: string;
  corpusIdent?: string;
  documentIdent?: string;
  annotationIds?: string[];
  onResolved?: (result: SlugResolverResult) => void;
}
interface SlugResolverResult {
  loading: boolean;
  error: Error | undefined;
  corpus: CorpusType | null;
  document: DocumentType | null;
}
Resolution Strategy:
- Document in Corpus (3 identifiers)
 - Pattern: 
userIdent + corpusIdent + documentIdent - Query: 
RESOLVE_DOCUMENT_IN_CORPUS_BY_SLUGS_FULL -  
Returns both corpus and document
 -  
Standalone Document (2 identifiers)
 - Pattern: 
userIdent + documentIdent - Query: 
RESOLVE_DOCUMENT_BY_SLUGS_FULL -  
Returns document only
 -  
Corpus (2 identifiers)
 - Pattern: 
userIdent + corpusIdent - Query: 
RESOLVE_CORPUS_BY_SLUGS_FULL - Returns corpus only
 
Data Hydration¶
State Management¶
The routing system manages global state through Apollo reactive variables:
// Reactive variables for global state
export const openedCorpus = makeVar<CorpusType | null>(null);
export const openedDocument = makeVar<DocumentType | null>(null);
export const selectedAnnotationIds = makeVar<string[]>([]);
Hydration Flow¶
```mermaid sequenceDiagram participant Route participant Resolver participant GraphQL participant ReactiveVars participant Component
Route->>Resolver: Extract params
Resolver->>GraphQL: Query by slugs/IDs
GraphQL-->>Resolver: Return data
Resolver->>ReactiveVars: Update openedCorpus/Document
ReactiveVars->>Component: Trigger re-render
Component->>Component: Display hydrated data
```
Hydration Guards¶
The system includes guards to prevent incomplete data from being set:
// Never set incomplete corpus objects
if (corpus && corpus.id && corpus.slug && corpus.title) {
  openedCorpus(corpus);
}
// Never set incomplete document objects
if (document && document.id && document.slug && document.title) {
  openedDocument(document);
}
Component Unmount Cleanup¶
Components clean up global state on unmount:
useEffect(() => {
  return () => {
    // Clear reactive vars on unmount
    openedDocument(null);
    selectedAnnotationIds([]);
  };
}, []);
Navigation Utilities¶
URL Generation¶
// Builds corpus URL - always with /c/ prefix
getCorpusUrl(corpus); // Returns: /c/john/my-corpus
// Builds document URL - always with /d/ prefix
getDocumentUrl(document, corpus); // Returns: /d/john/my-corpus/doc
getDocumentUrl(document); // Returns: /d/john/doc
Important: These functions return "#" if slugs are missing, preventing navigation to invalid routes.
Smart Navigation¶
// Navigate to corpus (won't navigate if already there)
navigateToCorpus(corpus, navigate, currentPath);
// Navigate to document (won't navigate if already there)
navigateToDocument(document, corpus, navigate, currentPath);
These functions: - Check if already at destination - Prevent navigation without slugs - Use replace navigation for cleaner history - Use React Router's navigate() to avoid full page reloads
Document Closing Behavior¶
When closing a document:
const handleClose = () => {
  // Uses React Router navigation (no page reload)
  if (corpus) {
    navigate(`/c/${corpus.creator.slug}/${corpus.slug}`);
  } else {
    navigate("/documents");
  }
};
Benefits: - No Page Reload: Smooth SPA transition - State Preservation: App state remains intact - Fast Navigation: Instant UI updates - Proper Cleanup: Reactive vars cleared on unmount
GraphQL Integration¶
Query Strategy¶
The resolver selects queries based on available parameters:
// Document in corpus - full context
if (userIdent && corpusIdent && documentIdent) {
  // Uses RESOLVE_DOCUMENT_IN_CORPUS_BY_SLUGS_FULL
  // Returns both corpus and document
}
// Standalone document
else if (userIdent && documentIdent && !corpusIdent) {
  // Uses RESOLVE_DOCUMENT_BY_SLUGS_FULL
  // Returns document only
}
// Corpus only
else if (userIdent && corpusIdent && !documentIdent) {
  // Uses RESOLVE_CORPUS_BY_SLUGS_FULL
  // Returns corpus only
}
GraphQL Queries¶
Corpus Resolution¶
query ResolveCorpusBySlugsFull($userSlug: String!, $corpusSlug: String!) {
  corpusBySlugs(userSlug: $userSlug, corpusSlug: $corpusSlug) {
    id
    slug
    title
    description
    mdDescription
    created
    modified
    isPublic
    myPermissions
    creator {
      id
      username
      slug
    }
    labelSet {
      id
      title
      description
    }
    descriptionRevisions(first: 5) {
      edges {
        node {
          id
          description
          created
        }
      }
    }
  }
}
Document Resolution¶
query ResolveDocumentBySlugsFull($userSlug: String!, $documentSlug: String!) {
  documentBySlugs(userSlug: $userSlug, documentSlug: $documentSlug) {
    id
    slug
    title
    description
    pdfFile {
      id
      url
      name
      size
    }
    creator {
      id
      username
      slug
    }
    corpus {
      id
      slug
      title
      creator {
        id
        username
        slug
      }
    }
    # ... other fields
  }
}
Fetch Policies¶
fetchPolicy: "cache-first"
nextFetchPolicy: "cache-and-network"
- Initial load uses cache if available
 - Subsequent loads check network for updates
 - Provides fast initial render with fresh data updates
 
Annotation Handling¶
Overview¶
The navigation system supports deep linking to specific annotations within documents through query parameters.
URL Format¶
/d/john/my-document?ann=annotation-id
/d/john/my-corpus/doc?ann=id1,id2,id3
Parameter Processing¶
// Extract annotation IDs from URL
const [searchParams] = useSearchParams();
const annParam = searchParams.get("ann");
const annotationIds = annParam ? annParam.split(",").filter(Boolean) : [];
// Set global state
if (annotationIds.length > 0) {
  selectedAnnotationIds(annotationIds);
}
Navigation with Annotations¶
// Navigate to annotation
const url = getDocumentUrl(document, corpus);
navigate(`${url}?ann=${annotation.id}`);
// Navigate to multiple annotations
const annotationIds = [id1, id2, id3].join(",");
navigate(`${url}?ann=${annotationIds}`);
Components That Navigate with Annotations¶
- QueryResultsViewer: Navigate to annotation from query results
 - AnnotationCards: Navigate to selected annotation
 - ExtractCellFormatter: Navigate to source annotations
 - DataCell: Navigate to extract source annotations
 
ID-Based Navigation & Redirection¶
Overview¶
The system supports GraphQL ID-based navigation with automatic redirection to canonical slug URLs, ensuring backward compatibility while maintaining SEO-friendly URLs.
ID Detection¶
The system automatically detects GraphQL IDs:
- Numeric IDs (e.g., 
123,456789) - Base64 encoded IDs (e.g., 
Q29ycHVzOjEyMw==) - GID prefixed IDs (e.g., 
gid://app/Corpus/123) 
Resolution Process¶
// When an ID is detected in the URL
if (isValidGraphQLId(identifier)) {
  // 1. Query for entity by ID
  const entity = await resolveEntityById(identifier);
  // 2. Extract slug information
  const slugUrl = buildCanonicalUrl(entity);
  // 3. Redirect to slug URL (preserving query params)
  navigate(slugUrl, { replace: true });
}
Supported ID Patterns¶
| Pattern | Example | Resolution | Final URL | 
|---|---|---|---|
| Corpus ID | /c/john/123 |  Queries corpus by ID | /c/john-doe/my-corpus |  
| Document ID | /d/john/456 |  Queries document by ID | /d/john-doe/my-document |  
| Mixed | /d/john/corpus/789 |  Queries document by ID | /d/john/corpus/my-document |  
| Single ID | /d/789 |  Queries document by ID | /d/john-doe/my-document |  
ID Resolution Queries¶
# Corpus ID resolution
query GetCorpusByIdForRedirect($id: ID!) {
  corpus(id: $id) {
    id
    slug
    title
    creator {
      id
      slug
      username
    }
  }
}
# Document ID resolution
query GetDocumentByIdForRedirect($id: String!) {
  document(id: $id) {
    id
    slug
    title
    creator {
      id
      slug
      username
    }
    corpus {
      id
      slug
      title
      creator {
        id
        slug
        username
      }
    }
  }
}
Performance & Caching¶
Three-Layer Caching Strategy¶
-  
Route Cache (In-Memory)
// Prevents re-processing of same route const routeCache = new Map<string, ProcessedRoute>(); -  
Slug Cache (In-Memory)
// Caches slug resolutions const slugCache = new Map<string, { corpus?: string; document?: string }>(); -  
Apollo Cache (GraphQL)
 - Normalized cache for GraphQL responses
 - Shared across all components
 - Automatic cache updates on mutations
 
Request Deduplication¶
class RequestTracker {
  private pendingRequests = new Map<string, Promise<any>>();
  // Prevents duplicate simultaneous requests
  // Returns existing promise if request pending
  async track(key: string, request: () => Promise<any>) {
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key);
    }
    const promise = request();
    this.pendingRequests.set(key, promise);
    try {
      const result = await promise;
      return result;
    } finally {
      this.pendingRequests.delete(key);
    }
  }
}
Performance Monitoring¶
performanceMonitor.startMetric("slug-resolution", {
  userIdent,
  corpusIdent,
  documentIdent,
});
// ... resolution logic ...
performanceMonitor.endMetric("slug-resolution", {
  success: true,
  cacheHit: fromCache,
});
Tracks: - Resolution time - Success/failure rates - Cache hit rates - Query performance
Error Handling¶
Error States¶
-  
Missing Slugs
if (!corpus.slug || !corpus.creator?.slug) { console.warn("Cannot generate corpus URL without slugs"); return "#"; // Safe fallback } -  
Not Found
if (!document) { navigate("/404", { replace: true }); return; } -  
Invalid Routes
if (!userIdent) { setState({ error: new Error("Missing required route parameters"), loading: false, }); } -  
Network Errors
catch (error) { console.error("Failed to resolve route:", error); navigate("/404", { replace: true }); } 
User Experience¶
- Clear error messages in console
 - Graceful fallbacks to safe states
 - 404 page for not found content
 - Loading states during resolution
 - Network error handling with retries
 
Testing Guide¶
Unit Tests¶
describe("Navigation URLs", () => {
  it("generates corpus URL with prefix", () => {
    const corpus = {
      id: "1",
      slug: "my-corpus",
      creator: { slug: "john" },
    };
    expect(getCorpusUrl(corpus)).toBe("/c/john/my-corpus");
  });
  it("returns safe fallback without slugs", () => {
    const corpus = { id: "1" };
    expect(getCorpusUrl(corpus)).toBe("#");
  });
});
Integration Tests¶
describe("Route Resolution", () => {
  it("resolves corpus route", async () => {
    render(<CorpusLandingRoute />, {
      route: "/c/john/my-corpus",
    });
    await waitFor(() => {
      expect(screen.getByText("My Corpus")).toBeInTheDocument();
    });
  });
  it("redirects ID to slug", async () => {
    const { result } = renderHook(() => useSlugResolver({
      userIdent: "john",
      corpusIdent: "123", // ID
    }));
    await waitFor(() => {
      expect(mockNavigate).toHaveBeenCalledWith(
        "/c/john/my-corpus",
        { replace: true }
      );
    });
  });
});
Component Tests (Playwright)¶
test("navigates to document with annotation", async ({ mount, page }) => {
  await mount(<DocumentCard document={mockDocument} />);
  await page.click('[data-testid="annotation-link"]');
  await expect(page).toHaveURL(/\/d\/.*\?ann=\d+/);
});
E2E Test Scenarios¶
- Corpus Navigation
 - Navigate to 
/c/john/my-corpus - Verify corpus loads
 -  
Check meta tags
 -  
Document Navigation
 - Navigate to 
/d/john/doc - Verify document loads
 -  
Test close navigation
 -  
Document in Corpus
 - Navigate to 
/d/john/corpus/doc - Verify both corpus and document load
 -  
Check breadcrumbs
 -  
Annotation Navigation
 - Navigate to 
/d/john/doc?ann=123 - Verify annotation selected
 -  
Test multiple annotations
 -  
ID Redirection
 - Navigate to 
/c/john/123 - Verify redirect to 
/c/john/my-corpus -  
Check query params preserved
 -  
Error Handling
 - Navigate to invalid routes
 - Verify 404 page
 - Check console warnings
 
Migration Guide¶
From Legacy Routes¶
All legacy routes must be updated to use the new explicit patterns:
| Old Pattern | New Pattern | 
|---|---|
/corpuses/[id] |  /c/[user]/[corpus] |  
/documents/[id] |  /d/[user]/[document] |  
/corpus/[id]/document/[id] |  /d/[user]/[corpus]/[document] |  
/[user]/[item] |  /c/[user]/[corpus] or /d/[user]/[document] |  
Backend Requirements¶
- Slugs Required
 - All entities must have 
slugfield -  
All entities must have
creator.slug -  
GraphQL Queries
 - Implement slug-based resolution queries
 -  
Support ID-based fallback queries
 -  
URL Generation
 - Use utility functions for all URL generation
 - Never hardcode URLs
 
Component Updates¶
// Old
const url = `/corpuses/${corpus.id}`;
// New
import { getCorpusUrl } from "utils/navigationUtils";
const url = getCorpusUrl(corpus);
Handling Missing Slugs¶
// Check before navigation
const url = getDocumentUrl(document);
if (url === "#") {
  console.error("Cannot navigate: missing slugs");
  return;
}
navigate(url);
Best Practices¶
DO¶
- ✅ Always use navigation utility functions
 - ✅ Use explicit route prefixes (
/c/,/d/) - ✅ Handle missing slugs gracefully
 - ✅ Use TypeScript types for route params
 - ✅ Test navigation flows thoroughly
 - ✅ Preserve annotation parameters when navigating
 - ✅ Use comma-separated IDs for multiple annotations
 - ✅ Clean up reactive vars on component unmount
 - ✅ Check for "#" return from URL generators
 
DON'T¶
- ❌ Hardcode navigation URLs
 - ❌ Assume slugs exist
 - ❌ Use raw IDs for navigation (they will redirect)
 - ❌ Create ambiguous routes
 - ❌ Add fallback logic for legacy routes
 - ❌ Forget to append 
?ann=when navigating to annotations - ❌ Navigate without checking URL generation success
 - ❌ Set incomplete objects in reactive vars
 - ❌ Skip cleanup on component unmount
 
Code Examples¶
Correct Navigation¶
// Good - uses utility function
const handleNavigate = () => {
  const url = getCorpusUrl(corpus);
  if (url !== "#") {
    navigate(url);
  }
};
// Good - preserves annotations
const navigateWithAnnotation = (annotationId: string) => {
  const baseUrl = getDocumentUrl(document, corpus);
  navigate(`${baseUrl}?ann=${annotationId}`);
};
Incorrect Navigation¶
// Bad - hardcoded URL
navigate(`/corpuses/${corpus.id}`);
// Bad - assumes slug exists
navigate(`/c/${corpus.creator.slug}/${corpus.slug}`);
// Bad - doesn't check for valid URL
navigate(getCorpusUrl(corpus)); // Could be "#"
Summary¶
The OpenContracts routing system provides:
- Explicit Routes: Clear 
/c/and/d/prefixes eliminate ambiguity - Simple Implementation: Minimal code, easy to understand
 - Deterministic Behavior: Each URL maps to exactly one handler
 - Performance: Single query per route, efficient caching
 - Maintainability: Clean separation of concerns
 - SEO Friendly: Canonical slug-based URLs with ID redirection
 - Developer Experience: TypeScript types, utility functions, clear patterns
 - User Experience: Fast navigation, deep linking, error handling
 
The system requires slugs for canonical URLs but supports ID-based navigation through automatic redirection, providing both backward compatibility and forward-looking clean URLs.