diff --git a/apps/website/src/assets/.gitkeep b/apps/website/src/assets/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/apps/website/src/index.html b/apps/website/src/index.html
index b2f4c821..8071d36e 100644
--- a/apps/website/src/index.html
+++ b/apps/website/src/index.html
@@ -1,4 +1,4 @@
-
+
@@ -18,7 +18,7 @@
/>
diff --git a/libs/blog/feature-home/src/lib/blog-home.component.ts b/libs/blog/feature-home/src/lib/blog-home.component.ts
index 2a9d7302..96ebcb6c 100644
--- a/libs/blog/feature-home/src/lib/blog-home.component.ts
+++ b/libs/blog/feature-home/src/lib/blog-home.component.ts
@@ -11,6 +11,7 @@ import {
} from '@valerymelou/blob/data-access';
import { LinkComponent } from '@valerymelou/shared/ui';
import { RouterModule } from '@angular/router';
+import { MetadataService } from '@valerymelou/shared/seo';
@Component({
selector: 'blog-blog-home',
@@ -23,12 +24,18 @@ export class BlogHomeComponent implements OnInit {
tags$!: Observable>;
ngOnInit(): void {
+ this.metadataService.updateMetadata({
+ title: 'Inside my head | Valery Melou',
+ description:
+ 'I talk about Django, Angular... Web Development in general and many other topics. These are just a few of the things in my head.',
+ });
this.loadArticles();
this.loadTags();
}
constructor(
private articleService: ArticleService,
+ private metadataService: MetadataService,
private tagService: TagService,
) {}
diff --git a/libs/pages/about/src/lib/about.component.ts b/libs/pages/about/src/lib/about.component.ts
index f43ebf76..776a6ab1 100644
--- a/libs/pages/about/src/lib/about.component.ts
+++ b/libs/pages/about/src/lib/about.component.ts
@@ -1,5 +1,6 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { MetadataService } from '@valerymelou/shared/seo';
@Component({
selector: 'app-about',
@@ -7,4 +8,14 @@ import { CommonModule } from '@angular/common';
imports: [CommonModule],
templateUrl: './about.component.html',
})
-export class AboutComponent {}
+export class AboutComponent implements OnInit {
+ constructor(private metadataService: MetadataService) {}
+
+ ngOnInit(): void {
+ this.metadataService.updateMetadata({
+ title: 'About myself (Valery Melou)',
+ description:
+ "I'm now specialized into web development. Building RESTfull APIs with Django and Python then, consuming those APIs with Angular and Typescript.",
+ });
+ }
+}
diff --git a/libs/pages/home/src/lib/home.component.html b/libs/pages/home/src/lib/home.component.html
index c28563b2..930718d8 100644
--- a/libs/pages/home/src/lib/home.component.html
+++ b/libs/pages/home/src/lib/home.component.html
@@ -9,7 +9,7 @@
mobile.
diff --git a/libs/pages/home/src/lib/home.component.ts b/libs/pages/home/src/lib/home.component.ts
index 94616b73..9cb3eb59 100644
--- a/libs/pages/home/src/lib/home.component.ts
+++ b/libs/pages/home/src/lib/home.component.ts
@@ -1,12 +1,24 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ButtonComponent } from '@valerymelou/shared/ui';
+import { MetadataService } from '@valerymelou/shared/seo';
+import { RouterModule } from '@angular/router';
@Component({
selector: 'app-home',
standalone: true,
- imports: [CommonModule, ButtonComponent],
+ imports: [CommonModule, RouterModule, ButtonComponent],
templateUrl: './home.component.html',
styles: ':host {display: flex; flex-direction: column; flex: 1;}',
})
-export class HomeComponent {}
+export class HomeComponent implements OnInit {
+ constructor(private metadataService: MetadataService) {}
+
+ ngOnInit(): void {
+ this.metadataService.updateMetadata({
+ title: 'Home of Valery Melou',
+ description:
+ 'I build beautiful, interactive and accessible experiences for web and mobile.',
+ });
+ }
+}
diff --git a/libs/pages/projects/src/lib/projects.component.html b/libs/pages/projects/src/lib/projects.component.html
index 863f55b7..3b41bc82 100644
--- a/libs/pages/projects/src/lib/projects.component.html
+++ b/libs/pages/projects/src/lib/projects.component.html
@@ -2,7 +2,7 @@
- Some things I've built
+ Some of the things I've built
diff --git a/libs/pages/projects/src/lib/projects.component.ts b/libs/pages/projects/src/lib/projects.component.ts
index 02edef3c..d480c054 100644
--- a/libs/pages/projects/src/lib/projects.component.ts
+++ b/libs/pages/projects/src/lib/projects.component.ts
@@ -1,5 +1,6 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
+import { MetadataService } from '@valerymelou/shared/seo';
@Component({
selector: 'app-projects',
@@ -7,4 +8,13 @@ import { CommonModule } from '@angular/common';
imports: [CommonModule],
templateUrl: './projects.component.html',
})
-export class ProjectsComponent {}
+export class ProjectsComponent implements OnInit {
+ constructor(private metadataService: MetadataService) {}
+
+ ngOnInit(): void {
+ this.metadataService.updateMetadata({
+ title: 'Some of the things I have built | Valery Melou',
+ description: 'Here are some of the projects I have worked on.',
+ });
+ }
+}
diff --git a/libs/shared/seo/.eslintrc.json b/libs/shared/seo/.eslintrc.json
new file mode 100644
index 00000000..8ebcbfd5
--- /dev/null
+++ b/libs/shared/seo/.eslintrc.json
@@ -0,0 +1,36 @@
+{
+ "extends": ["../../../.eslintrc.json"],
+ "ignorePatterns": ["!**/*"],
+ "overrides": [
+ {
+ "files": ["*.ts"],
+ "extends": [
+ "plugin:@nx/angular",
+ "plugin:@angular-eslint/template/process-inline-templates"
+ ],
+ "rules": {
+ "@angular-eslint/directive-selector": [
+ "error",
+ {
+ "type": "attribute",
+ "prefix": "app",
+ "style": "camelCase"
+ }
+ ],
+ "@angular-eslint/component-selector": [
+ "error",
+ {
+ "type": "element",
+ "prefix": "app",
+ "style": "kebab-case"
+ }
+ ]
+ }
+ },
+ {
+ "files": ["*.html"],
+ "extends": ["plugin:@nx/angular-template"],
+ "rules": {}
+ }
+ ]
+}
diff --git a/libs/shared/seo/README.md b/libs/shared/seo/README.md
new file mode 100644
index 00000000..c451127c
--- /dev/null
+++ b/libs/shared/seo/README.md
@@ -0,0 +1,7 @@
+# shared-seo
+
+This library was generated with [Nx](https://nx.dev).
+
+## Running unit tests
+
+Run `nx test shared-seo` to execute the unit tests.
diff --git a/libs/shared/seo/jest.config.ts b/libs/shared/seo/jest.config.ts
new file mode 100644
index 00000000..ab80251f
--- /dev/null
+++ b/libs/shared/seo/jest.config.ts
@@ -0,0 +1,22 @@
+/* eslint-disable */
+export default {
+ displayName: 'shared-seo',
+ preset: '../../../jest.preset.js',
+ setupFilesAfterEnv: ['/src/test-setup.ts'],
+ coverageDirectory: '../../../coverage/libs/shared/seo',
+ transform: {
+ '^.+\\.(ts|mjs|js|html)$': [
+ 'jest-preset-angular',
+ {
+ tsconfig: '/tsconfig.spec.json',
+ stringifyContentPathRegex: '\\.(html|svg)$',
+ },
+ ],
+ },
+ transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
+ snapshotSerializers: [
+ 'jest-preset-angular/build/serializers/no-ng-attributes',
+ 'jest-preset-angular/build/serializers/ng-snapshot',
+ 'jest-preset-angular/build/serializers/html-comment',
+ ],
+};
diff --git a/libs/shared/seo/project.json b/libs/shared/seo/project.json
new file mode 100644
index 00000000..b0011006
--- /dev/null
+++ b/libs/shared/seo/project.json
@@ -0,0 +1,20 @@
+{
+ "name": "shared-seo",
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+ "sourceRoot": "libs/shared/seo/src",
+ "prefix": "app",
+ "projectType": "library",
+ "tags": [],
+ "targets": {
+ "test": {
+ "executor": "@nx/jest:jest",
+ "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
+ "options": {
+ "jestConfig": "libs/shared/seo/jest.config.ts"
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint"
+ }
+ }
+}
diff --git a/libs/shared/seo/src/index.ts b/libs/shared/seo/src/index.ts
new file mode 100644
index 00000000..e7479bee
--- /dev/null
+++ b/libs/shared/seo/src/index.ts
@@ -0,0 +1 @@
+export * from './lib/metadata.service';
diff --git a/libs/shared/seo/src/lib/constants.ts b/libs/shared/seo/src/lib/constants.ts
new file mode 100644
index 00000000..de2ee6ca
--- /dev/null
+++ b/libs/shared/seo/src/lib/constants.ts
@@ -0,0 +1,11 @@
+import { PageMetadata } from './page-metadata';
+
+export const APP_NAME = 'Valery Melou';
+export const DEFAULT_METADATA: PageMetadata = {
+ title: '',
+ description: 'Web developer from Yaounde, Cameroon',
+ keywords: ['angular', 'django', 'angular', 'web', 'developer', 'yaounde'],
+ type: 'website',
+ image: '/assets/images/logo.png',
+ imageAlt: APP_NAME,
+};
diff --git a/libs/shared/seo/src/lib/metadata.service.spec.ts b/libs/shared/seo/src/lib/metadata.service.spec.ts
new file mode 100644
index 00000000..9eed80ed
--- /dev/null
+++ b/libs/shared/seo/src/lib/metadata.service.spec.ts
@@ -0,0 +1,20 @@
+import { TestBed } from '@angular/core/testing';
+
+import { MetadataService } from './metadata.service';
+
+describe('MetadataService', () => {
+ let service: MetadataService;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({});
+ service = TestBed.inject(MetadataService);
+ });
+
+ it('should be created', () => {
+ service.updateMetadata({
+ title: 'Welcome',
+ description: 'Welcome',
+ });
+ expect(service).toBeTruthy();
+ });
+});
diff --git a/libs/shared/seo/src/lib/metadata.service.ts b/libs/shared/seo/src/lib/metadata.service.ts
new file mode 100644
index 00000000..c219b103
--- /dev/null
+++ b/libs/shared/seo/src/lib/metadata.service.ts
@@ -0,0 +1,66 @@
+import { Injectable } from '@angular/core';
+import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
+import { PageMetadata } from './page-metadata';
+import { DEFAULT_METADATA } from './constants';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class MetadataService {
+ constructor(
+ private meta: Meta,
+ private title: Title,
+ ) {}
+
+ updateMetadata(metadata: Partial, index = true): void {
+ const pageMetadata: PageMetadata = { ...DEFAULT_METADATA, ...metadata };
+ const metaTags: MetaDefinition[] = [
+ { name: 'robots', content: index ? 'index, follow' : 'noindex' },
+ ...this.generateMetaDefinitions(pageMetadata),
+ ];
+
+ metaTags.forEach((tag: MetaDefinition) => {
+ if (tag.content) {
+ this.meta.updateTag(tag);
+ }
+ });
+
+ this.title.setTitle(`${metadata.title}`);
+ }
+
+ private generateMetaDefinitions(metadata: PageMetadata): MetaDefinition[] {
+ return [
+ // Standard
+ { name: 'title', content: metadata.title },
+ { name: 'description', content: metadata.description },
+ { name: 'keywords', content: metadata.keywords.join(', ') },
+ // og
+ ...this.generateOgMetaDefinitions(metadata),
+ // Twitter
+ ...this.generateXMetaDefinitions(metadata),
+ ];
+ }
+
+ private generateOgMetaDefinitions(metadata: PageMetadata): MetaDefinition[] {
+ return [
+ { name: 'og:url', content: window.location.href },
+ { property: 'og:title', content: metadata.title },
+ { property: 'og:description', content: metadata.description },
+ { property: 'og:type', content: metadata.type },
+ { property: 'og:image', content: metadata.image },
+ { property: 'og:image:secure_url', content: metadata.image },
+ { property: 'og:image:alt', content: metadata.imageAlt },
+ ];
+ }
+
+ private generateXMetaDefinitions(metadata: PageMetadata): MetaDefinition[] {
+ return [
+ { name: 'twitter:card', content: 'summary' },
+ { name: 'twitter:site', content: '@valerymelou' },
+ { name: 'twitter:title', content: metadata.title },
+ { name: 'twitter:description', content: metadata.description },
+ { name: 'twitter:image', content: metadata.image },
+ { name: 'twitter:image:alt', content: metadata.imageAlt },
+ ];
+ }
+}
diff --git a/libs/shared/seo/src/lib/page-metadata.ts b/libs/shared/seo/src/lib/page-metadata.ts
new file mode 100644
index 00000000..f73627e9
--- /dev/null
+++ b/libs/shared/seo/src/lib/page-metadata.ts
@@ -0,0 +1,8 @@
+export interface PageMetadata {
+ title: string;
+ description: string;
+ keywords: string[];
+ type: 'website';
+ image: string;
+ imageAlt: string;
+}
diff --git a/libs/shared/seo/src/test-setup.ts b/libs/shared/seo/src/test-setup.ts
new file mode 100644
index 00000000..ab1eeeb3
--- /dev/null
+++ b/libs/shared/seo/src/test-setup.ts
@@ -0,0 +1,8 @@
+// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment
+globalThis.ngJest = {
+ testEnvironmentOptions: {
+ errorOnUnknownElements: true,
+ errorOnUnknownProperties: true,
+ },
+};
+import 'jest-preset-angular/setup-jest';
diff --git a/libs/shared/seo/tsconfig.json b/libs/shared/seo/tsconfig.json
new file mode 100644
index 00000000..5cf0a165
--- /dev/null
+++ b/libs/shared/seo/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "useDefineForClassFields": false,
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "files": [],
+ "include": [],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ },
+ {
+ "path": "./tsconfig.spec.json"
+ }
+ ],
+ "extends": "../../../tsconfig.base.json",
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/libs/shared/seo/tsconfig.lib.json b/libs/shared/seo/tsconfig.lib.json
new file mode 100644
index 00000000..1908d2ea
--- /dev/null
+++ b/libs/shared/seo/tsconfig.lib.json
@@ -0,0 +1,18 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "declaration": true,
+ "declarationMap": true,
+ "inlineSources": true,
+ "module": "commonjs",
+ "types": []
+ },
+ "exclude": [
+ "src/**/*.spec.ts",
+ "src/test-setup.ts",
+ "jest.config.ts",
+ "src/**/*.test.ts"
+ ],
+ "include": ["src/**/*.ts"]
+}
diff --git a/libs/shared/seo/tsconfig.spec.json b/libs/shared/seo/tsconfig.spec.json
new file mode 100644
index 00000000..f858ef78
--- /dev/null
+++ b/libs/shared/seo/tsconfig.spec.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../../dist/out-tsc",
+ "module": "commonjs",
+ "target": "es2016",
+ "types": ["jest", "node"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": [
+ "jest.config.ts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.d.ts"
+ ]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index bee08d97..2f725c0f 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -23,7 +23,8 @@
"@valerymelou/pages/home": ["libs/pages/home/src/index.ts"],
"@valerymelou/pages/projects": ["libs/pages/projects/src/index.ts"],
"@valerymelou/shared/layout": ["libs/shared/layout/src/index.ts"],
- "@valerymelou/shared/ui": ["libs/shared/ui/src/index.ts"]
+ "@valerymelou/shared/ui": ["libs/shared/ui/src/index.ts"],
+ "@valerymelou/shared/seo": ["libs/shared/seo/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"]