diff --git a/.github/workflows/nuxtjs.yml b/.github/workflows/nuxtjs.yml index 81c019b..536becb 100644 --- a/.github/workflows/nuxtjs.yml +++ b/.github/workflows/nuxtjs.yml @@ -42,8 +42,8 @@ jobs: run: pip install python-frontmatter && deno install - name: Update Page list run: deno --allow-all utils/page_updater/update_pagelist.ts - - name: Update Page history - run: python utils/page_updater/commit_post_history.py + # - name: Update Page history + # run: python utils/page_updater/commit_post_history.py # - name: Generate RSS feed # run: python utils/page_updater/rss_xml_gen.py - name: Commit changes diff --git a/assets/markdown_conf.ts b/assets/markdown_conf.ts deleted file mode 100644 index 031504b..0000000 --- a/assets/markdown_conf.ts +++ /dev/null @@ -1,275 +0,0 @@ -// TypeScript utilities for rendering Markdown/HTML content - -import hljs from "highlight.js"; -import MarkdownIt from "markdown-it"; -import { alert } from "@mdit/plugin-alert"; -import { tab } from "@mdit/plugin-tab"; -import { tasklist } from "@mdit/plugin-tasklist"; -import { mark } from "@mdit/plugin-mark"; -import { footnote } from "@mdit/plugin-footnote"; -import fm, { type FrontMatterResult } from "front-matter"; - -export interface MarkdownMetadata { - title: string; - description: string; - date: string; - tags: string[]; - background: string; - next?: string; - previous?: string; -} - -export interface MarkdownOutput { - metadata: MarkdownMetadata; - contents: string; -} - -function isolate_markdown(input: string): MarkdownOutput { - const front_matter: FrontMatterResult = fm(input); - const content = front_matter.body; - - const metadata: MarkdownMetadata = { - title: front_matter.attributes.title || "", - description: front_matter.attributes.description || "", - date: front_matter.attributes.date || "", - tags: front_matter.attributes.tags || [], - background: front_matter.attributes.background || "", - next: front_matter.attributes.next || "", - previous: front_matter.attributes.previous || "" - }; - - return { - metadata: metadata, - contents: content - }; -} - -function isolate_html(input: string): MarkdownOutput { - const title = input.match(/([^<]+)<\/title>/); - const meta = input.match(/<meta name="([^"]+)" content="([^"]+)">/g); - const metadata: MarkdownMetadata = { - title: "", - description: "", - date: "", - tags: [], - background: "" - }; - - if (meta) { - - for (const tag of meta) { - const match = tag.match(/<meta name="([^"]+)" content="([^"]+)">/); - if (match) { - switch (match[1]) { - case "title": - metadata.title = match[2]; - break; - case "description": - metadata.description = match[2]; - break; - case "date": - metadata.date = match[2]; - break; - case "tags": - metadata.tags = match[2].split(","); - break; - case "background": - metadata.background = match[2]; - break; - } - } - } - } - - const front_matter: FrontMatterResult<any> = fm(input); - - if (front_matter.attributes) { - metadata.title = front_matter.attributes.title || metadata.title; - metadata.description = front_matter.attributes.description || metadata.description; - metadata.date = front_matter.attributes.date || metadata.date; - metadata.tags = front_matter.attributes.tags || metadata.tags; - metadata.background = front_matter.attributes.background || metadata.background; - metadata.next = front_matter.attributes.next || ""; - metadata.previous = front_matter.attributes.previous || ""; - } - - return { - metadata: metadata, - contents: input - }; -} - -export class MarkdownInput { - type: "markdown" | "html"; - private contents: string; - private metadata: MarkdownMetadata; - - constructor(type: "markdown" | "html", data: string) { - this.type = type; - this.metadata = this.get_metadata(); - - switch (this.type) { - case "markdown": - const result = isolate_markdown(data); - this.contents = result.contents; - this.metadata = result.metadata; - break; - case "html": - const result_html = isolate_html(data); - this.contents = data; - this.metadata = result_html.metadata; - break; - } - - } - - static from_markdown(data: string): MarkdownInput { - return new MarkdownInput("markdown", data); - } - - static from_html(data: string): MarkdownInput { - return new MarkdownInput("html", data); - } - - static async from_url(url: string): Promise<MarkdownInput> { - const response = await fetch(url); - const data = await response.text(); - - // Check the content type of the response - const content_type = response.headers.get("content-type"); - if (content_type) { - if (content_type.includes("text/markdown")) { - return MarkdownInput.from_markdown(data); - } else if (content_type.includes("text/html")) { - return MarkdownInput.from_html(data); - } - } - - if (url.endsWith(".md")) { - return MarkdownInput.from_markdown(data); - } else if (url.endsWith(".html")) { - return MarkdownInput.from_html(data); - } - - // Fallback to markdown - return MarkdownInput.from_markdown(data); - } - - public get_contents(): string { - return this.contents; - } - - public get_metadata(): MarkdownMetadata { - return this.metadata; - } -} - -export class MarkdownContext { - private md: MarkdownIt; - - constructor(md: MarkdownIt | undefined) { - if (md) { - this.md = md; - } else { - this.md = configured_markdown(); - } - } - - private render_markdown(input: MarkdownInput): MarkdownOutput { - console.log("Rendering markdown") - - const content = configured_markdown().render(input.get_contents()); - - const result: MarkdownOutput = { - metadata: input.get_metadata(), - contents: content - }; - return result; - } - - private render_html(inputs: MarkdownInput): MarkdownOutput { - const result: MarkdownOutput = { - metadata: inputs.get_metadata(), - contents: inputs.get_contents() - }; - - return result; - } - - render(input: MarkdownInput): MarkdownOutput { - switch (input.type) { - case "markdown": - return this.render_markdown(input); - case "html": - return this.render_html(input); - } - } -} - -function configured_markdown(): MarkdownIt { - var md: MarkdownIt = MarkdownIt({ - breaks: true, - typographer: true, - html: true, - highlight: function (str, lang) { - if (lang && hljs.getLanguage(lang)) { - try { - return '<pre><code class="hljs">' + - hljs.highlight(str, { - language: lang, - ignoreIllegals: true, - }).value + - "</code></pre>"; - } catch (__) {} - } - - return '<pre><code class="hljs">' + md.utils.escapeHtml(str) + - "</code></pre>"; - }, - }); - - md = md - .use(tasklist) - .use(mark) - .use(footnote) - .use(alert, { - alertNames: [ - "note", "info", "warning", "danger", "todo", "tip", - "important", "success", "caution", "question", "done", - "quote", "deprecated", "example" - ], - }).use(tab, { - name: "tabs" - }); - - md.renderer.rules.text = function (tokens, idx, options, env, self) { - // headers 1-3 get an <hr> after them - With a class (md-hr-N) for styling - if (tokens[idx].type === "heading_open") { - const level = tokens[idx].tag; - return `<${level} class="md-hr-${level}">${tokens[idx + 1].content}</${level}>`; - } - return self.renderToken(tokens, idx, options); - } - - md.renderer.rules.softbreak = function (tokens, idx, options, env, self) { - return "<br>"; - }; - - md.renderer.rules.hardbreak = function (tokens, idx, options, env, self) { - return "<br><br>"; - }; - - md.renderer.rules.text = function (tokens, idx, options, env, self) { - return tokens[idx].content; - } - - return md; -} - -export var globalMarkdown = new MarkdownContext(undefined); - -export default { - MarkdownInput, - MarkdownContext, - globalMarkdown -} \ No newline at end of file diff --git a/assets/meta/blog_list.json b/assets/meta/blog_list.json index 7d04492..055e298 100644 --- a/assets/meta/blog_list.json +++ b/assets/meta/blog_list.json @@ -1,117 +1,124 @@ { - "last_generated": "2025-01-03 21:53:59", - "posts": [ - { - "metadata": { - "title": "3DS Programming - Using RomFS", - "description": "A guide to using RomFS on the 3DS. (Old)", - "date": "2025-01-01 00:00:00", - "tags": [ - "3ds", - "programming", - "c", - "devkitpro", - "old" - ], - "previous": "old3ds_helloworld.md", - "next": "old3ds_touchscreen.md" - }, - "id": "/old3ds_romfs.md", - "url": "/blog/old3ds_romfs.md", - "hash": "0b28a366868e9fa564b6a33d9b1aa1d8269f7971497f25488f05f54929e88410" - }, - { - "metadata": { - "title": "Styling Test", - "description": "A test post to see how the site styling looks", - "date": "2025-01-01 00:00:00", - "tags": [ - "meta", - "web" - ] - }, - "id": "/styling_test.md", - "url": "/blog/styling_test.md", - "hash": "8e6c14fdef5fd67ea17dcc8b58d59f2040d8250c15c2d18d3e1fdc1b1b60dc54" - }, - { - "metadata": { - "title": "Awesome", - "description": "A curated list of awesome stuff I like", - "date": "2024-11-26 00:00:00", - "tags": [ - "awesome", - "curated" - ] - }, - "id": "/awesome.md", - "url": "/blog/awesome.md", - "hash": "0632400858006b93f2f36d87953538c2a400bacc75aaa29928aee226e8b343b1" - }, - { - "metadata": { - "title": "LGBTQ+ Resources", - "description": "A list of resources for LGBTQ+ individuals", - "date": "2025-01-02 00:00:00", - "tags": [ - "lgbtq+", - "resources" - ] - }, - "id": "/lgbtq_resources.md", - "url": "/blog/lgbtq_resources.md", - "hash": "1dd63f74b3f74077510f1043cb471ab9c691711545bb6ef1f3d6eff4f6a23c89" - }, - { - "metadata": { - "title": "3DS Programming - Hello World", - "description": "A guide to creating a simple Hello, World program for the 3DS. (Old)", - "date": "2025-01-01 00:00:00", - "tags": [ - "3ds", - "programming", - "c", - "devkitpro", - "old" - ], - "next": "old3ds_romfs.md" - }, - "id": "/old3ds_helloworld.md", - "url": "/blog/old3ds_helloworld.md", - "hash": "86e0bd1deae0d00b17ab0960634ea7292d6387063f70600cec4001564fde9514" - }, - { - "metadata": { - "title": "3DS Programming - Touchscreen Input", - "description": "A guide to using the touchscreen on the 3DS. (Old)", - "date": "2025-01-01 00:00:00", - "tags": [ - "3ds", - "programming", - "c", - "devkitpro", - "old" - ], - "previous": "old3ds_romfs.md" - }, - "id": "/old3ds_touchscreen.md", - "url": "/blog/old3ds_touchscreen.md", - "hash": "59e0b9d701646fd5f747713832c47ce451e0ebe0975d4a148a820ca795741c2b" - }, - { - "metadata": { - "title": "Badges!", - "description": "A collection of 88x31 badges for various things", - "date": "2024-12-21 00:00:00", - "tags": [ - "badges", - "retro", - "web" - ] - }, - "id": "/badges.md", - "url": "/blog/badges.md", - "hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00" + "last_generated": "2025-01-04 10:34:37", + "categories": { + "Blog": { + "posts": [ + { + "metadata": { + "title": "LGBTQ+ Resources", + "description": "A list of resources for LGBTQ+ individuals", + "date": "2025-01-01 16:00:00", + "tags": [ + "lgbtq+", + "resources" + ] + }, + "id": "blog/lgbtq_resources.md", + "url": "/blog/lgbtq_resources.md", + "hash": "3da76064aa95cc06937bde01128ed44aafb850f35a43bd214ce0cd89a875c674" + }, + { + "metadata": { + "title": "3DS Programming - Using RomFS", + "description": "A guide to using RomFS on the 3DS. (Old)", + "date": "2024-12-31 16:00:00", + "tags": [ + "3ds", + "programming", + "c", + "devkitpro", + "old" + ], + "previous": "old3ds_helloworld.md", + "next": "old3ds_touchscreen.md" + }, + "id": "blog/old3ds_romfs.md", + "url": "/blog/old3ds_romfs.md", + "hash": "0b28a366868e9fa564b6a33d9b1aa1d8269f7971497f25488f05f54929e88410" + }, + { + "metadata": { + "title": "3DS Programming - Touchscreen Input", + "description": "A guide to using the touchscreen on the 3DS. (Old)", + "date": "2024-12-31 16:00:00", + "tags": [ + "3ds", + "programming", + "c", + "devkitpro", + "old" + ], + "previous": "old3ds_romfs.md" + }, + "id": "blog/old3ds_touchscreen.md", + "url": "/blog/old3ds_touchscreen.md", + "hash": "59e0b9d701646fd5f747713832c47ce451e0ebe0975d4a148a820ca795741c2b" + }, + { + "metadata": { + "title": "Styling Test", + "description": "A test post to see how the site styling looks", + "date": "2024-12-31 16:00:00", + "tags": [ + "meta", + "web" + ] + }, + "id": "blog/styling_test.md", + "url": "/blog/styling_test.md", + "hash": "0ff9f34321a27f462ca26656a1dc5024c0e800ea1e176ff36316b158ab4606c9" + }, + { + "metadata": { + "title": "3DS Programming - Hello World", + "description": "A guide to creating a simple Hello, World program for the 3DS. (Old)", + "date": "2024-12-31 16:00:00", + "tags": [ + "3ds", + "programming", + "c", + "devkitpro", + "old" + ], + "next": "old3ds_romfs.md" + }, + "id": "blog/old3ds_helloworld.md", + "url": "/blog/old3ds_helloworld.md", + "hash": "86e0bd1deae0d00b17ab0960634ea7292d6387063f70600cec4001564fde9514" + }, + { + "metadata": { + "title": "Badges!", + "description": "A collection of 88x31 badges for various things", + "date": "2024-12-20 16:00:00", + "tags": [ + "badges", + "retro", + "web" + ] + }, + "id": "blog/badges.md", + "url": "/blog/badges.md", + "hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00" + }, + { + "metadata": { + "title": "Awesome", + "description": "A curated list of awesome stuff I like", + "date": "2024-11-25 16:00:00", + "tags": [ + "awesome", + "curated" + ] + }, + "id": "blog/awesome.md", + "url": "/blog/awesome.md", + "hash": "0632400858006b93f2f36d87953538c2a400bacc75aaa29928aee226e8b343b1" + } + ], + "title": "", + "description": "", + "tags": [] } - ] + } } \ No newline at end of file diff --git a/components/Navbar.vue b/components/Navbar.vue index b4e983b..99ef79d 100644 --- a/components/Navbar.vue +++ b/components/Navbar.vue @@ -9,9 +9,9 @@ import Card from './Card.vue'; <Card> <div class="flex justify-center"> <NuxtLink href="/" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Home</NuxtLink> - <NuxtLink href="/blog/" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Blog</NuxtLink> - <NuxtLink href="/blog/?post=/blog/awesome.md" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Awesome</NuxtLink> - <NuxtLink href="/blog/?post=/blog/badges.md" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Badges</NuxtLink> + <NuxtLink href="/article/" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Blog</NuxtLink> + <NuxtLink href="/article/blog/awesome.md" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Awesome</NuxtLink> + <NuxtLink href="/article/blog/badges.md" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Badges</NuxtLink> </div> <div class="flex justify-center text-white"> <small class="ml-3 mr-3">Hosted with <NuxtLink href="https://github.com/TheFelidae/thefelidae.github.io" class="text-blue-500">GitHub Pages</NuxtLink></small> diff --git a/components/PostCard.vue b/components/PostCard.vue index 29b9037..7104e1c 100644 --- a/components/PostCard.vue +++ b/components/PostCard.vue @@ -70,7 +70,7 @@ onMounted(() => { </script> <template> - <NuxtLink :href="'/blog?post=' +url"> + <NuxtLink :href="'/article' + url"> <!-- Large --> <div v-if = "size === 'full'"> <div class="m-4 min-h-30 min-width-90 text-white transition hover:bg-purple-600 bg-opacity-50 hover:bg-opacity-70"> diff --git a/nuxt.config.ts b/nuxt.config.ts index 84521f1..00f1046 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -2,27 +2,20 @@ import * as pages from '~/utils/page_updater/update_pagelist'; const blog_list: pages.PageList = (await import('./assets/meta/blog_list.json')) as pages.PageList; -const blog_routes: any = blog_list.posts.map((post) => { - return { - ['/blog?post=' + post.id]: { - prerender: true - } +// nitro only needs string array +const blog_nitro_routes: any = []; +// key value +for (let [key, category] of Object.entries(blog_list.categories)) { + for (let post of category.posts) { + blog_nitro_routes.push('/article' + post.url); } -}); - -blog_routes.push({ - '/blog': { - prerender: true - } -}); - -console.log(blog_routes); +} +console.log(blog_nitro_routes); // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: '2024-11-01', ssr: true, - routeRules: blog_routes, postcss: { plugins: { tailwindcss: {}, @@ -43,6 +36,11 @@ export default defineNuxtConfig({ // ... options }, + nitro: { + prerender: { + routes: blog_nitro_routes + } + }, particles: { mode: 'slim', lazy: true diff --git a/pages/article/[category]/[id].vue b/pages/article/[category]/[id].vue new file mode 100644 index 0000000..817de44 --- /dev/null +++ b/pages/article/[category]/[id].vue @@ -0,0 +1,144 @@ +<script setup lang="ts"> +import { onMounted, watch, ref } from 'vue'; +import fm from 'front-matter'; + +import PostCard from '~/components/PostCard.vue'; +import * as pages from '~/utils/page_updater/update_pagelist'; +import type { PageInfo, PageInfoMetdata } from '~/utils/page_updater/pages'; +import type { ParsedContent } from '@nuxt/content'; + +let route = useRoute() +console.log(route) + +const url: Ref<string> = ref("") +url.value = '/' + route.params.category.concat('/' + route.params.id as string) as string + +console.log(url.value) + +const loading: Ref<boolean> = ref(false) + +const tagFilter: Ref<string[]> = ref([]) +tagFilter.value = [] + +const markdown: Ref<any> = ref(null) + +const title: Ref<string> = ref("") +const description: Ref<string> = ref("") +const date: Ref<string> = ref("") +const tags: Ref<string[]> = ref([]) +const background: Ref<string> = ref("") +const next: Ref<string> = ref("") +const previous: Ref<string> = ref("") + +watch(markdown , (newVal) => { + if (newVal) { + title.value = newVal.title ? newVal.title : "" + description.value = newVal.description ? newVal.description : "" + date.value = newVal.date ? new Date(newVal.date).toLocaleDateString() : "" + tags.value = newVal.tags ? newVal.tags : [] + background.value = newVal.background ? newVal.background : "" + } +}) + +// watch the params of the route to fetch the data again +watch(route, async () => { + url.value = '/' + route.params.category.concat('/' + route.params.id as string) as string + if (url.value) { + console.log("Fetching article") + loading.value = true + try { + await fetchArticle(url.value) + } + finally { + loading.value = false + } + } +}, { + immediate: true +}); + +// Fetch the article contents from the URL +async function fetchArticle(url: string) { + if (!url) { + return + } + // Trim the .md extension + var url = url.replace(/\.md$/, "") + console.log("Fetching article: " + url) + const { data } = await useAsyncData(url, () => queryContent(url).findOne()) + console.log(data) + + markdown.value = data.value; +} + +function resetReadingPosition() { + window.scrollTo(0, 0) +} + +fetchArticle(url.value) + +console.log("Prefetching article") +onMounted(async () => { + console.log("Fetching article :3") + await fetchArticle(url.value) +}) +const temp_url = route.query.post as string +await fetchArticle(temp_url); + +</script> + +<template> + <div class="relative z-50 flex w-full justify-center text-white"> + <!-- Metadata --> + <MetaSet :title="title" :description="description" :date="date" + :background="background" tags="tags" /> + <!-- Article Viewer --> + <div class="mt-8 flex-col text-center"> + <Transition name="list"> + <div class="flex flex-col" :key="url"> + <h1>{{ title }}</h1> + <small>{{ date }}</small> + <div class="max-w-50 flex flex-row justify-center"> + <div v-for="tag in tags" :key="tag" class="m-1 text-center"> + <span + class="text-xs bg-black border-purple-400 border-2 text-white p-1 rounded-md">{{ + tag }}</span> + </div> + </div> + <div> + <!-- Next/Prev controls, on the left and right side using PostCards --> + <div class="flex max-w-4xl max-md:w-screen"> + <div class="justify-start"> + <NuxtLink v-if="previous" :onclick="resetReadingPosition" :to="previous" + class="m-2 text-white">Previous</NuxtLink> + </div> + <div class="justify-end"> + <NuxtLink v-if="next" :onclick="resetReadingPosition" :to="next" + class="m-2 text-white">Next</NuxtLink> + </div> + </div> + </div> + <!-- Article Content --> + <Card class="text-pretty max-w-4xl mt-4 max-md:w-screen text-left"> + <article> + <div v-if="markdown != null"> + <Markdown :input="markdown" /> + </div> + <!-- Aligned next/prev controls --> + <div class="flex"> + <div class="justify-start"> + <NuxtLink v-if="previous" :onclick="resetReadingPosition" :to="previous" + class="m-2 text-white">Previous</NuxtLink> + </div> + <div class="justify-end"> + <NuxtLink v-if="next" :onclick="resetReadingPosition" :to="next" + class="m-2 text-white">Next</NuxtLink> + </div> + </div> + </article> + </Card> + </div> + </Transition> + </div> + </div> +</template> \ No newline at end of file diff --git a/pages/article/index.vue b/pages/article/index.vue new file mode 100644 index 0000000..41487a6 --- /dev/null +++ b/pages/article/index.vue @@ -0,0 +1,91 @@ +<script setup lang="ts"> +import { onMounted, watch, ref } from 'vue'; +import fm from 'front-matter'; + +import PostCard from '~/components/PostCard.vue'; +import * as pages from '~/utils/page_updater/update_pagelist'; +import type { PageInfo, PageInfoMetdata } from '~/utils/page_updater/pages'; +import type { ParsedContent } from '@nuxt/content'; + +// Automatically maintained is a blog_list.json in assets/meta. This file contains a list of all blog posts and their metadata. +// This file is generated by a script in the utils/pageupdater folder. +const article_list: pages.PageList = await import('~/assets/meta/blog_list.json') as pages.PageList; + +let route = useRoute() +console.log(route) + +const loading: Ref<boolean> = ref(false) + +const listCategoryKeys: Ref<string[]> = ref([]) + +const tagList: Ref<string[]> = ref([]) +const tagFilter: Ref<string[]> = ref([]) +tagFilter.value = [] + +function resetReadingPosition() { + window.scrollTo(0, 0) +} + +onMounted(() => { + // Extract the tags from each post in each category + var tags: string[] = [] + for (const category in article_list.categories) { + console.log("Category: " + category) + for (const post of article_list.categories[category].posts) { + if (post.metadata.tags) { + for (const tag of post.metadata.tags) { + if (!tags.includes(tag)) { + console.log("Adding tag: " + tag) + tags.push(tag) + } + } + } + } + } + tagList.value = tags +}); +</script> + +<template> + <div class="relative z-50 flex w-full justify-center text-white"> + <!-- Metadata --> + <MetaSet title="Articles" description="Ramblings." background="https://avatars.githubusercontent.com/u/94077364?v=4" + tags="blog, personal, author" /> + + <!-- Main Content --> + <div class="mt-8 flex-col text-center"> + <Transition name="list"> + <div> + <!-- Article List --> + <h1>Articles</h1> + <div class="flex justify-center"> + + <div class="flex flex-wrap justify-center m-5 max-w-96"> + <div v-for="tag in tagList" :key="tag" class="m-1"> + <button + @click="tagFilter.includes(tag) ? tagFilter.splice(tagFilter.indexOf(tag), 1) : tagFilter.push(tag)" + class="text-xs bg-black border-purple-400 border text-white p-1 rounded-md" + :class="tagFilter.includes(tag) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">{{ + tag }}</button> + </div> + </div> + </div> + <div> + <div v-for="categoryKey in Object.keys(article_list.categories)" :key="categoryKey"> + <div class="lg:w-[48rem] md:w-max flex flex-col bg-secondary bg-opacity-50 rounded-md shadow-md shadow-secondary p-2 m-2"> + <h2>{{ categoryKey }}</h2> + <div + v-for="post in tagFilter.length == 0 ? article_list.categories[categoryKey].posts : article_list.categories[categoryKey].posts.filter((post) => post.metadata.tags ? post.metadata.tags.some((tag) => tagFilter.includes(tag)) : true)"> + <PostCard class="lg:w-[48rem]" :url="post.url" :key="post.id" :tagFilter="tagFilter" /> + </div> + </div> + </div> + </div> + </div> + </Transition> + </div> + </div> +</template> + +<style scoped> +</style> \ No newline at end of file diff --git a/pages/blog.vue b/pages/blog.vue deleted file mode 100644 index 0ce1381..0000000 --- a/pages/blog.vue +++ /dev/null @@ -1,215 +0,0 @@ -<script setup lang="ts"> -import { onMounted, watch, ref } from 'vue'; -import fm from 'front-matter'; - -import PostCard from '../components/PostCard.vue'; -import { globalMarkdown, MarkdownInput, type MarkdownMetadata } from '~/assets/markdown_conf'; -import * as pages from '~/utils/page_updater/update_pagelist'; -import type { PageInfo, PageInfoMetdata } from '~/utils/page_updater/pages'; -import type { ParsedContent } from '@nuxt/content'; - -// Automatically maintained is a blog_list.json in assets/meta. This file contains a list of all blog posts and their metadata. -// This file is generated by a script in the utils/pageupdater folder. -const blog_list: pages.PageList = (await import('~/assets/meta/blog_list.json')) as pages.PageList; - - -let route = useRoute() -console.log(route) - -const url: Ref<string> = ref("") -url.value = route.query.post as string - -console.log(url.value) - -const loading: Ref<boolean> = ref(false) - -const list: Ref<any> = ref([]) -const tagList: Ref<any> = ref([]) -const tagFilter: Ref<string[]> = ref([]) -tagFilter.value = [] - -const markdown: Ref<any> = ref(null) - -const title: Ref<string> = ref("") -const description: Ref<string> = ref("") -const date: Ref<string> = ref("") -const tags: Ref<string[]> = ref([]) -const background: Ref<string> = ref("") -const next: Ref<string> = ref("") -const previous: Ref<string> = ref("") - -watch(markdown , (newVal) => { - if (newVal) { - title.value = newVal.title ? newVal.title : "" - description.value = newVal.description ? newVal.description : "" - date.value = newVal.date ? new Date(newVal.date).toLocaleDateString() : "" - tags.value = newVal.tags ? newVal.tags : [] - background.value = newVal.background ? newVal.background : "" - } -}) - -// watch the params of the route to fetch the data again -watch(route, async () => { - url.value = route.query.post as string - if (url.value) { - console.log("Fetching article") - loading.value = true - try { - await fetchArticle(url.value) - } - finally { - loading.value = false - } - } -}, { - immediate: true -}); - -async function fetchList() { - // Extract the posts - list.value = blog_list.posts - - // Extract the tags - tagList.value = blog_list.posts.flatMap(post => post.metadata.tags).filter((tag, index, self) => self.indexOf(tag) === index) - - // Sort the posts by date, most recent first - list.value.sort((a: any, b: any) => b.metadata.date.localeCompare(a.metadata.date)) -} - -onMounted(async () => { - await fetchList() - await fetchArticle(url.value) -}) - -// Fetch the article contents from the URL -async function fetchArticle(url: string) { - const post = blog_list.posts.find(post => post.url === url) - if (post) { - // Trim the .md extension - var url = url.replace(/\.md$/, "") - console.log("Fetching article: " + url) - const { data } = await useAsyncData(url, () => queryContent(url).findOne()) - console.log(data) - - markdown.value = data.value; - } -} - -function resetReadingPosition() { - window.scrollTo(0, 0) -} - -fetchArticle(url.value) - - -console.log("Prefetching blog") -await fetchList(); -const temp_url = route.query.post as string -await fetchArticle(temp_url); - - -</script> - -<template> - <div class="relative z-50 flex w-full justify-center text-white"> - <!-- Metadata --> - <MetaSet :title="title" :description="description" :date="date" - :background="background" :tags="tags" /> - - <!-- Main Content --> - <div class="mt-8 flex-col text-center"> - <Transition name="list"> - <!-- Article List --> - <div v-if="url == null" :key="url"> - <h1>Blog</h1> - <div class="flex justify-center"> - - <div class="flex flex-wrap justify-center m-5 max-w-96"> - <div v-for="tag in tagList" :key="tag" class="m-1"> - <button - @click="tagFilter.includes(tag) ? tagFilter.splice(tagFilter.indexOf(tag), 1) : tagFilter.push(tag)" - class="text-xs bg-black border-purple-400 border text-white p-1 rounded-md" - :class="tagFilter.includes(tag) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">{{ - tag }}</button> - </div> - </div> - </div> - <div> - <TransitionGroup name="list"> - <div - v-for="post in tagFilter.length == 0 ? list : list.filter((post: PageInfo) => post.metadata.tags ? post.metadata.tags.some(tag => tagFilter.includes(tag)) : false)"> - <PostCard :url="post.url" key="{{post.id}}" :tagFilter="tagFilter" /> - </div> - </TransitionGroup> - </div> - </div> - <!-- Article Viewer --> - <div v-else> - <Transition name="list"> - <div class="flex flex-col" :key="url"> - <h1>{{ title }}</h1> - <small>{{ date }}</small> - <div class="max-w-50 flex flex-row justify-center"> - <div v-for="tag in tags" :key="tag" class="m-1 text-center"> - <span - class="text-xs bg-black border-purple-400 border-2 text-white p-1 rounded-md">{{ - tag }}</span> - </div> - </div> - <div> - <!-- Next/Prev controls, on the left and right side using PostCards --> - <div class="flex max-w-4xl max-md:w-screen"> - <div class="justify-start"> - <NuxtLink v-if="previous" :onclick="resetReadingPosition" :to="previous" - class="m-2 text-white">Previous</NuxtLink> - </div> - <div class="justify-end"> - <NuxtLink v-if="next" :onclick="resetReadingPosition" :to="next" - class="m-2 text-white">Next</NuxtLink> - </div> - </div> - </div> - <!-- Article Content --> - <Card class="text-pretty max-w-4xl mt-4 max-md:w-screen text-left"> - <article> - <div v-if="markdown != null"> - <Markdown :input="markdown" /> - </div> - <!-- Aligned next/prev controls --> - <div class="flex"> - <div class="justify-start"> - <NuxtLink v-if="previous" :onclick="resetReadingPosition" :to="previous" - class="m-2 text-white">Previous</NuxtLink> - </div> - <div class="justify-end"> - <NuxtLink v-if="next" :onclick="resetReadingPosition" :to="next" - class="m-2 text-white">Next</NuxtLink> - </div> - </div> - </article> - </Card> - </div> - </Transition> - </div> - </Transition> - </div> - </div> -</template> - -<style scoped> -.list-move, -.list-enter-active, -.list-leave-active { - transition: all 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55); -} - -.list-enter-from, -.list-leave-to { - opacity: 0; - transform: translate(100px, 0); -} - -.list-leave-active { - position: absolute; -} -</style> \ No newline at end of file diff --git a/pages/index.vue b/pages/index.vue index 3bea3d4..4c99283 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -13,7 +13,7 @@ const { data } = await useAsyncData('about_me', () => queryContent('/about_me'). <template> <div class="relative flex w-full justify-center text-white"> <!-- Metadata --> - <MetaSet title="TheFelidae" description="TheFelidae's personal site :3" background="https://avatars.githubusercontent.com/u/94077364?v=4" + <MetaSet title="Home" description="TheFelidae's personal site" background="https://avatars.githubusercontent.com/u/94077364?v=4" tags="home, personal, author" /> <div class="mt-8 flex-col text-center"> <div class="flex justify-center"> diff --git a/utils/page_updater/pages.ts b/utils/page_updater/pages.ts index f7b83a9..5f1114c 100644 --- a/utils/page_updater/pages.ts +++ b/utils/page_updater/pages.ts @@ -3,6 +3,14 @@ import * as path from 'node:path'; import * as crypto from 'node:crypto'; import fm, { type FrontMatterResult } from 'front-matter'; +export interface PageLocation { + title: string; + description: string; + tags: string[]; + map: string; + root: string; +} + export interface PageInfoMetdata { title?: string; description?: string; @@ -24,6 +32,13 @@ export interface PageInfo { path: string; } +export interface PageInfoCategory { + posts: PageInfo[]; + title: string; + description: string; + tags: string[]; +} + // Function to get metadata from a file function getMetadata(filePath: string): PageInfoMetdata { const fileContent = fs.readFileSync(filePath).toString(); @@ -51,17 +66,19 @@ function getWordCount(filePath: string): number { } // Function to get pages info -export function getPagesInfo(searchDirectory: string, rootDirectory: string): Record<string, PageInfo> { +export function getPagesInfo(searchDirectory: string, pageLocation: PageLocation): Record<string, PageInfo> { const pageInfo: Record<string, PageInfo> = {}; - const currentDirectory = path.join(rootDirectory, searchDirectory); + const currentDirectory = path.join(pageLocation.root, searchDirectory); const files = fs.readdirSync(currentDirectory); + console.log(files); files.forEach((file) => { const fullPath = path.join(currentDirectory, file); - const localPath = fullPath.replace(rootDirectory, ''); - + const localPath = fullPath.replace(pageLocation.root, pageLocation.map); + console.log(fullPath); + console.log(localPath); if (fs.lstatSync(fullPath).isDirectory()) { - Object.assign(pageInfo, getPagesInfo(path.join(searchDirectory, file), rootDirectory)); + Object.assign(pageInfo, getPagesInfo(path.join(searchDirectory, file), pageLocation)); } else if (file.endsWith('.md')) { const metadata = getMetadata(fullPath); const sha256Hash = getSha256Hash(fullPath); diff --git a/utils/page_updater/update_pagelist.ts b/utils/page_updater/update_pagelist.ts index b15eb95..8a7f753 100644 --- a/utils/page_updater/update_pagelist.ts +++ b/utils/page_updater/update_pagelist.ts @@ -10,13 +10,19 @@ export interface Page { hash: string; } +export interface PageCategory { + posts: Page[]; + title: string; + description: string; + tags: string[]; +} export interface PageList { last_generated: string; - posts: Page[]; + categories: Record<string, PageCategory>; } // Function to generate the page list -function generatePageList(pagesInfo: Record<string, any>): PageList { +function generatePageCategory(pagesInfo: Record<string, any>): PageCategory { const pageList: Page[] = []; for (const [path, page] of Object.entries(pagesInfo)) { @@ -35,16 +41,49 @@ function generatePageList(pagesInfo: Record<string, any>): PageList { } }); - const pageListDict: PageList = { - last_generated: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), - posts: pageList + const pageListDict: PageCategory = { + posts: pageList, + title: "", + description: "", + tags: [] }; return pageListDict; } -// Get the page list and print it -const postList = generatePageList(pages.getPagesInfo("", "content/blog")); +const postDirectories: pages.PageLocation[] = [ + { + title: "Blog", + description: "A collection of blog posts", + tags: ["blog"], + map: "blog", + root: "content/blog" + } +] + +var postList: PageList = { + last_generated: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), + categories: {} +} + +for (const postDirectory of postDirectories) { + const pagesInfo = pages.getPagesInfo("", postDirectory); + postList.categories[postDirectory.title] = generatePageCategory(pagesInfo); +} + +// Sort the posts by date +for (const category of Object.values(postList.categories)) { + category.posts.sort((a, b) => { + if (!a.metadata.date) { + return 1; + } + if (!b.metadata.date) { + return -1; + } + return new Date(b.metadata.date).getTime() - new Date(a.metadata.date).getTime(); + }); +} + console.log(JSON.stringify(postList, null, 2)); // Output to assets/blog_list.json (overwriting)