Skip to main content

ScullyRoutesService

The ScullyRoutesService provides methods and observables that give you access to all routes rendered by Scully. This allows you to build dynamic navigation, sitemaps, breadcrumbs, and more based on your generated static routes.

Overview

When Scully builds your site, it creates a scully-routes.json file containing metadata about all generated routes. The ScullyRoutesService loads this file and exposes the route data through RxJS observables, making it easy to integrate route information into your Angular application.

Installation

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

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

ScullyRoute Interface

All routes returned by the service conform to the ScullyRoute interface:
export interface ScullyRoute {
  route: string;              // The route path (e.g., '/blog/my-post')
  title?: string;             // Optional page title
  slugs?: string[];           // Optional array of route slugs
  published?: boolean;        // Publication status (default: true)
  slug?: string;              // Individual slug value
  sourceFile?: string;        // Source markdown/file path
  lang?: string;              // Language code
  [prop: string]: any;        // Custom properties from route config
}

Observables

allRoutes$

Returns all routes generated by Scully, both published and unpublished.
allRoutes$: Observable<ScullyRoute[]>
Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-sitemap',
  template: `
    <ul>
      <li *ngFor="let route of allRoutes$ | async">
        <a [routerLink]="route.route">
          {{ route.title || route.route }}
          <span *ngIf="route.published === false">(Unpublished)</span>
        </a>
      </li>
    </ul>
  `
})
export class SitemapComponent implements OnInit {
  allRoutes$: Observable<ScullyRoute[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.allRoutes$ = this.scullyRoutes.allRoutes$;
  }
}

available$

Returns only published routes (routes where published is not false).
available$: Observable<ScullyRoute[]>
Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-blog-list',
  template: `
    <article *ngFor="let post of blogPosts$ | async">
      <h2>
        <a [routerLink]="post.route">{{ post.title }}</a>
      </h2>
      <p>{{ post.description }}</p>
    </article>
  `
})
export class BlogListComponent implements OnInit {
  blogPosts$: Observable<ScullyRoute[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.blogPosts$ = this.scullyRoutes.available$;
  }
}

unPublished$

Returns only unpublished routes (routes where published is explicitly false).
unPublished$: Observable<ScullyRoute[]>
Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-drafts',
  template: `
    <h2>Draft Posts</h2>
    <div *ngFor="let draft of drafts$ | async">
      <a [routerLink]="draft.route">{{ draft.title }}</a>
      <span class="badge">Draft</span>
    </div>
  `
})
export class DraftsComponent implements OnInit {
  drafts$: Observable<ScullyRoute[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.drafts$ = this.scullyRoutes.unPublished$;
  }
}

topLevel$

Returns only top-level routes (routes without additional path segments).
topLevel$: Observable<ScullyRoute[]>
For example, given these routes:
  • /blog - included (top-level)
  • /about - included (top-level)
  • /blog/post-1 - not included (has subroute)
  • /blog/post-2 - not included (has subroute)
Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-nav',
  template: `
    <nav>
      <a *ngFor="let route of topRoutes$ | async" 
         [routerLink]="route.route"
         routerLinkActive="active">
        {{ route.title || route.route }}
      </a>
    </nav>
  `
})
export class NavComponent implements OnInit {
  topRoutes$: Observable<ScullyRoute[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.topRoutes$ = this.scullyRoutes.topLevel$;
  }
}

Methods

getCurrent()

Returns an observable that emits the route information for the currently active route. Automatically updates when navigation occurs.
getCurrent(): Observable<ScullyRoute>
Returns: An observable that emits the current route’s metadata Example:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-page-header',
  template: `
    <header *ngIf="currentRoute$ | async as route">
      <h1>{{ route.title }}</h1>
      <p *ngIf="route.description">{{ route.description }}</p>
      <div class="meta">
        <span *ngIf="route.author">By {{ route.author }}</span>
        <span *ngIf="route.date">{{ route.date | date }}</span>
      </div>
    </header>
  `
})
export class PageHeaderComponent implements OnInit {
  currentRoute$: Observable<ScullyRoute>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.currentRoute$ = this.scullyRoutes.getCurrent();
  }
}

reload()

Forces a reload of the scully-routes.json file. Useful if routes are added dynamically.
reload(): void
Example:
import { Component } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-admin',
  template: `
    <button (click)="refreshRoutes()">Refresh Routes</button>
    <p>{{ message }}</p>
  `
})
export class AdminComponent {
  message = '';

  constructor(private scullyRoutes: ScullyRoutesService) {}

  refreshRoutes() {
    this.scullyRoutes.reload();
    this.message = 'Routes refreshed!';
  }
}

Common Use Cases

Building a Navigation Menu

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

@Component({
  selector: 'app-navigation',
  template: `
    <nav>
      <a *ngFor="let item of navItems$ | async"
         [routerLink]="item.route"
         routerLinkActive="active">
        {{ item.title }}
      </a>
    </nav>
  `
})
export class NavigationComponent implements OnInit {
  navItems$: Observable<ScullyRoute[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    // Get only routes with a 'nav' property
    this.navItems$ = this.scullyRoutes.available$.pipe(
      map(routes => routes.filter(r => r.nav === true))
    );
  }
}

Creating a Blog Archive

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

interface BlogArchive {
  year: number;
  posts: ScullyRoute[];
}

@Component({
  selector: 'app-blog-archive',
  template: `
    <div *ngFor="let archive of archives$ | async">
      <h2>{{ archive.year }}</h2>
      <ul>
        <li *ngFor="let post of archive.posts">
          <a [routerLink]="post.route">{{ post.title }}</a>
          <span>{{ post.date | date:'MMM d' }}</span>
        </li>
      </ul>
    </div>
  `
})
export class BlogArchiveComponent implements OnInit {
  archives$: Observable<BlogArchive[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.archives$ = this.scullyRoutes.available$.pipe(
      map(routes => {
        // Filter blog posts only
        const blogPosts = routes.filter(r => r.route.startsWith('/blog/'));
        
        // Group by year
        const grouped = blogPosts.reduce((acc, post) => {
          const year = new Date(post.date).getFullYear();
          if (!acc[year]) acc[year] = [];
          acc[year].push(post);
          return acc;
        }, {});
        
        // Convert to array and sort
        return Object.keys(grouped)
          .map(year => ({
            year: +year,
            posts: grouped[year].sort((a, b) => 
              new Date(b.date).getTime() - new Date(a.date).getTime()
            )
          }))
          .sort((a, b) => b.year - a.year);
      })
    );
  }
}

Building Breadcrumbs

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

interface Breadcrumb {
  label: string;
  route: string;
}

@Component({
  selector: 'app-breadcrumbs',
  template: `
    <nav class="breadcrumbs">
      <a *ngFor="let crumb of breadcrumbs$ | async; let last = last"
         [routerLink]="crumb.route"
         [class.active]="last">
        {{ crumb.label }}
        <span *ngIf="!last"> / </span>
      </a>
    </nav>
  `
})
export class BreadcrumbsComponent implements OnInit {
  breadcrumbs$: Observable<Breadcrumb[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.breadcrumbs$ = this.scullyRoutes.getCurrent().pipe(
      map(current => {
        if (!current) return [];
        
        const segments = current.route.split('/').filter(s => s);
        const breadcrumbs: Breadcrumb[] = [{
          label: 'Home',
          route: '/'
        }];
        
        let path = '';
        segments.forEach((segment, index) => {
          path += '/' + segment;
          breadcrumbs.push({
            label: segment.replace(/-/g, ' '),
            route: path
          });
        });
        
        return breadcrumbs;
      })
    );
  }
}

Filtering Routes by Tag

import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-tag-page',
  template: `
    <h1>Posts tagged: {{ tag$ | async }}</h1>
    <article *ngFor="let post of posts$ | async">
      <h2>
        <a [routerLink]="post.route">{{ post.title }}</a>
      </h2>
      <div class="tags">
        <span *ngFor="let tag of post.tags">{{ tag }}</span>
      </div>
    </article>
  `
})
export class TagPageComponent implements OnInit {
  tag$: Observable<string>;
  posts$: Observable<ScullyRoute[]>;

  constructor(
    private route: ActivatedRoute,
    private scullyRoutes: ScullyRoutesService
  ) {}

  ngOnInit() {
    this.tag$ = this.route.params.pipe(map(params => params['tag']));
    
    this.posts$ = this.tag$.pipe(
      switchMap(tag => 
        this.scullyRoutes.available$.pipe(
          map(routes => routes.filter(r => 
            r.tags && Array.isArray(r.tags) && r.tags.includes(tag)
          ))
        )
      )
    );
  }
}

Custom Route Properties

You can add custom properties to routes in your markdown frontmatter or route configuration:
---
title: My Blog Post
date: 2024-01-15
author: John Doe
tags: [angular, scully, ssg]
featured: true
category: tutorials
---

# My Blog Post

Content here...
Access these properties through the ScullyRoute interface:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService, ScullyRoute } from '@scullyio/ng-lib';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-featured-posts',
  template: `
    <section class="featured">
      <article *ngFor="let post of featuredPosts$ | async">
        <h2>{{ post.title }}</h2>
        <p>{{ post.description }}</p>
        <span class="category">{{ post.category }}</span>
      </article>
    </section>
  `
})
export class FeaturedPostsComponent implements OnInit {
  featuredPosts$: Observable<ScullyRoute[]>;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.featuredPosts$ = this.scullyRoutes.available$.pipe(
      map(routes => routes.filter(r => r.featured === true))
    );
  }
}

Error Handling

The service handles missing route files gracefully:
import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Component({
  selector: 'app-route-list'
})
export class RouteListComponent implements OnInit {
  routes$: Observable<any>;
  error: string;

  constructor(private scullyRoutes: ScullyRoutesService) {}

  ngOnInit() {
    this.routes$ = this.scullyRoutes.available$.pipe(
      catchError(err => {
        this.error = 'Failed to load routes';
        console.error('Route loading error:', err);
        return of([]);
      })
    );
  }
}
If the scully-routes.json file is not found, the service logs a warning:
Scully routes file not found, are you running the Scully generated version of your site?

Best Practices

Always use the async pipe in templates to automatically handle subscriptions:
<div *ngFor="let route of routes$ | async">
  {{ route.title }}
</div>
Use shareReplay() when transforming route data to avoid recomputing:
this.featuredPosts$ = this.scullyRoutes.available$.pipe(
  map(routes => routes.filter(r => r.featured)),
  shareReplay(1)
);
Extend routes with custom properties for filtering, sorting, and grouping:
---
title: My Post
category: tutorials
difficulty: beginner
featured: true
---
Always check if a route exists before using it:
getCurrent().pipe(
  filter(route => !!route),
  // ... rest of the pipe
)

Source Code

View the complete source code:

Scully Library

Learn about the Angular library overview

Transfer State

Cache application state during rendering

Build docs developers (and LLMs) love