// Tutorial //

How To Build a Jamstack Portfolio with Angular 11 and Scully

Published on December 15, 2021
Default avatar
By zramcooper
Developer and author at DigitalOcean.
How To Build a Jamstack Portfolio with Angular 11 and Scully

The author selected /dev/color to receive a donation as part of the Write for DOnations program.

Introduction

As a developer, there are many ways you can demonstrate your skills, accomplishments, and work. These include open source contributions, personal projects, speaking engagements, blog posts, etc. However, doing all this may be in vain if your work is scattered on multiple platforms and difficult to find when people look you up. Not having a central place to showcase your achievements can work against you and may cause potential clients, recruiters, and employers to underestimate your worth. A portfolio allows you to put your best work forward, makes your accomplishments easy to find, helps you brand yourself, and facilitates connections that lead to potentially lucrative opportunities. Pairing your portfolio with a blog gives you the means to share your thoughts, document what you learn, and further build your credibility.

Using their wide range of robust features, you can build an engaging and fast portfolio using Angular and Scully. Angular is a versatile platform that allows you to create everything from web to native and mobile apps. It provides various useful tools that simplify app development, resulting in faster apps with great performance.

Angular apps can become even faster by making them static. Using Scully, you can convert Angular apps to quicker-to-deliver Jamstack apps. Scully is a static site generator that creates a static HTML page for every route in your Angular application. These pages are faster to deliver and are effective for SEO. It also offers tools like a blog generator that you can make your portfolio blog with.

In this tutorial, you will build a portfolio and a blog using Angular 11 and Scully. You will generate an Angular app, add pages to show your projects and profile, and add services to populate these pages. Additionally, you will generate a blog and create posts for it. Lastly, you will convert the app into a static site using Scully.

Prerequisites

To complete this tutorial, you will need:

Step 1 — Setting Up the Angular App

In this step, you will generate the portfolio app using the Angular CLI. The CLI will scaffold the app and install all necessary dependencies to run it. You will then add dependencies like Bootstrap, Font Awesome, and Scully. Scully will make the app static. Bootstrap will provide components and styling. Font Awesome will supply icons. After installing these dependencies, you will add assets like fonts and JSON files that contain your portfolio data.

To begin, run the following command to generate the app. It will be called portfolio. This tutorial does not include tests for the app, so you can use the -S flag to skip test file generation. If you wish to add tests later, you can exclude this flag.

  1. ng new portfolio -S

When asked whether you’d like to add Angular routing respond with yes. This tells the CLI tool to generate a separate module to handle routing for the app. It’s going to be available at src/app/app-routing.module.ts.

You will also be prompted to pick a stylesheet format. Select CSS. Angular offers other styling options like SCSS, Sass, Less, and Stylus. CSS is simpler and that’s why you’re going to use it here.

Output
? Would you like to add Angular routing? Yes ? Which stylesheet format would you like to use? (Use arrow keys) ❯ CSS

Installing Dependencies

You will need three dependencies for this app: Scully, ng-bootstrap, and Font Awesome. Scully will convert the Angular app into a static one. The other dependencies, Bootstrap and Font Awesome, customize the look and feel of the portfolio. ng-bootstrap will provide Angular components styled using Bootstrap. This is especially useful since not all vanilla Bootstrap components work out of the box with Angular. Bootstrap also cuts down on the amount of styling you’d have to add to the application because it already provides it for you. Font Awesome will supply icons.

When you add Scully as a dependency, it runs its init schematic which adds changes to your app in preparation for static site generation. From your project directory, use this command to add Scully to your project:

  1. ng add @scullyio/init@2.0.0

Next, add ng-bootstrap using this command.

  1. ng add @ng-bootstrap/ng-bootstrap@9.1.3

The final dependency to add is Font Awesome.

  1. npm install --save @fortawesome/fontawesome-free@5.15.4

Now that the dependencies are added, you’re ready to add configuration.

Adding Configuration

To make Font Awesome available to the app, you will need to add a reference to its minified CSS in the angular.json file. This file can be found at the base of the project. Using nano or your favorite text editor, open this file:

  1. nano angular.json

In the file, look for the architect/build section. This section provides the configuration for the ng build command. You will add the minified Font Awesome CSS reference, node_modules/@fortawesome/fontawesome-free/css/all.min.css, to the styles array. The styles array is under projects/portfolio/architect/build/options. Add the highlighted line to your file:

angular.json
{
  ...
  "projects": {
    "portfolio": {
      ...
      "architect": {
        "build": {
          ...
          "options": {
              ...
              "styles": [
                  "node_modules/bootstrap/dist/css/bootstrap.min.css",
                  "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
                  "src/styles.css"
              ],
          }
        },
        ...
      }
    }
  }
}

Now Font Awesome will be available to the build.

Save and close the file.

Next, you’ll add a new font to the project, which can help personalize the look and feel of your portfolio. You can add fonts to your Angular project using Google Fonts, which provides a wide range of available fonts. You can link to selected fonts in the head tag using the link tag.

In this tutorial, you will use Nunito font. Open src/index.html and add the lines highlighted below:

src/index.html
... 
<head>
  <meta charset="utf-8">
  <title>Portfolio</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;400;800&display=swap" rel="stylesheet">
</head>
... 

The highlighted lines link to the Nunito font on Google Fonts. You’ll be getting it in three weights: extra light, regular, and extra bold.

Save and close the file.

Adding Bio and Project Data

The next thing you’ll do is create the JSON files that hold all the data you’d like to put in your portfolio. Separating the templates from the data makes it easier to make changes and add more things in the future without tampering with the app.

Begin by creating a json folder in src/assets.

  1. mkdir src/assets/json

In the json folder, you will create two JSON files: bio.json and projects.json file. bio.json holds the profile you’d like to display on the site. projects.json is a list of projects you’d like to showcase.

The structure of the bio.json file will look similar to this:

src/assets/json/bio.json
{
    "firstName": "Jane",
    "lastName": "Doe",
    "intro": [ "paragraph 1", "paragraph 2" ],
    "about": [ "paragraph 1", "paragraph 2" ]
}

The intro is a short introduction displayed on the home page. The about is a more extended profile shown on the “About” page.

For this tutorial, you can use an example bio or customize your own. To use a sample bio, open bio.json and add the following:

src/assets/json/bio.json
{
    "firstName": "Jane",
    "lastName": "Doe",
    "intro": [
        "I'm a software developer with a passion for web development. I am currently based somewhere in the world. My main focus is building fast, accessible, and beautiful websites that users enjoy.",
        "You can have a look at some of my work here."
    ],
    "about": [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam aliquam auctor fringilla. Proin scelerisque lacinia nisl vel ultrices. Ut gravida finibus velit sit amet pulvinar. Nunc nisi arcu, pretium quis ultrices nec, volutpat sit amet nulla. Mauris semper elementum placerat. Aenean velit risus, aliquet quis lectus id, laoreet accumsan erat. Curabitur varius facilisis velit, et rutrum ligula mollis et. Sed imperdiet sit amet urna ut eleifend. Suspendisse consectetur velit nunc, at fermentum eros volutpat nec. Vivamus scelerisque nec turpis volutpat sagittis. Aenean eu sem et diam consequat euismod.",
        "Mauris dolor tellus, sagittis vel pellentesque sit amet, viverra in enim. Maecenas non lectus eget augue convallis iaculis mattis malesuada nisl. Suspendisse malesuada purus et luctus scelerisque. Cras hendrerit, eros malesuada blandit scelerisque, nulla dui gravida arcu, nec maximus nunc felis sit amet mauris. Donec lorem elit, feugiat sit amet condimentum quis, consequat id diam. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras rutrum sodales condimentum. Aenean ultrices mi vel augue dapibus mattis. Donec ut ornare nisl. Curabitur feugiat pharetra dictum."
    ]
}

Save and close the file when you’re done making edits.

The other JSON file, projects.json, will have a structure similar to this:

src/assets/json/projects.json
[
    {
        "name": "",
        "stack": {
            "name": "Vue.js",
            "iconClasses": "fab fa-vuejs"
        },
        "description": "",
        "sourceUrl": "",
        "previewUrl": "",
        "featured": false
    }
]

Each project has a name, description, a URL to where its source code is hosted, and a preview URL if it is deployed somewhere. If the project does not have a preview URL, you can just omit it.

The stack object is used to show the language or framework the project was built off of. So the name will be the name of the language/framework and the iconClasses are the Font Awesome CSS classes for the icon of the language/framework. The featured property indicates whether the project should be displayed on the home page. If featured is set to false, it’s displayed only on the “Projects” page instead of on both the home and “Projects” pages.

For this tutorial, you can use some example projects or add your own. To use sample projects, open projects.json and add the following:

src/assets/json/projects.json
[
    {
        "name": "Soduko",
        "stack": {
            "name": "Angular",
            "iconClasses": "fab fa-angular"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    },
    {
        "name": "E-commerce Store",
        "stack": {
            "name": "React",
            "iconClasses": "fab fa-react"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    },
    {
        "name": "Algorithm Visualization App",
        "stack": {
            "name": "Angular",
            "iconClasses": "fab fa-angular"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    },
    {
        "name": "Time Tracking CLI App",
        "stack": {
            "name": "Node.js",
            "iconClasses": "fab fa-node-js"
        },
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
        "sourceUrl": "https://github.com",
        "previewUrl": "https://github.com",
        "featured": true
    }
]

Save and close the file.

Starting the Server

To check that your app works as expected, run the server provided by the Angular CLI using this command:

  1. ng serve

This command builds the app and serves it. If you make any changes, it rebuilds it. Once completed, the app will be served at http://localhost:4200/.

Open your browser and navigate to http://localhost:4200. You should see a placeholder page that looks like the following:

Image displayed when the app is started

You should see output similar to this every time you save a change:

✔ Browser application bundle generation complete.

Initial Chunk Files           | Names                      |      Size
vendor.js                     | vendor                     |   3.83 MB
styles.css                    | styles                     | 202.25 kB
polyfills.js                  | polyfills                  | 141.85 kB
main.js                       | main                       |  26.08 kB
runtime.js                    | runtime                    |   9.06 kB

                              | Initial Total              |   4.20 MB

Build at:  - Hash:  - Time: 13312ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **

✔ Compiled successfully.

Once you complete a step, check that the ✔ Compiled successfully. message is present. If there is a problem, the ng serve command will output an error. When this happens, run through the step to make sure you did not miss anything or make a mistake. Once you’ve completed the tutorial, the portfolio home page should look something like this:

Screenshot of completed portfolio home page

In this step, you generated the app and added all the necessary dependencies, assets, and configurations. You have also started the server provided by the Angular CLI. In the next step, you will create the core module.

Step 2 — Creating the Core Module

In this tutorial, the app you are building will contain three modules: core, blog, and portfolio. The app will be structured as follows:

src/app
src/app
├── blog
├── core
└── portfolio 

The blog module is for the blog landing and blog post pages. The core module contains everything central to the app. These include the header component, data services, and data models. The portfolio module holds all your portfolio pages: the “About,” “Projects,” and home page.

In this step, you will generate the core module. You will also generate and populate the header component, the services, and the data models. The header is displayed at the top of each page and contains the site name and menu. The models structure the portfolio data. The services fetch the portfolio data.

This is what the core module should look like:

src/app/core/
src/app/core
├── header
├── models
└── services

To generate the core module, run the following command from the project root:

  1. ng generate module core

This command adds the core module within the src/app/core folder and adds the module file at src/app/core/core.module.ts.

The core module file defines the core module and its imports. In the newly generated module file, you will need to add a few imports to support the header and the services.

Open core.module.ts and add the highlighted lines (be sure to include the comma after the CommonModule import):

src/app/core/core.module.ts
...
import { RouterModule } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [],
  imports: [
    CommonModule,
    RouterModule,
    NgbModule,
    HttpClientModule
  ]
})
...

This module will use the HttpClientModule to fetch data from the JSON files you created earlier. It will also use a couple of ng-bootstrap components from NgbModule and the RouterModule for routing. You will also need to add them to the imports of the CoreModule.

Save and close the file when you’re done.

Generating Data Models

In this section, you will generate data models. The data models are interfaces that you’ll use to define the structure of data from the JSON files. They will be used in the services and throughout the rest of the components as return and parameter types. You will need two models: bio, which defines the structure of your bio data, and project, which defines the structure of your project data.

src/app/core/models
src/app/core/models
├── bio.ts
└── project.ts

The Bio model represents a profile while the Project model is a project to showcase. You will generate both models by running the following command at the project root:

  1. for model in bio project ; do ng generate interface "core/models/${model}"; done

This command loops through the file paths and passes them to ng generate interface, which creates them in the src/app/core/models folder.

The bio model file defines the information you’d like your bio to contain. In the src/app/core/models/bio.ts file that’s generated, add the fields that are highlighted below.

src/app/core/models/bio.ts
export interface Bio {
    firstName: string;
    lastName: string;
    about: string[];
    intro: string[];
}

In this code block, you added the first name, last name, about, and introduction fields to the bio model. The first two fields are string types and the last two are arrays of strings because they might contain multiple paragraphs.

Save and close the file.

The project file defines the structure of a project. Here, you will list the fields you’d like to use for each project. In the src/app/core/models/project.ts file, add the lines that are highlighted.

src/app/core/models/project.ts
export interface Project {
    name: string;
    stack: { iconClasses: string, name: string };
    description: string;
    sourceUrl: string;
    previewUrl: string;
    featured?: boolean;
}

You’ve added fields for the project model. Each project has a name, description, a URL to its source code, and a preview URL if it is deployed somewhere. The stack object is used to show the language or framework of the project. (The name will be the name of the language/framework and the iconClasses are the Font Awesome CSS classes for the icon of the language/framework.) The featured property indicates whether the project should be displayed on the home page. If featured is set to false, it’s displayed on the “Projects” page only instead of both the home and “Projects” pages.

Save and close the file when you’re done.

In this section, you created the models for the data. Next, you will make services that fetch the portfolio data and use these models.

Generating Services

The services that you will create in this section fetch the data from the JSON files you made earlier. Once they fetch this data, components can call these services and consume the data. The models will be used as return types in these services. The bio model is used in the bio service and the project model is used in the project service. You’ll include an additional header service that will help decide what routes to use for items in the header and other components. The core module has three services: BioService, HeaderService, and ProjectsService.

src/app/core/services
src/app/core/services
├── bio.service.ts
├── header.service.ts
└── projects.service.ts

To generate these services, run this command from the project’s root directory:

  1. for service in bio projects header; do ng generate service "core/services/${service}"; done

This command loops through the file paths and passes them to ng generate service, which creates them in the src/app/core/services folder.

The bio service fetches your bio data from the bio JSON file. To do this, you will add a method to fetch this data. Open the src/app/core/services/bio.service.ts file and add the following highlighted lines:

src/app/core/services/bio.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Bio } from '../models/bio';

@Injectable({
  providedIn: 'root'
})
export class BioService {

  constructor(private http: HttpClient) { }

  getBio() {
   return this.http.get<Bio>('assets/json/bio.json');
  }
}

The getBio method of the BioService fetches your bio from the assets/json/bio.json file. You’ll inject the HttpClient service into its constructor and use that in the getBio() method to make a GET request to the file.

Save and close the file.

Next, you will modify the HeaderService. The header service is used to check whether the current route is the home page. You will add a method that determines whether the current page is the home page. Open the src/app/core/services/header.service.ts file and add the highlighted lines:

src/app/core/services/header.service.ts
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { filter, map, startWith } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class HeaderService {

  constructor(private router: Router) { }

  isHome() {
    return this.router.events.pipe(
      filter(event => event instanceof NavigationEnd),
      map(event => {
        if (event instanceof NavigationEnd) {
          if (this.checkForHomeUrl(event.url)) {
            return true;
          }
        }

        return false;
      }),
      startWith(this.checkForHomeUrl(this.router.url))
    );
  }

  private checkForHomeUrl(url: string): boolean {
    return url.startsWith('/#') || url == '/';
  }
}

In the HeaderService, the isHome method checks whether the current page you are on is the home page. This is useful for scrolling to an anchor and showing featured projects on the home page.

Save and close the file.

Finally, you will modify ProjectsService. The projects service fetches the project data from the projects JSON file. You will add a method to fetch the projects data. Open the src/app/core/services/projects.service.ts file and change the contents to the following:

src/app/core/services/projects.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { filter, mergeAll, toArray } from 'rxjs/operators';
import { Project } from '../models/project';

@Injectable({
  providedIn: 'root'
})
export class ProjectsService {

  constructor(private http: HttpClient) { }

  getProjects(featured?: boolean): Observable<Project[]> {
    let projects$ = this.http.get<Project[]>('assets/json/projects.json');

    if (featured) {
      return projects$.pipe(
        mergeAll(),
        filter(project => project.featured || false),
        toArray()
      );
    }

    return projects$;
  }
}

The ProjectsService has a getProjects method that gets and filters projects. It gets the projects from the assets/json/projects.json file. You’ll inject the HttpClient service into its constructor and use that in the getProjects() method to make a GET request to the file. Using the featured parameter, you can choose to only return featured projects for brevity. This is useful on the home page when you only want to show important projects.

Save and close the file.

In this section, you added the bio and project data services that fetch bio and project data from the bio and projects JSON files. You also created a header service that checks whether the current page is the home page. In the next section, you will create a header that will be displayed at the top of each page of your portfolio. It will use the bio and header services.

Generating the Header

The header component is displayed at the top of all pages. It contains your name and links to the “About” and “Projects” pages as well as the blog. The bio and header service will be used in the header. The bio service will provide bio data to the header. The header service will be used to check whether the current page is the home page and will set links to the “About” and “Projects” sections or pages based on that. You will generate it by running this command from the project root:

  1. ng generate component core/header

The header component is displayed at the top of each page. It will use the bio service to get your first and last name. It will also use the header service to determine whether the current page is the home page. Using this information, it will set links to the “About” and “Projects” sections or pages.

In the header.component.ts file, you will inject the bio and header services and add a styling property to handle the responsiveness of the component on different screen sizes.

Open header.component.ts and add the highlighted lines:

src/app/core/header/header.component.ts
import { Component } from '@angular/core';
import { BioService } from '../services/bio.service';
import { HeaderService } from '../services/header.service';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {
  bio$ = this.bioService.getBio();
  isHome$ = this.headerService.isHome();

  menuItems = [
    { title: 'About Me', homePath: '/', fragment: 'about', pagePath: '/about' },
    { title: 'My Projects', homePath: '/', fragment: 'projects', pagePath: '/projects' },
    { title: 'My Blog', homePath: '/blog', fragment: '', pagePath: '/blog' }
  ];

  constructor(private bioService: BioService, private headerService: HeaderService) { }
}

In this component file, you will inject two services: the bioService to get your name from the bio JSON file and the headerService to figure out if the page currently displayed is the home page. The latter allows you to decide whether the buttons should go to a separate page like /projects or perform anchor scrolling like with /#project. menuItems contains all the menu items to display. The bio$ and isHome$ properties hold observables from the aforementioned services.

Save and close the file.

Next, you will modify the template for the header component. Here is where the data fetched from the bio service is displayed. Links to the “About” and “Projects” sections or pages are also added here. In the src/app/core/header/header.component.html template file, add the code below.

src/app/core/header/header.component.html
<div class="d-flex min-vh-10 w-100 justify-content-center pb-3 pt-3 pr-4 pl-4">
    <div class="d-flex justify-content-start" *ngIf="bio$ | async as bio" routerLink="/">
        <h2 class="font-weight-bold">{{bio.firstName}}</h2>
        <h2 class="font-weight-light">{{bio.lastName}}</h2>
    </div>
    <div class="d-none d-md-flex flex-grow-1 justify-content-end align-items-start">
        <button type="button" class="ml-2 mr-2 btn btn-outline-dark border-0 font-weight-bold"
            *ngFor="let item of menuItems" [routerLink]="(isHome$ | async) ? item.homePath : item.pagePath"
            [fragment]="(isHome$ | async) ? item.fragment : ''">{{item.title}}</button>
    </div>
    <div class="d-flex d-md-none justify-content-end flex-grow-1">
        <div ngbDropdown class="d-inline-block" display="dynamic" container="body">
            <button class="btn btn-outline-dark border-0" ngbDropdownToggle>
                <i class="fas fa-lg fa-bars"></i>
            </button>
            <div ngbDropdownMenu class="dropdown-menu dropdown-menu-right">
                <button ngbDropdownItem *ngFor="let item of menuItems"
                    [routerLink]="(isHome$ | async) ? item.homePath : item.pagePath"
                    [fragment]="(isHome$ | async) ? item.fragment : ''">{{item.title}}</button>
            </div>
        </div>
    </div>
</div>

In the template, your names (bio.firstName and bio.lastName) are displayed using data from the bio property. Depending on the size of the screen, either a dropdown or a list of buttons from menuItems is shown. The alias pipe in the template will handle unsubscriptions from the observables. This pattern will be followed throughout this tutorial.

Save and close the file.

The header has to be visible on all pages. To make this happen, you will need to take a couple of steps. First, CoreModule needs to export HeaderComponent to make it accessible. To export it, add the highlighted lines to src/app/core/core.module.ts. Don’t forget to add the comma after the imports array.

src/app/core/core.module.ts
...
@NgModule({
  ...
  
  imports: [
    ...
  ],
  exports: [ HeaderComponent ]
})
...

To make the header visible, you will also need to add it to the AppComponent template, which is in the AppModule. AppModule also has to import CoreModule to have access to the header. You will complete these additional tasks in a later step.

In this step, you created models that organize your portfolio data. You also created services to fetch the portfolio data using the models. Additionally, you made a header service that helps decide what routes to use for header items. Lastly, you generated a header component to be displayed on all portfolio pages. In the next step, you will generate the portfolio module, which contains all the primary pages of your portfolio. The pages in the portfolio module will use the bio and projects services and models you created in this section.

Step 3 — Generating the Portfolio Module

In the previous step, you created the core module, which holds the header and contains all the services and models that you will use to fetch the portfolio data. In this step, you will generate the portfolio module, which contains all the essential pages of the portfolio. These include the home, “About,” and “Projects” pages. You will use the services and models you made in the core module to create these pages in this step. You will also add routes for each of the pages.

The home page will display a summary of your profile using the header and bio services. The “About” page is a more in-depth profile, and will use the bio service and model. The “Projects” page showcases your projects, using the projects service and model to display your projects. At the end of this step, your portfolio module will be structured as follows:

src/app/portfolio
src/app/portfolio
├── about
├── home
└── projects

First, you will generate two modules: a portfolio module and a portfolio routing module. The portfolio module contains all the primary pages of your portfolio. The portfolio routing module is responsible for routing to these pages.

To generate both modules, run this command from the project root:

  1. ng generate module portfolio --module app --routing --route portfolio

This command creates the app/portfolio folder and adds a module file at app/portfolio/portfolio.module.ts. You will see this route added in app/src/app-routing.module.ts. The --routing flag specifies that the portfolio routing module be generated. This routing module will be located at app/portfolio/portfolio-routing.module.ts.

The --route flag creates a lazy-loaded route in the app module, as specified by the --module flag. You will see this route added in app/src/app-routing.module.ts. It also adds a placeholder component for routing purposes that’s discussed in the next section.

This portfolio module should be available at the / path. This requires that you supply the --route flag with an empty string, like --route="". However, ng generate module doesn’t allow empty strings for the --route flag. So you will have to use a placeholder, portfolio. You will then replace this placeholder with an empty string in src/app/app-routing.module.ts, which handles routing for the whole app.

Open src/app/app-routing.module.ts and replace the highlighted lines:

src/app/app-routing.module.ts
...
const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./portfolio/portfolio.module').then(m => m.PortfolioModule)
  }
];
...

This ensures that all the pages in the portfolio module are available starting at the / path.

Save and close the file.

Creating the Home Page

The command that creates the portfolio module also creates a PortfolioComponent. This is a placeholder component that’s used when you are setting routing for the module. However, a more appropriate name for this component would be HomeComponent. The home page is the landing page of your portfolio. It will have a summary of your whole portfolio. This makes it easier for users to get an overview of your work without having to navigate to multiple pages, reducing the risk of losing interest.

To change the name of this component, you will first create a new folder to house it. From the project root, run the following command:

  1. mkdir -p src/app/portfolio/home

Next, you will move all the PortfolioComponent files into this new folder.

  1. mv src/app/portfolio/portfolio.component.* src/app/portfolio/home/

This command moves all the files with names that begin with portfolio.component.* into the src/app/portfolio/home/ folder.

You will then rename the portfolio.component.* files to home.component.*.

  1. find src/app/portfolio/home -name 'portfolio*' -exec bash -c ' mv $0 ${0/\portfolio./home.}' {} \;

After you run the above commands, you will get some errors because of the change in the component’s name and path. You have to make some changes to a couple of files to fix this: the portfolio routing module, the portfolio module, and the home component files. In these files, you will change all instances of PortfolioComponent to HomeComponent. You will also update the paths from ./portfolio.component to ./home/home.component.

Start by opening src/app/portfolio/portfolio-routing.module, which handles routing for the portfolio module. Make the highlighted changes:

src/app/portfolio/portfolio-routing.module
...
import { HomeComponent } from './home/home.component';

const routes: Routes = [{ path: '', component: HomeComponent }];
...

Save and close the file.

Next, open src/app/portfolio/portfolio.module.ts, the portfolio module file. Make the highlighted changes:

src/app/portfolio/portfolio.module.ts
...
import { HomeComponent } from './home/home.component';


@NgModule({
  declarations: [
    HomeComponent
  ],
  ...
})
...

Save and close the file.

Finally, open src/app/portfolio/home/home.component.ts, the home component file. Make the highlighted changes:

src/app/portfolio/home/home.component.ts
...
@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
...
}

Save and close the file.

In these files, you changed all instances of PortfolioComponent to HomeComponent and updated the paths to point to HomeComponent. After doing all this, the portfolio module should look like this.

src/app/portfolio
src/app/portfolio
├── home
│   ├── home.component.css
│   ├── home.component.html
│   └── home.component.ts
├── portfolio-routing.module.ts
└── portfolio.module.ts

You’ve now updated the names and paths to the home component files.

Next, you will populate the home component template with content and style it. The home component is the main page of the portfolio and displays a profile summary. (This is the component that was renamed from portfolio component to home component above.) In this component, you will need to fetch the bio data to display and add styling to make the page responsive on different screen sizes.

Open src/app/portfolio/home/home.component.ts and update the code to match the following:

src/app/portfolio/home/home.component.ts
import { Component } from '@angular/core';
import { BioService } from '../../core/services/bio.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {
  bio$ = this.bioService.getBio();

  respOptions = [
    { viewClasses: 'd-none d-md-flex', headingClass: 'display-3', useSmallerHeadings: false },
    { viewClasses: 'd-flex d-md-none', headingClass: '', useSmallerHeadings: true }
  ];

  constructor(private bioService: BioService) { }
}

The home page will display your name and a short bio, which is retrieved from the BioService you inject here. Once you call its getBio method, the resultant observable will be stored in the bio$ property. The respOptions property stores config that helps ensure that the view is responsive.

Save and close the file.

Next, you will modify the template for the home component. It is responsible for displaying the information from the bio service across responsively different screen sizes. You will add your name, a brief intro, and the about and projects components that will be covered later.

Open src/app/portfolio/home/home.component.html and add the following code:

src/app/portfolio/home/home.component.html
<div class="d-flex flex-column justify-content-center align-items-center w-100" *ngIf="bio$ | async as bio">
    <div class="d-flex flex-column min-vh-95 justify-content-center align-items-center w-100">
        <div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
            class="flex-column justify-content-center align-items-start w-75">
            <h1 [ngClass]="options.headingClass" class="text-left">Hello, 👋. My name is <span
                    class="font-weight-bold">{{bio.firstName+'
                    '+bio.lastName}}.</span></h1>
            <div *ngFor="let par of bio.intro">
                <h2 class="text-left" *ngIf="!options.useSmallerHeadings">{{par}}</h2>
                <h5 class="text-left" *ngIf="options.useSmallerHeadings">{{par}}</h5>
            </div>
            <button class="mt-3 mb-5 btn btn-outline-dark" routerLink="/" fragment="projects">
                See My Work
                <i class="ml-1 fas fa-angle-right"></i>
            </button>
        </div>
    </div>

    <div class="d-none d-md-block mt-5"></div>
    <app-about id="about" class="mb-3"></app-about>

    <div class="d-none d-md-block mt-5"></div>
    <app-projects id="projects" class="mb-5"></app-projects>
</div>

In this template, you display the names bio.firstName+ and +bio.lastName as well as an introduction, bio.intro from the bio. You are also showing the about component app-about and the projects component app-projects, which you will generate in the next step.

Note: There are a few other components added to this template that do not exist yet. These are the About and Projects components. These are what you will add next. If you are running the server, this will generate an error. You can comment these lines out until after you’ve generated them.

src/app/portfolio/home/home.component.html
...
<app-about id="about" class="mb-3"></app-about>

...

<app-projects id="projects" class="mb-5"></app-projects>
...

Next, you can add styling for the home component. Open src/app/portfolio/home/home.component.css and add these lines:

src/app/portfolio/home/home.component.css
.min-vh-95 {
    height: 95vh;
}

Here you are adding styling to the home component so that there is some space between the main contents of the home page and the edges of the browser window.

Once completed, the home page will look like this (you’ll be able to preview the site in the last step):

Home Page

In this step, you created the home page that displays a summary of your portfolio. In the next section, you will generate the “About” and “Project” components. These will be displayed on the home page and will be used as standalone pages as well.

Generating the About and Project Pages

Instead of generating every page individually, you can run a single command to make the remaining “Projects” and “About” pages all at once. You do this by running the following command from the project root:

  1. for page in about projects; do ng generate component "portfolio/${page}"; done

This command will loop through each of the page names and generate them.

Populating the About Page

The “About” page will display a more in-depth profile of you. The information on this page is retrieved from the bio service and uses the bio model as well. This component will be displayed on the home page. It will also be a standalone page with its own route.

To populate the “About” page with your bio, you will modify the “About” component file to use the bio service. You will also set options to make the page responsive across different displays. Open src/app/portfolio/about/about.component.ts and add the highlighted lines:

src/app/portfolio/about/about.component.ts
import { Component } from '@angular/core';
import { BioService } from '../../core/services/bio.service';

@Component({
  selector: 'app-about',
  templateUrl: './about.component.html',
  styleUrls: ['./about.component.css']
})
export class AboutComponent {
  bio$ = this.bioService.getBio();

  respOptions = [
    { viewClasses: 'd-none d-md-flex', headingClass: 'display-3', useSmallerHeadings: false },
    { viewClasses: 'd-flex d-md-none', headingClass: '', useSmallerHeadings: true }
  ];

  constructor(private bioService: BioService) { }
}

The “About” information will come from the BioService, and once its getBio method is called, the observable will be stored in the bio$ property. respOptions helps with responsiveness by providing optional CSS classes for different display sizes.

Save and close the file.

Next, you will modify the template for the “About” page so that you can display the information retrieved from the bio service. Open src/app/portfolio/about/about.component.html and add the following lines:

src/app/portfolio/about/about.component.html
<div class="d-flex justify-content-center vw-90 mx-auto" *ngIf="bio$ | async as bio">
    <div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
        class="flex-column align-items-center text-center w-75">
        <h1 [ngClass]="options.headingClass" class="mb-5"><span class="font-weight-bold">About</span> Me</h1>
        <div *ngFor="let par of bio.about">
            <h4 *ngIf="!options.useSmallerHeadings" class="mb-4">{{par}}</h4>
            <h5 *ngIf="options.useSmallerHeadings" class="mb-4">{{par}}</h5>
        </div>
    </div>
</div>

In this template, you will display the data from the bio$ observable. You will loop through the “About” section of the information and add it as paragraphs to the “About” page.

Save and close the file.

Once completed, the “About” page will look like this (you’ll be able to preview the site in the last step):

About Page

Populating the Projects Page

The “Projects” page will show all your projects, which are retrieved from the projects service. This component will be used on the home page and will also be a standalone page. It will be displayed on the home page together with the “About” component. When this component is used on the home page, only featured projects should be visible. There exists a See More Projects button that will only appear on the home page. When clicked, it redirects to a full-list projects page.

To populate the “Projects” page, you will modify its component file to get projects from the projects service. You will also use the header service to determine whether to display all or highlighted projects. You will also add options to make the page responsive across different screen sizes. Open src/app/portfolio/projects/projects.component.ts and add the highlighted lines:

src/app/portfolio/projects/projects.component.ts
import { Component } from '@angular/core';
import { mergeMap } from 'rxjs/operators';
import { HeaderService } from '../../core/services/header.service';
import { ProjectsService } from '../../core/services/projects.service';

@Component({
  selector: 'app-projects',
  templateUrl: './projects.component.html',
  styleUrls: ['./projects.component.css']
})
export class ProjectsComponent {
  isHome$ = this.headerService.isHome();
  projects$ = this.isHome$.pipe(
    mergeMap(atHome => this.projectsService.getProjects(atHome))
  );

  respOptions = [
    { viewClasses: 'd-none d-md-flex', displayInColumn: false, useSmallerHeadings: false, titleClasses: 'display-3' },
    { viewClasses: 'd-flex d-md-none', displayInColumn: true, useSmallerHeadings: true, titleClasses: '' }
  ];

  constructor(private projectsService: ProjectsService, private headerService: HeaderService) { }
}

The projects come from the ProjectsService. You will use the HeaderService to determine whether the current page is the home page. You will use the value of isHome$ to determine whether to fetch a full list of projects or just featured projects.

Save and close the file.

Next, you will modify the template for the projects components. Using the projects you got from the projects service, you will loop through and add them here. You will display basic information about each project in a card and add links to where their code is hosted and where you can preview them.

Open src/app/portfolio/projects/projects.component.html and add the following lines:

src/app/portfolio/projects/projects.component.html
<div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
    class="flex-column align-items-center text-center vw-90 mx-auto">
    <h1 [ngClass]="options.titleClasses" class="mb-5"><span class="font-weight-bold">My</span> Projects</h1>
    <div class="d-flex vw-90"
        [ngClass]="{'justify-content-center flex-wrap': !options.displayInColumn, 'flex-column  align-items-center': options.displayInColumn}"
        *ngIf="projects$ | async as projects">
        <div *ngFor="let project of projects" class="card project-card m-3"
            [ngClass]="{'m-3': !options.displayInColumn, 'mb-3': options.displayInColumn}">
            <div class="card-body d-flex flex-column">
                <h5 class="card-title font-weight-bold text-left project-title" [title]="project.name">
                    {{project.name}}
                </h5>
                <h6 class="card-subtitle mb-2 font-weight-lighter text-left">
                    <i [ngClass]="project.stack.iconClasses"></i>
                    {{project.stack.name}}
                </h6>
                <p class="card-text text-left">
                    {{project.description}}
                </p>
                <div class="d-flex flex-row justify-content-start">
                    <a [href]="project.previewUrl" *ngIf="project.previewUrl" class="btn btn-dark mr-2">
                        <i class="fa-lg mr-1 far fa-eye"></i>
                        Preview
                    </a>
                    <a [href]="project.sourceUrl" *ngIf="project.sourceUrl" class="btn btn-dark">
                        <i class="fa-lg mr-1 fab fa-github-alt"></i>
                        Source
                    </a>
                </div>
            </div>
        </div>
    </div>
    <button *ngIf="isHome$ | async" routerLink="/projects" class="mt-3 btn btn-dark">
        See More Projects
        <i class="ml-1 fas fa-angle-right"></i>
    </button>
</div>

Here you add each project from projects$ to a card. In the card, you will display the project name (project.name), the technology stack used in it (project.stack), and a brief description (project.description) of what it does. You will also add links to where the code for the project is hosted. Additionally, you will add a link to where the project can be previewed if it is deployed. Lastly, there is a See More Projects button that is displayed only on the home page. On the home page, only featured projects are displayed. When this button is clicked, a user is routed to a full list of projects.

Save and close the file.

Next, you’ll style the project cards by modifying the projects template. Open src/app/portfolio/projects/projects.component.css and add the following lines:

src/app/portfolio/projects/projects.component.css
.vw-20 {
    width: 20vw;
}

.project-card {
    width: 290px;
    height: 250px;
}

.project-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 20ch;
}

Here, you set the size of the project card and the project titles, which tend to be a bit longer.

Once completed, the full-list “Project” page will look like this (you’ll be able to preview the site in the last step):

Full-list Projects Page

Adding the Rest of the Portfolio Routes

To make each page accessible, you will need to create a route for each one. You will add these in the PortfolioRoutingModule, which handles routing for the PortfolioModule. The “About” page should be available at /about and the “Projects” page at /projects.

To create routes for the portfolio module pages, you’ll modify the portfolio routing module file, which is responsible for routing. Open src/app/portfolio/portfolio-routing.module.ts and add the highlighted lines:

src/app/portfolio/portfolio-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { ProjectsComponent } from './projects/projects.component';
import { AboutComponent } from './about/about.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'about', component: AboutComponent },
  { path: 'projects', component: ProjectsComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class PortfolioRoutingModule { }

Here, you added routes to the “About” and “Projects” pages by specifying paths to the components and adding them to the routes array.

In this step, you completed the portfolio module by creating each of its three pages and adding routes for them. In the next step, you will generate the blog component.

Step 4 — Generating the Blog Module

In this step, you will generate the blog module, which contains your blog landing and post pages. Instead of building the blog from scratch, you will use a Scully schematic to set up all that’s required for a functioning blog. The Scully schematic generates the module, adds a routing module to handle routing to the blog, and creates a blog component that displays a blog post. The blog component will display posts that you write in markdown files. You will see where these markdown files reside in a later step when you create new blog posts. When rendering the blog, Scully takes markdown versions of blog posts you created and converts them into static HTML pages, which are faster to deliver to readers.

You can enable blog support for the app and generate the module by running the following command from the project root:

  1. ng generate @scullyio/init:blog

The above command creates the blog module at src/app/blog, makes a blog folder at the project’s base where blog markdown files will reside, adds a lazy-loaded route for the module in AppRoutingModule, and creates a blog component at the base of the module.

Next, create a folder within the module where the blog component will reside.

  1. mkdir src/app/blog/blog

To move the blog component into this folder, run:

  1. mv src/app/blog/blog.component.* src/app/blog/blog/

This will result in this blog module structure:

src/app/blog
src/app/blog
├── blog
│   ├── blog.component.css
│   ├── blog.component.html
│   ├── blog.component.spec.ts
│   └── blog.component.ts
├── blog-routing.module.ts
└── blog.module.ts

Since this module has been restructured, some paths will be broken and will need updating. Two files, blog-routing.module.ts and blog.module.ts, will have to be updated with the new paths to the BlogComponent.

Open blog-routing.module.ts and update the import as shown:

src/app/blog/blog-routing.module.ts
...
import { BlogComponent } from './blog/blog.component';
...

Save and close the file.

Next, open blog.module.ts and update the import as shown:

src/app/blog/blog.module.ts
...
import { BlogComponent } from './blog/blog.component';
...

Save and close the file.

Next, you will modify the template for the blog component. The blog component’s role is to display a blog post. This component requires very minimal editing as the Scully blog schematic already populates it. You will add styling to the container that will hold blog post content. Open src/app/blog/blog/blog.component.html and replace the boilerplate content with the following lines:

src/app/blog/blog/blog.component.html
<div class="vw-70">
    <scully-content></scully-content>
</div> 

The styling added to the template will make the blog component better spaced within the page. <scully-content></scully-content> will render the markdown blog content.

Save and close the file.

Next, you will modify styling by centering the headings, which creates a better look and feel to the blog component. Open src/app/blog/blog/blog.component.css and replace the content with these lines:

src/app/blog/blog/blog.component.css
h1, h2, h3, h4, h5, h6 {
  text-align: center;
  padding: 1rem;
}

Save and close the file.

Once completed, the blog will look like this (you’ll be able to preview the site in the last step):

Blog Post Page

Generating the Blog Landing Page

Now that you have created the blog module and have added styling to blog posts, you will generate the blog landing page and add styling to the landing page.

The blog landing page will list all your blog posts. You can generate it by running the following command at the project root:

  1. ng generate component blog/blog-landing

This will result in this structure:

src/app/blog
src/app/blog
├── blog
│   ├── blog.component.css
│   ├── blog.component.html
│   ├── blog.component.spec.ts
│   └── blog.component.ts
├── blog-landing
│   ├── blog-landing.component.css
│   ├── blog-landing.component.html
│   └── blog-landing.component.ts
├── blog-routing.module.ts
└── blog.module.ts

Next, you will modify the component file for the blog landing page to list all the blog posts. Here you will get all the pages that have a /blog/ in their route and display them in a list. You will also add options to make the page responsive across different screen sizes.

Open src/app/blog/blog-landing/blog-landing.component.ts and make the following changes:

src/app/blog/blog-landing/blog-landing.component.ts
import { Component } from '@angular/core';
import { ScullyRoute, ScullyRoutesService } from '@scullyio/ng-lib';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-blog-landing',
  templateUrl: './blog-landing.component.html',
  styleUrls: ['./blog-landing.component.css']
})
export class BlogLandingComponent {
  links$ = this.scully.available$.pipe(
    map(routes => routes.filter((route: ScullyRoute) => route.route.startsWith('/blog/')))
  );

  respOptions = [
    { viewClasses: 'd-none d-md-flex', displayInColumn: false, titleClasses: 'display-3' },
    { viewClasses: 'd-flex d-md-none', displayInColumn: true, titleClasses: '' }
  ];

  constructor(private scully: ScullyRoutesService) { }
}

To get a list of all blog routes, you will use the ScullyRoutesService. The available$ observable will return all the routes rendered by Scully and marked as published. You can mark whether a blog post is published or not in its markdown file frontmatter. (This will be covered in the next step.) This observable will return all routes, including those from the portfolio. So you will filter only routes containing the prefix /blog/. The blog routes will be held by the links$ property. The respOptions property will help with responsiveness.

Save and close the file.

Next, you will modify the template for the blog landing page to list all the available blog posts in cards and link to them. It also contains the title of the blog. Open src/app/blog/blog-landing/blog-landing.component.html and add the following lines:

src/app/blog/blog-landing/blog-landing.component.html
<div *ngFor="let options of respOptions" [ngClass]="options.viewClasses"
    class="flex-column align-items-center text-center vw-90 mx-auto">
    <h1 [ngClass]="options.titleClasses" class="mb-5"><span class="font-weight-bold">Jane's</span> Blog</h1>
    <div [ngClass]="{'justify-content-center flex-wrap': !options.displayInColumn,  'flex-column align-items-center': options.displayInColumn}"
        class="d-flex vw-90">
        <div *ngFor="let page of links$ | async" class="card post-card m-3">
            <div class="card-img-top bg-dark">
                <i class="far fa-newspaper fa-4x m-5 text-white"></i>
            </div>
            <div class="card-body d-flex flex-column">
                <h5 class="card-title post-title" [title]="page.title">{{page.title}}</h5>
                <p class="card-text post-description flex-grow-1">{{page.description}}</p>
                <a [routerLink]="page.route" class="btn btn-outline-dark align-self-center">
                    <i class="fa-lg mr-1 far fa-eye"></i>
                    Read
                </a>
            </div>
        </div>
    </div>
</div>

In this template, you will loop through all the blog posts returned by the Scully router service. For each blog post, you will add a card. In each card, the title of the blog post and a description are displayed. There is also a link added which can be clicked to go to the blog post.

Save and close the file.

Finally, you will add styling to the blog landing template. It will style the project cards that are added to the page. Open src/app/blog/blog-landing/blog-landing.component.css and add the following lines:

src/app/blog/blog-landing/blog-landing.component.css
.post-card {
    width: 290px;
    height: 360px;
}

.post-title {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 20ch;
}

Save and close the file.

Once completed (and after you have added blog posts), the blog landing page will look like this (you’ll be able to preview the site in the last step):

Blog Landing

Adding the Blog Landing Route

To make the blog landing page accessible at the /blog path, you will have to add a route for it in the BlogRoutingModule. Without adding this, it will not be available to the app. Open src/app/blog/blog-routing.module.ts and add the highlighted lines:

src/app/blog/blog-routing.module.ts
...
import { BlogLandingComponent } from './blog-landing/blog-landing.component';

const routes: Routes = [
  { path: '', component: BlogLandingComponent },
  { path: ':slug', component: BlogComponent },
  { path: '**', component: BlogComponent }
];
...

Here you added the route for the BlogLandingComponent to the routes array. This will make it accessible at the /blog route.

Save and close the file.

In this step, you created a blog module that contains two pages: a blog post page and a blog landing page. You added styling to these pages and added the blog landing route so that the landing page will be accessible at the /blog path. In the next step, you will add new blog posts.

Step 5 — Adding New Blog Posts

In this step, you will use Scully to generate new blog posts that will be displayed on the blog landing page. With Scully, you can generate markdown files that will serve as your blog posts. The blog component you generated in the previous step will read the markdown version of a blog post and then display it. Markdown makes it easy to write rich formatted blog content quickly and easily. Scully will create these files as well as add folders to house them for you. It will also add metadata like a title and description to each post. Some of the metadata is used to determine how a post should be displayed. Later, you will use Scully to generate static HTML page versions of these markdown blog posts.

Before you can make a post, you need to come up with a name. For this tutorial, you’ll create a post titled “Blog Post 1”. You will provide this name to the command below using the --name flag from the project root.

  1. ng generate @scullyio/init:post --name="Blog Post 1"

The output will look similar to this:

Output
? What's the target folder for this post? blog ✅️ Blog ./blog/blog-post-1.md file created CREATE blog/blog-post-1.md (103 bytes)

This will create a /blog/blog-post-1.md file at the project root. The contents of the file will look similar to this:

blog/blog-post-1.md
---
title: Blog Post 1
description: blog description
published: false
---

# Blog Post 1

Once you’ve added content to your blog post and are satisfied with it, you can change published to true and it will appear on the blog landing page when you render the site. To view posts that are still unpublished, you can use the slug property.

For example, suppose you added this slug:

blog/blog-post-1.md
---
title: Blog Post 1
description: blog description
published: true
slug: alternate-url-for-blog-post-1
---

# Blog Post 1

You would be able to view this post at https://localhost:1668/blog/alternate-url-for-blog-post-1 when you run the server. However, this unpublished post would not show up on the blog landing page unless marked as published: true. When generating Scully routes as you will see in a later step, Scully will add a slug for all your unpublished posts so you do not have to.

To add content to your post, start after the title. All the post content needs to be in markdown. Here’s an example of content you can use in the markdown post you generated:

/blog/blog-post-1.md
---
title: Blog Post 1
description: Your first blog post
published: true
---

# Blog Post 1

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vitae tempor erat, eget accumsan lorem. Ut id sem id massa mattis dictum ullamcorper vitae massa. In luctus neque lectus, quis dictum tortor elementum sit amet. Mauris non lacinia nisl. Nulla tristique arcu quam, quis posuere diam elementum nec. Curabitur in mi ut purus bibendum interdum ut sit amet orci. Duis aliquam tristique auctor. Suspendisse magna magna, pellentesque vitae aliquet ac, sollicitudin faucibus est. Integer semper finibus leo, eget placerat enim auctor quis. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed aliquam nibh in mi convallis mattis nec ac mi. Nam sed sagittis purus.

Save and close the file when you’re done.

You can generate other posts by running:

  1. ng generate @scullyio/init:post --name="Blog Post 2"
  2. ng generate @scullyio/init:post --name="Blog Post 3"

These commands will create two other markdown files in the /blog/ folder with the names you assigned. You can populate the generated files with the sample content above as you did in the first post.

In this step, you created your first Scully blog post. The following step will cover changes you’ll need to make to complete the app.

Step 6 — Enabling Anchor Scrolling and Cleaning Up the App Component Template

The last thing to do before previewing the app involves enabling anchor scrolling, adding global styling, and cleaning up app.component.html.

On the home page, when a visitor clicks the items in the header, they should be directed to the specific sections on the same page. To make this happen, you need to enable anchor scrolling on the Angular app. Making all these changes will make scrolling to sections of the home page possible.

First, you will modify the module file for the app routing module. This module is responsible for routing throughout the entire app. Here you will enable anchor scrolling. Open src/app/app-routing.module.ts and add the highlighted portion:

src/app/app-routing.module.ts
...
@NgModule({
  imports: [RouterModule.forRoot(routes, { anchorScrolling: 'enabled' })],
  exports: [RouterModule]
})
...

Adding { anchorScrolling: 'enabled' } enables anchorScrolling on the router module so you can jump to different sections on the home page.

Save and close the file.

When you generate the Angular app, the template for the main app component (src/app/app.component.html) contains placeholder content. This placeholder content is displayed on all the pages of your portfolio. It looks something like this:

Placeholder content on app component

Since you won’t be needing this placeholder content on your portfolio, you will remove this.

To remove generated placeholder code from the main page, open src/app/app.component.html and replace its contents with the following lines:

src/app/app.component.html
<div class="d-flex flex-column h-100 w-100">
    <app-header></app-header>
    <div class="d-flex flex-column flex-grow-1 align-items-center justify-content-center">
        <router-outlet></router-outlet>
    </div>
</div>

In this file, you add app-header, the header component, and placed a container div around router-outlet so that routed pages are displayed under it.

Next, you’ll need to ensure that AppModule has access to app-header. Since app-header exists in a different module, App Module does not currently have access to it. You will need to add CoreModule as an import to src/app/app.module.ts because CoreModule provides access to the header component. Open app.module.ts and add the import as highlighted below.

src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { ScullyLibModule } from '@scullyio/ng-lib';
import { CoreModule } from './core/core.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ScullyLibModule,
    CoreModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Making this change ensures that AppModule has access to app-header.

Finally, you’ll make some adjustments to the global styling for the app by modifying src/styles.css. Several components across the app use the styling in this file. It contributes to the overall look and feel of the app and prevents repetition because styling is reused across components.

Before proceeding to run the site, open src/styles.css and add the following lines:

src/styles.css
html, body {
    width: 100%;
    height: 100%;
}

body {
    font-family: 'Nunito', Arial, Verdana, Geneva, Tahoma, sans-serif;
    background: white;
    background-image: radial-gradient(lightgray 5.5%, transparent 0);
    background-size: 30px 30px;
}

.vw-90 {
    width: 90vw;
}

.vw-80 {
    width: 80vw;
}

.vw-70 {
    width: 80vw;
}

.min-vh-10 {
    min-height: 10vh;
}

In this file, you ensure that html and body take full-page heights and widths. You also make Nunito the default font and include various style classes for setting widths and heights.

In this step, you enabled anchor scrolling, added global styling, and cleaned up the app component template. In the next step, you will build the site, render Scully routes, and serve the static portfolio.

Step 7 — Previewing the Static Site

Now that you have completed all the necessary code changes, you can preview your portfolio with Scully. This will involve building your site, generating the Scully routes, then serving the static version of the site. In this step, Scully pre-renders your Angular app into a static site and provides a server to serve both the Angular app and the static portfolio.

Before Scully can pre-render your portfolio, you will need to build it.

  1. ng build

This command will compile your portfolio to dist/portfolio.

The output will look similar to this:

Compiling @angular/core : es2015 as esm2015
Compiling @angular/common : es2015 as esm2015
Compiling @angular/platform-browser : es2015 as esm2015
Compiling @angular/router : es2015 as esm2015
Compiling @angular/platform-browser-dynamic : es2015 as esm2015
Compiling @angular/common/http : es2015 as esm2015
Compiling @angular/forms : es2015 as esm2015
Compiling @scullyio/ng-lib : es2015 as esm2015
Compiling @ng-bootstrap/ng-bootstrap : es2015 as esm2015
✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names                      |      Size
vendor.js                     | vendor                     |   3.49 MB
styles.css                    | styles                     | 202.25 kB
polyfills.js                  | polyfills                  | 141.85 kB
main.js                       | main                       |  24.91 kB
runtime.js                    | runtime                    |   9.06 kB

                              | Initial Total              |   3.86 MB

Lazy Chunk Files              | Names                      |      Size
portfolio-portfolio-module.js | portfolio-portfolio-module |  34.19 kB
blog-blog-module.js           | blog-blog-module           |  15.28 kB

Build at:  - Hash:  - Time: 29012ms

When the build completes, run:

  1. npx scully

Scully will pre-render the whole portfolio by taking each route and creating a separate index.html for each of them. The pre-rendered portfolio will be located in dist/static. This folder should resemble this. (Some files have been removed for clarity.)

dist/static
dist/static
├── about
│   └── index.html
├── assets
├── blog
│   ├── angular-unit-testing
│   │   └── index.html
│   ├── create-a-blog-using-vue.js
│   │   └── index.html
│   ├── how-to-create-a-twitter-bot
│   │   └── index.html
│   └── index.html
├── index.html
└── projects
    └── index.html

Notice how each route has its own separate index.html file.

To preview the static site, run:

  1. npm run scully:serve

This command will start a static Scully server on http://localhost:1668/ and serve your static portfolio. (Once you’re done previewing your site, you can kill the server with Ctrl + C on the terminal where the server is running.)

Note: Scully might have a problem locating Puppeteer. This happens when it tries to run the app in a restricted environment like on a CI service or a virtual machine in the cloud. You may get this error if you attempt to run the app on a DigitalOcean Droplet. The error looks something like this:

=================================================================================================
Puppeteer cannot find or launch the browser. (by default chrome)
 Try adding 'puppeteerLaunchOptions: {executablePath: CHROMIUM_PATH}'
 to your scully.*.config.ts file.
Also, this might happen because the default timeout (60 seconds) is to short on this system
this can be fixed by adding the --serverTimeout=x cmd line option.
   (where x = the new timeout in milliseconds)
When this happens in CI/CD you can find some additional information here:
https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md
=================================================================================================

To fix this, add the highlighted portions to scully.portfolio.config.ts:

scully.portfolio.config.ts
import { ScullyConfig } from '@scullyio/scully';
export const config: ScullyConfig = {
  projectRoot: "./src",
  projectName: "portfolio",
  outDir: './dist/static',
  routes: {
    '/blog/:slug': {
      type: 'contentFolder',
      slug: {
        folder: "./blog"
      }
    },
  },
  puppeteerLaunchOptions: {args: ['--no-sandbox', '--disable-setuid--sandbox']}
};

The puppeteerLaunchOptions option makes it possible to change Puppeteer’s default options and overwrite them with ones that will work in your environment. The --no-sandbox and --disable-setuid--sandbox disable the multiple layers of sandboxing provided for Puppeteer. You can read more about this Chrome troubleshooting resources. Depending on your setup, you may also need to install additional dependencies to run Chromium, which you can learn more about in the Puppeteer troubleshooting guide.

This is what the home page at http://localhost:4200 should look like:

Home Page

In this step, you built your Angular app, pre-rendered it into a static site, and served it using Scully.

Conclusion

In this tutorial, you generated and configured an Angular portfolio app. You also created a core module to handle your portfolio data and hold components central to the app. Moreover, you made a portfolio module consisting of essential pages showcasing your bio, projects, and profile. You built a blog module made of your blog landing and post pages. Lastly, you converted your Angular portfolio into a static site using Scully.

There’s still so much to do with your portfolio. You can add pages to show your skills and articles you’ve written. You can also add a contact page so people can get in touch with you. If you have speaking engagements, a video tutorial channel, a podcast, or conference talks, you could create pages to show them off.

Additionally, Scully offers other useful features like syntax highlighting integration for code in your blog posts. You can learn more about syntax highlighting with prismjs in the Scully product docs.

Finally, you could add tests and deploy the portfolio. You can view a live version of this app at the author’s GitHub. The source code for this project (as well as a more advanced version) is available on GitHub. To find out how to deploy a static site like this on DigitalOcean, check out these tutorials on App Platform.

Note: This tutorial was created using Angular major version 11. Consider upgrading to a more recent version compatible with the most up-to-date version of Scully. You can use Angular’s update tool to figure out how to do this. You might also consider updating other dependencies used in the tutorial, such as Font Awesome and ng-bootstrap.


Want to learn more? Join the DigitalOcean Community!

Join our DigitalOcean community of over a million developers for free! Get help and share knowledge in our Questions & Answers section, find tutorials and tools that will help you grow as a developer and scale your project or business, and subscribe to topics of interest.

Sign up
About the authors
Default avatar
Developer and author at DigitalOcean.

Default avatar
Technical Editor

Still looking for an answer?

Was this helpful?
1 Comments

This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

You can view a deployed version of this project here: https://scully-portfolio-tutorial.github.io .