Skip to content

Commit 6f5166f

Browse files
committed
.
1 parent 9cabe82 commit 6f5166f

14 files changed

+881
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Build blog data
2+
3+
on:
4+
push:
5+
branches:
6+
- build
7+
8+
jobs:
9+
build-and-deploy:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout
14+
uses: actions/checkout@v4
15+
16+
- name: Use Node.js 22.x
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: 22.x
20+
21+
- name: Build
22+
env:
23+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24+
run: |
25+
cd build
26+
npm install
27+
28+
29+
- name: Deploy to branch
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32+
run: npx angular-cli-ghpages --no-nojekyll --dir=../dist --branch=blogdata --no-silent --name="Ferdinand Malcher" --email="[email protected]"

.github/workflows/purge-cache.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Purge Cloudflare Cache
2+
3+
on: [page_build]
4+
5+
jobs:
6+
checkout-and-purge:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- name: Checkout
11+
uses: actions/checkout@v2
12+
13+
- name: Purge Everything
14+
env:
15+
PURGE_API_TOKEN: ${{ secrets.PURGE_API_TOKEN }}
16+
PURGE_ZONE: ${{ secrets.PURGE_ZONE }}
17+
run: |
18+
sh .github/workflows/purge-cache.sh
19+

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
.DS_Store
22
Thumbs.db
3+
_site
4+
dist

_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ name: Angular.Schule
22
author: Angular.Schule
33
author_link: https://angular.schule
44
github_link: https://github.com/angular-schule/website-articles
5+
exclude: [build]

build/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
dist

build/blog.service.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import axios from 'axios';
2+
import * as emoji from 'node-emoji'
3+
import { JekyllMarkdownParser } from './jekyll-markdown-parser';
4+
import { readdir, readFile } from 'fs/promises';
5+
6+
import { BlogEntry } from './types';
7+
8+
export class BlogService {
9+
10+
private blogListFetched = this.getBlogListFromAPI();
11+
12+
constructor(private settings: {
13+
headers: {
14+
'User-Agent': string,
15+
'Authorization': string
16+
},
17+
contentsRepo: string,
18+
branch: string,
19+
markdownBaseUrl: string
20+
}) {}
21+
22+
getBlogList() {
23+
return this.blogListFetched;
24+
}
25+
26+
async getBlogEntry(slug: string): Promise<BlogEntry> {
27+
const blogList = await this.getBlogList();
28+
const entry = blogList.find(e => e.slug === slug);
29+
if (!entry) {
30+
throw new Error(`No entry found for "${slug}"`);
31+
}
32+
return entry;
33+
}
34+
35+
private sortBy<T extends { [key: string]: string }, K extends keyof T>(a: T, b: T, prop: K) {
36+
return a[prop].localeCompare(b[prop]);
37+
}
38+
39+
// dead simple way to sort things: create a sort key that can be easily sorted
40+
private getSortKey(entry: BlogEntry) {
41+
return (entry.meta.sticky ? 'Z' : 'A') + '---' + (+entry.meta.published) + '---' + entry.slug;
42+
}
43+
44+
private async readBlogFolders() {
45+
const folderContents = await readdir('../blog', { withFileTypes: true });
46+
return folderContents
47+
.filter(dirent => dirent.isDirectory())
48+
.map(dirent => dirent.name);
49+
}
50+
51+
private async readBlogFileFromFolder(folder: string) {
52+
const path = `../blog/${folder}/README.md`;
53+
return readFile(path, 'utf8');
54+
}
55+
56+
private async getBlogListFromAPI() {
57+
const blogDirs = await this.readBlogFolders();
58+
const blogEntries: BlogEntry[] = [];
59+
60+
for (const blogDir of blogDirs) {
61+
62+
try {
63+
const readme = await this.readBlogFileFromFolder(blogDir);
64+
const blogEntry = this.readmeToBlogEntry(readme, blogDir);
65+
blogEntries.push(blogEntry);
66+
67+
} catch (e: any) {
68+
const errorBlogEntry = {
69+
slug: this.slugifyPath(blogDir),
70+
html_url: '',
71+
error: e.message || 'Error',
72+
meta: {
73+
title: 'A minor error occurred while reading: ' + blogDir,
74+
hidden: true
75+
}
76+
} as BlogEntry;
77+
blogEntries.push(errorBlogEntry);
78+
79+
}
80+
}
81+
82+
return blogEntries.sort((a, b) => this.getSortKey(b).localeCompare(this.getSortKey(a)));
83+
}
84+
85+
86+
private readmeToBlogEntry(readme: string, folder: string) {
87+
const parser = new JekyllMarkdownParser(this.settings.markdownBaseUrl + folder);
88+
const parsedJekyllMarkdown = parser.parse(readme);
89+
90+
const meta = parsedJekyllMarkdown.parsedYaml || {};
91+
92+
if (meta.thumbnail &&
93+
!meta.thumbnail.startsWith('http') &&
94+
!meta.thumbnail.startsWith('//')) {
95+
meta.thumbnail
96+
= this.settings.markdownBaseUrl + folder + meta.thumbnail;
97+
}
98+
99+
return {
100+
slug: this.slugifyPath(folder),
101+
html: emoji.emojify(parsedJekyllMarkdown.html),
102+
meta: meta
103+
} as BlogEntry;
104+
}
105+
106+
private slugifyPath(path: string): string {
107+
if (!path) {
108+
return 'error - no path??';
109+
}
110+
111+
return path
112+
.replace(/README\.md$/, '')
113+
.replace('blog/', '')
114+
.replace(/\/$/, '')
115+
.replace('/', '|');
116+
}
117+
}
118+
119+

build/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const githubConfig = {
2+
headers: {
3+
'User-Agent': 'No1 Fancy AngularSchule Request Handler',
4+
Authorization: 'token ba1ad58d9eb880fbd128abddb0df8ffae47cd407',
5+
},
6+
org: 'angular-schule',
7+
contentsRepo: 'website-articles',
8+
branch: 'gh-pages',
9+
markdownBaseUrl: 'https://angular-schule.github.io/website-articles/',
10+
};

build/fetch-data.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { githubConfig } from './config';
2+
import { BlogService } from './blog.service';
3+
import { writeJSON, makeLightBlogList, createFolderIfNotExists } from './utils';
4+
5+
(async () => {
6+
const blogService = new BlogService(githubConfig);
7+
8+
await createFolderIfNotExists('./dist/data/posts');
9+
10+
const blogList = await blogService.getBlogList();
11+
const blogListLight = makeLightBlogList(blogList);
12+
await writeJSON('bloglist.json', blogListLight);
13+
14+
blogList.forEach(async (entry) => {
15+
await writeJSON(`posts/${entry.slug}.json`, entry);
16+
});
17+
})();

build/jekyll-markdown-parser.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { load } from 'js-yaml';
2+
import { marked } from 'marked';
3+
import hljs from 'highlight.js';
4+
5+
// Synchronous highlighting with highlight.js
6+
marked.setOptions({
7+
highlight: (code) => hljs.highlightAuto(code).value
8+
});
9+
10+
// original from: https://github.com/bouzuya/jekyll-markdown-parser/blob/master/src/index.ts
11+
export class JekyllMarkdownParser {
12+
13+
constructor(private baseUrl: string) {
14+
}
15+
16+
private _imageRenderer(href, title, text) {
17+
let out = `<img src="${this.baseUrl + href}" alt="${text}"`;
18+
if (title) {
19+
out += ' title="' + title + '"';
20+
}
21+
out += '>';
22+
return out;
23+
};
24+
25+
private getMarkdownRenderer() {
26+
const renderer = new marked.Renderer();
27+
renderer.image = this._imageRenderer.bind(this);
28+
return renderer;
29+
}
30+
31+
private separate(jekyllMarkdown: string): {
32+
markdown: string;
33+
yaml: string;
34+
} {
35+
const re = new RegExp('^---\s*$\r?\n', 'm');
36+
const m1 = jekyllMarkdown.match(re); // first separator
37+
38+
if (m1 === null) {
39+
return { markdown: jekyllMarkdown, yaml: '' };
40+
}
41+
42+
const s1 = jekyllMarkdown.substring((m1.index ?? 0) + m1[0].length);
43+
const m2 = s1.match(re); // second separator
44+
45+
if (m2 === null) {
46+
return { markdown: jekyllMarkdown, yaml: '' };
47+
}
48+
49+
const yaml = s1.substring(0, m2.index);
50+
const markdown = s1.substring((m2.index ?? 0) + m2[0].length);
51+
return { markdown, yaml };
52+
}
53+
54+
private compileMarkdown(markdown: string): string {
55+
const renderer = this.getMarkdownRenderer();
56+
return marked(markdown, { renderer: renderer });
57+
}
58+
59+
private parseYaml(yaml: string): any {
60+
return load(yaml);
61+
}
62+
63+
public parse(jekyllMarkdown: string): {
64+
html: string;
65+
yaml: string;
66+
parsedYaml: any;
67+
markdown: string;
68+
} {
69+
const { yaml, markdown } = this.separate(jekyllMarkdown);
70+
const parsedYaml = this.parseYaml(yaml);
71+
const html = this.compileMarkdown(markdown);
72+
73+
return {
74+
html,
75+
markdown,
76+
parsedYaml,
77+
yaml
78+
};
79+
}
80+
}

0 commit comments

Comments
 (0)