Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49978afd4c | ||
|
|
1079ae9df1 | ||
|
|
2145c0f703 | ||
|
|
b70d4d7b38 | ||
|
|
f0eb08b97a | ||
|
|
303e8e2dd2 | ||
|
|
c567a6a9d5 | ||
|
|
f92baf3b82 | ||
|
|
6680d464c3 | ||
|
|
b6a5460a04 | ||
|
|
84c408d149 | ||
|
|
53d366f6b6 | ||
|
|
6beca83fa3 | ||
|
|
cbd83360a6 | ||
|
|
767fb8cfc6 | ||
|
|
1b626c1e89 | ||
|
|
44a08ad715 | ||
|
|
2639c87576 | ||
|
|
85783efb81 | ||
|
|
6127e132e4 | ||
|
|
89b83e3e43 | ||
|
|
39cf54624a | ||
|
|
722548118a | ||
|
|
a5b24dc9bf | ||
|
|
99867aa03e | ||
|
|
65f4dd1ac5 | ||
|
|
748f52241f | ||
|
|
21e1469b51 | ||
|
|
649102d1aa | ||
|
|
169f35b556 | ||
|
|
8d7388835b |
+17
-5
@@ -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
|
||||
|
||||
@@ -1,36 +1,65 @@
|
||||
name: Deploy
|
||||
|
||||
concurrency:
|
||||
group: deploy-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/30 * * * *"
|
||||
push:
|
||||
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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"oxc.oxc-vscode",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
@@ -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)"
|
||||
@@ -1,3 +1,5 @@
|
||||
[](https://git.klemek.fr/klemek/md-blog/actions?workflow=lint.yml) [](https://git.klemek.fr/klemek/md-blog/actions?workflow=deploy.yml)
|
||||
|
||||
# md-blog
|
||||
|
||||
## Minimal setup
|
||||
@@ -19,8 +21,10 @@ bun run build
|
||||
- [x] SPA and opengraph
|
||||
- [x] build with github actions
|
||||
- [x] config in sub repo
|
||||
- [ ] nav bar on top
|
||||
- [ ] date updated
|
||||
- [x] copyright
|
||||
- [x] nav bar on top
|
||||
- [x] date updated
|
||||
- [ ] archive page
|
||||
- [x] about page
|
||||
- [ ] link to previous/next article
|
||||
- [ ] proper docs
|
||||
- [ ] proper docs
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 806 B |
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Demo
|
||||
author: kleπek
|
||||
draft: false
|
||||
thumbnail: ./thumbnail.jpg
|
||||
date: 2026-01-01
|
||||
---
|
||||
|
||||
## Images
|
||||
|
||||
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
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"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>",
|
||||
"footer": ""
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+24
-18
@@ -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",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
+25
-21
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "md-blog",
|
||||
"version": "1.2.0",
|
||||
"version": "1.9.4",
|
||||
"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,40 +12,44 @@
|
||||
"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",
|
||||
"mermaid": "^11.14.0",
|
||||
"vue": "^3.5.33",
|
||||
"vue-router": "^5.0.6"
|
||||
"katex": "^0.16.47",
|
||||
"lucide": "^1.25.0",
|
||||
"mermaid": "^11.16.0",
|
||||
"vue": "^3.5.40",
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.12.2",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/eslint-config-typescript": "^14.7.0",
|
||||
"@types/node": "^24.13.3",
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.9.0",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint": "^10.7.0",
|
||||
"eslint-plugin-oxlint": "~1.60.0",
|
||||
"eslint-plugin-vue": "~10.8.0",
|
||||
"feed": "^5.2.1",
|
||||
"jiti": "^2.6.1",
|
||||
"jiti": "^2.7.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.60.0",
|
||||
"prettier": "3.8.3",
|
||||
"sass-embedded": "^1.99.0",
|
||||
"tsx": "^4.21.0",
|
||||
"sass-embedded": "^1.100.0",
|
||||
"tsx": "^4.23.1",
|
||||
"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-vue-devtools": "^8.1.1",
|
||||
"vue-tsc": "^3.2.7"
|
||||
"vite-plugin-vue-devtools": "^8.1.5",
|
||||
"vue-tsc": "^3.3.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
|
||||
+83
-71
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -1,17 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { REPOSITORY, NAME, VERSION, BASE_URL } from '@/lib/meta'
|
||||
import { REPOSITORY, NAME, VERSION, TITLE, COPYRIGHT, FOOTER } 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>
|
||||
<small>
|
||||
{{ new Date().getFullYear() }} — Made with
|
||||
<a :href="REPOSITORY">{{ NAME }} {{ VERSION }}</a> —
|
||||
<a :href="BASE_URL + 'atom.xml'"><i icon="rss"></i> RSS</a>
|
||||
</small>
|
||||
{{ stripHTML(TITLE) }} © {{ new Date().getFullYear() }}, <span v-html="COPYRIGHT"></span>
|
||||
<span v-if="FOOTER"> | <span v-html="FOOTER"></span></span>
|
||||
| Made with <a :href="REPOSITORY">{{ NAME }} {{ VERSION }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
+2
-1
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
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 FOOTER: string = articlesConfig.footer;
|
||||
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
@@ -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")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +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 BASE_URL = import.meta.env.BASE_URL
|
||||
@@ -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
@@ -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 },
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
+26
-22
@@ -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">
|
||||
<article v-if="!loading" class="article h-entry">
|
||||
<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) }}
|
||||
<h1 class="article-title p-name" v-html="article.metadata.title"></h1>
|
||||
<div class="article-info">
|
||||
<span v-if="article.metadata.author"
|
||||
><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>
|
||||
<img
|
||||
v-if="article.metadata.thumbnail"
|
||||
@@ -48,10 +53,9 @@ onBeforeRouteUpdate(loadPage)
|
||||
alt="thumbnail"
|
||||
/>
|
||||
</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>
|
||||
<br />
|
||||
<PageFooter />
|
||||
</main>
|
||||
</template>
|
||||
<BackHomeButton />
|
||||
</template>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
+21
-15
@@ -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">
|
||||
<RouterLink :to="metadata.path">
|
||||
<h2 v-html="metadata.title"></h2>
|
||||
<span class="article-published">Published on {{ simpleDateFormat(metadata.date) }}</span>
|
||||
<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" class="h-entry">
|
||||
<h2 class="p-name" v-html="metadata.title"></h2>
|
||||
<span class="article-info"
|
||||
><span v-html="PUBLISHED_ON"></span> <time class="dt-published" :datetime="simpleDateFormat(metadata.date)">{{ simpleDateFormat(metadata.date) }}</time></span
|
||||
>
|
||||
<img
|
||||
v-if="metadata.thumbnail"
|
||||
alt="thumbnail"
|
||||
@@ -32,6 +39,5 @@ onBeforeMount(async () => {
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
<PageFooter />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+47
-23
@@ -1,34 +1,58 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import { plugin as mdPlugin, Mode } from 'vite-plugin-markdown'
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vueDevTools from "vite-plugin-vue-devtools";
|
||||
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/
|
||||
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_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_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] })],
|
||||
base: process.env.VITE_BASE_URL,
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
mdPlugin({ mode: [Mode.HTML] }),
|
||||
obfuscatorPlugin({
|
||||
include: [/.*\.md/],
|
||||
options: { splitStrings: true, stringArrayThreshold: 1 },
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@views': fileURLToPath(new URL('./src/views', import.meta.url)),
|
||||
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
|
||||
'@articles': fileURLToPath(new URL('./articles', import.meta.url)),
|
||||
'@interfaces': fileURLToPath(new URL('./src/interfaces.ts', import.meta.url)),
|
||||
'@components': fileURLToPath(new URL('./src/components', import.meta.url)),
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
"@views": fileURLToPath(new URL("./src/views", import.meta.url)),
|
||||
"@lib": fileURLToPath(new URL("./src/lib", import.meta.url)),
|
||||
"@articles": fileURLToPath(new URL("./articles", import.meta.url)),
|
||||
"@interfaces": fileURLToPath(new URL("./src/interfaces.ts", 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,24 +2,11 @@ declare module '*.md' {
|
||||
// "unknown" would be more detailed depends on how you structure frontmatter
|
||||
const attributes: Record<string, unknown>
|
||||
|
||||
// When "Mode.TOC" is requested
|
||||
const toc: { level: string; content: string }[]
|
||||
|
||||
// When "Mode.HTML" is requested
|
||||
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
|
||||
export { attributes, toc, html, ReactComponent, VueComponent, VueComponentWith }
|
||||
export { html }
|
||||
}
|
||||
|
||||
declare module '*.vue'
|
||||
|
||||
Reference in New Issue
Block a user