Build a blog with Angular, Strapi and Apollo

Introduction

Goal

Prerequisites

Setup

Back-end setup

Front-end setup

cd frontend  
ng serve
"scripts": [
"node_modules/jquery/dist/jquery.min.js",
"node_modules/uikit/dist/js/uikit.min.js",
"node_modules/uikit/dist/js/uikit-icons.min.js"
]
/* You can add global styles to this file, and also import other style files */
@import "../node_modules/uikit/dist/css/uikit.min.css";
@import "../node_modules/uikit/dist/css/uikit.css";
@import "../node_modules/uikit/dist/css/uikit-core.css";
@import url("https://fonts.googleapis.com/css?family=Staatliches");
a {
text-decoration: none;
}
h1 {
font-family: Staatliches;
font-size: 120px;
}
#category {
font-family: Staatliches;
font-weight: 500;
}
#title {
letter-spacing: 0.4px;
font-size: 22px;
font-size: 1.375rem;
line-height: 1.13636;
}
#banner {
margin: 20px;
height: 800px;
}
#editor {
font-size: 16px;
font-size: 1rem;
line-height: 1.75;
}
.uk-navbar-container {
background: #fff !important;
font-family: Staatliches;
}
img:hover {
opacity: 1;
transition: opacity 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
}

Designing the data structure

Create categories

Nav component

import gql from "graphql-tag";const CATEGORIES_QUERY = gql`  
query Categories {
categories {
id
name
}
}
`;
export default CATEGORIES_QUERY;
import { Apollo } from "apollo-angular";  
import gql from "graphql-tag";
import CATEGORIES_QUERY from "../apollo/queries/category/categories";
import { Subscription } from "rxjs";
export class NavComponent implements OnInit {  
data: any = {};
loading = true;
errors: any;
private queryCategories: Subscription; constructor(private apollo: Apollo) {} ngOnInit() {
this.queryCategories = this.apollo
.watchQuery({
query: CATEGORIES_QUERY
})
.valueChanges.subscribe(result => {
this.data = result.data;
this.loading = result.loading;
this.errors = result.errors;
});
}
ngOnDestroy() {
this.queryCategories.unsubscribe();
}
}
<nav class="uk-navbar-container" uk-navbar>  
<div class="uk-navbar-left">
<ul class="uk-navbar-nav">
<li class="uk-active"><a href="#">Strapi blog</a></li>
</ul>
</div>
<div class="uk-navbar-right">
<ul *ngIf="data" class="uk-navbar-nav">
<li *ngFor="let category of data.categories" class="uk-active">
<a
routerLink="/category/{{ category.id }}"
routerLinkActive="active"
class="uk-link-reset"
>
{{ category.name }}
</a>
</li>
</ul>
</div>
</nav>
...
import { NavComponent } from "./nav/nav.component";
...
declarations: [
AppComponent,
NavComponent
],
...
<app-nav></app-nav><router-outlet></router-outlet>

Articles component

import gql from "graphql-tag";const ARTICLES_QUERY = gql`  
query Articles {
articles {
id
title
category {
id
name
}
image {
url
}
}
}
`;
export default ARTICLES_QUERY;
import { Component, OnInit } from "@angular/core";  
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";
import ARTICLES_QUERY from "../apollo/queries/article/articles";
import { Subscription } from "rxjs";
@Component({
selector: "app-articles",
templateUrl: "./articles.component.html",
styleUrls: ["./articles.component.css"]
})
export class ArticlesComponent implements OnInit {
data: any = {};
loading = true;
errors: any;
leftArticlesCount: any;
leftArticles: any[];
rightArticles: any[];
private queryArticles: Subscription; constructor(private apollo: Apollo) {} ngOnInit() {
this.queryArticles = this.apollo
.watchQuery({
query: ARTICLES_QUERY
})
.valueChanges.subscribe(result => {
this.data = result.data;
this.leftArticlesCount = Math.ceil(this.data.articles.length / 5);
this.leftArticles = this.data.articles.slice(0, this.leftArticlesCount);
this.rightArticles = this.data.articles.slice(
this.leftArticlesCount,
this.data.articles.length
);
this.loading = result.loading;
this.errors = result.errors;
});
}
ngOnDestroy() {
this.queryArticles.unsubscribe();
}
}
<div class="uk-section">  
<div class="uk-container uk-container-large">
<h1>Strapi blog</h1>
<div class="uk-child-width-1-2" uk-grid>
<div>
<a
routerLink="/article/{{ article.id }}"
routerLinkActive="active"
*ngFor="let article of leftArticles"
class="uk-link-reset"
>
<div class="uk-card uk-card-muted">
<div *ngIf="article.image" class="uk-card-media-top">
<img
src="http://localhost:1337{{ article.image.url }}"
alt=""
height="100"
/>
</div>
<div class="uk-card-body">
<p
id="category"
*ngIf="article.category"
class="uk-text-uppercase"
>
{{ article.category.name }}
</p>
<p id="title" class="uk-text-large">{{ article.title }}</p>
</div>
</div>
</a>
</div>
<div>
<div class="uk-child-width-1-2@m uk-grid-match" uk-grid>
<a
routerLink="/article/{{ article.id }}"
routerLinkActive="active"
*ngFor="let article of rightArticles"
class="uk-link-reset"
>
<div class="uk-card uk-card-muted">
<div *ngIf="article.image" class="uk-card-media-top">
<img
src="http://localhost:1337{{ article.image.url }}"
alt=""
height="100"
/>
</div>
<div class="uk-card-body">
<p
id="category"
*ngIf="article.category"
class="uk-text-uppercase"
>
{{ article.category.name }}
</p>
<p id="title" class="uk-text-large">{{ article.title }}</p>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
...
import { ArticlesComponent } from "./articles/articles.component";
...
declarations: [
AppComponent,
ArticlesComponent,
NavComponent,
],
import { RouterModule, Routes } from "@angular/router";  
...
const appRoutes: Routes = [
{ path: "", component: ArticlesComponent }
];
...
imports: [
RouterModule.forRoot(appRoutes, { enableTracing: true }),
BrowserModule,
AppRoutingModule,
GraphQLModule,
HttpClientModule
],
...

Article Component

import gql from "graphql-tag";const ARTICLE_QUERY = gql`  
query Articles($id: ID!) {
article(id: $id) {
id
title
content
image {
url
}
category {
id
name
}
published_at
}
}
`;
export default ARTICLE_QUERY;
import { Component, OnInit } from "@angular/core";  
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";
import ARTICLE_QUERY from "../apollo/queries/article/article";
import { ActivatedRoute } from "@angular/router";
import { Subscription } from "rxjs";
@Component({
selector: "app-article",
templateUrl: "./article.component.html",
styleUrls: ["./article.component.css"]
})
export class ArticleComponent implements OnInit {
data: any = {};
loading = true;
errors: any;
private queryArticle: Subscription; constructor(private apollo: Apollo, private route: ActivatedRoute) {} ngOnInit() {
this.queryArticle = this.apollo
.watchQuery({
query: ARTICLE_QUERY,
variables: {
id: this.route.snapshot.paramMap.get("id")
}
})
.valueChanges.subscribe(result => {
this.data = result.data;
this.loading = result.loading;
this.errors = result.errors;
});
}
ngOnDestroy() {
this.queryArticle.unsubscribe();
}
}
...
import { MarkdownModule } from "ngx-markdown";
...
imports: [
MarkdownModule.forRoot(),
RouterModule.forRoot(appRoutes, { enableTracing: true }),
BrowserModule,
AppRoutingModule,
GraphQLModule,
HttpClientModule
],
...
<div  
id="banner"
class="uk-height-small uk-flex uk-flex-center uk-flex-middle uk-background-cover uk-light uk-padding"
[style.background-image]="
'url(http://localhost:1337' + data.article.image.url + ')'
"
uk-img
>
<h1>{{ data.article.title }}</h1>
</div>
<div class="uk-section">
<div class="uk-container uk-container-small">
<p>
<markdown ngPreserveWhitespaces>
{{ data.article.content }}
</markdown>
</p>
<p></p>
</div>
</div>
...
import { ArticleComponent } from "./article/article.component";
...
const appRoutes: Routes = [
{ path: "", component: ArticlesComponent },
{ path: "article/:id", component: ArticleComponent },
];
...
declarations: [
AppComponent,
NavComponent,
ArticlesComponent,
ArticleComponent
],
...

Category component

import gql from "graphql-tag";const CATEGORY_ARTICLES_QUERY = gql`  
query Category($id: ID!) {
category(id: $id) {
id
name
articles {
id
title
content
image {
url
}
category {
id
name
}
}
}
}
`;
export default CATEGORY_ARTICLES_QUERY;
import { Component, OnInit } from "@angular/core";  
import { Apollo } from "apollo-angular";
import gql from "graphql-tag";
import CATEGORY_ARTICLES_QUERY from "../apollo/queries/category/articles";
import { ActivatedRoute, ParamMap } from "@angular/router";
import { Subscription } from "rxjs";
@Component({
selector: "app-category",
templateUrl: "./category.component.html",
styleUrls: ["./category.component.css"]
})
export class CategoryComponent implements OnInit {
data: any = {};
category: any = {};
loading = true;
errors: any;
leftArticlesCount: any;
leftArticles: any[];
rightArticles: any[];
id: any;
private queryCategoriesArticles: Subscription; constructor(private apollo: Apollo, private route: ActivatedRoute) {} ngOnInit() {
this.route.paramMap.subscribe((params: ParamMap) => {
this.id = params.get("id");
this.queryCategoriesArticles = this.apollo
.watchQuery({
query: CATEGORY_ARTICLES_QUERY,
variables: {
id: this.id
}
})
.valueChanges.subscribe(result => {
this.data = result.data
this.category = this.data.category.name
console.log(this.data)
this.leftArticlesCount = Math.ceil(this.data.category.articles.length / 5);
this.leftArticles = this.data.category.articles.slice(0, this.leftArticlesCount);
this.rightArticles = this.data.category.articles.slice(
this.leftArticlesCount,
this.data.category.articles.length
);
this.loading = result.loading;
this.errors = result.errors;
});
});
}
ngOnDestroy() {
this.queryCategoriesArticles.unsubscribe();
}
}
<div class="uk-section">  
<div class="uk-container uk-container-large">
<h1>{{ category }}</h1>
<div class="uk-child-width-1-2" uk-grid>
<div>
<a
routerLink="/article/{{ article.id }}"
routerLinkActive="active"
*ngFor="let article of leftArticles"
class="uk-link-reset"
>
<div class="uk-card uk-card-muted">
<div *ngIf="article.image" class="uk-card-media-top">
<img
src="http://localhost:1337{{ article.image.url }}"
alt=""
height="100"
/>
</div>
<div class="uk-card-body">
<p
id="category"
*ngIf="article.category"
class="uk-text-uppercase"
>
{{ article.category.name }}
</p>
<p id="title" class="uk-text-large">{{ article.title }}</p>
</div>
</div>
</a>
</div>
<div>
<div class="uk-child-width-1-2@m uk-grid-match" uk-grid>
<a
routerLink="/article/{{ article.id }}"
routerLinkActive="active"
*ngFor="let article of rightArticles"
class="uk-link-reset"
>
<div class="uk-card uk-card-muted">
<div *ngIf="article.image" class="uk-card-media-top">
<img
src="http://localhost:1337{{ article.image.url }}"
alt=""
height="100"
/>
</div>
<div class="uk-card-body">
<p
id="category"
*ngIf="article.category"
class="uk-text-uppercase"
>
{{ article.category.name }}
</p>
<p id="title" class="uk-text-large">{{ article.title }}</p>
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
...
import { CategoryComponent } from "./category/category.component";
...
const appRoutes: Routes = [
{ path: "", component: ArticlesComponent },
{ path: "article/:id", component: ArticleComponent },
{ path: "category/:id", component: CategoryComponent }
];
...
declarations: [
AppComponent,
ArticlesComponent,
ArticleComponent,
NavComponent,
CategoryComponent
],
...

Conclusion

The open source Headless CMS Front-End Developers love.