21 Commits

Author SHA1 Message Date
klemek 6680d464c3 ci: detect changes and use actions/stapler-deploy
Lint / Oxlint (push) Successful in 3m36s
Lint / ESLint (push) Successful in 3m36s
Lint / TypeScript (push) Successful in 3m36s
Deploy / Deploy to Stapler (push) Successful in 4m24s
Deploy / Build (push) Successful in 6m3s
2026-05-02 23:50:47 +02:00
klemek b6a5460a04 fix(ci): bug with Type Check job name
Lint / TypeScript (push) Successful in 18m9s
Lint / ESLint (push) Successful in 18m48s
Lint / Oxlint (push) Successful in 18m32s
Deploy / Build (push) Successful in 19m31s
Deploy / Deploy to Stapler (push) Successful in 3m54s
2026-05-02 21:16:42 +02:00
klemek 84c408d149 fix(deploy): stapler fail with body
Lint / Type Check (push) Failing after 7m40s
Lint / Oxlint (push) Successful in 10m30s
Lint / ESLint (push) Successful in 10m36s
Deploy / Build (push) Successful in 11m13s
Deploy / Deploy to Stapler (push) Successful in 3m35s
2026-05-02 20:11:31 +02:00
klemek 53d366f6b6 ci: use local bun action
Lint / Type Check (push) Failing after 2m2s
Lint / Oxlint (push) Successful in 2m14s
Lint / ESLint (push) Successful in 2m57s
Deploy / Build (push) Successful in 2m58s
Deploy / Deploy to Stapler (push) Successful in 3m56s
2026-05-02 19:57:21 +02:00
klemek 6beca83fa3 fix(actions): dont add bun version latest
Lint / Type Check (push) Failing after 5m19s
Lint / Oxlint (push) Successful in 6m57s
Lint / ESLint (push) Successful in 7m25s
Deploy / Deploy to Stapler (push) Successful in 2m53s
Deploy / Build (push) Successful in 8m12s
2026-05-02 00:49:39 +02:00
klemek cbd83360a6 chore: version 1.9.2
Lint / Oxlint (push) Successful in 15m8s
Lint / ESLint (push) Successful in 15m12s
Deploy / Build (push) Successful in 16m20s
Deploy / Deploy to Stapler (push) Successful in 9m32s
Lint / Type Check (push) Failing after 1m4s
2026-05-02 00:08:51 +02:00
klemek 767fb8cfc6 ci: add Makefile and stricter eslint config
Deploy / Build (push) Has been cancelled
Deploy / Deploy to Stapler (push) Has been cancelled
Lint / ESLint (push) Has been cancelled
Lint / Oxlint (push) Has been cancelled
Lint / Type Check (push) Has been cancelled
2026-05-02 00:08:13 +02:00
klemek 1b626c1e89 ci: lint CI
Lint / Oxlint (push) Successful in 4m19s
Lint / ESLint (push) Successful in 4m23s
Deploy / Build (push) Successful in 4m26s
Deploy / Deploy to Stapler (push) Successful in 4m9s
Lint / Type Check (push) Failing after 3m11s
2026-05-01 23:44:54 +02:00
klemek 44a08ad715 fix(actions): clone articles repo before build
Deploy / build (push) Successful in 3m43s
Deploy / deploy (push) Successful in 1m48s
2026-05-01 23:25:51 +02:00
klemek 2639c87576 chore: update dependencies
Deploy / build (push) Failing after 2m37s
Deploy / deploy (push) Has been skipped
2026-05-01 23:08:21 +02:00
klemek 85783efb81 ci: update deploy ci for gitea
Deploy / build (push) Failing after 3m22s
Deploy / deploy (push) Has been cancelled
2026-05-01 22:56:23 +02:00
klemek 6127e132e4 fix: faster page loading with no async 2026-04-27 13:27:48 +02:00
klemek 89b83e3e43 feat: updated on and better vue layout 2026-04-27 11:33:06 +02:00
klemek 39cf54624a fix: add back home button on about page 2026-04-27 10:59:11 +02:00
klemek 722548118a feat: about page and better config 2026-04-26 22:35:24 +02:00
klemek a5b24dc9bf feat: custom not found page 2026-04-26 22:10:44 +02:00
klemek 99867aa03e feat: show at least n posts and configure last text 2026-04-26 22:02:41 +02:00
klemek 65f4dd1ac5 feat: add example articles 2026-04-26 21:48:29 +02:00
klemek 748f52241f feat: navbar 2026-04-26 18:38:49 +02:00
klemek 21e1469b51 feat: glsl support with shaderview 2026-04-26 17:05:01 +02:00
klemek 649102d1aa ci: add workflow dispatch button 2026-04-26 16:47:29 +02:00
32 changed files with 823 additions and 315 deletions
+17 -5
View File
@@ -1,8 +1,20 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[Makefile]
indent_style = tab
indent_size = 2
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
indent_size = 2
max_line_length = 100
+62 -34
View File
@@ -1,37 +1,65 @@
name: Deploy
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
on:
schedule:
- cron: "*/30 * * * *"
push:
branches:
- "main"
push:
branches:
- "main"
paths:
- '.github/workflows/deploy.yml'
- 'package.json'
- 'bun.lock'
- 'src/**'
- 'index.html'
- 'vite.config.ts'
- 'post-build.ts'
- 'tsconfig.*'
- 'env.d.ts'
- 'vite.d.ts'
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- run: bun install
- name: Setup SSH Key
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- run: git clone ${{ vars.ARTICLES_REPOSITORY }} articles
- run: bun run build
- uses: actions/upload-artifact@v7
with:
name: production-files
path: ./dist
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v8
with:
name: production-files
path: ./dist
- run: tar -czC dist -f dist.tar.gz .
- run: |
curl -v -X PUT -H 'X-Token: ${{ secrets.STAPLER_TOKEN }}' -H 'X-Host-Only: ${{ vars.TARGET_HOST }}' -H 'X-SPA: index.html' --data-binary "@dist.tar.gz" ${{ vars.STAPLER_TARGET }}
build:
name: "Build"
runs-on: ubuntu-latest
steps:
- name: Set up Bun
uses: actions/setup-bun@v2
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: bun ci
- name: Checkout articles repository
uses: actions/checkout@v6
with:
repository: ${{ vars.ARTICLES_REPOSITORY }}
ref: main
token: ${{ secrets.PRIVATE_CLONE_TOKEN }}
path: ./articles
- name: Build project
run: bun run build
- name: Upload production files
uses: actions/upload-artifact@v3
with:
name: production-files
path: dist/
stapler:
name: "Deploy to Stapler"
runs-on: ubuntu-latest
needs: build
steps:
- name: Download production files
uses: actions/download-artifact@v3
with:
name: production-files
path: ./dist
- name: Upload to Stapler server
uses: actions/stapler-deploy@v1
with:
path: dist
token: ${{ secrets.STAPLER_TOKEN }}
target: ${{ github.repository }}
extra_curl_args: ${{ vars.STAPLER_CURL_ARGS }}
+66
View File
@@ -0,0 +1,66 @@
name: Lint
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
push:
paths:
- '.github/workflows/lint.yml'
- 'package.json'
- 'bun.lock'
- 'src/**'
- 'articles.example/**'
- '.oxlintrc.json'
- 'eslint.config.ts'
- 'tsconfig.*'
- 'env.d.ts'
- 'vite.d.ts'
jobs:
lint-eslint:
name: 'ESLint'
runs-on: ubuntu-latest
steps:
- name: Set up Bun
uses: actions/setup-bun@v2
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: bun ci
- name: Create fake articles
run: mv articles.example articles
- name: Run ESLint
run: bun run lint:eslint
lint-oxlint:
name: 'Oxlint'
runs-on: ubuntu-latest
steps:
- name: Set up Bun
uses: actions/setup-bun@v2
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: bun ci
- name: Create fake articles
run: mv articles.example articles
- name: Run Oxlint
run: bun run lint:oxlint
tsc:
name: 'TypeScript'
runs-on: ubuntu-latest
steps:
- name: Set up Bun
uses: actions/setup-bun@v2
- name: Checkout repository
uses: actions/checkout@v6
- name: Install dependencies
run: bun ci
- name: Create fake articles
run: mv articles.example articles
- name: Run type check
run: bun run type-check
-6
View File
@@ -1,6 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}
-9
View File
@@ -1,9 +0,0 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"oxc.oxc-vscode",
"esbenp.prettier-vscode"
]
}
+61
View File
@@ -0,0 +1,61 @@
# ENV
ifeq (,$(shell which bun))
NPM ?= npm
endif
NPM ?= bun
GIT ?= git
.PHONY: help
help: ## show this message
@echo "Usage: $(MAKE) [target1] [target2] ..."
@echo ""
@echo "Commands/Targets:"
@cat $(MAKEFILE_LIST) | grep -E '(^[a-zA-Z0-9_%-]+:.*?##.*$$)|(^##)' | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-20s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
@echo ""
@echo "Environment:"
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z0-9_-]+\s*\??=.*$$' | grep -Eo '^[a-zA-Z0-9_-]+' | xargs -I {} $(MAKE) -s print-{} 2> /dev/null
.PHONY: print-%
print-%:
@echo -e '\033[32m$*\033[0m = $($*)'
# FILES
node_modules: bun.lock
@$(MAKE) -s npm-install
# ACTIONS
.PHONY: install
install: npm-install ## install project
.PHONY: update
update: npm-update ## update project
.PHONY: build
build: npm-run-build ## build static site in "dist"
.PHONY: dev
dev: npm-run-dev ## run dev server
.PHONY: lint
lint: npm-run-lint npm-run-type-check ## lint code
.PHONY: format
format: npm-run-lint-fix ## fix and reformat code
# TOOLS
.PHONY: npm-install
npm-install: ## npm install
$(NPM) install
.PHONY: npm-update
npm-update: ## npm update
$(NPM) update
.PHONY: npm-run-%
npm-run-%: node_modules ## npm run (script)
$(NPM) run $*
+6 -5
View File
@@ -1,3 +1,5 @@
[![](https://git.klemek.fr/klemek/md-blog/actions/workflows/lint.yml/badge.svg?branch=main&style=flat-square)](https://git.klemek.fr/klemek/md-blog/actions?workflow=lint.yml) [![](https://git.klemek.fr/klemek/md-blog/actions/workflows/deploy.yml/badge.svg?branch=main&style=flat-square)](https://git.klemek.fr/klemek/md-blog/actions?workflow=deploy.yml)
# md-blog
## Minimal setup
@@ -20,10 +22,9 @@ bun run build
- [x] build with github actions
- [x] config in sub repo
- [x] copyright
- [ ] nav bar on top
- [ ] date updated
- [x] nav bar on top
- [x] date updated
- [ ] archive page
- [ ] about page
- [ ] contact/links
- [x] about page
- [ ] link to previous/next article
- [ ] proper docs
- [ ] proper docs
Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

+40
View File
@@ -0,0 +1,40 @@
---
title: Demo
author: kleπek
draft: false
thumbnail: ./thumbnail.jpg
date: 2026-01-01
---
## Images
PNG: ![png file](./atom.png)
## highlight.js
```typescript
function test(arg1: string, arg2: number): string {
return `${arg1}: ${arg2}`
}
```
```text
text/plain
```
## Mermaid
```mermaid
flowchart LR
A --> B
```
## LaTex
$$\Large\color{Red}{\frac{\Delta K}{2}=\frac{T_{2}}{x_{2}(x_{2}-x_{1})}-\frac{T_{1}}{x_{1}(x_{2}-x_{1})}}$$
Inline LaTex $$\frac{\Delta K}{2}$$ in text
## Lucide Icons
Icon inside <i icon="atom"></i> text
+15
View File
@@ -0,0 +1,15 @@
{
"base_url": "http://localhost/",
"title": "<i icon=notepad-text></i> My Blog",
"signature": "By <b>Me</b>",
"lang": "en",
"custom_head": "",
"home_count": 5,
"rss_link": "<i icon=rss></i> RSS",
"about_link": "<i icon=info></i> ABOUT",
"back_link": "<i icon=undo-2></i> Back to home",
"published_on": "Published on",
"updated_on": "Updated on",
"authored": "By",
"copyright": "<a style=\"text-decoration:none;color:inherit;\" target=_blank href=\"https://creativecommons.org/licenses/by-nc/4.0/\">CC BY-NC</a>"
}
+187
View File
@@ -0,0 +1,187 @@
/*
=================================================
https://www.joshwcomeau.com/css/custom-css-reset/
=================================================
*/
/*
1. Use a more-intuitive box-sizing model.
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
3. Allow percentage-based heights in the application
*/
html,
body,
div#app {
height: 100%;
}
/*
Typographic tweaks!
4. Add accessible line-height
5. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/*
6. Improve media defaults
*/
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
/*
7. Remove built-in form typography styles
*/
input,
button,
textarea,
select {
font: inherit;
}
/*
8. Avoid text overflows
*/
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
/*
9. Create a root stacking context
*/
#root,
#__next {
isolation: isolate;
}
/*
=================================================
https://blog.koley.in/2019/339-bytes-of-responsive-css
https://www.swyx.io/css-100-bytes
https://gist.github.com/JoeyBurzynski/617fb6201335779f8424ad9528b72c41
=================================================
*/
html,
body {
padding: 0;
max-width: 100%;
color: #222;
font-family: Verdana, serif;
}
body {
background-color: #eee;
}
main {
padding: 1.5rem;
margin: auto;
background-color: #ccc;
min-height: 100%;
}
table {
border-collapse: collapse;
width: 100%;
font-size: 0.9em;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 1em 0 0.5em;
}
p,
ul,
ol,
pre,
img,
blockquote,
details {
margin-bottom: 1em;
color: #444;
}
hr {
opacity: 25%;
border-bottom: 0;
margin-bottom: 0.5em;
}
textarea,
input,
select,
pre,
.mono {
font-family: monospace;
}
@media only screen and (min-width: 768px) {
main {
max-width: 42rem;
}
table {
font-size: inherit;
}
}
/*
* MD BLOG
*/
nav {
display: flex;
align-items: flex-end;
}
.home nav {
margin-bottom: 1em;
vertical-align: text-bottom;
}
.home .nav-title {
font-size: xx-large;
}
nav .nav-title {
font-size: larger;
font-weight: bold;
color: inherit;
display: inline-block;
flex-grow: 1;
}
nav .nav-items {
display: flex;
gap: 0.5em;
}
.article-info {
font-style: italic;
}
+20 -5
View File
@@ -5,9 +5,10 @@
"": {
"name": "md-blog",
"dependencies": {
"@keithclark/shaderview": "https://github.com/keithclark/shaderview/archive/refs/tags/1.2.0.tar.gz",
"highlight.js": "^11.11.1",
"katex": "^0.16.45",
"lucide": "^1.11.0",
"lucide": "^1.14.0",
"mermaid": "^11.14.0",
"vue": "^3.5.33",
"vue-router": "^5.0.6",
@@ -16,10 +17,10 @@
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.2",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.2.1",
"eslint-config-prettier": "^10.1.8",
"eslint": "^10.3.0",
"eslint-plugin-oxlint": "~1.60.0",
"eslint-plugin-vue": "~10.8.0",
"feed": "^5.2.1",
@@ -210,6 +211,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@keithclark/shaderview": ["@keithclark/shaderview@https://github.com/keithclark/shaderview/archive/refs/tags/1.2.0.tar.gz", {}],
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
@@ -288,6 +291,8 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
"@pkgr/core": ["@pkgr/core@0.2.9", "", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
@@ -454,6 +459,8 @@
"@vue/devtools-shared": ["@vue/devtools-shared@8.1.1", "", {}, "sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ=="],
"@vue/eslint-config-prettier": ["@vue/eslint-config-prettier@10.2.0", "", { "dependencies": { "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw=="],
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.7.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.56.0", "fast-glob": "^3.3.3", "typescript-eslint": "^8.56.0", "vue-eslint-parser": "^10.4.0" }, "peerDependencies": { "eslint": "^9.10.0 || ^10.0.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg=="],
"@vue/language-core": ["@vue/language-core@3.2.7", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.1.2", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.4" } }, "sha512-Gn4q/tRxbpVGLEuARQ43p3YELlNAFgRUVCgW9U5Cr+5q4vfD2bWDWpl3ABbJMXUt5xlE1dF8dkigg2aUq7JYYw=="],
@@ -638,12 +645,14 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
"eslint": ["eslint@10.3.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="],
"eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="],
"eslint-plugin-oxlint": ["eslint-plugin-oxlint@1.60.0", "", { "dependencies": { "jsonc-parser": "^3.3.1" }, "peerDependencies": { "oxlint": "~1.60.0" } }, "sha512-9RUD23k7ablez1qg7JWnyPYPOlbucDDqaDr+qNUi0TbIQCPqIPCLzfllgqKF9lOxlg+l17H8hISErmarvm2J1w=="],
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.5", "", { "dependencies": { "prettier-linter-helpers": "^1.0.1", "synckit": "^0.11.12" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw=="],
"eslint-plugin-vue": ["eslint-plugin-vue@10.8.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^7.1.0", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "optionalPeers": ["@stylistic/eslint-plugin", "@typescript-eslint/parser"] }, "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
@@ -668,6 +677,8 @@
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
@@ -802,7 +813,7 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide": ["lucide@1.11.0", "", {}, "sha512-2uvbGlcztUY+z0Ef++YCaxD6mtzrPsUJ1qWbIfqZrZGRxZBCL3icE6g2nzRTtJ6YywOoXC5blL/NmkLI66en5g=="],
"lucide": ["lucide@1.14.0", "", {}, "sha512-IoRC3lHwemJWvsXKcHK90hkgY4h1HGztBL63w2XwFtIu8gFDPp4/kiuqVtlN3vaM9bxsLQ4ZUBJfGsbKFaB2IA=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
@@ -892,6 +903,8 @@
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
@@ -988,6 +1001,8 @@
"sync-message-port": ["sync-message-port@1.2.0", "", {}, "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg=="],
"synckit": ["synckit@0.11.12", "", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
+24 -18
View File
@@ -1,26 +1,32 @@
import { globalIgnores } from 'eslint/config'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
import pluginVue from 'eslint-plugin-vue'
import pluginOxlint from 'eslint-plugin-oxlint'
import skipFormatting from 'eslint-config-prettier/flat'
import { globalIgnores } from "eslint/config";
import {
defineConfigWithVueTs,
vueTsConfigs,
} from "@vue/eslint-config-typescript";
import pluginVue from "eslint-plugin-vue";
import skipFormatting from "@vue/eslint-config-prettier/skip-formatting";
import { configureVueProject } from "@vue/eslint-config-typescript";
import pluginOxlint from "eslint-plugin-oxlint";
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
configureVueProject({ scriptLangs: ["ts", "tsx"] });
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{vue,ts,mts,tsx}'],
name: "app/files-to-lint",
files: ["**/*.{ts,mts,tsx,vue}"],
},
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
...pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
globalIgnores(["**/dist/**"]),
pluginVue.configs["flat/recommended"],
vueTsConfigs.strictTypeChecked,
vueTsConfigs.stylisticTypeChecked,
...pluginOxlint.buildFromOxlintConfigFile(".oxlintrc.json"),
skipFormatting,
)
{
rules: {
"vue/no-v-html": "off",
},
},
);
+11 -8
View File
@@ -1,9 +1,9 @@
{
"name": "md-blog",
"version": "1.3.0",
"version": "1.9.2",
"private": true,
"type": "module",
"repository": "https://github.com/klemek/md-blog",
"repository": "https://git.klemek.fr/klemek/md-blog",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" -- && run-p post-build",
@@ -12,14 +12,17 @@
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache",
"format": "prettier --write src/ *.ts *.json"
"lint-fix": "run-s lint-fix:*",
"lint:eslint": "eslint . --cache",
"lint:oxlint": "oxlint .",
"lint-fix:eslint": "eslint . --fix --cache",
"lint-fix:oxlint": "oxlint . --fix"
},
"dependencies": {
"@keithclark/shaderview": "https://github.com/keithclark/shaderview/archive/refs/tags/1.2.0.tar.gz",
"highlight.js": "^11.11.1",
"katex": "^0.16.45",
"lucide": "^1.11.0",
"lucide": "^1.14.0",
"mermaid": "^11.14.0",
"vue": "^3.5.33",
"vue-router": "^5.0.6"
@@ -28,10 +31,10 @@
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.2",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.7.0",
"@vue/tsconfig": "^0.9.1",
"eslint": "^10.2.1",
"eslint-config-prettier": "^10.1.8",
"eslint": "^10.3.0",
"eslint-plugin-oxlint": "~1.60.0",
"eslint-plugin-vue": "~10.8.0",
"feed": "^5.2.1",
+83 -71
View File
@@ -1,129 +1,141 @@
import fs from 'fs'
import process from 'process'
import { Feed } from 'feed'
import articlesConfig from './articles/config.json'
import fs from "fs";
import process from "process";
import { Feed } from "feed";
import articlesConfig from "./articles/config.json";
function getFiles(dir: string): string[] {
return fs.readdirSync(dir).flatMap((name) => {
const path = `${dir}/${name}`
const path = `${dir}/${name}`;
if (fs.statSync(path).isDirectory()) {
return getFiles(path)
return getFiles(path);
} else {
return [path]
return [path];
}
})
});
}
const METADATA_BLOCK_REGEX = /^---\n([\s\S]*?)---\n/m
const METADATA_REGEX = /^(\w+):(.*)$/gm
const METADATA_BLOCK_REGEX = /^---\n([\s\S]*?)---\n/m;
const METADATA_REGEX = /^(\w+):(.*)$/gm;
function readArticleMetadata(path: string): Record<string, string> | null {
const content = fs.readFileSync(path, { encoding: 'utf8' })
const match: RegExpExecArray | null = METADATA_BLOCK_REGEX.exec(content)
if (!match || !match[1]) {
console.warn(`No metadata for: ${path}`)
return null
const content = fs.readFileSync(path, { encoding: "utf8" });
const match: RegExpExecArray | null = METADATA_BLOCK_REGEX.exec(content);
if (!match?.[1]) {
console.warn(`No metadata for: ${path}`);
return null;
}
let subMatch: RegExpExecArray | null = null
let subMatch: RegExpExecArray | null = null;
const metadata: Record<string, string> = {
path: path.replaceAll('/index.md', ''),
}
path: path.replaceAll("/index.md", ""),
};
do {
subMatch = METADATA_REGEX.exec(match[1])
if (subMatch && subMatch[1] && subMatch[2]) {
metadata[subMatch[1]] = subMatch[2].trim()
subMatch = METADATA_REGEX.exec(match[1]);
if (subMatch?.[1] && subMatch[2]) {
metadata[subMatch[1]] = subMatch[2].trim();
}
} while (subMatch)
return metadata
} while (subMatch);
return metadata;
}
function formatArticlePage(metadata: Record<string, string>, baseHtml: string): string {
let outHtml = baseHtml
outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, '')
function formatArticlePage(
metadata: Record<string, string>,
baseHtml: string,
): string {
let outHtml = baseHtml;
outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, "");
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:url" content="${articlesConfig['base_url']}${metadata.path}/">\n</head>`,
)
const blog_title = articlesConfig['title']?.replace(/(<([^>]+)>)/gi, '').trim()
`<meta property="og:url" content="${articlesConfig.base_url}${metadata.path}/">\n</head>`,
);
const blog_title = articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim();
if (metadata.title) {
const title = metadata.title.replace(/(<([^>]+)>)/gi, '').trim()
outHtml = outHtml.replace(/<title>.*?<\/title>/gm, `<title>${blog_title}${title}</title>`)
outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, '')
const title = metadata.title.replace(/(<([^>]+)>)/gi, "").trim();
outHtml = outHtml.replace(
/<title>.*?<\/title>/gm,
`<title>${blog_title}${title}</title>`,
);
outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, "");
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:title" content="${title}">\n</head>`,
)
outHtml = outHtml.replace(/<.*?property="og:description".*?>/gm, '')
);
outHtml = outHtml.replace(/<.*?property="og:description".*?>/gm, "");
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:description" content="${blog_title}">\n</head>`,
)
);
}
if (metadata.thumbnail) {
outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, '')
outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, "");
outHtml = outHtml.replace(
/<\/head>/gm,
`<meta property="og:image" content="${metadata.thumbnail.replace('./', articlesConfig['base_url'] + metadata.path + '/')}">\n</head>`,
)
`<meta property="og:image" content="${metadata.thumbnail.replace("./", articlesConfig.base_url + metadata.path + "/")}">\n</head>`,
);
}
return outHtml
return outHtml;
}
function addFeedArticle(metadata: Record<string, string>, feed: Feed) {
if (metadata.draft !== 'true') {
if (metadata.draft !== "true") {
feed.addItem({
title: metadata.title.replace(/(<([^>]+)>)/gi, '').trim(),
title: metadata.title.replace(/(<([^>]+)>)/gi, "").trim(),
id: metadata.path,
link: `${articlesConfig['base_url']}${metadata.path}/`,
link: `${articlesConfig.base_url}${metadata.path}/`,
date: new Date(Date.parse(metadata.date)),
image: metadata.thumbnail.replace('./', articlesConfig['base_url'] + metadata.path + '/'),
})
image: metadata.thumbnail.replace(
"./",
articlesConfig.base_url + metadata.path + "/",
),
});
}
}
const indexContent = fs.readFileSync('dist/index.html', { encoding: 'utf8' })
const indexContent = fs.readFileSync("dist/index.html", { encoding: "utf8" });
if (!indexContent) {
console.error('Could not read dist/index.html')
process.exit(1)
console.error("Could not read dist/index.html");
process.exit(1);
}
const metadatas = getFiles('articles')
.filter((path) => path.match(/\/index.md$/))
const metadatas = getFiles("articles")
.filter((path) => /\/index.md$/.exec(path))
.map((path) => readArticleMetadata(path))
.filter((metadata) => !!metadata)
.filter((metadata) => !!metadata);
const feed = new Feed({
title: articlesConfig['title']?.replace(/(<([^>]+)>)/gi, '').trim() ?? '',
id: articlesConfig['base_url'],
link: articlesConfig['base_url'],
language: articlesConfig['lang'],
favicon: articlesConfig['base_url'] + 'articles/favicon.ico',
generator: 'md-blog',
title: articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim(),
id: articlesConfig.base_url,
link: articlesConfig.base_url,
language: articlesConfig.lang,
favicon: articlesConfig.base_url + "articles/favicon.ico",
generator: "md-blog",
feedLinks: {
json: articlesConfig['base_url'] + 'feed.json',
atom: articlesConfig['base_url'] + 'atom.xml',
rss: articlesConfig['base_url'] + 'rss',
json: articlesConfig.base_url + "feed.json",
atom: articlesConfig.base_url + "atom.xml",
rss: articlesConfig.base_url + "rss",
},
updated: new Date(
Math.max(
0,
...metadatas
.filter((metadata) => metadata.draft !== 'true')
.filter((metadata) => metadata.draft !== "true")
.map((metadata) => Date.parse(metadata.date)),
),
),
})
});
metadatas.forEach((metadata) => {
fs.writeFileSync(`dist/${metadata.path}/index.html`, formatArticlePage(metadata, indexContent))
console.info(`Wrote dist/${metadata.path}/index.html`)
addFeedArticle(metadata, feed)
})
fs.writeFileSync(
`dist/${metadata.path}/index.html`,
formatArticlePage(metadata, indexContent),
);
console.info(`Wrote dist/${metadata.path}/index.html`);
addFeedArticle(metadata, feed);
});
fs.writeFileSync('dist/feed.json', feed.json1())
console.info(`Wrote dist/feed.json`)
fs.writeFileSync('dist/atom.xml', feed.atom1())
console.info(`Wrote dist/atom.xml`)
fs.writeFileSync('dist/rss.xml', feed.rss2())
console.info(`Wrote dist/rss.xml`)
fs.writeFileSync("dist/feed.json", feed.json1());
console.info(`Wrote dist/feed.json`);
fs.writeFileSync("dist/atom.xml", feed.atom1());
console.info(`Wrote dist/atom.xml`);
fs.writeFileSync("dist/rss.xml", feed.rss2());
console.info(`Wrote dist/rss.xml`);
+7 -24
View File
@@ -1,29 +1,12 @@
<script setup lang="ts">
import hljs from 'highlight.js'
import { createIcons, icons } from 'lucide'
import { onMounted, onUpdated, nextTick } from 'vue'
import mermaid from 'mermaid'
async function update() {
setTimeout(async () => {
await nextTick()
hljs.highlightAll()
createIcons({
icons,
nameAttr: 'icon',
attrs: {
width: '1.1em',
height: '1.1em',
},
})
mermaid.run()
}, 100)
}
onMounted(update)
onUpdated(update)
import PageFooter from '@components/PageFooter.vue'
import NavBar from '@components/NavBar.vue'
</script>
<template>
<RouterView @vue:mounted="update" @vue:updated="update" />
<main>
<NavBar />
<RouterView />
<PageFooter />
</main>
</template>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import { BACK_LINK } from '@lib/config'
</script>
<template>
<template v-if="$route.fullPath != '/'">
<RouterLink class="link-back" to="/"><span v-html="BACK_LINK"></span></RouterLink>
</template>
</template>
+13
View File
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { BASE_URL, TITLE, RSS_LINK, ABOUT_LINK } from '@lib/config'
</script>
<template>
<nav>
<RouterLink to="/" class="nav-title"><span v-html="TITLE"></span></RouterLink>
<span class="nav-items">
<RouterLink to="/about/"><span v-html="ABOUT_LINK"></span></RouterLink>
<a :href="BASE_URL + 'atom.xml'" v-html="RSS_LINK"></a>
</span>
</nav>
</template>
+5 -9
View File
@@ -1,16 +1,12 @@
<script setup lang="ts">
import { REPOSITORY, NAME, VERSION, BASE_URL, TITLE, COPYRIGHT } from '@/lib/meta'
import { stripHTML } from '@/lib/strings';
import { REPOSITORY, NAME, VERSION, TITLE, COPYRIGHT } from '@lib/config'
import { stripHTML } from '@lib/strings'
</script>
<template>
<template v-if="$route.fullPath != '/'">
<RouterLink to="/"><i icon="undo-2"></i> Back to home</RouterLink>
</template>
<hr />
<footer>
{{ stripHTML(TITLE) }} &copy; {{ new Date().getFullYear() }}, <span v-html="COPYRIGHT"></span> | Made with
<a :href="REPOSITORY">{{ NAME }} {{ VERSION }}</a> |
<a :href="BASE_URL + 'atom.xml'"><i icon="rss"></i> RSS</a>
{{ stripHTML(TITLE) }} &copy; {{ new Date().getFullYear() }}, <span v-html="COPYRIGHT"></span> |
Made with
<a :href="REPOSITORY">{{ NAME }} {{ VERSION }}</a>
</footer>
</template>
+2 -1
View File
@@ -7,7 +7,8 @@ export interface ArticleMetadata {
path: string
title: string
date: Date
author: string
updated?: Date
author?: string
thumbnail?: string
draft?: boolean
}
+92 -51
View File
@@ -1,105 +1,146 @@
import type { MarkdownData, Article, ArticleMetadata } from '@interfaces'
import katex from 'katex'
import type { MarkdownData, Article, ArticleMetadata } from "@interfaces";
import katex from "katex";
import { nextTick } from "vue";
import hljs from "highlight.js";
import mermaid from "mermaid";
import { createIcons, icons } from "lucide";
export async function updateDynamicContent() {
await nextTick();
hljs.highlightAll();
createIcons({
icons,
nameAttr: "icon",
attrs: {
width: "1.1em",
height: "1.1em",
},
});
await mermaid.run();
}
function parseMetadata(
srcAttributes: Record<string, unknown>,
pathPrefix: string,
): ArticleMetadata {
const draft = !!srcAttributes.draft;
return {
path: pathPrefix,
title: decodeURIComponent((srcAttributes.title as string) ?? 'Untitled'),
date: new Date(Date.parse((srcAttributes.date as string) ?? '')),
author: decodeURIComponent((srcAttributes.author as string) ?? ''),
thumbnail: (srcAttributes.thumbnail as string) ?? '',
draft: !!srcAttributes.draft,
}
title:
(draft ? "[DRAFT] " : "") +
decodeURIComponent(
(srcAttributes.title as string | undefined) ?? "Untitled",
),
date: (srcAttributes.date as string | undefined)
? new Date(Date.parse(srcAttributes.date as string))
: new Date(),
updated: (srcAttributes.updated as string | undefined)
? new Date(Date.parse(srcAttributes.updated as string))
: undefined,
author: decodeURIComponent(
(srcAttributes.author as string | undefined) ?? "",
),
thumbnail: (srcAttributes.thumbnail as string | undefined) ?? "",
draft: draft,
};
}
const LATEX_REGEX = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m
const MERMAID_REGEX = /<pre>\s*<code class="language-mermaid">([\s\S]*)<\/code>\s*<\/pre>/m
const LATEX_REGEX = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const MERMAID_REGEX =
/<pre>\s*<code class="language-mermaid">([\s\S]*)<\/code>\s*<\/pre>/m;
function transformLatexBlocks(srcHtml: string): string {
let outHtml = srcHtml
let match: RegExpExecArray | null = null
let outHtml = srcHtml;
let match: RegExpExecArray | null = null;
do {
match = LATEX_REGEX.exec(outHtml)
if (match && match[1]) {
match = LATEX_REGEX.exec(outHtml);
if (match?.[1]) {
try {
outHtml = outHtml.replace(match[0], katex.renderToString(match[1]))
outHtml = outHtml.replace(match[0], katex.renderToString(match[1]));
} catch (ex) {
outHtml = outHtml.replace(match[0], `<i>katex error: ${ex}</i>`)
outHtml = outHtml.replace(
match[0],
`<i>katex error: ${ex as Error}</i>`,
);
}
}
} while (match && match[1])
return outHtml
} while (match?.[1]);
return outHtml;
}
function transformMermaidBlocks(srcHtml: string): string {
let outHtml = srcHtml
let match: RegExpExecArray | null = null
let outHtml = srcHtml;
let match: RegExpExecArray | null = null;
do {
match = MERMAID_REGEX.exec(outHtml)
if (match && match[1]) {
outHtml = outHtml.replace(match[0], `<pre class="mermaid">\n${match[1]}\n</pre>`)
match = MERMAID_REGEX.exec(outHtml);
if (match?.[1]) {
outHtml = outHtml.replace(
match[0],
`<pre class="mermaid">\n${match[1]}\n</pre>`,
);
}
} while (match && match[1])
return outHtml
} while (match?.[1]);
return outHtml;
}
function transformHtml(srcHtml: string): string {
let outHtml: string = srcHtml
outHtml = transformLatexBlocks(outHtml)
outHtml = transformMermaidBlocks(outHtml)
return outHtml
let outHtml: string = srcHtml;
outHtml = transformLatexBlocks(outHtml);
outHtml = transformMermaidBlocks(outHtml);
return outHtml;
}
/**
* @deprecated
*/
export async function loadArticleOld(date: Date): Promise<Article | null> {
const year = date.getFullYear().toString()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const path = `./articles/${year}/${month}/${day}/`
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const path = `./articles/${year}/${month}/${day}/`;
try {
const data = (await import(`@articles/${year}/${month}/${day}/index.md`)) as MarkdownData
const data = (await import(
`@articles/${year}/${month}/${day}/index.md`
)) as MarkdownData;
return {
metadata: parseMetadata(data.attributes, path),
html: transformHtml(data.html),
}
};
} catch (ex) {
console.error(ex)
return null
console.error(ex);
return null;
}
}
export async function loadArticle(path: string): Promise<Article | null> {
const raw_articles = import.meta.glob('@articles/**/index.md')
const key = `/articles/${path}index.md`
if (!raw_articles[key]) return null
const raw_articles = import.meta.glob("@articles/**/index.md");
const key = `/articles/${path}index.md`;
if (!raw_articles[key]) return null;
try {
const data = (await raw_articles[key]()) as MarkdownData
const data = (await raw_articles[key]()) as MarkdownData;
return {
metadata: parseMetadata(data.attributes, path),
html: transformHtml(data.html),
}
};
} catch (ex) {
console.error(ex)
return null
console.error(ex);
return null;
}
}
export async function listArticles(): Promise<ArticleMetadata[]> {
const raw_articles = import.meta.glob('@articles/**/index.md')
const raw_articles = import.meta.glob("@articles/**/index.md");
const articles: ArticleMetadata[] = (
await Promise.all(
Object.keys(raw_articles).map(async (key) => {
if (!raw_articles[key]) return null
const data = (await raw_articles[key]()) as MarkdownData
return parseMetadata(data.attributes, key.replace('index.md', ''))
if (!raw_articles[key]) return null;
const data = (await raw_articles[key]()) as MarkdownData;
return parseMetadata(data.attributes, key.replace("index.md", ""));
}),
)
).filter((item) => item !== null)
articles.sort((article1, article2) => article2.date.valueOf() - article1.date.valueOf())
return articles
).filter((item) => item !== null);
articles.sort(
(article1, article2) => article2.date.valueOf() - article1.date.valueOf(),
);
return articles;
}
+18
View File
@@ -0,0 +1,18 @@
import packageJson from '@/../package.json'
import articlesConfig from '@articles/config.json'
export const NAME: string = packageJson.name
export const VERSION: string = packageJson.version
export const REPOSITORY: string = packageJson.repository
export const TITLE: string = articlesConfig.title
export const SIGNATURE: string = articlesConfig.signature
export const COPYRIGHT: string = articlesConfig.copyright
export const RSS_LINK: string = articlesConfig.rss_link
export const BACK_LINK: string = articlesConfig.back_link
export const ABOUT_LINK: string = articlesConfig.about_link
export const HOME_COUNT: number = articlesConfig.home_count
export const PUBLISHED_ON: string = articlesConfig.published_on
export const UPDATED_ON: string = articlesConfig.updated_on
export const AUTHORED: string = articlesConfig.authored
export const BASE_URL: string = import.meta.env.BASE_URL
export const PROD: boolean = import.meta.env.PROD
+6 -6
View File
@@ -1,9 +1,9 @@
export function simpleDateFormat(date: Date): string {
return (
date.getFullYear() +
'-' +
(date.getMonth() + 1).toString().padStart(2, '0') +
'-' +
date.getDate().toString().padStart(2, '0')
)
date.getFullYear().toString() +
"-" +
(date.getMonth() + 1).toString().padStart(2, "0") +
"-" +
date.getDate().toString().padStart(2, "0")
);
}
-9
View File
@@ -1,9 +0,0 @@
import packageJson from '@/../package.json'
export const NAME = packageJson.name
export const VERSION = packageJson.version
export const REPOSITORY = packageJson.repository
export const TITLE = import.meta.env.VITE_APP_TITLE
export const SIGNATURE = import.meta.env.VITE_APP_SIGNATURE
export const COPYRIGHT = import.meta.env.VITE_APP_COPYRIGHT
export const BASE_URL = import.meta.env.BASE_URL
+3
View File
@@ -2,6 +2,9 @@ import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ShaderviewElement from '@keithclark/shaderview'
customElements.define('kc-shaderview', ShaderviewElement)
const app = createApp(App)
app.use(router)
+5 -3
View File
@@ -1,12 +1,14 @@
import ArticleView from '@views/ArticleView.vue'
import HomeView from '@views/HomeView.vue'
import NotFoundView from '@views/NotFoundView.vue'
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '@views/HomeView.vue'
import AboutView from '@views/AboutView.vue'
import ArticleView from '@views/ArticleView.vue'
import NotFoundView from '@views/NotFoundView.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomeView },
{ path: '/about/', component: AboutView },
{ path: '/articles/:pathMatch(.*)/', component: ArticleView },
{ path: '/:pathMatch(.*)', component: NotFoundView },
],
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { onMounted, onUpdated } from 'vue'
import { updateDynamicContent } from '@lib/articles'
import { html } from '@articles/about.md'
onMounted(updateDynamicContent)
onUpdated(updateDynamicContent)
</script>
<template>
<div class="article">
<div v-html="html"></div>
<BackHomeButton />
</div>
</template>
+24 -20
View File
@@ -1,13 +1,13 @@
<script setup lang="ts">
import type { Article } from '@interfaces'
import { ref, onBeforeMount } from 'vue'
import { loadArticle } from '@lib/articles'
import { ref, onBeforeMount, onUpdated, onMounted } from 'vue'
import { loadArticle, updateDynamicContent } from '@lib/articles'
import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router'
import NotFoundView from './NotFoundView.vue'
import { simpleDateFormat } from '@lib/dates'
import { SIGNATURE, TITLE } from '@lib/meta'
import PageFooter from '@components/PageFooter.vue'
import { stripHTML } from '@/lib/strings'
import { AUTHORED, PUBLISHED_ON, SIGNATURE, TITLE, UPDATED_ON } from '@lib/config'
import { stripHTML } from '@lib/strings'
import BackHomeButton from '@components/BackHomeButton.vue'
import NotFoundView from '@views/NotFoundView.vue'
const article = ref<Article | null>(null)
const loading = ref<boolean>(true)
@@ -19,27 +19,32 @@ async function loadPage(target: RouteLocation) {
window.document.title =
stripHTML(TITLE) + ' — ' + stripHTML(article.value?.metadata.title ?? 'Not Found')
loading.value = false
await updateDynamicContent()
}
onBeforeMount(() => loadPage(route))
onBeforeRouteUpdate(loadPage)
onMounted(updateDynamicContent)
onUpdated(updateDynamicContent)
</script>
<template>
<template v-if="loading">
<main></main>
</template>
<template v-else-if="!article">
<template v-if="!loading && !article">
<NotFoundView />
</template>
<template v-else>
<main class="article">
<div v-if="!loading" class="article">
<template v-if="!loading && article">
<div class="article-header">
<RouterLink class="link-home" to="/"><i icon="undo-2"></i></RouterLink>
<h1 class="article-title" v-html="article.metadata.title"></h1>
<div class="article-published">
{{ article.metadata.draft ? 'Drafted on' : 'Published on' }}
{{ simpleDateFormat(article.metadata.date) }}
<div class="article-info">
<span v-if="article.metadata.author"
><span v-html="AUTHORED"></span> <span v-html="article.metadata.author"></span>
</span>
<span v-html="PUBLISHED_ON"></span> {{ simpleDateFormat(article.metadata.date) }}
<span v-if="article.metadata.updated"
> <span v-html="UPDATED_ON"></span>
{{ simpleDateFormat(article.metadata.updated) }}</span
>
</div>
<img
v-if="article.metadata.thumbnail"
@@ -50,8 +55,7 @@ onBeforeRouteUpdate(loadPage)
</div>
<div class="article-text" v-html="article.html"></div>
<div class="article-signature" v-html="SIGNATURE"></div>
<br />
<PageFooter />
</main>
</template>
<BackHomeButton />
</template>
</div>
</template>
+19 -13
View File
@@ -1,29 +1,36 @@
<script setup lang="ts">
import { ref, onBeforeMount } from 'vue'
import { listArticles } from '@lib/articles'
import { ref, onBeforeMount, onUpdated, onMounted } from 'vue'
import { listArticles, updateDynamicContent } from '@lib/articles'
import type { ArticleMetadata } from '@interfaces'
import { simpleDateFormat } from '@lib/dates'
import { TITLE } from '@lib/meta'
import PageFooter from '@components/PageFooter.vue'
import { stripHTML } from '@/lib/strings'
import { HOME_COUNT, PROD, PUBLISHED_ON, TITLE } from '@lib/config'
import { stripHTML } from '@lib/strings'
const articles = ref<ArticleMetadata[]>([])
const loading = ref<boolean>(true)
onBeforeMount(async () => {
const newArticles = await listArticles()
const newArticles = (await listArticles())
.filter((metadata) => (!metadata.draft || !PROD) && metadata.path)
.slice(0, HOME_COUNT)
articles.value.splice(0, articles.value.length, ...newArticles)
window.document.title = stripHTML(TITLE) + ' — Home'
loading.value = false
await updateDynamicContent()
})
onMounted(updateDynamicContent)
onUpdated(updateDynamicContent)
</script>
<template>
<main>
<h1 class="title" v-html="TITLE"></h1>
<template v-for="(metadata, index) in articles" v-bind:key="index">
<div v-if="!metadata.draft && metadata.path" class="article-item">
<div v-if="!loading" class="home">
<template v-for="(metadata, index) in articles" :key="index">
<div v-if="(!metadata.draft || !PROD) && metadata.path" class="article-item">
<RouterLink :to="metadata.path">
<h2 v-html="metadata.title"></h2>
<span class="article-published">Published on {{ simpleDateFormat(metadata.date) }}</span>
<span class="article-info"
><span v-html="PUBLISHED_ON"></span> {{ simpleDateFormat(metadata.date) }}</span
>
<img
v-if="metadata.thumbnail"
alt="thumbnail"
@@ -32,6 +39,5 @@ onBeforeMount(async () => {
</RouterLink>
</div>
</template>
<PageFooter />
</main>
</div>
</template>
+6 -11
View File
@@ -1,17 +1,12 @@
<script setup lang="ts">
import { TITLE } from '@/lib/meta'
import { stripHTML } from '@/lib/strings'
import PageFooter from '@components/PageFooter.vue'
import { onBeforeMount } from 'vue'
import { onMounted, onUpdated } from 'vue'
import { updateDynamicContent } from '@lib/articles'
import { html } from '@articles/not_found.md'
onBeforeMount(() => {
window.document.title = stripHTML(TITLE) + ' — Not Found'
})
onMounted(updateDynamicContent)
onUpdated(updateDynamicContent)
</script>
<template>
<main>
<h1>Page not found</h1>
<PageFooter />
</main>
<div class="article" v-html="html"></div>
</template>
+5 -7
View File
@@ -10,13 +10,11 @@ import articlesConfig from './articles/config.json'
export default ({ mode }: { mode: string }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) }
process.env.VITE_BASE_URL = articlesConfig['base_url']
process.env.VITE_APP_TITLE = articlesConfig['title']
process.env.VITE_APP_TITLE_NO_HTML = articlesConfig['title'].replace(/(<([^>]+)>)/gi, '').trim()
process.env.VITE_APP_LANG = articlesConfig['lang']
process.env.VITE_APP_SIGNATURE = articlesConfig['signature']
process.env.VITE_CUSTOM_HEAD = articlesConfig['custom_head']
process.env.VITE_APP_COPYRIGHT = articlesConfig['copyright']
process.env.VITE_BASE_URL = articlesConfig.base_url
process.env.VITE_APP_TITLE = articlesConfig.title
process.env.VITE_APP_TITLE_NO_HTML = articlesConfig.title.replace(/(<([^>]+)>)/gi, '').trim()
process.env.VITE_APP_LANG = articlesConfig.lang
process.env.VITE_CUSTOM_HEAD = articlesConfig.custom_head
return defineConfig({
plugins: [vue(), vueDevTools(), mdPlugin({ mode: [Mode.HTML] })],
Vendored
+2
View File
@@ -23,3 +23,5 @@ declare module '*.md' {
// Modify below per your usage
export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith }
}
declare module '*.vue'