feat: better article parsing + html dom base

This commit is contained in:
2026-04-24 23:03:02 +02:00
parent cbab7c74e5
commit 9109678195
11 changed files with 157 additions and 63 deletions
+12
View File
@@ -2,6 +2,18 @@
This template should help get you started developing with Vue 3 in Vite. This template should help get you started developing with Vue 3 in Vite.
## TODO
- [ ] render code highlight
- [ ] render mathjax
- [ ] render plantuml
- [ ] custom css in sub repo
- [ ] custom layout in sub repo ?
- [ ] build RSS feed
- [ ] link to home
- [ ] link to previous/next article
- [ ] set page title
## Recommended IDE Setup ## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). [VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
+1
View File
@@ -3,6 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"repository": "https://github.com/klemek/md-blog",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
+15 -8
View File
@@ -1,11 +1,18 @@
export interface MarkdownAttributes {
title?: string
draft?: string
thumbnail?: string
path?: string
}
export interface MarkdownData { export interface MarkdownData {
attributes: MarkdownAttributes attributes: Record<string, unknown>
html: string
}
export interface ArticleMetadata {
path: string
title: string
date: Date
author: string
thumbnail: string
draft: boolean
}
export interface Article {
metadata: ArticleMetadata
html: string html: string
} }
+37 -22
View File
@@ -1,44 +1,59 @@
import type { ArticleList, MarkdownAttributes } from '@/interfaces' import type { MarkdownData, Article, ArticleMetadata } from '@interfaces'
import type { MarkdownData } from '@interfaces' import { dateFromParts } from './dates'
function completeAttributes( function parseMetadata(
srcAttributes: MarkdownAttributes, srcAttributes: Record<string, unknown>,
pathPrefix: string, pathPrefix: string,
): MarkdownAttributes { date: Date,
): ArticleMetadata {
return { return {
draft: srcAttributes.draft ?? 'false',
thumbnail: srcAttributes.thumbnail
? pathPrefix + srcAttributes.thumbnail
: pathPrefix + '/thumbnail.jpg',
title: srcAttributes.title ?? 'Untitled',
path: pathPrefix, path: pathPrefix,
title: (srcAttributes.title as string) ?? 'Untitled',
date: date,
author: (srcAttributes.author as string) ?? '',
thumbnail: srcAttributes.thumbnail
? (srcAttributes.thumbnail as string).replaceAll('./', pathPrefix + '/')
: pathPrefix + '/thumbnail.jpg',
draft: !!srcAttributes.draft,
} }
} }
export async function loadArticle(year: number, month: number, day: number): MarkdownData | null { function transformHtml(srcHtml: string, pathPrefix: string): string {
const path = `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}` return srcHtml.replaceAll('./', pathPrefix + '/')
console.log(path) }
export async function loadArticle(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}`
try { try {
const data = (await import( const data = (await import(`@articles/${year}/${month}/${day}/index.md`)) as MarkdownData
`@articles/${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/index.md`
)) as MarkdownData
return { return {
attributes: completeAttributes(data.attributes, `./articles/${path}`), metadata: parseMetadata(data.attributes, path, date),
html: data.html.replaceAll('./', `./articles/${path}/`), html: transformHtml(data.html, path),
} }
} catch { } catch {
return null return null
} }
} }
export async function listArticles(): Promise<MarkdownAttributes[]> { 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: MarkdownAttributes[] = await Promise.all( const articles: ArticleMetadata[] = (
await Promise.all(
Object.keys(raw_articles).map(async (key) => { Object.keys(raw_articles).map(async (key) => {
if (!raw_articles[key]) return null
const data = (await raw_articles[key]()) as MarkdownData const data = (await raw_articles[key]()) as MarkdownData
return completeAttributes(data.attributes, key.replace('/index.md', '')) const match = key.match(/\/(\d+)\/(\d+)\/(\d+)\//)
if (match === null) {
return null
}
const date = dateFromParts(match[1], match[2], match[3])
return parseMetadata(data.attributes, key.replace('/index.md', ''), date)
}), }),
) )
articles.sort((article1, article2) => article1.path?.localeCompare(article2.path ?? '') ?? 0) ).filter((item) => item !== null)
articles.sort((article1, article2) => article2.date.valueOf() - article1.date.valueOf())
return articles return articles
} }
+17
View File
@@ -0,0 +1,17 @@
export function dateFromParts(
year: string | undefined,
month: string | undefined,
day: string | undefined,
): Date {
return new Date(parseInt(year ?? ''), parseInt(month ?? '') - 1, parseInt(day ?? ''))
}
export function simpleDateFormat(date: Date): string {
return (
date.getFullYear() +
'-' +
(date.getMonth() + 1).toString().padStart(2, '0') +
'-' +
date.getDate().toString().padStart(2, '0')
)
}
+5
View File
@@ -0,0 +1,5 @@
import packageJson from '@/../package.json'
export const NAME = packageJson.name
export const VERSION = packageJson.version
export const REPOSITORY = packageJson.repository
+38 -17
View File
@@ -1,39 +1,60 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MarkdownData } from '@interfaces' import type { Article } from '@interfaces'
import { ref, onBeforeMount } from 'vue' import { ref, onBeforeMount } from 'vue'
import { loadArticle } from '@lib/articles' import { loadArticle } 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 NotFoundView from './NotFoundView.vue'
import { dateFromParts, simpleDateFormat } from '@lib/dates'
import { NAME, REPOSITORY, VERSION } from '@lib/meta'
const markdownData = ref<MarkdownData | null>(null) const article = ref<Article | null>(null)
const route = useRoute() const route = useRoute()
const articleDate = ref<Date>(new Date())
async function loadPage(target: RouteLocation) { async function loadPage(target: RouteLocation) {
articleDate.value = new Date( article.value = await loadArticle(
parseInt(target.params.year), dateFromParts(
parseInt(target.params.month) - 1, target.params.year as string,
parseInt(target.params.day), target.params.month as string,
) target.params.day as string,
markdownData.value = await loadArticle( ),
articleDate.value.getFullYear(),
articleDate.value.getMonth() + 1,
articleDate.value.getDate(),
) )
} }
function scrollTop() {
window.scrollTo(0, 0)
}
onBeforeMount(() => loadPage(route)) onBeforeMount(() => loadPage(route))
onBeforeRouteUpdate(loadPage) onBeforeRouteUpdate(loadPage)
</script> </script>
<template> <template>
<template v-if="!markdownData"> <template v-if="!article">
<NotFoundView /> <NotFoundView />
</template> </template>
<template v-else> <template v-else>
<h1>ArticleView - {{ articleDate }}</h1> <main class="article">
<div v-html="markdownData.html"></div> <div class="header">
<RouterLink class="link-home" to="/"></RouterLink>
<h1>{{ article.metadata.title }}</h1>
<span class="time">
<span>{{ article.metadata.draft ? 'Drafted on' : 'Published on' }}</span>
{{ simpleDateFormat(article.metadata.date) }}
</span>
</div>
<img :src="article.metadata.thumbnail" alt="thumbnail" />
</main>
<div id="text" v-html="article.html"></div>
<div id="signature">TODO signature</div>
<br />
<a @click.prevent="scrollTop" href="#">Go to top</a> -
<RouterLink to="/">Back to home</RouterLink>
<hr />
<footer>
<small>
{{ new Date().getFullYear() }} - Made with <a :href="REPOSITORY">{{ NAME }}</a>
{{ VERSION }}
</small>
</footer>
</template> </template>
</template> </template>
<style scoped></style>
+17 -8
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onBeforeMount } from 'vue' import { ref, onBeforeMount } from 'vue'
import { listArticles } from '@/lib/articles' import { listArticles } from '@lib/articles'
import type { MarkdownAttributes } from '@/interfaces' import type { ArticleMetadata } from '@interfaces'
import { simpleDateFormat } from '@/lib/dates'
const articles = ref<MarkdownAttributes[]>([]) const articles = ref<ArticleMetadata[]>([])
onBeforeMount(async () => { onBeforeMount(async () => {
const newArticles = await listArticles() const newArticles = await listArticles()
@@ -13,10 +14,18 @@ onBeforeMount(async () => {
</script> </script>
<template> <template>
<h1>Home View</h1> <main>
<div v-for="(attr, index) in articles" v-bind:key="index"> <h1 class="title">Articles</h1>
<RouterLink :to="attr.path ?? ''">{{ attr.title }}</RouterLink> <template v-for="(metadata, index) in articles" v-bind:key="index">
<div v-if="!metadata.draft && metadata.path">
<RouterLink :to="metadata.path">
<h3>{{ metadata.title }}</h3>
<span class="time"
><span>Published on</span>&nbsp;&nbsp;{{ simpleDateFormat(metadata.date) }}</span
>
<img alt="thumbnail" :src="metadata.thumbnail" />
</RouterLink>
</div> </div>
</template> </template>
</main>
<style scoped></style> </template>
+4 -1
View File
@@ -1,7 +1,10 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>
<h1>Not found</h1> <main>
<h1>Page not found</h1>
<RouterLink to="/">Back to home</RouterLink>
</main>
</template> </template>
<style scoped></style> <style scoped></style>
+5 -1
View File
@@ -8,7 +8,11 @@
// Path mapping for cleaner imports. // Path mapping for cleaner imports.
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"],
"@lib/*": ["./src/lib/*"],
"@views/*": ["./src/views/*"],
"@interfaces": ["./src/interfaces.ts"],
"@articles/*": ["./articles/*"]
}, },
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.