From 0e545e21216b2e764a5039abd2034a8d51863469 Mon Sep 17 00:00:00 2001
From: Robinson Zimmermann
<16561945+robinsonzimmermann@users.noreply.github.com>
Date: Thu, 2 May 2024 16:12:01 +0200
Subject: [PATCH] Feature/improve image processing (#117)
* Keep only generated images in production
* Remove necessity of processing images on local
---
angular.json | 23 +++++-
.../post.md | 4 +-
package.json | 2 +-
src/app/app.config.ts | 10 ++-
.../components/author/author.component.html | 6 +-
.../components/author/author.component.scss | 8 ---
src/app/components/author/author.component.ts | 14 ++--
.../components/avatar/avatar.component.html | 47 ++++++++++--
.../components/avatar/avatar.component.scss | 55 ++++++++------
src/app/components/avatar/avatar.component.ts | 25 +++++--
.../post-featured.component.html | 5 +-
.../post-featured.component.scss | 3 +-
.../post-image/post-image.component.html | 18 ++---
.../post-image/post-image.component.ts | 21 +-----
.../post-item/post-item.component.html | 4 +-
src/app/core/config/configuration-tokens.ts | 4 ++
src/app/core/model/author.model.ts | 3 +
src/app/core/model/content.model.ts | 4 ++
src/app/core/model/post.model.ts | 2 +
src/app/core/services/assets.service.spec.ts | 16 +++++
src/app/core/services/assets.service.ts | 19 +++++
src/app/core/services/authors.service.ts | 38 +++++++---
.../core/services/html-in-markdown.service.ts | 4 ++
src/app/core/services/posts.service.ts | 17 ++++-
src/app/features/author/author.component.html | 2 +-
src/app/features/author/author.component.scss | 5 ++
.../features/authors/authors.component.html | 2 +-
src/app/features/post/post.component.scss | 2 +-
src/app/markdown.config.ts | 71 +++++++++----------
tools/process-images/index.js | 1 +
30 files changed, 302 insertions(+), 133 deletions(-)
create mode 100644 src/app/core/services/assets.service.spec.ts
create mode 100644 src/app/core/services/assets.service.ts
diff --git a/angular.json b/angular.json
index c2a40a50..729f5023 100644
--- a/angular.json
+++ b/angular.json
@@ -28,7 +28,7 @@
"src/assets",
{
"glob": "**/*.*",
- "input": "content/authors/avatars/dist",
+ "input": "content/authors/avatars",
"output": "/authors"
},
{
@@ -79,6 +79,27 @@
"maximumError": "6kb"
}
],
+ "assets": [
+ "src/favicon.ico",
+ "src/assets",
+ {
+ "glob": "**/*.*",
+ "input": "content/authors/avatars",
+ "output": "/authors",
+ "ignore": ["*.*"]
+ },
+ {
+ "glob": "**/*.*",
+ "input": "content/posts",
+ "output": "/",
+ "ignore": ["**/assets/*.*"]
+ },
+ {
+ "glob": "authors.json",
+ "input": "content/authors",
+ "output": "/"
+ }
+ ],
"outputHashing": "all"
},
"development": {
diff --git a/content/posts/2020/03/04/kubernetes-application-developer-certification-tips/post.md b/content/posts/2020/03/04/kubernetes-application-developer-certification-tips/post.md
index 84245657..20ad4897 100644
--- a/content/posts/2020/03/04/kubernetes-application-developer-certification-tips/post.md
+++ b/content/posts/2020/03/04/kubernetes-application-developer-certification-tips/post.md
@@ -24,9 +24,9 @@ First, you need a base, if you have no work experience with production Kubernete
Here some nice intros:
-![The illustrated Children’s Guide to Kubernetes](https://www.youtube.com/embed/4ht22ReBjno)
+
-![Kubernetes explained](https://www.youtube.com/embed/aSrqRSk43lY)
+
For something more academic I would recommend following the course:
diff --git a/package.json b/package.json
index 492af076..bd506be7 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
- "prestart": "npm run posts:update && npm run images:authors && npm run images:posts && npm run build:utils",
+ "prestart": "npm run posts:update && npm run build:utils",
"start": "ng serve",
"prebuild": "npm run posts:update && npm run images:authors && npm run images:posts",
"build": "ng build",
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index b8081fd8..f9cf5e14 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -3,6 +3,7 @@ import {
ApplicationConfig,
SecurityContext,
importProvidersFrom,
+ isDevMode,
} from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router';
@@ -13,8 +14,9 @@ import { MarkdownModule, MarkdownService } from 'ngx-markdown';
import { HttpClient, provideHttpClient, withFetch } from '@angular/common/http';
import markdownConfig from './markdown.config';
import { DOCUMENT } from '@angular/common';
-import { AUTHORS_AVATAR_PATH_TOKEN } from './core/config/configuration-tokens';
import { HtmlInMarkdownService } from './core/services/html-in-markdown.service';
+import { AssetsService } from './core/services/assets.service';
+import { AUTHORS_AVATAR_PATH_TOKEN, USE_PROCESSED_IMAGES } from './core/config/configuration-tokens';
export const appConfig: ApplicationConfig = {
providers: [
@@ -37,11 +39,15 @@ export const appConfig: ApplicationConfig = {
{
provide: APP_INITIALIZER,
useFactory: markdownConfig,
- deps: [MarkdownService, DOCUMENT, HtmlInMarkdownService],
+ deps: [MarkdownService, DOCUMENT, HtmlInMarkdownService, AssetsService],
},
{
provide: AUTHORS_AVATAR_PATH_TOKEN,
useValue: 'authors',
},
+ {
+ provide: USE_PROCESSED_IMAGES,
+ useValue: !isDevMode(),
+ }
],
};
diff --git a/src/app/components/author/author.component.html b/src/app/components/author/author.component.html
index 390b039b..997d3ebb 100644
--- a/src/app/components/author/author.component.html
+++ b/src/app/components/author/author.component.html
@@ -7,9 +7,11 @@
lg: size === 'lg',
muted: muted
}">
-
+ [url]="author.displayAvatar?.[size]"
+ format="circle"
+ >
{{ author.fullname }}
@if (size === 'lg') {
diff --git a/src/app/components/author/author.component.scss b/src/app/components/author/author.component.scss
index 39e1bfd7..81e5298f 100644
--- a/src/app/components/author/author.component.scss
+++ b/src/app/components/author/author.component.scss
@@ -11,14 +11,6 @@
&__avatar {
width: 1.2rem;
height: 1.2rem;
- min-width: 1.2rem;
- border-radius: 100rem;
- background-size: cover;
- background-position: center;
-
- img {
- width: 100%;
- }
}
&__fullname {
font-weight: 500;
diff --git a/src/app/components/author/author.component.ts b/src/app/components/author/author.component.ts
index b066c5af..643d3190 100644
--- a/src/app/components/author/author.component.ts
+++ b/src/app/components/author/author.component.ts
@@ -1,13 +1,14 @@
-import { Component, Inject, Input } from '@angular/core';
+import { Component, Input } from '@angular/core';
import { Author } from '../../core/model/author.model';
import { NgClass, NgStyle } from '@angular/common';
-import { AUTHORS_AVATAR_PATH_TOKEN } from '../../core/config/configuration-tokens';
import { RouterLink } from '@angular/router';
+import { ImageSize } from '../../core/model/content.model';
+import { AvatarComponent } from '../avatar/avatar.component';
@Component({
selector: 'blog-author',
standalone: true,
- imports: [NgStyle, NgClass, RouterLink],
+ imports: [NgStyle, NgClass, RouterLink, AvatarComponent],
templateUrl: './author.component.html',
styleUrl: './author.component.scss',
})
@@ -24,14 +25,9 @@ export class AuthorComponent {
this.author = value;
}
}
- @Input() size: string = 'sm';
+ @Input() size: ImageSize = 'sm';
@Input() muted = false;
author!: Author;
- get imagePath() {
- return `${this.basePath}/${this.size}/${this.author.avatar}`;
- }
-
- constructor(@Inject(AUTHORS_AVATAR_PATH_TOKEN) protected basePath: string) {}
}
diff --git a/src/app/components/avatar/avatar.component.html b/src/app/components/avatar/avatar.component.html
index 0aa9d9c8..d78edcbd 100644
--- a/src/app/components/avatar/avatar.component.html
+++ b/src/app/components/avatar/avatar.component.html
@@ -1,4 +1,43 @@
-
+
diff --git a/src/app/components/avatar/avatar.component.scss b/src/app/components/avatar/avatar.component.scss
index ced08760..219cd8bf 100644
--- a/src/app/components/avatar/avatar.component.scss
+++ b/src/app/components/avatar/avatar.component.scss
@@ -1,28 +1,43 @@
-:host {
- aspect-ratio: 1;
+.avatar {
position: relative;
line-height: 1;
- margin-right: 1rem;
- margin-bottom: 1rem;
+ display: inline-flex;
+ width: 100%;
+ height: 100%;
+
+ &__circle {
+ img, svg, &::before {
+ border-radius: 100rem;
+ }
+ }
- &::before {
- content: '';
- background: repeating-linear-gradient(
- 55deg,
- var(--blog-palette-accent),
- var(--blog-palette-accent) 5px,
- transparent 5px,
- transparent 10px
- );
- position: absolute;
- height: 100%;
- width: 100%;
- top: 0;
- left: 0;
- transform: translate(1rem, 1rem);
+ &__shadow {
+ &::before {
+ content: '';
+ background: repeating-linear-gradient(
+ 55deg,
+ var(--blog-palette-accent),
+ var(--blog-palette-accent) 5px,
+ transparent 5px,
+ transparent 10px
+ );
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ top: 0;
+ left: 0;
+ transform: translate(8%, 8%);
+ }
}
- img {
+ img, svg {
position: relative;
+ background: var(--blog-palette-neutral);
+ width: 100%;
+ height: 100%;
+ }
+
+ svg > * {
+ opacity: .5;
}
}
diff --git a/src/app/components/avatar/avatar.component.ts b/src/app/components/avatar/avatar.component.ts
index 3faf811f..42816da2 100644
--- a/src/app/components/avatar/avatar.component.ts
+++ b/src/app/components/avatar/avatar.component.ts
@@ -1,13 +1,28 @@
-import { Component, Input } from '@angular/core';
-import { CommonModule } from '@angular/common';
+import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
+import { NgClass } from '@angular/common';
@Component({
selector: 'blog-avatar',
standalone: true,
- imports: [CommonModule],
+ imports: [NgClass],
templateUrl: './avatar.component.html',
styleUrl: './avatar.component.scss',
})
-export class AvatarComponent {
- @Input() url: string | null = null;
+export class AvatarComponent implements AfterViewInit {
+
+ @Input() url!: string | undefined;
+ @Input() format: 'circle' | 'square' = 'circle';
+ @Input() shadow = false;
+
+ @ViewChild('avatar')
+ image!: ElementRef;
+
+ placeholder: boolean = false;
+
+ ngAfterViewInit(): void {
+ this.image?.nativeElement?.addEventListener('error', () => {
+ this.placeholder = true
+ });
+ }
+
}
diff --git a/src/app/components/post-featured/post-featured.component.html b/src/app/components/post-featured/post-featured.component.html
index 08dbf0ba..222c5952 100644
--- a/src/app/components/post-featured/post-featured.component.html
+++ b/src/app/components/post-featured/post-featured.component.html
@@ -3,7 +3,10 @@
data-role="button"
[routerLink]="post | postUrl">
-
+
diff --git a/src/app/components/post-featured/post-featured.component.scss b/src/app/components/post-featured/post-featured.component.scss
index 1e1c61d1..74208294 100644
--- a/src/app/components/post-featured/post-featured.component.scss
+++ b/src/app/components/post-featured/post-featured.component.scss
@@ -28,7 +28,7 @@
position: relative;
}
- @include responsive.up('sm') {
+ @include responsive.up('md') {
display: flex;
flex-direction: row-reverse;
background-color: transparent;
@@ -39,6 +39,7 @@
flex-grow: 1;
display: flex;
align-items: center;
+ max-width: 50%;
.post-content {
width: 100%;
diff --git a/src/app/components/post-image/post-image.component.html b/src/app/components/post-image/post-image.component.html
index b8c644bf..65afbc57 100644
--- a/src/app/components/post-image/post-image.component.html
+++ b/src/app/components/post-image/post-image.component.html
@@ -1,14 +1,16 @@
-@if (isShown) {
+@if (asset) {
}
diff --git a/src/app/components/post-image/post-image.component.ts b/src/app/components/post-image/post-image.component.ts
index e3e45391..34835cd0 100644
--- a/src/app/components/post-image/post-image.component.ts
+++ b/src/app/components/post-image/post-image.component.ts
@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
+import { ProcessedAsset } from '../../core/model/content.model';
@Component({
selector: 'blog-post-image',
@@ -10,22 +11,6 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostImageComponent {
- images = [
- { size: 'md', width: 800, url: '' },
- { size: 'lg', width: 1200, url: '' },
- ];
-
- isShown = true;
-
- @Input() set url(value: string) {
- this.isShown = !!value;
- if (!this.isShown) {
- return;
- }
- const splittedHref = value.split('/');
- const lastItem = splittedHref.pop();
- this.images.forEach(element => {
- element.url = [...splittedHref, 'dist', element.size, lastItem].join('/');
- });
- }
+ @Input() asset!: ProcessedAsset;
+ @Input() postUrl!: string;
}
diff --git a/src/app/components/post-item/post-item.component.html b/src/app/components/post-item/post-item.component.html
index d298c5d1..f21d6834 100644
--- a/src/app/components/post-item/post-item.component.html
+++ b/src/app/components/post-item/post-item.component.html
@@ -6,7 +6,9 @@
diff --git a/src/app/core/config/configuration-tokens.ts b/src/app/core/config/configuration-tokens.ts
index b3783b08..6835698c 100644
--- a/src/app/core/config/configuration-tokens.ts
+++ b/src/app/core/config/configuration-tokens.ts
@@ -3,3 +3,7 @@ import { InjectionToken } from '@angular/core';
export const AUTHORS_AVATAR_PATH_TOKEN = new InjectionToken(
'[CONFIG][ASSETS] Authors avatar directory path'
);
+
+export const USE_PROCESSED_IMAGES = new InjectionToken(
+ '[CONFIG][ASSETS] Token to control source of content assets'
+);
diff --git a/src/app/core/model/author.model.ts b/src/app/core/model/author.model.ts
index aa314bd9..64de531f 100644
--- a/src/app/core/model/author.model.ts
+++ b/src/app/core/model/author.model.ts
@@ -1,5 +1,8 @@
+import { ProcessedAsset } from "./content.model";
+
export interface Author {
avatar: string;
+ displayAvatar?: ProcessedAsset;
fullname: string;
role: string;
url: string;
diff --git a/src/app/core/model/content.model.ts b/src/app/core/model/content.model.ts
index 25e5142f..01e52b18 100644
--- a/src/app/core/model/content.model.ts
+++ b/src/app/core/model/content.model.ts
@@ -17,3 +17,7 @@ export interface Header {
id: string;
heading: string;
}
+
+export type ImageSize = 'sm' | 'md' | 'lg';
+
+export type ProcessedAsset = { [size in ImageSize]: string } | undefined;
\ No newline at end of file
diff --git a/src/app/core/model/post.model.ts b/src/app/core/model/post.model.ts
index 6434165e..2e10c125 100644
--- a/src/app/core/model/post.model.ts
+++ b/src/app/core/model/post.model.ts
@@ -1,10 +1,12 @@
import { Author } from './author.model';
import { Category } from './categories.model';
+import { ProcessedAsset } from './content.model';
export interface Post {
title: string;
excerpt: string;
teaser: string;
+ displayTeaser?: ProcessedAsset;
date: string | undefined;
authors: Array;
featured?: boolean;
diff --git a/src/app/core/services/assets.service.spec.ts b/src/app/core/services/assets.service.spec.ts
new file mode 100644
index 00000000..29d9f61c
--- /dev/null
+++ b/src/app/core/services/assets.service.spec.ts
@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AssetsService } from './assets.service';
+
+describe('AssetsService', () => {
+ let service: AssetsService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(AssetsService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/src/app/core/services/assets.service.ts b/src/app/core/services/assets.service.ts
new file mode 100644
index 00000000..5e5d5bf8
--- /dev/null
+++ b/src/app/core/services/assets.service.ts
@@ -0,0 +1,19 @@
+import { Inject, Injectable } from '@angular/core';
+import { USE_PROCESSED_IMAGES } from '../config/configuration-tokens';
+import { ImageSize } from '../model/content.model';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AssetsService {
+ constructor(@Inject(USE_PROCESSED_IMAGES) private useProcessedImages: Boolean) { }
+
+ getAssetPath(url: string, size: ImageSize) {
+ if (this.useProcessedImages && !url.startsWith('http')) {
+ const fragments = url.split('/');
+ fragments.splice(fragments.length - 1, 0, 'dist', size);
+ return fragments.filter(Boolean).join('/');
+ }
+ return url;
+ }
+}
diff --git a/src/app/core/services/authors.service.ts b/src/app/core/services/authors.service.ts
index 482b2db1..3279f459 100644
--- a/src/app/core/services/authors.service.ts
+++ b/src/app/core/services/authors.service.ts
@@ -1,7 +1,10 @@
import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
+import { Inject, Injectable } from '@angular/core';
import { Author, AuthorsList } from '../model/author.model';
-import { Observable, map, shareReplay } from 'rxjs';
+import { Observable, map, shareReplay, tap } from 'rxjs';
+import { AUTHORS_AVATAR_PATH_TOKEN } from '../config/configuration-tokens';
+import { AssetsService } from './assets.service';
+import { ImageSize } from '../model/content.model';
@Injectable({
providedIn: 'root',
@@ -9,12 +12,7 @@ import { Observable, map, shareReplay } from 'rxjs';
export class AuthorsService {
private cached: Observable = this.httpClient
.get('authors.json')
- .pipe(shareReplay());
-
- constructor(private httpClient: HttpClient) {}
-
- getAuthors(): Observable {
- return this.cached.pipe(
+ .pipe(
map(authors =>
Object.keys(authors).reduce(
(acc, curr) => ({
@@ -23,12 +21,23 @@ export class AuthorsService {
...authors[curr],
fullname: curr,
url: `people/${curr.toLowerCase().replace(/\W/g, '-')}`,
+ displayAvatar: this.generateAvatarPaths(authors[curr]?.avatar)
},
}),
{}
)
- )
+ ),
+ shareReplay()
);
+
+ constructor(
+ private httpClient: HttpClient,
+ private assetsService: AssetsService,
+ @Inject(AUTHORS_AVATAR_PATH_TOKEN) private basePath: string,
+ ) {}
+
+ getAuthors(): Observable {
+ return this.cached;
}
getAuthorsDetails(
@@ -42,4 +51,15 @@ export class AuthorsService {
)
);
}
+
+ private generateAvatarPaths(url?: string) {
+ const sizes: ImageSize[] = ['sm', 'lg'];
+ if (url) {
+ return sizes.reduce((acc, curr) => ({
+ ...acc,
+ [curr]: `authors/${this.assetsService.getAssetPath(url, curr)}`
+ }), {});
+ }
+ return undefined;
+ }
}
diff --git a/src/app/core/services/html-in-markdown.service.ts b/src/app/core/services/html-in-markdown.service.ts
index 348dbb73..822e2804 100644
--- a/src/app/core/services/html-in-markdown.service.ts
+++ b/src/app/core/services/html-in-markdown.service.ts
@@ -9,6 +9,10 @@ export class HtmlInMarkdownService {
constructor(@Inject(DOCUMENT) private document: Document) { }
add(html: string) {
+ if (html.startsWith('
`
}
diff --git a/src/app/core/services/posts.service.ts b/src/app/core/services/posts.service.ts
index 375cb524..02d9b4a3 100644
--- a/src/app/core/services/posts.service.ts
+++ b/src/app/core/services/posts.service.ts
@@ -5,6 +5,8 @@ import { HttpClient } from '@angular/common/http';
import { getPermalink } from '@blog/utils';
import { AuthorsService } from './authors.service';
import { Category, SpecialCategories } from '../model/categories.model';
+import { ImageSize } from '../model/content.model';
+import { AssetsService } from './assets.service';
const POSTS_PER_PAGE = 8;
@@ -19,13 +21,15 @@ export class PostsService {
...post,
specialCategory: SpecialCategories.includes(post.category),
date: post.date?.match(/^\d{4}-\d{2}-\d{2}/) ? post.date : undefined,
+ displayTeaser: this.generateDisplayAssets(post.teaser)
}))),
shareReplay()
);
constructor(
private httpClient: HttpClient,
- private authorsService: AuthorsService
+ private authorsService: AuthorsService,
+ private assetsService: AssetsService,
) {}
getAllPosts(): Observable {
@@ -107,4 +111,15 @@ export class PostsService {
})
);
}
+
+ private generateDisplayAssets(url?: string): { [size in ImageSize]: string } | undefined {
+ const sizes: ImageSize[] = ['sm', 'md', 'lg'];
+ if (url) {
+ return sizes.reduce((acc, curr) => ({
+ ...acc,
+ [curr]: this.assetsService.getAssetPath(url, curr)
+ }), {} as { [size in ImageSize]: string });
+ }
+ return undefined;
+ }
}
diff --git a/src/app/features/author/author.component.html b/src/app/features/author/author.component.html
index 57036e33..ed587b44 100644
--- a/src/app/features/author/author.component.html
+++ b/src/app/features/author/author.component.html
@@ -7,7 +7,7 @@ {{ author.fullname }}
{{ author.role }}
-
+