This commit is contained in:
Mrrp 2025-01-04 11:45:02 -08:00
parent c7f546f58a
commit b62a50b12e
12 changed files with 444 additions and 638 deletions

View file

@ -42,8 +42,8 @@ jobs:
run: pip install python-frontmatter && deno install run: pip install python-frontmatter && deno install
- name: Update Page list - name: Update Page list
run: deno --allow-all utils/page_updater/update_pagelist.ts run: deno --allow-all utils/page_updater/update_pagelist.ts
- name: Update Page history # - name: Update Page history
run: python utils/page_updater/commit_post_history.py # run: python utils/page_updater/commit_post_history.py
# - name: Generate RSS feed # - name: Generate RSS feed
# run: python utils/page_updater/rss_xml_gen.py # run: python utils/page_updater/rss_xml_gen.py
- name: Commit changes - name: Commit changes

View file

@ -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<any> = 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>([^<]+)<\/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
}

View file

@ -1,11 +1,27 @@
{ {
"last_generated": "2025-01-03 21:53:59", "last_generated": "2025-01-04 10:34:37",
"categories": {
"Blog": {
"posts": [ "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": { "metadata": {
"title": "3DS Programming - Using RomFS", "title": "3DS Programming - Using RomFS",
"description": "A guide to using RomFS on the 3DS. (Old)", "description": "A guide to using RomFS on the 3DS. (Old)",
"date": "2025-01-01 00:00:00", "date": "2024-12-31 16:00:00",
"tags": [ "tags": [
"3ds", "3ds",
"programming", "programming",
@ -16,75 +32,15 @@
"previous": "old3ds_helloworld.md", "previous": "old3ds_helloworld.md",
"next": "old3ds_touchscreen.md" "next": "old3ds_touchscreen.md"
}, },
"id": "/old3ds_romfs.md", "id": "blog/old3ds_romfs.md",
"url": "/blog/old3ds_romfs.md", "url": "/blog/old3ds_romfs.md",
"hash": "0b28a366868e9fa564b6a33d9b1aa1d8269f7971497f25488f05f54929e88410" "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": { "metadata": {
"title": "3DS Programming - Touchscreen Input", "title": "3DS Programming - Touchscreen Input",
"description": "A guide to using the touchscreen on the 3DS. (Old)", "description": "A guide to using the touchscreen on the 3DS. (Old)",
"date": "2025-01-01 00:00:00", "date": "2024-12-31 16:00:00",
"tags": [ "tags": [
"3ds", "3ds",
"programming", "programming",
@ -94,24 +50,75 @@
], ],
"previous": "old3ds_romfs.md" "previous": "old3ds_romfs.md"
}, },
"id": "/old3ds_touchscreen.md", "id": "blog/old3ds_touchscreen.md",
"url": "/blog/old3ds_touchscreen.md", "url": "/blog/old3ds_touchscreen.md",
"hash": "59e0b9d701646fd5f747713832c47ce451e0ebe0975d4a148a820ca795741c2b" "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": { "metadata": {
"title": "Badges!", "title": "Badges!",
"description": "A collection of 88x31 badges for various things", "description": "A collection of 88x31 badges for various things",
"date": "2024-12-21 00:00:00", "date": "2024-12-20 16:00:00",
"tags": [ "tags": [
"badges", "badges",
"retro", "retro",
"web" "web"
] ]
}, },
"id": "/badges.md", "id": "blog/badges.md",
"url": "/blog/badges.md", "url": "/blog/badges.md",
"hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00" "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": []
}
}
} }

View file

@ -9,9 +9,9 @@ import Card from './Card.vue';
<Card> <Card>
<div class="flex justify-center"> <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="/" 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="/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="/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="/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="/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/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>
<div class="flex justify-center text-white"> <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> <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>

View file

@ -70,7 +70,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<NuxtLink :href="'/blog?post=' +url"> <NuxtLink :href="'/article' + url">
<!-- Large --> <!-- Large -->
<div v-if = "size === 'full'"> <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"> <div class="m-4 min-h-30 min-width-90 text-white transition hover:bg-purple-600 bg-opacity-50 hover:bg-opacity-70">

View file

@ -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_list: pages.PageList = (await import('./assets/meta/blog_list.json')) as pages.PageList;
const blog_routes: any = blog_list.posts.map((post) => { // nitro only needs string array
return { const blog_nitro_routes: any = [];
['/blog?post=' + post.id]: { // key value
prerender: true for (let [key, category] of Object.entries(blog_list.categories)) {
for (let post of category.posts) {
blog_nitro_routes.push('/article' + post.url);
} }
} }
}); console.log(blog_nitro_routes);
blog_routes.push({
'/blog': {
prerender: true
}
});
console.log(blog_routes);
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2024-11-01', compatibilityDate: '2024-11-01',
ssr: true, ssr: true,
routeRules: blog_routes,
postcss: { postcss: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
@ -43,6 +36,11 @@ export default defineNuxtConfig({
// ... options // ... options
}, },
nitro: {
prerender: {
routes: blog_nitro_routes
}
},
particles: { particles: {
mode: 'slim', mode: 'slim',
lazy: true lazy: true

View file

@ -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>

91
pages/article/index.vue Normal file
View file

@ -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>

View file

@ -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>

View file

@ -13,7 +13,7 @@ const { data } = await useAsyncData('about_me', () => queryContent('/about_me').
<template> <template>
<div class="relative flex w-full justify-center text-white"> <div class="relative flex w-full justify-center text-white">
<!-- Metadata --> <!-- 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" /> tags="home, personal, author" />
<div class="mt-8 flex-col text-center"> <div class="mt-8 flex-col text-center">
<div class="flex justify-center"> <div class="flex justify-center">

View file

@ -3,6 +3,14 @@ import * as path from 'node:path';
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
import fm, { type FrontMatterResult } from 'front-matter'; import fm, { type FrontMatterResult } from 'front-matter';
export interface PageLocation {
title: string;
description: string;
tags: string[];
map: string;
root: string;
}
export interface PageInfoMetdata { export interface PageInfoMetdata {
title?: string; title?: string;
description?: string; description?: string;
@ -24,6 +32,13 @@ export interface PageInfo {
path: string; path: string;
} }
export interface PageInfoCategory {
posts: PageInfo[];
title: string;
description: string;
tags: string[];
}
// Function to get metadata from a file // Function to get metadata from a file
function getMetadata(filePath: string): PageInfoMetdata { function getMetadata(filePath: string): PageInfoMetdata {
const fileContent = fs.readFileSync(filePath).toString(); const fileContent = fs.readFileSync(filePath).toString();
@ -51,17 +66,19 @@ function getWordCount(filePath: string): number {
} }
// Function to get pages info // 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 pageInfo: Record<string, PageInfo> = {};
const currentDirectory = path.join(rootDirectory, searchDirectory); const currentDirectory = path.join(pageLocation.root, searchDirectory);
const files = fs.readdirSync(currentDirectory); const files = fs.readdirSync(currentDirectory);
console.log(files);
files.forEach((file) => { files.forEach((file) => {
const fullPath = path.join(currentDirectory, 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()) { 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')) { } else if (file.endsWith('.md')) {
const metadata = getMetadata(fullPath); const metadata = getMetadata(fullPath);
const sha256Hash = getSha256Hash(fullPath); const sha256Hash = getSha256Hash(fullPath);

View file

@ -10,13 +10,19 @@ export interface Page {
hash: string; hash: string;
} }
export interface PageCategory {
posts: Page[];
title: string;
description: string;
tags: string[];
}
export interface PageList { export interface PageList {
last_generated: string; last_generated: string;
posts: Page[]; categories: Record<string, PageCategory>;
} }
// Function to generate the page list // Function to generate the page list
function generatePageList(pagesInfo: Record<string, any>): PageList { function generatePageCategory(pagesInfo: Record<string, any>): PageCategory {
const pageList: Page[] = []; const pageList: Page[] = [];
for (const [path, page] of Object.entries(pagesInfo)) { for (const [path, page] of Object.entries(pagesInfo)) {
@ -35,16 +41,49 @@ function generatePageList(pagesInfo: Record<string, any>): PageList {
} }
}); });
const pageListDict: PageList = { const pageListDict: PageCategory = {
last_generated: format(new Date(), 'yyyy-MM-dd HH:mm:ss'), posts: pageList,
posts: pageList title: "",
description: "",
tags: []
}; };
return pageListDict; return pageListDict;
} }
// Get the page list and print it const postDirectories: pages.PageLocation[] = [
const postList = generatePageList(pages.getPagesInfo("", "content/blog")); {
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)); console.log(JSON.stringify(postList, null, 2));
// Output to assets/blog_list.json (overwriting) // Output to assets/blog_list.json (overwriting)