Skip to main content

ScullyRoutesService

The ScullyRoutesService provides methods and observables that give you access to all routes rendered by Scully, including their metadata and published status.

Overview

This service reads the scully-routes.json file generated during the Scully build process and exposes various observables to access route information throughout your Angular application.

Installation

The service is automatically available when you import ScullyLibModule:
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule
  ]
})
export class AppModule { }

Interface

ScullyRoute

export interface ScullyRoute {
  route: string;           // The route path
  title?: string;          // Optional page title
  slugs?: string[];        // Optional array of slugs
  published?: boolean;     // Publication status
  slug?: string;           // Single slug
  sourceFile?: string;     // Source markdown file path
  lang?: string;           // Language code
  [prop: string]: any;     // Custom properties
}

Observables

allRoutes$

allRoutes$: Observable<ScullyRoute[]>
Emits an array of all routes, both published and unpublished. Emission Pattern: Emits once on initialization and whenever reload() is called. Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';

@Component({
  selector: 'app-sitemap',
  template: `
    <ul>
      <li *ngFor="let route of routes">
        {{ route.route }} - {{ route.title }}
      </li>
    </ul>
  `
})
export class SitemapComponent implements OnInit {
  routes: ScullyRoute[] = [];

  constructor(private scully: ScullyRoutesService) {}

  ngOnInit() {
    this.scully.allRoutes$.subscribe(routes => {
      this.routes = routes;
    });
  }
}

available$

available$: Observable<ScullyRoute[]>
Emits only routes that are published (where published !== false). Emission Pattern: Emits whenever allRoutes$ emits, filtering for published routes. Example:
import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-blog-list',
  template: `
    <article *ngFor="let post of posts$ | async">
      <a [routerLink]="post.route">
        <h2>{{ post.title }}</h2>
      </a>
    </article>
  `
})
export class BlogListComponent {
  posts$ = this.scully.available$;

  constructor(private scully: ScullyRoutesService) {}
}

unPublished$

unPublished$: Observable<ScullyRoute[]>
Emits only routes marked as unpublished (where published === false). Emission Pattern: Emits whenever allRoutes$ emits, filtering for unpublished routes. Example:
import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-draft-posts',
  template: `
    <div *ngIf="(drafts$ | async) as drafts">
      <h3>Draft Posts ({{ drafts.length }})</h3>
      <ul>
        <li *ngFor="let draft of drafts">
          {{ draft.title }}
        </li>
      </ul>
    </div>
  `
})
export class DraftPostsComponent {
  drafts$ = this.scully.unPublished$;

  constructor(private scully: ScullyRoutesService) {}
}

topLevel$

topLevel$: Observable<ScullyRoute[]>
Emits only top-level routes (routes without any slashes after the first character). Emission Pattern: Emits whenever available$ emits, filtering for top-level routes. Example:
import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-main-nav',
  template: `
    <nav>
      <a *ngFor="let route of mainRoutes$ | async" 
         [routerLink]="route.route">
        {{ route.title }}
      </a>
    </nav>
  `
})
export class MainNavComponent {
  mainRoutes$ = this.scully.topLevel$;

  constructor(private scully: ScullyRoutesService) {}
}

Methods

getCurrent()

getCurrent(): Observable<ScullyRoute>
Returns an observable that emits the route information for the currently active route. Subscribes to Angular Router events to update when navigation occurs. Emission Pattern: Emits immediately with current route, then on every NavigationEnd event. Returns: Observable that emits the current ScullyRoute or undefined if not found. Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';

@Component({
  selector: 'app-page-header',
  template: `
    <header *ngIf="currentRoute">
      <h1>{{ currentRoute.title }}</h1>
      <p *ngIf="currentRoute.description">
        {{ currentRoute.description }}
      </p>
    </header>
  `
})
export class PageHeaderComponent implements OnInit {
  currentRoute?: ScullyRoute;

  constructor(private scully: ScullyRoutesService) {}

  ngOnInit() {
    this.scully.getCurrent().subscribe(route => {
      this.currentRoute = route;
    });
  }
}

reload()

reload(): void
Forces a reload of the scully-routes.json file. Useful when routes are dynamically added and you need to refresh the route list. Example:
import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-admin-panel',
  template: `
    <button (click)="refreshRoutes()">
      Refresh Routes
    </button>
  `
})
export class AdminPanelComponent {
  constructor(private scully: ScullyRoutesService) {}

  refreshRoutes() {
    this.scully.reload();
    console.log('Routes reloaded from scully-routes.json');
  }
}

Advanced Usage

Filtering Routes by Custom Properties

import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-category-filter',
  template: `
    <div *ngFor="let post of filteredPosts$ | async">
      <h3>{{ post.title }}</h3>
      <span class="category">{{ post.category }}</span>
    </div>
  `
})
export class CategoryFilterComponent {
  filteredPosts$ = this.scully.available$.pipe(
    map(routes => routes.filter(route => 
      route.category === 'tutorials'
    ))
  );

  constructor(private scully: ScullyRoutesService) {}
}

Sorting Routes by Date

import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-recent-posts',
  template: `
    <article *ngFor="let post of recentPosts$ | async">
      <a [routerLink]="post.route">{{ post.title }}</a>
      <time>{{ post.date }}</time>
    </article>
  `
})
export class RecentPostsComponent {
  recentPosts$ = this.scully.available$.pipe(
    map(routes => routes
      .filter(route => route.date)
      .sort((a, b) => 
        new Date(b.date).getTime() - new Date(a.date).getTime()
      )
      .slice(0, 5)
    )
  );

  constructor(private scully: ScullyRoutesService) {}
}

Building a Tag Cloud

import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-tag-cloud',
  template: `
    <div class="tag-cloud">
      <a *ngFor="let tag of tags$ | async" 
         [routerLink]="['/tag', tag.name]">
        {{ tag.name }} ({{ tag.count }})
      </a>
    </div>
  `
})
export class TagCloudComponent {
  tags$ = this.scully.available$.pipe(
    map(routes => {
      const tagMap = new Map<string, number>();
      routes.forEach(route => {
        if (route.tags && Array.isArray(route.tags)) {
          route.tags.forEach(tag => {
            tagMap.set(tag, (tagMap.get(tag) || 0) + 1);
          });
        }
      });
      return Array.from(tagMap.entries())
        .map(([name, count]) => ({ name, count }))
        .sort((a, b) => b.count - a.count);
    })
  );

  constructor(private scully: ScullyRoutesService) {}
}

Error Handling

If the scully-routes.json file is not found, the service logs a warning and returns an empty array:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-route-checker',
  template: `
    <div *ngIf="hasRoutes; else noRoutes">
      Routes loaded successfully
    </div>
    <ng-template #noRoutes>
      <p>No routes found. Are you running the Scully generated version?</p>
    </ng-template>
  `
})
export class RouteCheckerComponent implements OnInit {
  hasRoutes = false;

  constructor(private scully: ScullyRoutesService) {}

  ngOnInit() {
    this.scully.allRoutes$.subscribe(routes => {
      this.hasRoutes = routes.length > 0;
    });
  }
}

Notes

  • All observables use shareReplay with refCount: false, meaning they maintain their last value even without active subscriptions
  • The service automatically handles duplicate routes that may exist due to multiple slugs
  • Routes are loaded from assets/scully-routes.json via HTTP request
  • The service is provided in root, making it a singleton across the application

Source

View source on GitHub

Build docs developers (and LLMs) love