Author SHA1 Message Date
klemek 4b02934a42 chore: release 1.10.0
Lint / Oxlint (push) Successful in 7m41s
Lint / TypeScript (push) Successful in 7m41s
Deploy / Build (push) Successful in 2m42s
Lint / ESLint (push) Successful in 4m29s
Deploy / Deploy to Stapler (push) Successful in 5m3s
2026-07-24 12:33:12 +02:00
klemek a7fba1897b feat: archive link
Deploy / Deploy to Stapler (push) Has been cancelled
Deploy / Build (push) Has been cancelled
Lint / ESLint (push) Has been cancelled
Lint / TypeScript (push) Has been cancelled
Lint / Oxlint (push) Has been cancelled
2026-07-24 12:32:47 +02:00
klemek b13bb738ce fix: favicon path 2026-07-24 12:32:40 +02:00
klemek 49978afd4c fix: space missing between footer parts
Lint / Oxlint (push) Successful in 57s
Lint / TypeScript (push) Successful in 1m1s
Lint / ESLint (push) Successful in 1m4s
Deploy / Build (push) Successful in 1m39s
Deploy / Deploy to Stapler (push) Successful in 4m16s
2026-07-20 15:24:54 +02:00
klemek 1079ae9df1 feat: indieweb standards
Lint / ESLint (push) Successful in 2m3s
Lint / Oxlint (push) Successful in 2m3s
Lint / TypeScript (push) Successful in 2m14s
Deploy / Build (push) Successful in 5m4s
Deploy / Deploy to Stapler (push) Successful in 4m9s
2026-07-20 11:06:21 +02:00
klemek 2145c0f703 tools: add 0.0.0.0 server
Deploy / Build (push) Successful in 1m42s
Deploy / Deploy to Stapler (push) Successful in 1m20s
2026-07-19 16:05:29 +02:00
klemek b70d4d7b38 chore: release 1.9.4
Deploy / Build (push) Successful in 7m33s
Lint / ESLint (push) Successful in 3m52s
Lint / Oxlint (push) Successful in 2m55s
Deploy / Deploy to Stapler (push) Successful in 3m23s
Lint / TypeScript (push) Successful in 4m42s
2026-07-17 11:39:21 +02:00
klemek f0eb08b97a fix(vite): better obfuscate
Deploy / Deploy to Stapler (push) Has been cancelled
Deploy / Build (push) Has been cancelled
Lint / ESLint (push) Has been cancelled
Lint / Oxlint (push) Has been cancelled
Lint / TypeScript (push) Has been cancelled
2026-07-17 11:39:17 +02:00
klemek 303e8e2dd2 chore: release 1.9.3
Deploy / Build (push) Successful in 1m13s
Lint / ESLint (push) Successful in 2m58s
Deploy / Deploy to Stapler (push) Successful in 3m15s
Lint / Oxlint (push) Successful in 3m36s
Lint / TypeScript (push) Successful in 3m31s
2026-07-17 11:15:09 +02:00
klemek c567a6a9d5 tools(make): release target 2026-07-17 11:13:17 +02:00
klemek f92baf3b82 feat: obfuscate build files
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 / TypeScript (push) Has been cancelled
2026-07-17 11:09:43 +02:00
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
Deploy / Build (push) Successful in 16m20s
Deploy / Deploy to Stapler (push) Successful in 9m32s
Lint / ESLint (push) Has been cancelled
Lint / Oxlint (push) Has been cancelled
Lint / Type Check (push) Has been cancelled
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
30 changed files with 1197 additions and 576 deletions
+17 -5
View File
@@ -1,8 +1,20 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] # EditorConfig is awesome: https://EditorConfig.org
charset = utf-8
indent_size = 2 # top-most EditorConfig file
root = true
[*]
indent_style = space indent_style = space
insert_final_newline = true indent_size = 4
trim_trailing_whitespace = true
end_of_line = lf 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 max_line_length = 100
+61 -35
View File
@@ -1,39 +1,65 @@
name: Deploy name: Deploy
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
on: on:
schedule: push:
- cron: "*/30 * * * *" branches:
push: - "main"
branches: paths:
- "main" - '.github/workflows/deploy.yml'
workflow_dispatch: - 'package.json'
- 'bun.lock'
- 'src/**'
- 'index.html'
- 'vite.config.ts'
- 'post-build.ts'
- 'tsconfig.*'
- 'env.d.ts'
- 'vite.d.ts'
jobs: jobs:
build: build:
name: Build name: "Build"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - name: Set up Bun
- uses: oven-sh/setup-bun@v2 uses: actions/setup-bun@v2
- run: bun install - name: Checkout repository
- name: Setup SSH Key uses: actions/checkout@v6
uses: webfactory/ssh-agent@v0.9.1 - name: Install dependencies
with: run: bun ci
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Checkout articles repository
- run: git clone ${{ vars.ARTICLES_REPOSITORY }} articles uses: actions/checkout@v6
- run: bun run build with:
- uses: actions/upload-artifact@v7 repository: ${{ vars.ARTICLES_REPOSITORY }}
with: ref: main
name: production-files token: ${{ secrets.PRIVATE_CLONE_TOKEN }}
path: ./dist path: ./articles
deploy: - name: Build project
name: Deploy run: bun run build
needs: build - name: Upload production files
runs-on: ubuntu-latest uses: actions/upload-artifact@v3
steps: with:
- uses: actions/download-artifact@v8 name: production-files
with: path: dist/
name: production-files
path: ./dist stapler:
- run: tar -czC dist -f dist.tar.gz . name: "Deploy to Stapler"
- run: | runs-on: ubuntu-latest
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 }} 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"
]
}
+69
View File
@@ -0,0 +1,69 @@
# 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 $*
.PHONY: release-%
release-%: node_modules ## release major/minor/patch
$(NPM) pm version $* --no-git-tag-version || true
git add package.json
git commit -m "chore: release $$(cat package.json | jq -r .version)"
git tag "release/$$(cat package.json | jq -r .version)" -m "md-blog $$(cat package.json | jq -r .version)"
+3 -1
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 # md-blog
## Minimal setup ## Minimal setup
@@ -21,7 +23,7 @@ bun run build
- [x] config in sub repo - [x] config in sub repo
- [x] copyright - [x] copyright
- [x] nav bar on top - [x] nav bar on top
- [ ] date updated - [x] date updated
- [ ] archive page - [ ] archive page
- [x] about page - [x] about page
- [ ] link to previous/next article - [ ] link to previous/next article
+16 -11
View File
@@ -1,13 +1,18 @@
{ {
"base_url": "http://localhost/", "base_url": "http://localhost/",
"title": "<i icon=notepad-text></i> My Blog", "title": "<i icon=notepad-text></i> My Blog",
"signature": "By <b>Me</b>", "signature": "By <b>Me</b>",
"lang": "en", "lang": "en",
"custom_head": "", "custom_head": "",
"home_count": 5, "home_count": 5,
"rss_link": "<i icon=rss></i> RSS", "archive_link": "<i icon=archive></i> ARCHIVE",
"about_link": "<i icon=info></i> ABOUT", "rss_link": "<i icon=rss></i> RSS",
"back_link": "<i icon=undo-2></i> Back to home", "about_link": "<i icon=info></i> ABOUT",
"published_on": "Published on", "back_link": "<i icon=undo-2></i> Back to home",
"copyright": "<a style=\"text-decoration:none;color:inherit;\" target=_blank href=\"https://creativecommons.org/licenses/by-nc/4.0/\">CC BY-NC</a>" "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>",
"footer": "",
"favicon_ico": ""
} }
+5 -10
View File
@@ -22,7 +22,8 @@ https://www.joshwcomeau.com/css/custom-css-reset/
3. Allow percentage-based heights in the application 3. Allow percentage-based heights in the application
*/ */
html, html,
body, div#app { body,
div#app {
height: 100%; height: 100%;
} }
/* /*
@@ -178,15 +179,9 @@ nav .nav-title {
nav .nav-items { nav .nav-items {
display: flex; display: flex;
gap: .5em; gap: 0.5em;
} }
.article-published { .article-info {
font-style: italic; font-style: italic;
}
.link-home {
text-decoration: none;
float: right;
line-height: 2.4;
} }
+557 -174
View File
File diff suppressed because it is too large Load Diff
+24 -18
View File
@@ -1,26 +1,32 @@
import { globalIgnores } from 'eslint/config' import { globalIgnores } from "eslint/config";
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' import {
import pluginVue from 'eslint-plugin-vue' defineConfigWithVueTs,
import pluginOxlint from 'eslint-plugin-oxlint' vueTsConfigs,
import skipFormatting from 'eslint-config-prettier/flat' } 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: configureVueProject({ scriptLangs: ["ts", "tsx"] });
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs( export default defineConfigWithVueTs(
{ {
name: 'app/files-to-lint', name: "app/files-to-lint",
files: ['**/*.{vue,ts,mts,tsx}'], files: ["**/*.{ts,mts,tsx,vue}"],
}, },
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), globalIgnores(["**/dist/**"]),
...pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
...pluginOxlint.buildFromOxlintConfigFile('.oxlintrc.json'),
pluginVue.configs["flat/recommended"],
vueTsConfigs.strictTypeChecked,
vueTsConfigs.stylisticTypeChecked,
...pluginOxlint.buildFromOxlintConfigFile(".oxlintrc.json"),
skipFormatting, skipFormatting,
)
{
rules: {
"vue/no-v-html": "off",
},
},
);
+1 -1
View File
@@ -2,10 +2,10 @@
<html lang="%VITE_APP_LANG%"> <html lang="%VITE_APP_LANG%">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="@articles/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no" />
<title>%VITE_APP_TITLE_NO_HTML%</title> <title>%VITE_APP_TITLE_NO_HTML%</title>
<link rel="stylesheet" type="text/css" href="/src/style.scss" /> <link rel="stylesheet" type="text/css" href="/src/style.scss" />
<link rel='shortcut icon' href='/favicon.ico' />
<meta property="og:url" content="%VITE_BASE_URL%" /> <meta property="og:url" content="%VITE_BASE_URL%" />
<meta property="og:title" content="%VITE_APP_TITLE_NO_HTML%" /> <meta property="og:title" content="%VITE_APP_TITLE_NO_HTML%" />
<link rel="alternate" href="%VITE_BASE_URL%/rss.xml" type="application/rss+xml" title="RSS 2.0" /> <link rel="alternate" href="%VITE_BASE_URL%/rss.xml" type="application/rss+xml" title="RSS 2.0" />
+25 -22
View File
@@ -1,9 +1,9 @@
{ {
"name": "md-blog", "name": "md-blog",
"version": "1.8.0", "version": "1.10.0",
"private": true, "private": true,
"type": "module", "type": "module",
"repository": "https://github.com/klemek/md-blog", "repository": "https://git.klemek.fr/klemek/md-blog",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" -- && run-p post-build", "build": "run-p type-check \"build-only {@}\" -- && run-p post-build",
@@ -12,41 +12,44 @@
"build-only": "vite build", "build-only": "vite build",
"type-check": "vue-tsc --build", "type-check": "vue-tsc --build",
"lint": "run-s lint:*", "lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix", "lint-fix": "run-s lint-fix:*",
"lint:eslint": "eslint . --fix --cache", "lint:eslint": "eslint . --cache",
"format": "prettier --write src/ *.ts *.json" "lint:oxlint": "oxlint .",
"lint-fix:eslint": "eslint . --fix --cache",
"lint-fix:oxlint": "oxlint . --fix"
}, },
"dependencies": { "dependencies": {
"@keithclark/shaderview": "git+https://github.com/keithclark/shaderview", "@keithclark/shaderview": "https://github.com/keithclark/shaderview/archive/refs/tags/1.2.0.tar.gz",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"katex": "^0.16.45", "katex": "^0.16.47",
"lucide": "^1.11.0", "lucide": "^1.25.0",
"mermaid": "^11.14.0", "mermaid": "^11.16.0",
"vue": "^3.5.33", "vue": "^3.5.40",
"vue-router": "^5.0.6" "vue-router": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node24": "^24.0.4", "@tsconfig/node24": "^24.0.4",
"@types/node": "^24.12.2", "@types/node": "^24.13.3",
"@vitejs/plugin-vue": "^6.0.6", "@vitejs/plugin-vue": "^6.0.8",
"@vue/eslint-config-typescript": "^14.7.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.9.0",
"@vue/tsconfig": "^0.9.1", "@vue/tsconfig": "^0.9.1",
"eslint": "^10.2.1", "eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-oxlint": "~1.60.0", "eslint-plugin-oxlint": "~1.60.0",
"eslint-plugin-vue": "~10.8.0", "eslint-plugin-vue": "~10.8.0",
"feed": "^5.2.1", "feed": "^5.2.1",
"jiti": "^2.6.1", "jiti": "^2.7.0",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.4",
"oxlint": "~1.60.0", "oxlint": "~1.60.0",
"prettier": "3.8.3", "prettier": "3.8.3",
"sass-embedded": "^1.99.0", "sass-embedded": "^1.100.0",
"tsx": "^4.21.0", "tsx": "^4.23.1",
"typescript": "~6.0.3", "typescript": "~6.0.3",
"vite": "^8.0.10", "vite": "^8.1.5",
"vite-plugin-javascript-obfuscator": "^3.1.0",
"vite-plugin-markdown": "^2.2.0", "vite-plugin-markdown": "^2.2.0",
"vite-plugin-vue-devtools": "^8.1.1", "vite-plugin-vue-devtools": "^8.1.5",
"vue-tsc": "^3.2.7" "vue-tsc": "^3.3.7"
}, },
"engines": { "engines": {
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
+75 -70
View File
@@ -1,129 +1,134 @@
import fs from 'fs' import fs from "fs";
import process from 'process' import process from "process";
import { Feed } from 'feed' import { Feed } from "feed";
import articlesConfig from './articles/config.json' import articlesConfig from "./articles/config.json";
function getFiles(dir: string): string[] { function getFiles(dir: string): string[] {
return fs.readdirSync(dir).flatMap((name) => { return fs.readdirSync(dir).flatMap((name) => {
const path = `${dir}/${name}` const path = `${dir}/${name}`;
if (fs.statSync(path).isDirectory()) { if (fs.statSync(path).isDirectory()) {
return getFiles(path) return getFiles(path);
} else { } else {
return [path] return [path];
} }
}) });
} }
const METADATA_BLOCK_REGEX = /^---\n([\s\S]*?)---\n/m const METADATA_BLOCK_REGEX = /^---\n([\s\S]*?)---\n/m;
const METADATA_REGEX = /^(\w+):(.*)$/gm const METADATA_REGEX = /^(\w+):(.*)$/gm;
function readArticleMetadata(path: string): Record<string, string> | null { function readArticleMetadata(path: string): Record<string, string> | null {
const content = fs.readFileSync(path, { encoding: 'utf8' }) const content = fs.readFileSync(path, { encoding: "utf8" });
const match: RegExpExecArray | null = METADATA_BLOCK_REGEX.exec(content) const match: RegExpExecArray | null = METADATA_BLOCK_REGEX.exec(content);
if (!match || !match[1]) { if (!match?.[1]) {
console.warn(`No metadata for: ${path}`) console.warn(`No metadata for: ${path}`);
return null return null;
} }
let subMatch: RegExpExecArray | null = null let subMatch: RegExpExecArray | null = null;
const metadata: Record<string, string> = { const metadata: Record<string, string> = {
path: path.replaceAll('/index.md', ''), path: path.replaceAll("/index.md", ""),
} };
do { do {
subMatch = METADATA_REGEX.exec(match[1]) subMatch = METADATA_REGEX.exec(match[1]);
if (subMatch && subMatch[1] && subMatch[2]) { if (subMatch?.[1] && subMatch[2]) {
metadata[subMatch[1]] = subMatch[2].trim() metadata[subMatch[1]] = subMatch[2].trim();
} }
} while (subMatch) } while (subMatch);
return metadata return metadata;
} }
function formatArticlePage(metadata: Record<string, string>, baseHtml: string): string { function formatArticlePage(metadata: Record<string, string>, baseHtml: string): string {
let outHtml = baseHtml let outHtml = baseHtml;
outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, '') outHtml = outHtml.replace(/<.*?property="og:url".*?>/gm, "");
outHtml = outHtml.replace( outHtml = outHtml.replace(
/<\/head>/gm, /<\/head>/gm,
`<meta property="og:url" content="${articlesConfig['base_url']}${metadata.path}/">\n</head>`, `<meta property="og:url" content="${articlesConfig.base_url}${metadata.path}/">\n</head>`,
) );
const blog_title = articlesConfig['title']?.replace(/(<([^>]+)>)/gi, '').trim() const blog_title = articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim();
if (metadata.title) { if (metadata.title) {
const title = metadata.title.replace(/(<([^>]+)>)/gi, '').trim() const title = metadata.title.replace(/(<([^>]+)>)/gi, "").trim();
outHtml = outHtml.replace(/<title>.*?<\/title>/gm, `<title>${blog_title}${title}</title>`) outHtml = outHtml.replace(/<title>.*?<\/title>/gm, `<title>${blog_title}${title}</title>`);
outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, '') outHtml = outHtml.replace(/<.*?property="og:title".*?>/gm, "");
outHtml = outHtml.replace( outHtml = outHtml.replace(
/<\/head>/gm, /<\/head>/gm,
`<meta property="og:title" content="${title}">\n</head>`, `<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( outHtml = outHtml.replace(
/<\/head>/gm, /<\/head>/gm,
`<meta property="og:description" content="${blog_title}">\n</head>`, `<meta property="og:description" content="${blog_title}">\n</head>`,
) );
} }
if (metadata.thumbnail) { if (metadata.thumbnail) {
outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, '') outHtml = outHtml.replace(/<.*?property="og:image".*?>/gm, "");
outHtml = outHtml.replace( outHtml = outHtml.replace(
/<\/head>/gm, /<\/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) { function addFeedArticle(metadata: Record<string, string>, feed: Feed) {
if (metadata.draft !== 'true') { if (metadata.draft !== "true") {
feed.addItem({ feed.addItem({
title: metadata.title.replace(/(<([^>]+)>)/gi, '').trim(), title: metadata.title.replace(/(<([^>]+)>)/gi, "").trim(),
id: metadata.path, id: metadata.path,
link: `${articlesConfig['base_url']}${metadata.path}/`, link: `${articlesConfig.base_url}${metadata.path}/`,
date: new Date(Date.parse(metadata.date)), 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) { if (!indexContent) {
console.error('Could not read dist/index.html') console.error("Could not read dist/index.html");
process.exit(1) process.exit(1);
} }
const metadatas = getFiles('articles') const metadatas = getFiles("articles")
.filter((path) => path.match(/\/index.md$/)) .filter((path) => /\/index.md$/.exec(path))
.map((path) => readArticleMetadata(path)) .map((path) => readArticleMetadata(path))
.filter((metadata) => !!metadata) .filter((metadata) => !!metadata);
const feed = new Feed({ const feed = new Feed({
title: articlesConfig['title']?.replace(/(<([^>]+)>)/gi, '').trim() ?? '', title: articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim(),
id: articlesConfig['base_url'], id: articlesConfig.base_url,
link: articlesConfig['base_url'], link: articlesConfig.base_url,
language: articlesConfig['lang'], language: articlesConfig.lang,
favicon: articlesConfig['base_url'] + 'articles/favicon.ico', favicon: articlesConfig.base_url + "/favicon.ico",
generator: 'md-blog', generator: "md-blog",
feedLinks: { feedLinks: {
json: articlesConfig['base_url'] + 'feed.json', json: articlesConfig.base_url + "feed.json",
atom: articlesConfig['base_url'] + 'atom.xml', atom: articlesConfig.base_url + "atom.xml",
rss: articlesConfig['base_url'] + 'rss', rss: articlesConfig.base_url + "rss",
}, },
updated: new Date( updated: new Date(
Math.max( Math.max(
0, 0,
...metadatas ...metadatas
.filter((metadata) => metadata.draft !== 'true') .filter((metadata) => metadata.draft !== "true")
.map((metadata) => Date.parse(metadata.date)), .map((metadata) => Date.parse(metadata.date)),
), ),
), ),
}) });
metadatas.forEach((metadata) => { metadatas.forEach((metadata) => {
fs.writeFileSync(`dist/${metadata.path}/index.html`, formatArticlePage(metadata, indexContent)) fs.writeFileSync(`dist/${metadata.path}/index.html`, formatArticlePage(metadata, indexContent));
console.info(`Wrote dist/${metadata.path}/index.html`) console.info(`Wrote dist/${metadata.path}/index.html`);
addFeedArticle(metadata, feed) addFeedArticle(metadata, feed);
}) });
fs.writeFileSync('dist/feed.json', feed.json1()) fs.writeFileSync("dist/feed.json", feed.json1());
console.info(`Wrote dist/feed.json`) console.info(`Wrote dist/feed.json`);
fs.writeFileSync('dist/atom.xml', feed.atom1()) fs.writeFileSync("dist/atom.xml", feed.atom1());
console.info(`Wrote dist/atom.xml`) console.info(`Wrote dist/atom.xml`);
fs.writeFileSync('dist/rss.xml', feed.rss2()) fs.writeFileSync("dist/rss.xml", feed.rss2());
console.info(`Wrote dist/rss.xml`) console.info(`Wrote dist/rss.xml`);
if (articlesConfig.favicon_ico) {
fs.copyFileSync(`articles/${articlesConfig.favicon_ico}`, "dist/favicon.ico");
console.info(`Wrote dist/favicon.ico`);
}
+10 -1
View File
@@ -1,3 +1,12 @@
<script setup lang="ts">
import PageFooter from '@components/PageFooter.vue'
import NavBar from '@components/NavBar.vue'
</script>
<template> <template>
<RouterView /> <main>
<NavBar />
<RouterView />
<PageFooter />
</main>
</template> </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>
+2 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { BASE_URL, TITLE, RSS_LINK, ABOUT_LINK } from '@lib/config' import { BASE_URL, TITLE, RSS_LINK, ABOUT_LINK, ARCHIVE_LINK } from '@lib/config'
</script> </script>
<template> <template>
@@ -7,6 +7,7 @@ import { BASE_URL, TITLE, RSS_LINK, ABOUT_LINK } from '@lib/config'
<RouterLink to="/" class="nav-title"><span v-html="TITLE"></span></RouterLink> <RouterLink to="/" class="nav-title"><span v-html="TITLE"></span></RouterLink>
<span class="nav-items"> <span class="nav-items">
<RouterLink to="/about/"><span v-html="ABOUT_LINK"></span></RouterLink> <RouterLink to="/about/"><span v-html="ABOUT_LINK"></span></RouterLink>
<RouterLink to="/archive/"><span v-html="ARCHIVE_LINK"></span></RouterLink>
<a :href="BASE_URL + 'atom.xml'" v-html="RSS_LINK"></a> <a :href="BASE_URL + 'atom.xml'" v-html="RSS_LINK"></a>
</span> </span>
</nav> </nav>
+4 -4
View File
@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { REPOSITORY, NAME, VERSION, TITLE, COPYRIGHT } from '@lib/config' import { REPOSITORY, NAME, VERSION, TITLE, COPYRIGHT, FOOTER } from '@lib/config'
import { stripHTML } from '@lib/strings' import { stripHTML } from '@lib/strings'
</script> </script>
<template> <template>
<footer> <footer>
{{ stripHTML(TITLE) }} &copy; {{ new Date().getFullYear() }}, <span v-html="COPYRIGHT"></span> | {{ stripHTML(TITLE) }} &copy; {{ new Date().getFullYear() }}, <span v-html="COPYRIGHT"></span>
Made with <span v-if="FOOTER">&nbsp;| <span v-html="FOOTER"></span></span>
<a :href="REPOSITORY">{{ NAME }} {{ VERSION }}</a> &nbsp;| Made with <a :href="REPOSITORY">{{ NAME }} {{ VERSION }}</a>
</footer> </footer>
</template> </template>
+2 -1
View File
@@ -7,7 +7,8 @@ export interface ArticleMetadata {
path: string path: string
title: string title: string
date: Date date: Date
author: string updated?: Date
author?: string
thumbnail?: string thumbnail?: string
draft?: boolean draft?: boolean
} }
+83 -62
View File
@@ -1,125 +1,146 @@
import type { MarkdownData, Article, ArticleMetadata } from '@interfaces' import type { MarkdownData, Article, ArticleMetadata } from "@interfaces";
import katex from 'katex' import katex from "katex";
import { nextTick } from 'vue' import { nextTick } from "vue";
import hljs from 'highlight.js' import hljs from "highlight.js";
import mermaid from 'mermaid' import mermaid from "mermaid";
import { createIcons, icons } from 'lucide' import { createIcons, icons } from "lucide";
export async function updateDynamicContent() { export async function updateDynamicContent() {
await nextTick() await nextTick();
hljs.highlightAll() hljs.highlightAll();
createIcons({ createIcons({
icons, icons,
nameAttr: 'icon', nameAttr: "icon",
attrs: { attrs: {
width: '1.1em', width: "1.1em",
height: '1.1em', height: "1.1em",
}, },
}) });
mermaid.run() await mermaid.run();
} }
function parseMetadata( function parseMetadata(
srcAttributes: Record<string, unknown>, srcAttributes: Record<string, unknown>,
pathPrefix: string, pathPrefix: string,
): ArticleMetadata { ): ArticleMetadata {
const draft = !!srcAttributes.draft const draft = !!srcAttributes.draft;
return { return {
path: pathPrefix, path: pathPrefix,
title: title:
(draft ? '[DRAFT] ' : '') + decodeURIComponent((srcAttributes.title as string) ?? 'Untitled'), (draft ? "[DRAFT] " : "") +
date: new Date(Date.parse((srcAttributes.date as string) ?? '')), decodeURIComponent(
author: decodeURIComponent((srcAttributes.author as string) ?? ''), (srcAttributes.title as string | undefined) ?? "Untitled",
thumbnail: (srcAttributes.thumbnail as string) ?? '', ),
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, draft: draft,
} };
} }
const LATEX_REGEX = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m const LATEX_REGEX = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const MERMAID_REGEX = /<pre>\s*<code class="language-mermaid">([\s\S]*)<\/code>\s*<\/pre>/m const MERMAID_REGEX =
/<pre>\s*<code class="language-mermaid">([\s\S]*)<\/code>\s*<\/pre>/m;
function transformLatexBlocks(srcHtml: string): string { function transformLatexBlocks(srcHtml: string): string {
let outHtml = srcHtml let outHtml = srcHtml;
let match: RegExpExecArray | null = null let match: RegExpExecArray | null = null;
do { do {
match = LATEX_REGEX.exec(outHtml) match = LATEX_REGEX.exec(outHtml);
if (match && match[1]) { if (match?.[1]) {
try { try {
outHtml = outHtml.replace(match[0], katex.renderToString(match[1])) outHtml = outHtml.replace(match[0], katex.renderToString(match[1]));
} catch (ex) { } 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]) } while (match?.[1]);
return outHtml return outHtml;
} }
function transformMermaidBlocks(srcHtml: string): string { function transformMermaidBlocks(srcHtml: string): string {
let outHtml = srcHtml let outHtml = srcHtml;
let match: RegExpExecArray | null = null let match: RegExpExecArray | null = null;
do { do {
match = MERMAID_REGEX.exec(outHtml) match = MERMAID_REGEX.exec(outHtml);
if (match && match[1]) { if (match?.[1]) {
outHtml = outHtml.replace(match[0], `<pre class="mermaid">\n${match[1]}\n</pre>`) outHtml = outHtml.replace(
match[0],
`<pre class="mermaid">\n${match[1]}\n</pre>`,
);
} }
} while (match && match[1]) } while (match?.[1]);
return outHtml return outHtml;
} }
function transformHtml(srcHtml: string): string { function transformHtml(srcHtml: string): string {
let outHtml: string = srcHtml let outHtml: string = srcHtml;
outHtml = transformLatexBlocks(outHtml) outHtml = transformLatexBlocks(outHtml);
outHtml = transformMermaidBlocks(outHtml) outHtml = transformMermaidBlocks(outHtml);
return outHtml return outHtml;
} }
/** /**
* @deprecated * @deprecated
*/ */
export async function loadArticleOld(date: Date): Promise<Article | null> { export async function loadArticleOld(date: Date): Promise<Article | null> {
const year = date.getFullYear().toString() const year = date.getFullYear().toString();
const month = (date.getMonth() + 1).toString().padStart(2, '0') const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, '0') const day = date.getDate().toString().padStart(2, "0");
const path = `./articles/${year}/${month}/${day}/` const path = `./articles/${year}/${month}/${day}/`;
try { 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 { return {
metadata: parseMetadata(data.attributes, path), metadata: parseMetadata(data.attributes, path),
html: transformHtml(data.html), html: transformHtml(data.html),
} };
} catch (ex) { } catch (ex) {
console.error(ex) console.error(ex);
return null return null;
} }
} }
export async function loadArticle(path: string): Promise<Article | null> { export async function loadArticle(path: string): Promise<Article | null> {
const raw_articles = import.meta.glob('@articles/**/index.md') const raw_articles = import.meta.glob("@articles/**/index.md");
const key = `/articles/${path}index.md` const key = `/articles/${path}index.md`;
if (!raw_articles[key]) return null if (!raw_articles[key]) return null;
try { try {
const data = (await raw_articles[key]()) as MarkdownData const data = (await raw_articles[key]()) as MarkdownData;
return { return {
metadata: parseMetadata(data.attributes, path), metadata: parseMetadata(data.attributes, path),
html: transformHtml(data.html), html: transformHtml(data.html),
} };
} catch (ex) { } catch (ex) {
console.error(ex) console.error(ex);
return null return null;
} }
} }
export async function listArticles(): Promise<ArticleMetadata[]> { 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[] = ( const articles: ArticleMetadata[] = (
await Promise.all( await Promise.all(
Object.keys(raw_articles).map(async (key) => { Object.keys(raw_articles).map(async (key) => {
if (!raw_articles[key]) return null if (!raw_articles[key]) return null;
const data = (await raw_articles[key]()) as MarkdownData const data = (await raw_articles[key]()) as MarkdownData;
return parseMetadata(data.attributes, key.replace('index.md', '')) return parseMetadata(data.attributes, key.replace("index.md", ""));
}), }),
) )
).filter((item) => item !== null) ).filter((item) => item !== null);
articles.sort((article1, article2) => article2.date.valueOf() - article1.date.valueOf()) articles.sort(
return articles (article1, article2) => article2.date.valueOf() - article1.date.valueOf(),
);
return articles;
} }
+19 -15
View File
@@ -1,16 +1,20 @@
import packageJson from '@/../package.json' import packageJson from "@/../package.json";
import articlesConfig from '@articles/config.json' import articlesConfig from "@articles/config.json";
export const NAME: string = packageJson.name export const NAME: string = packageJson.name;
export const VERSION: string = packageJson.version export const VERSION: string = packageJson.version;
export const REPOSITORY: string = packageJson.repository export const REPOSITORY: string = packageJson.repository;
export const TITLE: string = articlesConfig['title'] export const TITLE: string = articlesConfig.title;
export const SIGNATURE: string = articlesConfig['signature'] export const SIGNATURE: string = articlesConfig.signature;
export const COPYRIGHT: string = articlesConfig['copyright'] export const COPYRIGHT: string = articlesConfig.copyright;
export const RSS_LINK: string = articlesConfig['rss_link'] export const FOOTER: string = articlesConfig.footer;
export const BACK_LINK: string = articlesConfig['back_link'] export const RSS_LINK: string = articlesConfig.rss_link;
export const ABOUT_LINK: string = articlesConfig['about_link'] export const BACK_LINK: string = articlesConfig.back_link;
export const HOME_COUNT: number = articlesConfig['home_count'] export const ARCHIVE_LINK: string = articlesConfig.archive_link;
export const PUBLISHED_ON: string = articlesConfig['published_on'] export const ABOUT_LINK: string = articlesConfig.about_link;
export const BASE_URL: string = import.meta.env.BASE_URL export const HOME_COUNT: number = articlesConfig.home_count;
export const PROD: boolean = import.meta.env.PROD 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 { export function simpleDateFormat(date: Date): string {
return ( return (
date.getFullYear() + date.getFullYear().toString() +
'-' + "-" +
(date.getMonth() + 1).toString().padStart(2, '0') + (date.getMonth() + 1).toString().padStart(2, "0") +
'-' + "-" +
date.getDate().toString().padStart(2, '0') date.getDate().toString().padStart(2, "0")
) );
} }
+13 -11
View File
@@ -1,17 +1,19 @@
import AboutView from '@views/AboutView.vue' import { createRouter, createWebHistory } from "vue-router";
import ArticleView from '@views/ArticleView.vue' import HomeView from "@views/HomeView.vue";
import HomeView from '@views/HomeView.vue' import ArchiveView from "@views/ArchiveView.vue";
import NotFoundView from '@views/NotFoundView.vue' import AboutView from "@views/AboutView.vue";
import { createRouter, createWebHistory } from 'vue-router' import ArticleView from "@views/ArticleView.vue";
import NotFoundView from "@views/NotFoundView.vue";
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
{ path: '/', component: HomeView }, { path: "/", component: HomeView },
{ path: '/about/', component: AboutView }, { path: "/archive/", component: ArchiveView },
{ path: '/articles/:pathMatch(.*)/', component: ArticleView }, { path: "/about/", component: AboutView },
{ path: '/:pathMatch(.*)', component: NotFoundView }, { path: "/articles/:pathMatch(.*)/", component: ArticleView },
{ path: "/:pathMatch(.*)", component: NotFoundView },
], ],
}) });
export default router export default router;
+6 -19
View File
@@ -1,28 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { ABOUT_LINK, TITLE } from '@lib/config' import { onMounted, onUpdated } from 'vue'
import { stripHTML } from '@lib/strings' import { updateDynamicContent } from '@lib/articles'
import PageFooter from '@components/PageFooter.vue' import { html } from '@articles/about.md'
import { onBeforeMount, onMounted, onUpdated, ref } from 'vue'
import NavBar from '@components/NavBar.vue'
import { updateDynamicContent } from '@/lib/articles'
const html = ref<string>('')
onBeforeMount(async () => {
window.document.title = stripHTML(TITLE) + ' — ' + stripHTML(ABOUT_LINK)
html.value = (await import('@articles/about.md')).html
await updateDynamicContent()
})
onMounted(updateDynamicContent) onMounted(updateDynamicContent)
onUpdated(updateDynamicContent) onUpdated(updateDynamicContent)
</script> </script>
<template> <template>
<main class="article"> <div class="article">
<NavBar />
<div v-html="html"></div> <div v-html="html"></div>
<PageFooter /> <BackHomeButton />
</main> </div>
</template> </template>
+38
View File
@@ -0,0 +1,38 @@
<script setup lang="ts">
import { ref, onBeforeMount, onUpdated, onMounted } from 'vue'
import { listArticles, updateDynamicContent } from '@lib/articles'
import type { ArticleMetadata } from '@interfaces'
import { simpleDateFormat } from '@lib/dates'
import { PROD, TITLE } from '@lib/config'
import { stripHTML } from '@lib/strings'
const articles = ref<ArticleMetadata[]>([])
const loading = ref<boolean>(true)
onBeforeMount(async () => {
const newArticles = (await listArticles())
.filter((metadata) => (!metadata.draft || !PROD) && metadata.path)
articles.value.splice(0, articles.value.length, ...newArticles)
window.document.title = stripHTML(TITLE) + ' — Archive'
loading.value = false
await updateDynamicContent()
})
onMounted(updateDynamicContent)
onUpdated(updateDynamicContent)
</script>
<template>
<div v-if="!loading" class="home">
<ul>
<li v-for="(metadata, index) in articles" :key="index">
<div v-if="(!metadata.draft || !PROD) && metadata.path" class="article-item">
<time class="dt-published mono" :datetime="simpleDateFormat(metadata.date)">{{ simpleDateFormat(metadata.date) }}</time>
&nbsp;
<RouterLink :to="metadata.path" class="h-entry">
<span class="p-name" v-html="metadata.title"></span>
</RouterLink>
</div>
</li>
</ul>
</div>
</template>
+20 -22
View File
@@ -3,12 +3,11 @@ import type { Article } from '@interfaces'
import { ref, onBeforeMount, onUpdated, onMounted } from 'vue' import { ref, onBeforeMount, onUpdated, onMounted } from 'vue'
import { loadArticle, updateDynamicContent } from '@lib/articles' import { loadArticle, updateDynamicContent } from '@lib/articles'
import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router' import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router'
import NotFoundView from './NotFoundView.vue'
import { simpleDateFormat } from '@lib/dates' import { simpleDateFormat } from '@lib/dates'
import { BACK_LINK, PUBLISHED_ON, SIGNATURE, TITLE } from '@lib/config' import { AUTHORED, PUBLISHED_ON, SIGNATURE, TITLE, UPDATED_ON } from '@lib/config'
import PageFooter from '@components/PageFooter.vue'
import { stripHTML } from '@lib/strings' import { stripHTML } from '@lib/strings'
import NavBar from '@components/NavBar.vue' import BackHomeButton from '@components/BackHomeButton.vue'
import NotFoundView from '@views/NotFoundView.vue'
const article = ref<Article | null>(null) const article = ref<Article | null>(null)
const loading = ref<boolean>(true) const loading = ref<boolean>(true)
@@ -30,20 +29,22 @@ onUpdated(updateDynamicContent)
</script> </script>
<template> <template>
<template v-if="loading"> <template v-if="!loading && !article">
<main></main>
</template>
<template v-else-if="!article">
<NotFoundView /> <NotFoundView />
</template> </template>
<template v-else> <article v-if="!loading" class="article h-entry">
<main class="article"> <template v-if="!loading && article">
<NavBar />
<div class="article-header"> <div class="article-header">
<RouterLink class="link-home" to="/"><i icon="undo-2"></i></RouterLink> <h1 class="article-title p-name" v-html="article.metadata.title"></h1>
<h1 class="article-title" v-html="article.metadata.title"></h1> <div class="article-info">
<div class="article-published"> <span v-if="article.metadata.author"
<span v-html="PUBLISHED_ON"></span> {{ simpleDateFormat(article.metadata.date) }} ><span v-html="AUTHORED"></span> <span class="p-author h-card" v-html="article.metadata.author"></span>
</span>
<span v-html="PUBLISHED_ON"></span> <time class="dt-published" :datetime="simpleDateFormat(article.metadata.date)">{{ simpleDateFormat(article.metadata.date) }}</time>
<span v-if="article.metadata.updated"
> <span v-html="UPDATED_ON"></span>
{{ simpleDateFormat(article.metadata.updated) }}</span
>
</div> </div>
<img <img
v-if="article.metadata.thumbnail" v-if="article.metadata.thumbnail"
@@ -52,12 +53,9 @@ onUpdated(updateDynamicContent)
alt="thumbnail" alt="thumbnail"
/> />
</div> </div>
<div class="article-text" v-html="article.html"></div> <div class="article-text e-content" v-html="article.html"></div>
<div class="article-signature" v-html="SIGNATURE"></div> <div class="article-signature" v-html="SIGNATURE"></div>
<template v-if="$route.fullPath != '/'"> <BackHomeButton />
<RouterLink class="link-back" to="/"><span v-html="BACK_LINK"></span></RouterLink> </template>
</template> </article>
<PageFooter />
</main>
</template>
</template> </template>
+10 -12
View File
@@ -4,11 +4,10 @@ import { listArticles, updateDynamicContent } from '@lib/articles'
import type { ArticleMetadata } from '@interfaces' import type { ArticleMetadata } from '@interfaces'
import { simpleDateFormat } from '@lib/dates' import { simpleDateFormat } from '@lib/dates'
import { HOME_COUNT, PROD, PUBLISHED_ON, TITLE } from '@lib/config' import { HOME_COUNT, PROD, PUBLISHED_ON, TITLE } from '@lib/config'
import PageFooter from '@components/PageFooter.vue' import { stripHTML } from '@lib/strings'
import { stripHTML } from '@/lib/strings'
import NavBar from '@components/NavBar.vue'
const articles = ref<ArticleMetadata[]>([]) const articles = ref<ArticleMetadata[]>([])
const loading = ref<boolean>(true)
onBeforeMount(async () => { onBeforeMount(async () => {
const newArticles = (await listArticles()) const newArticles = (await listArticles())
@@ -16,6 +15,7 @@ onBeforeMount(async () => {
.slice(0, HOME_COUNT) .slice(0, HOME_COUNT)
articles.value.splice(0, articles.value.length, ...newArticles) articles.value.splice(0, articles.value.length, ...newArticles)
window.document.title = stripHTML(TITLE) + ' — Home' window.document.title = stripHTML(TITLE) + ' — Home'
loading.value = false
await updateDynamicContent() await updateDynamicContent()
}) })
onMounted(updateDynamicContent) onMounted(updateDynamicContent)
@@ -23,14 +23,13 @@ onUpdated(updateDynamicContent)
</script> </script>
<template> <template>
<main class="home"> <div v-if="!loading" class="home">
<NavBar /> <template v-for="(metadata, index) in articles" :key="index">
<template v-for="(metadata, index) in articles" v-bind:key="index">
<div v-if="(!metadata.draft || !PROD) && metadata.path" class="article-item"> <div v-if="(!metadata.draft || !PROD) && metadata.path" class="article-item">
<RouterLink :to="metadata.path"> <RouterLink :to="metadata.path" class="h-entry">
<h2 v-html="metadata.title"></h2> <h2 class="p-name" v-html="metadata.title"></h2>
<span class="article-published" <span class="article-info"
><span v-html="PUBLISHED_ON"></span> {{ simpleDateFormat(metadata.date) }}</span ><span v-html="PUBLISHED_ON"></span> <time class="dt-published" :datetime="simpleDateFormat(metadata.date)">{{ simpleDateFormat(metadata.date) }}</time></span
> >
<img <img
v-if="metadata.thumbnail" v-if="metadata.thumbnail"
@@ -40,6 +39,5 @@ onUpdated(updateDynamicContent)
</RouterLink> </RouterLink>
</div> </div>
</template> </template>
<PageFooter /> </div>
</main>
</template> </template>
+4 -20
View File
@@ -1,28 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { TITLE } from '@lib/config' import { onMounted, onUpdated } from 'vue'
import { stripHTML } from '@lib/strings' import { updateDynamicContent } from '@lib/articles'
import PageFooter from '@components/PageFooter.vue' import { html } from '@articles/not_found.md'
import { onBeforeMount, onMounted, onUpdated, ref } from 'vue'
import NavBar from '@components/NavBar.vue'
import { updateDynamicContent } from '@/lib/articles'
const html = ref<string>('')
onBeforeMount(async () => {
window.document.title = stripHTML(TITLE)
html.value = (await import('@articles/not_found.md')).html
await updateDynamicContent()
})
onMounted(updateDynamicContent) onMounted(updateDynamicContent)
onUpdated(updateDynamicContent) onUpdated(updateDynamicContent)
</script> </script>
<template> <template>
<main class="article"> <div class="article" v-html="html"></div>
<NavBar />
<div v-html="html"></div>
<PageFooter />
</main>
</template> </template>
+47 -22
View File
@@ -1,33 +1,58 @@
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from "node:url";
import { defineConfig, loadEnv } from 'vite' import { defineConfig, loadEnv } from "vite";
import vue from '@vitejs/plugin-vue' import vue from "@vitejs/plugin-vue";
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from "vite-plugin-vue-devtools";
import { plugin as mdPlugin, Mode } from 'vite-plugin-markdown' import { plugin as mdPlugin, Mode } from "vite-plugin-markdown";
import obfuscatorPlugin from "vite-plugin-javascript-obfuscator";
import articlesConfig from './articles/config.json' import articlesConfig from "./articles/config.json";
// https://vite.dev/config/ // https://vite.dev/config/
export default ({ mode }: { mode: string }) => { export default ({ mode }: { mode: string }) => {
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) } process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
process.env.VITE_BASE_URL = articlesConfig['base_url'] process.env.VITE_BASE_URL = articlesConfig.base_url;
process.env.VITE_APP_TITLE = articlesConfig['title'] process.env.VITE_APP_TITLE = articlesConfig.title;
process.env.VITE_APP_TITLE_NO_HTML = articlesConfig['title'].replace(/(<([^>]+)>)/gi, '').trim() process.env.VITE_APP_TITLE_NO_HTML = articlesConfig.title.replace(/(<([^>]+)>)/gi, "").trim();
process.env.VITE_APP_LANG = articlesConfig['lang'] process.env.VITE_APP_LANG = articlesConfig.lang;
process.env.VITE_CUSTOM_HEAD = articlesConfig['custom_head'] process.env.VITE_CUSTOM_HEAD = articlesConfig.custom_head;
return defineConfig({ return defineConfig({
plugins: [vue(), vueDevTools(), mdPlugin({ mode: [Mode.HTML] })], server: {
base: process.env.VITE_BASE_URL, host: "0.0.0.0",
},
plugins: [
vue(),
vueDevTools(),
mdPlugin({ mode: [Mode.HTML] }),
obfuscatorPlugin({
include: [/.*\.md/],
options: { splitStrings: true, stringArrayThreshold: 1 },
}),
],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), "@": fileURLToPath(new URL("./src", import.meta.url)),
'@views': fileURLToPath(new URL('./src/views', import.meta.url)), "@views": fileURLToPath(new URL("./src/views", import.meta.url)),
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)), "@lib": fileURLToPath(new URL("./src/lib", import.meta.url)),
'@articles': fileURLToPath(new URL('./articles', import.meta.url)), "@articles": fileURLToPath(new URL("./articles", import.meta.url)),
'@interfaces': fileURLToPath(new URL('./src/interfaces.ts', import.meta.url)), "@interfaces": fileURLToPath(new URL("./src/interfaces.ts", import.meta.url)),
'@components': fileURLToPath(new URL('./src/components', import.meta.url)), "@components": fileURLToPath(new URL("./src/components", import.meta.url)),
}, },
}, },
}) build: {
} rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: (moduleId: string) => (moduleId.includes("node_modules") ? "libs" : "app"),
maxSize: 1024 * 1024,
},
],
},
},
},
},
});
};
Vendored
+3 -16
View File
@@ -2,24 +2,11 @@ declare module '*.md' {
// "unknown" would be more detailed depends on how you structure frontmatter // "unknown" would be more detailed depends on how you structure frontmatter
const attributes: Record<string, unknown> const attributes: Record<string, unknown>
// When "Mode.TOC" is requested
const toc: { level: string; content: string }[]
// When "Mode.HTML" is requested // When "Mode.HTML" is requested
const html: string const html: string
// When "Mode.RAW" is requested
const raw: string
// When "Mode.React" is requested. VFC could take a generic like React.VFC<{ MyComponent: TypeOfMyComponent }>
import React from 'react'
const ReactComponent: React.VFC
// When "Mode.Vue" is requested
import { ComponentOptions, Component } from 'vue'
const VueComponent: ComponentOptions
const VueComponentWith: (components: Record<string, Component>) => ComponentOptions
// Modify below per your usage // Modify below per your usage
export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith } export { html }
} }
declare module '*.vue'