DEV Community

loading...

Add a blog to your Angular website using markdown files

daviddalbusco profile image David Dal Busco Updated on ・5 min read

Add a blog to your Angular website using markdown files

Last week I wanted to add a blog to my Angular Universal website, but I didn’t wanted to implement a complex solution and spend to much time on it. Neither did I wanted to add a CMS or even store the articles in a database. That’s why I came up, I think, with a pretty handy, for not saying dumb simple, solution with the implementation of a blog based on markdown files 🚀

I originally published this article on Medium Sep 7, 2018

Before going further: If you are looking to implement a blog, you are looking to share your stories but you are also most probably looking to make your website more SEO friendly. Therefore, I assume that you already have implemented an Angular SSR website. If not, I would really advise you to have a look to that particular topic and if you do have, don’t miss the very last chapter of this article, a kind of hack is needed in order to load the resources correctly on the backend side

Installation

The only required extra library we will need to implement the solution is ngx-markdown of [Jean-Francois Cere (https://github.com/jfcere). It will add the markdown support to our website respectively this awesome library will do all the job for us, it will read the markdown files and parse them to html 💪😃

npm install ngx-markdown --save

Content

As I said above, the idea is to use markdown files as sources for the blog. In this solution I ship the files within the app, placing them under the assets folder

  • assets > blog > blog.md: the list of blog entries
  • assets > blog > post > *.md: the articles (= blog posts) themselves

Routes

For this solution we will need to add two routes to our website

  • A route “/blog” which will display a list of all articles
  • A route “/blog/post/name-of-the-article” which will display a particular blog post. In this route example the blog post name is “name-of-the-article”

I did group all routes under the same “blog” path, therefore the structure will look like the following once implemented

Implementation

First of all, we create a main blog.module.ts which will route the submodules and import the markdown library

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild([
      {path: '', pathMatch: 'full',
               loadChildren: './blog/blog-view.module#BlogViewModule'},
      {path: 'post', 
               loadChildren: './post/blog-post-view.module#BlogPostViewModule'},
    ]),
    MarkdownModule.forRoot()
  ]
})

Blog

Now we could create the blog-view which will display the list of blog entries with the help of the Angular cli. To the default moduleswe only have to add the import of the markdown module

@NgModule({
  declarations: [BlogViewComponent],
  imports: [
    CommonModule,
    RouterModule.forChild([
      { path: '', component: BlogViewComponent}
    ]),
    ComponentsModule,
    MarkdownModule.forChild()
  ]
})

Finally, we could add a markdown directive to our template blog view.component.html which will automatically load and display the content of the markdown file, told you it’s super easy 😂

<div markdown [src]="'./assets/blog/blog.md'"></div>

As you could notice, the directive reference the blog.md file which just consists of the list of entries (one title, subtitle and author per blog post) and relative urls which will be used for the navigation

##  [Title](/blog/post/name-of-the-article)
### [Subtitle](/blog/post/name-of-the-article)
Posted by [David](mailto:david@fluster.io) on September 6, 2018

Post

Now that our blog-view is ready, we could add our blog-post-view. As we did previously we need to add MarkdownModule.forChild() to the module but we also need to define the parameter of the route (‘:id’) which will allow us to retrieve the post to load, because here comes the trick: **the post route take as parameter the name of the post we want to display respectively the name of the markdown file to load. **So let’s say in our example that we want to display the blog post assets > blog > post > name-of-the-article.md our route will look like /blog/post/name-of-the-article

@NgModule({
  declarations: [BlogPostViewComponent],
  imports: [
    CommonModule,
    RouterModule.forChild([
      { path: '', component: BlogPostViewComponent},
      { path: ':id', component: BlogPostViewComponent, pathMatch: 'full'}
    ]),
    ComponentsModule,
    MarkdownModule.forChild()
  ]
})

Once done, we could again add the directive to our template, this time we are not going to reference a particular file but rather use a variable name post

<div markdown [src]="post"></div>

I guess you understood, since we are using a route parameter, we have to
initialize this variable using the interface ActivatedRoute

@Component({
  selector: 'app-blog-post-view',
  styleUrls: ['./blog-post-view.component.scss'],
  templateUrl: './blog-post-view.component.html'
})
export class BlogPostViewComponent implements OnInit, OnDestroy {

  private sub: Subscription;

  post: string;

  constructor(private route: ActivatedRoute) {

  }

  ngOnInit() {
    this.sub = this.route.params.subscribe(params => {
      this.post = './assets/blog/post/' +  params['id'] + '.md';
    });
  }

  ngOnDestroy() {
    if (this.sub) {
      this.sub.unsubscribe();
    }
  }

}

Et voilà, that’s it, nothing more nothing left in order to route, load and display our blog 👍

Of course the solution would probably need a bit of styling, I won’t cover that in this article but if you are using Bootstrap you could for example have a look to this [free clean blog theme (https://startbootstrap.com/template-overviews/clean-blog/)

Once implemented and styled our blog could look like the one I have implemented 👉 https://fluster.io/blog

Cherry on the cake 🍒🎂

I realized it afterwards, but you know what’s the cherry on the cake of this solution? You could easily export you Medium stories as markdown (for example with this Chrome extension) and therefore integrate them quickly in the blog we just created 🎉

Bonus hack 🤖

While developing this blog solution I faced the problem that ngx-markdown was not able to load the markdown files, from the assets folder using relative paths and the HttpClientModule , on the server side of my Angular Universal app which ultimately would have had for effect that such content would have been ignored by crawlers 😢 Prior to Angular v6 I would have solved this by loading resources directly from the filesystem (usingfs ) but unfortunately this isn’t possible anymore. Fortunately 😅 I found the following discussion and issue https://github.com/angular/universal/issues/858 which allowed my to solve the problem

In order to help our Angular Universal server to load correctly the resources we need for ngx-markdown, the idea is to intercept all Http requests with the goal to rewrite them with an understandable server side host path. Not rocket science but definitely a must have for our implementation of a friendly SEO blog based on markdown files

import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';

import {isPlatformServer} from '@angular/common';

import {Observable} from 'rxjs'

@Injectable({
  providedIn: 'root'
})
export class HttpInterceptorService implements HttpInterceptor {

  constructor(@Inject(PLATFORM_ID) private platformId: Object) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (isPlatformServer(this.platformId) && req.url.includes('./')) {
      return next.handle(req.clone({
        url: `http://localhost:8000/${req.url.replace('./', '')}`
      }));
    }

    return next.handle(req);
  }
}

Have fun blogging 🤓

To infinity and beyond 🚀

David

Discussion

pic
Editor guide
Collapse
mikepiper profile image
MikePiper

Thanks for this guide, David!

So far I've mostly been able to follow it, but I'm getting hung up on the SSR hack.

index.html files are being created for all the appropriate routes (i.e., one for each corresponding .md file). And they are drawing from the appropriate component.

However, each index.html file only has

where the actual content from the .md file should be.

Does anything come to mind as where to look for the problem? I'm new to Angular Universal and have exhausted everything I can think of.

Collapse
jdforsythe profile image
Jeremy Forsythe

I'm attempting something similar with Angular Universal and the pre-renderer in v9. I am having the same issue, with the <markdown> element in the generated html file but no content.

I thought it was related to HttpClient request for the markdown, so I had some scripts which will generate an articles.ts file at build time with all of the markdown, routes, etc. (it also generates my routes.txt file for prerendering). The markdown gets base64-encoded to put into the array. Then the component loads the data from articles.ts, converts it back to ASCII, and puts it in the [data] binding of the <markdown> element.

Now HttpClient was removed from the equation and the content is all available in TypeScript when the pre-renderer runs, however the result is the same - an empty <markdown> element in the rendered HTML.

The odd thing is that I have a "blog" page that lists the articles with thumbnails, data that is also pulled from the same file. It doesn't use ngx-markdown but just creates some cards with normal elements and Angular bindings. That page gets successfully rendered, with the data from the articles.ts file statically in the HTML.

I think it has something to do with the way ngx-markdown works, probably how it calls out to marked for the parsing.

Collapse
daviddalbusco profile image
David Dal Busco Author

Maybe something changed in ngx-markdown? It has been a while since I wrote this blog post (> 2 years) maybe it also need some other adjustments...

Thread Thread
jdforsythe profile image
Jeremy Forsythe

I stripped out ngx-markdown and tried using marked directly with the same result.

I did have some success loading the markdown with HttpClient and using marked. The prerender fails on the HTTP requests however if i use http://localhost:4300 in the URLs and run another copy of the app while I'm prerendering, it can successfully find the markdown files and renders properly. If i can just figure out why the relative URL calls are failing, I'll be in business. They are supposed to be rewritten to absolute URLs automatically now but it doesn't appear to be working.

Thread Thread
daviddalbusco profile image
David Dal Busco Author

Happy to hear you made progress. Can't help unfortunately, it's been a while since I don't used Angular for SSR. Good luck 🤞

Collapse
daviddalbusco profile image
David Dal Busco Author

I'm a bit confused about what you mean with "each index.html"? Your angular App as only one single index.html for the all app

Anyway I wrote that blog post a while ago, so it might be that the SSR workaround isn't valid anymore or maybe even not needed anymore. I developed that hack in that way as, back at that time, it was not possible to use fs in Angular 6 but according this post on Stackoverflow it is now possible. Therefore maybe that by allowing your app to use fs you will be able to fetch the markdown content on the server side

Collapse
mikepiper profile image
MikePiper

Thank you, David.

With regard to "each index.html" I meant the ones that are created by the prerender.ts script. That is, the way that Angular Universal has worked for me is that it creates separate index.html files for each prerendered url. For instance I have dist/browser/about/index.html -- and that file contains the html that would be seen by Google for instance.

But the index.html files that are made in this case simply have an empty div (with markdown="" ) where the pulled-in markdown content should be. So Google wouldn't actually see the content in question.

In any event, thank you for pointing me toward the fs option. I will look into that!

Thread Thread
daviddalbusco profile image
David Dal Busco Author

Oh cool super interesting, back then I wasn't able to get Prerendering up-and-running that's why I ended up using SSR. Really cool to hear it work out (if we omit the bug related to the blog).

I hope fs will solve the issue, would be interested to hear you voice once you solve the issue, keep me posted!