Project Persistence & Root URL Architecture

How project selection persists across sessions and URLs remain at root

đŸŽ¯ Project Persistence & Root URL Architecture

Date: October 28, 2025
Time: 10:57 AM PST
Status: ✅ COMPLETE & WORKING
Purpose: Document how project selection persists and how URLs remain clean at root


📋 Overview

The docs-angular application now implements true project-scoped architecture where:

  1. ✅ Project context stored in state (NGXS + localStorage)
  2. ✅ URLs remain clean at root (/ instead of /ProjectName/)
  3. ✅ Persistence works across browser sessions
  4. ✅ No redirects when project is loaded

🔄 How It Works

1. User Flow

┌─────────────────────────────────────────────────────────────┐
│  First Visit (No Persisted Project)                        │
├─────────────────────────────────────────────────────────────┤
│  1. User goes to http://localhost:5010/                    │
│  2. APP_INITIALIZER loads: No project found                │
│  3. Routes: No hardcoded redirects                          │
│  4. Catch-all route matches with projectSelectedGuard       │
│  5. Guard checks: No project → Redirect to /select-project │
│  6. User sees project selection page                        │
│  7. User selects "GoGrab-Space"                             │
│  8. Action dispatched: SelectProject('english/GoGrab-Space')│
│  9. localStorage stores: {"projectId": "...", timestamp}    │
│ 10. Router navigates to: / (just root!)                    │
│ 11. Guard allows: Project selected ✅                       │
│ 12. DynamicPageComponent loads content for GoGrab-Space    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  Subsequent Visits (Persisted Project Exists)              │
├─────────────────────────────────────────────────────────────┤
│  1. User goes to http://localhost:5010/                    │
│  2. APP_INITIALIZER loads: Project found! ✅               │
│  3. ProjectSelectionState restored from localStorage        │
│  4. Routes: NO redirects (removed hardcoded ones)           │
│  5. Catch-all route matches with projectSelectedGuard       │
│  6. Guard checks: Project selected ✅                       │
│  7. DynamicPageComponent loads content for GoGrab-Space    │
│  8. User stays at http://localhost:5010/ ✅                │
└─────────────────────────────────────────────────────────────┘

đŸ—‚ī¸ Key Components

1. ProjectSelectionState (NGXS State)

File: apps/docs-angular/src/app/state/project-selection.state.ts

Purpose: Centralized state management for project selection

Storage Key: docs-selected-project

Storage Format:

{
  "projectId": "english/GoGrab-Space",
  "timestamp": 1730148470000
}

Actions:

Key Features:

@Action(SelectProject)
async selectProject(ctx: StateContext<ProjectSelectionStateModel>, action: SelectProject) {
  const state = ctx.getState();
  const project = state.availableProjects.find(p => p.id === action.projectId);

  if (!project) {
    console.error('❌ Project not found:', action.projectId);
    return;
  }

  // đŸŽ¯ PERSISTENCE: Store to localStorage
  try {
    localStorage.setItem(this.STORAGE_KEY, JSON.stringify({
      projectId: action.projectId,
      timestamp: Date.now()
    }));
  } catch (error) {
    console.warn('âš ī¸ Failed to persist project selection:', error);
  }

  ctx.patchState({
    currentProjectId: action.projectId,
    currentProjectMetadata: project,
    lastSelectedTimestamp: Date.now()
  });

  console.log('✅ Project selected:', action.projectId);
}

2. APP_INITIALIZER (App Startup)

File: apps/docs-angular/src/app/app.config.ts

Purpose: Load persisted project before app renders

Timing: Runs BEFORE any routing happens

Logic:

provideAppInitializer(() => {
  const store = inject(Store);
  const router = inject(Router);

  return (async () => {
    try {
      console.log('🔄 Loading persisted project...');
      await store.dispatch(new LoadPersistedProject()).toPromise();
      
      const isProjectSelected = store.selectSnapshot(/* check state */);
      const currentPath = router.url;
      
      // đŸŽ¯ PROJECT-SCOPED ARCHITECTURE: Navigation rules
      // 1. If project IS selected → Allow navigation to proceed
      // 2. If project NOT selected and not on selection page → Redirect
      // 3. If already on selection page → Do nothing
      
      if (!isProjectSelected && !currentPath.includes('/select-project')) {
        console.log('â„šī¸ No project selected, redirecting to selection page');
        setTimeout(() => {
          router.navigate(['/select-project']);
        }, 100);
      } else if (isProjectSelected) {
        console.log('✅ Project loaded from persistence, allowing navigation');
      }
      
      return true;
    } catch (error) {
      console.error('❌ Failed to load persisted project:', error);
      return true;
    }
  })();
})

Why Delayed Redirect?


3. Dynamic Routes Service

File: apps/docs-angular/src/app/services/dynamic-routes.service.ts

Purpose: Generate routes WITHOUT hardcoded redirects

Critical Change: Removed hardcoded root redirects

Before ❌:

{
  path: '',
  redirectTo: '/select-project',
  pathMatch: 'full'
},
{
  path: 'es',
  redirectTo: '/select-project',
  pathMatch: 'full'
}

After ✅:

// đŸŽ¯ PROJECT-SCOPED ARCHITECTURE: Root paths handled by catch-all + guard
// NO redirects here - let guard decide based on project state
// If project selected → show content at root
// If no project → guard redirects to /select-project

Why This Matters:


4. Project Selected Guard

File: apps/docs-angular/src/app/guards/project-selected.guard.ts

Purpose: Enforce project selection before showing content

Logic:

export const projectSelectedGuard: CanActivateFn = (route) => {
  const store = inject(Store);
  const router = inject(Router);

  // ✅ ALWAYS ALLOW: /select-project bypasses checks
  const currentPath = route.url.map(segment => segment.path).join('/');
  if (currentPath === 'select-project' || currentPath.startsWith('select-project')) {
    return true;
  }

  const isProjectSelected = store.selectSnapshot(ProjectSelectionState.isProjectSelected);
  const isInitialized = store.selectSnapshot(ProjectSelectionState.isInitialized);

  // Wait for initialization
  if (!isInitialized) {
    return router.createUrlTree(['/select-project']);
  }

  // Check if project is selected
  if (!isProjectSelected) {
    return router.createUrlTree(['/select-project']);
  }

  // ✅ Project selected - allow navigation
  return true;
};

When It Runs:

Decision Flow:

User navigates to /
  ↓
Catch-all route matches
  ↓
projectSelectedGuard runs
  ↓
Is project selected in state?
  ├─ YES → Allow navigation ✅
  │         DynamicPageComponent renders
  │         Shows content for selected project
  │
  └─ NO  → Redirect to /select-project
            User must select a project

5. Project Selection Component

File: apps/docs-angular/src/app/pages/project-selection/project-selection.component.ts

Purpose: UI for selecting projects

Critical Change: Navigation after selection

Before ❌:

const projectPath = `${langPrefix}/${project.name}`;
await this.router.navigate([projectPath]);
// Result: http://localhost:5010/GoGrab-Space ❌

After ✅:

// đŸŽ¯ PROJECT-SCOPED ARCHITECTURE: Navigate to ROOT with project context in state only
const langPrefix = project.language === 'spanish' ? '/es' : '';
const rootPath = langPrefix || '/'; // Just "/" or "/es"

console.log('🚀 Navigating to project root (state-based):', rootPath);
console.log('đŸ“Ļ Project context stored in state:', project.id);

await this.router.navigate([rootPath]);
// Result: http://localhost:5010/ ✅

Why This Works:


🔍 Debugging Tips

Check Persistence

Browser DevTools → Application → Local Storage

Look for key: docs-selected-project

Expected Value:

{
  "projectId": "english/GoGrab-Space",
  "timestamp": 1730148470000
}

Console Logs

Successful Load:

🔄 Loading persisted project...
✅ Loaded persisted project: english/GoGrab-Space
🔍 APP_INITIALIZER status: {
  isProjectSelected: true,
  loadedProjectId: "english/GoGrab-Space",
  currentPath: "/",
  isSelectProjectPage: false
}
✅ Project loaded from persistence, allowing navigation to: /
✅ Guard: Project selected, allowing navigation

No Project (First Visit):

🔄 Loading persisted project...
â„šī¸ No persisted project found
🔍 APP_INITIALIZER status: {
  isProjectSelected: false,
  loadedProjectId: null,
  currentPath: "/",
  isSelectProjectPage: false
}
â„šī¸ No project selected, redirecting to selection page
đŸšĢ Guard: No project selected, redirecting to selection page

đŸŽ¯ Benefits

1. Clean URLs

2. Persistence

3. Separation of Concerns

4. Performance


🔧 Configuration

Storage Key

Change in project-selection.state.ts:

private readonly STORAGE_KEY = 'docs-selected-project';

NGXS Persistence

Configure in app.config.ts:

NgxsStoragePluginModule.forRoot({ 
  keys: ['projectDocs', 'projectSelection'] 
})

Persistence Strategy

Currently: localStorage (simple, fast)

Future Options:


🚨 Common Issues

Issue: Still Redirects to /select-project

Cause: Hardcoded redirects in route config

Solution: Check dynamic-routes.service.ts - ensure NO redirects for root paths

Verify:

private getBaseRoutes(): Route[] {
  return [
    // ✅ CORRECT: No hardcoded redirects
    { path: 'select-project', loadComponent: ... },
    // ❌ WRONG: Hardcoded redirect
    // { path: '', redirectTo: '/select-project', pathMatch: 'full' }
  ];
}

Issue: URL has project name

Cause: Navigation in ProjectSelectionComponent uses project name

Solution: Navigate to root only

Verify:

// ✅ CORRECT
const rootPath = langPrefix || '/';
await this.router.navigate([rootPath]);

// ❌ WRONG
await this.router.navigate([`/${project.name}`]);

Issue: Persistence not working

Cause: localStorage blocked or NGXS not configured

Checks:

  1. Browser DevTools → Application → Local Storage
  2. Check NGXS config in app.config.ts
  3. Verify SelectProject action saves to localStorage
  4. Check browser console for errors

✅ Testing Checklist

First Visit (No Persistence)

Subsequent Visits (With Persistence)

Project Switching

Multiple Languages


📚 Related Documentation


Last Updated: October 28, 2025
Maintained By: AI Agent + Development Team
Status: ✅ Production Ready