Skip to main content

ScullyContent Component

The scully-content component is responsible for loading and rendering the static HTML content generated by Scully during the build process.

Overview

This component works by:
  1. Loading the pre-rendered HTML from Scully’s static generation
  2. Injecting it into the DOM
  3. Upgrading internal links to use Angular Router for SPA navigation
  4. Handling route changes to load new content dynamically

Installation

Import the ScullyLibModule in your Angular module:
import { ScullyLibModule } from '@scullyio/ng-lib';

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

Basic Usage

Add the <scully-content> component to your template where you want the static content to appear:
<scully-content></scully-content>
Complete Example:
import { Component } from '@angular/core';

@Component({
  selector: 'app-blog-post',
  template: `
    <article>
      <!-- Scully will inject the rendered markdown here -->
      <scully-content></scully-content>
    </article>
  `,
  styles: [`
    article {
      max-width: 800px;
      margin: 0 auto;
      padding: 2rem;
    }
  `]
})
export class BlogPostComponent { }

Input Properties

localLinksOnly

@Input() localLinksOnly: boolean | ''
Controls the scope of link upgrade behavior. When enabled, only links within the <scully-content> element are upgraded to use Angular Router. When disabled (default), all links in the document are upgraded. Default: false Examples:
<!-- Upgrade only links within the content -->
<scully-content localLinksOnly></scully-content>
<!-- Or use property binding -->
<scully-content [localLinksOnly]="true"></scully-content>
Use Case: This is useful when you have navigation links in your header/footer that you want to control separately:
@Component({
  selector: 'app-layout',
  template: `
    <nav>
      <!-- These won't be affected when localLinksOnly is true -->
      <a routerLink="/home">Home</a>
      <a routerLink="/about">About</a>
    </nav>
    
    <main>
      <!-- Only links here will be upgraded -->
      <scully-content localLinksOnly></scully-content>
    </main>
  `
})
export class LayoutComponent { }

How It Works

Content Loading

The component loads content in different ways depending on the context: 1. Initial Static Rendering (during Scully build):
// Content is available in window.scullyContent
window.scullyContent = {
  html: '<p>Your rendered content</p>',
  cssId: '_ngcontent-c0'
};
2. On Hydrated Application: Fetches from [currentRoute]/index.html to get the pre-rendered content. 3. In Development Mode: If the production file isn’t found, falls back to Scully’s dev server at http://localhost:1668. The component automatically upgrades <a href> links to use Angular Router:
<!-- Original markdown/HTML -->
<a href="/blog/another-post">Read More</a>

<!-- After upgrade -->
<a href="/blog/another-post" (click)="navigateWithRouter($event)">
  Read More
</a>
Only links found in the scully-routes.json file are upgraded. External links remain unchanged.

Advanced Usage

Custom Styling

Style the content using CSS selectors:
@Component({
  selector: 'app-blog-post',
  template: `
    <div class="post-container">
      <scully-content></scully-content>
    </div>
  `,
  styles: [`
    .post-container {
      /* Styles for the container */
    }
    
    /* Target content rendered by Scully */
    .post-container ::ng-deep h1 {
      color: #333;
      font-size: 2.5rem;
    }
    
    .post-container ::ng-deep pre {
      background: #f5f5f5;
      padding: 1rem;
      border-radius: 4px;
    }
    
    .post-container ::ng-deep img {
      max-width: 100%;
      height: auto;
    }
  `]
})
export class BlogPostComponent { }

With Loading State

import { Component, OnInit } from '@angular/core';
import { ScullyRoutesService } from '@scullyio/ng-lib';

@Component({
  selector: 'app-blog-post',
  template: `
    <div *ngIf="isLoading" class="loading">
      <p>Loading content...</p>
    </div>
    
    <article [class.hidden]="isLoading">
      <header *ngIf="currentRoute">
        <h1>{{ currentRoute.title }}</h1>
        <time>{{ currentRoute.date | date }}</time>
      </header>
      
      <scully-content></scully-content>
    </article>
  `,
  styles: [`
    .hidden { display: none; }
    .loading { 
      text-align: center; 
      padding: 2rem;
    }
  `]
})
export class BlogPostComponent implements OnInit {
  isLoading = true;
  currentRoute: any;

  constructor(private scully: ScullyRoutesService) {}

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

With Metadata Display

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

@Component({
  selector: 'app-blog-post',
  template: `
    <article *ngIf="route">
      <header>
        <h1>{{ route.title }}</h1>
        <div class="meta">
          <time>{{ route.date | date:'fullDate' }}</time>
          <span *ngIf="route.author">by {{ route.author }}</span>
          <div class="tags">
            <span *ngFor="let tag of route.tags" class="tag">
              {{ tag }}
            </span>
          </div>
        </div>
      </header>
      
      <scully-content></scully-content>
      
      <footer *ngIf="route.sourceFile">
        <a [href]="getGitHubUrl(route.sourceFile)" target="_blank">
          Edit on GitHub
        </a>
      </footer>
    </article>
  `
})
export class BlogPostComponent implements OnInit {
  route?: ScullyRoute;

  constructor(private scully: ScullyRoutesService) {}

  ngOnInit() {
    this.scully.getCurrent().subscribe(route => {
      this.route = route;
    });
  }
  
  getGitHubUrl(sourceFile: string): string {
    return `https://github.com/username/repo/edit/main/${sourceFile}`;
  }
}

Handling Multiple Content Sections

import { Component } from '@angular/core';

@Component({
  selector: 'app-documentation',
  template: `
    <div class="docs-layout">
      <aside class="sidebar">
        <app-doc-navigation></app-doc-navigation>
      </aside>
      
      <main class="content">
        <!-- Scope link upgrades to just this section -->
        <scully-content [localLinksOnly]="true"></scully-content>
      </main>
      
      <aside class="toc">
        <app-table-of-contents></app-table-of-contents>
      </aside>
    </div>
  `
})
export class DocumentationComponent { }

Important Notes

Does Not Work Inside *ngIf

The component will not work if placed inside an *ngIf directive:
<!-- ❌ INCORRECT - Will not work -->
<div *ngIf="showContent">
  <scully-content></scully-content>
</div>
Solution: Use CSS to hide/show instead:
<!-- ✅ CORRECT - Use CSS visibility -->
<div [class.hidden]="!showContent">
  <scully-content></scully-content>
</div>
.hidden {
  display: none;
}

Content Markers

Scully wraps content with HTML comments:
<!--scullyContent-begin-->
<!-- Your rendered content here -->
<!--scullyContent-end-->
These markers help the component identify and extract the static content.

Error Handling

The component displays error messages when content cannot be loaded:
<h2 id="___scully-parsing-error___">
  Sorry, could not load static page content
</h2>
You can detect these errors in your CI/CD:
// In your E2E tests
it('should not have Scully parsing errors', () => {
  cy.visit('/blog/my-post');
  cy.get('#___scully-parsing-error___').should('not.exist');
});

Configuration

Configure the component behavior through ScullyLibModule.forRoot():
import { ScullyLibModule } from '@scullyio/ng-lib';

@NgModule({
  imports: [
    ScullyLibModule.forRoot({
      useTransferState: true,
      baseURIForScullyContent: 'http://localhost:1668'
    })
  ]
})
export class AppModule { }

Configuration Options

export interface ScullyLibConfig {
  useTransferState?: boolean;        // Enable transfer state (default: true)
  alwaysMonitor?: boolean;           // Always monitor for idle (default: false)
  manualIdle?: boolean;              // Use manual idle detection (default: false)
  baseURIForScullyContent?: string;  // Dev server URL (default: 'http://localhost:1668')
}

Component Lifecycle

  1. Constructor: Service initialization
  2. ngOnInit: Content loading begins
  3. handlePage: Fetches and injects static HTML
  4. upgradeToRoutelink: Upgrades links to use Router
  5. routeUpdates$: Monitors navigation for content updates
  6. ngOnDestroy: Cleanup and unsubscribe

TypeScript Interface

@Component({
  selector: 'scully-content',
  template: '<ng-content></ng-content>',
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  preserveWhitespaces: true
})
export class ScullyContentComponent implements OnInit, OnDestroy {
  @Input() set localLinksOnly(value: boolean | '');
  
  ngOnInit(): void;
  ngOnDestroy(): void;
}

Source

View source on GitHub

Build docs developers (and LLMs) love