feat: working SPA

This commit is contained in:
2026-04-24 18:25:27 +02:00
parent e89020ad96
commit cbab7c74e5
13 changed files with 166 additions and 14 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"lint": "run-s lint:*", "lint": "run-s lint:*",
"lint:oxlint": "oxlint . --fix", "lint:oxlint": "oxlint . --fix",
"lint:eslint": "eslint . --fix --cache", "lint:eslint": "eslint . --fix --cache",
"format": "prettier --write --experimental-cli src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"vue": "^3.5.32", "vue": "^3.5.32",
+1
View File
@@ -0,0 +1 @@
../articles
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

+1 -9
View File
@@ -1,11 +1,3 @@
<script setup lang="ts"></script>
<template> <template>
<h1>You did it!</h1> <RouterView />
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
</template> </template>
<style scoped></style>
+11
View File
@@ -0,0 +1,11 @@
export interface MarkdownAttributes {
title?: string
draft?: string
thumbnail?: string
path?: string
}
export interface MarkdownData {
attributes: MarkdownAttributes
html: string
}
+44
View File
@@ -0,0 +1,44 @@
import type { ArticleList, MarkdownAttributes } from '@/interfaces'
import type { MarkdownData } from '@interfaces'
function completeAttributes(
srcAttributes: MarkdownAttributes,
pathPrefix: string,
): MarkdownAttributes {
return {
draft: srcAttributes.draft ?? 'false',
thumbnail: srcAttributes.thumbnail
? pathPrefix + srcAttributes.thumbnail
: pathPrefix + '/thumbnail.jpg',
title: srcAttributes.title ?? 'Untitled',
path: pathPrefix,
}
}
export async function loadArticle(year: number, month: number, day: number): MarkdownData | null {
const path = `${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}`
console.log(path)
try {
const data = (await import(
`@articles/${year}/${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/index.md`
)) as MarkdownData
return {
attributes: completeAttributes(data.attributes, `./articles/${path}`),
html: data.html.replaceAll('./', `./articles/${path}/`),
}
} catch {
return null
}
}
export async function listArticles(): Promise<MarkdownAttributes[]> {
const raw_articles = import.meta.glob('@articles/**/index.md')
const articles: MarkdownAttributes[] = await Promise.all(
Object.keys(raw_articles).map(async (key) => {
const data = (await raw_articles[key]()) as MarkdownData
return completeAttributes(data.attributes, key.replace('/index.md', ''))
}),
)
articles.sort((article1, article2) => article1.path?.localeCompare(article2.path ?? '') ?? 0)
return articles
}
+10 -3
View File
@@ -1,8 +1,15 @@
import { createRouter, createWebHistory } from 'vue-router' import ArticleView from '@views/ArticleView.vue'
import HomeView from '@views/HomeView.vue'
import NotFoundView from '@views/NotFoundView.vue'
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHashHistory(),
routes: [], routes: [
{ path: '/', component: HomeView },
{ path: '/articles/:year(\\d{4})/:month(\\d{2})/:day(\\d{2})', component: ArticleView },
{ path: '/:pathMatch(.*)', component: NotFoundView },
],
}) })
export default router export default router
+39
View File
@@ -0,0 +1,39 @@
<script setup lang="ts">
import type { MarkdownData } from '@interfaces'
import { ref, onBeforeMount } from 'vue'
import { loadArticle } from '@lib/articles'
import { useRoute, onBeforeRouteUpdate, type RouteLocation } from 'vue-router'
import NotFoundView from './NotFoundView.vue'
const markdownData = ref<MarkdownData | null>(null)
const route = useRoute()
const articleDate = ref<Date>(new Date())
async function loadPage(target: RouteLocation) {
articleDate.value = new Date(
parseInt(target.params.year),
parseInt(target.params.month) - 1,
parseInt(target.params.day),
)
markdownData.value = await loadArticle(
articleDate.value.getFullYear(),
articleDate.value.getMonth() + 1,
articleDate.value.getDate(),
)
}
onBeforeMount(() => loadPage(route))
onBeforeRouteUpdate(loadPage)
</script>
<template>
<template v-if="!markdownData">
<NotFoundView />
</template>
<template v-else>
<h1>ArticleView - {{ articleDate }}</h1>
<div v-html="markdownData.html"></div>
</template>
</template>
<style scoped></style>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { ref, onBeforeMount } from 'vue'
import { listArticles } from '@/lib/articles'
import type { MarkdownAttributes } from '@/interfaces'
const articles = ref<MarkdownAttributes[]>([])
onBeforeMount(async () => {
const newArticles = await listArticles()
console.log(newArticles)
articles.value.splice(0, articles.value.length, ...newArticles)
})
</script>
<template>
<h1>Home View</h1>
<div v-for="(attr, index) in articles" v-bind:key="index">
<RouterLink :to="attr.path ?? ''">{{ attr.title }}</RouterLink>
</div>
</template>
<style scoped></style>
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<h1>Not found</h1>
</template>
<style scoped></style>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "include": ["env.d.ts", "vite.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
// Extra safety for array and object lookups, but may have false positives. // Extra safety for array and object lookups, but may have false positives.
+4
View File
@@ -11,7 +11,11 @@ export default defineConfig({
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)),
'@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)),
}, },
}, },
assetsInclude: [/\.\/articles\/.*(?!\.md)'/],
}) })
Vendored
+25
View File
@@ -0,0 +1,25 @@
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 }
}