Compare commits

...

2 commits

Author SHA1 Message Date
edbce48613 beginning i18n support
Some checks failed
Publish to OCI / publish (push) Has been cancelled
2025-02-16 19:31:39 -08:00
15f2688ca3 :3 2025-02-15 22:39:57 -08:00
33 changed files with 4961 additions and 630 deletions

View file

@ -4,6 +4,7 @@ export default {
siteDescription: "Luna's rambling place!",
siteUrl: 'https://mrrpnya.github.io',
siteImage: '',
siteDefaultLocale: 'en_us',
// Site personalization
siteColor: '#550077',

View file

@ -1,144 +1,212 @@
{
"last_generated": "2025-01-04 12:52:32",
"categories": {
"Site": {
"posts": [
{
"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": "site/styling_test",
"url": "/site/styling_test",
"hash": "0ff9f34321a27f462ca26656a1dc5024c0e800ea1e176ff36316b158ab4606c9"
"last_generated": "2025-02-16 14:47:00",
"languages": {
"en": {
"categories": {
"Site": {
"posts": [
{
"metadata": {
"title": "Styling Test",
"description": "A test post to see how the site styling looks",
"date": "2025-01-01T00:00:00.000Z",
"tags": [
"meta",
"web"
]
},
"id": "site/styling_test",
"url": "en/site/styling_test",
"hash": "e581ca6fef00cdc54a660744b295ff83ce05c2d75561a43695917dde2aa2d06f"
},
{
"metadata": {
"title": "About Me",
"prop": true
},
"id": "site/about_me",
"url": "en/site/about_me",
"hash": "85cd293e18e1f11b8f49c3858c78b98d7cb3dfcc31b347d76db1be7d8c400b81"
}
],
"title": "Site",
"description": "Articles to test site functionality",
"tags": [
"site"
],
"show": false
},
"Collections": {
"posts": [
{
"metadata": {
"title": "Neurodiverse Resources",
"description": "A list of neurodiversity resources",
"date": "2025-02-04T00:00:00.000Z",
"tags": [
"neurodiversity",
"resources"
],
"thumb": null
},
"id": "collections/neurodiverse_resources",
"url": "en/collections/neurodiverse_resources",
"hash": "18e37836ef191c0adb0a139f151e5827d79e170d0ab039ff63d67093cf7f9e36"
},
{
"metadata": {
"title": "Godot Resources",
"description": "A bunch of stuff for Godot",
"date": "2025-01-22T00:00:00.000Z",
"tags": [
"godot",
"curated"
]
},
"id": "collections/godot",
"url": "en/collections/godot",
"hash": "0fa66e5f5346902661fb67a979340701155c25d73dafa05b3bf7446aac5a49b9"
},
{
"metadata": {
"title": "LGBTQ+ Resources",
"description": "A list of resources for LGBTQ+ (and adjacent) individuals",
"date": "2025-01-02T00:00:00.000Z",
"tags": [
"lgbtq",
"resources"
]
},
"id": "collections/lgbtq_resources",
"url": "en/collections/lgbtq_resources",
"hash": "2f1f9c04ef62313bccecf7e5f22b95e862a36fce260b34ca64286e684a453196"
},
{
"metadata": {
"title": "Badges!",
"description": "Some 88x31 badges for various things",
"date": "2024-12-21T00:00:00.000Z",
"tags": [
"badges",
"retro",
"web"
]
},
"id": "collections/badges",
"url": "en/collections/badges",
"hash": "7fc0dbfff6dfba66b5a6e93ba4394a2034ab3935ae6acaf2b5ac4a815116d24e"
},
{
"metadata": {
"title": "Awesome",
"description": "A curated list of awesome stuff I like",
"date": "2024-11-26T00:00:00.000Z",
"tags": [
"awesome",
"curated"
]
},
"id": "collections/awesome",
"url": "en/collections/awesome",
"hash": "43704f5de68e422ca3187cda0e34084d6ad3b930b4238bdd1b80535c3013c191"
}
],
"title": "Collections",
"description": "Articles that are collections of information: Lists, Awesome lists, etc.",
"tags": [
"collection"
],
"show": true
},
"Guides": {
"posts": [
{
"metadata": {
"title": "3DS Programming - Using RomFS",
"description": "A guide to using RomFS on the 3DS. (Old)",
"date": "2025-01-01T00:00:00.000Z",
"tags": [
"3ds",
"programming",
"c",
"devkitpro",
"old"
],
"previous": "old3ds_helloworld.md",
"next": "old3ds_touchscreen.md"
},
"id": "guides/old3ds_romfs",
"url": "en/guides/old3ds_romfs",
"hash": "f518b6cdf7a5eb0d72f86c305089df5ee42a4c4aae1589c7abace33368dd4ede"
},
{
"metadata": {
"title": "3DS Programming - Touchscreen Input",
"description": "A guide to using the touchscreen on the 3DS. (Old)",
"date": "2025-01-01T00:00:00.000Z",
"tags": [
"3ds",
"programming",
"c",
"devkitpro",
"old"
],
"previous": "old3ds_romfs.md"
},
"id": "guides/old3ds_touchscreen",
"url": "en/guides/old3ds_touchscreen",
"hash": "c026e506fb60c8ed9943f5806e8adf611a382a7de34e30fc2a72f4578d66899e"
},
{
"metadata": {
"title": "3DS Programming - Hello World",
"description": "A guide to creating a simple Hello, World program for the 3DS. (Old)",
"date": "2025-01-01T00:00:00.000Z",
"tags": [
"3ds",
"programming",
"c",
"devkitpro",
"old"
],
"next": "old3ds_romfs.md"
},
"id": "guides/old3ds_helloworld",
"url": "en/guides/old3ds_helloworld",
"hash": "77a21a1201a35d6a85cb2305166cfb20a0a45546fea1f73fd620b2b84ec70fda"
}
],
"title": "Guides",
"description": "Guides and tutorials",
"tags": [
"guide"
],
"show": true
}
],
"title": "Site",
"description": "Articles to test site functionality",
"tags": [
"site"
]
}
},
"Collections": {
"posts": [
{
"metadata": {
"title": "LGBTQ+ Resources",
"description": "A list of resources for LGBTQ+ individuals",
"date": "2025-01-01 16:00:00",
"tags": [
"lgbtq+",
"resources"
]
},
"id": "collections/lgbtq_resources",
"url": "/collections/lgbtq_resources",
"hash": "3da76064aa95cc06937bde01128ed44aafb850f35a43bd214ce0cd89a875c674"
},
{
"metadata": {
"title": "Badges!",
"description": "A collection of 88x31 badges for various things",
"date": "2024-12-20 16:00:00",
"tags": [
"badges",
"retro",
"web"
]
},
"id": "collections/badges",
"url": "/collections/badges",
"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": "collections/awesome",
"url": "/collections/awesome",
"hash": "0632400858006b93f2f36d87953538c2a400bacc75aaa29928aee226e8b343b1"
"tp": {
"categories": {
"Site": {
"posts": [
{
"metadata": {
"title": "About Me",
"prop": true
},
"id": "site/about_me",
"url": "tp/site/about_me",
"hash": "c6bde941d29567f1a3b98e52d68d50dad233f55a0c4d23d60b17efe292bd4e39"
}
],
"title": "Site",
"description": "Articles to test site functionality",
"tags": [
"site"
],
"show": false
}
],
"title": "Collections",
"description": "Articles that are collections of information: Lists, Awesome lists, etc.",
"tags": [
"collection"
]
},
"Guides": {
"posts": [
{
"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": "guides/old3ds_romfs",
"url": "/guides/old3ds_romfs",
"hash": "34062b79909f5b18a647b484687cf862e779c08da9fc6052c4ebab3eef67151c"
},
{
"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": "guides/old3ds_touchscreen",
"url": "/guides/old3ds_touchscreen",
"hash": "c026e506fb60c8ed9943f5806e8adf611a382a7de34e30fc2a72f4578d66899e"
},
{
"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": "guides/old3ds_helloworld",
"url": "/guides/old3ds_helloworld",
"hash": "77a21a1201a35d6a85cb2305166cfb20a0a45546fea1f73fd620b2b84ec70fda"
}
],
"title": "Guides",
"description": "Guides and tutorials",
"tags": [
"guide"
]
}
}
}
}

View file

@ -9,6 +9,10 @@ html {
@apply min-h-screen bg-slate-950;
}
h1, h2, h3, h4, h5, h6, p {
color: white;
}
h1 {
font-family: 'Lobster', cursive, 'Courier New', Courier, monospace;
@apply text-3xl font-bold;

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import siteConfig from '../assets/config';
import siteConfig from '~/assets/config';
const props = defineProps({
title: {

View file

@ -1,19 +1,39 @@
<script setup>
import Card from './Card.vue';
import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const switchLocalePath = useSwitchLocalePath()
const { locale, setLocale } = useI18n();
const localePath = useLocalePath();
const userLocales = ref([
{ code: 'en', name: 'English' },
{ code: 'tp', name: 'toki pona' }
]);
watch(locale, (newLocale) => {
switchLocalePath(newLocale);
}, {immediate: true});
</script>
<template>
<div class="flex justify-center">
<div class="flex-col transition-[margin] justify-center md:rounded-md max-md:w-screen lg:mt-3 w-fit-content">
<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="/article/" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Articles</NuxtLink>
<NuxtLink href="/article/collections/awesome" 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/collections/badges" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">Badges</NuxtLink>
<div class="flex justify-center items-center gap-4">
<NuxtLink :to="localePath('/')" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">{{ $t('home') }}</NuxtLink>
<NuxtLink :to="localePath('/article/')" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">{{$t('articles')}}</NuxtLink>
<NuxtLink :to="localePath('/article/' + locale + '/collections/awesome')" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">{{$t('awesome')}}</NuxtLink>
<NuxtLink :to="localePath('/article/' + locale + '/collections/badges')" class="transition text-xl pl-2 pr-2 ease-in-out text-purple-100 hover:text-purple-400 duration-200">{{$t('badges')}}</NuxtLink>
<select v-model="locale" @change="setLocale(locale)" class="bg-transparent text-purple-100 border border-purple-400 rounded-md p-1">
<option v-for="loc in userLocales" :key="loc.code" :value="loc.code">
{{ loc.name }}
</option>
</select>
</div>
</Card>
</div>
</div>
</template>
</template>

View file

@ -67,10 +67,12 @@ async function fetchData(url: string) {
onMounted(() => {
fetchData(url.value)
})
const localePath = useLocalePath();
</script>
<template>
<NuxtLink :href="'/article' + url">
<NuxtLink :to="localePath('/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">
@ -97,6 +99,9 @@ onMounted(() => {
</div>
</div>
</div>
<div class="justify-center">
<slot></slot>
</div>
</div>
<p>{{ description }}</p>
</div>
@ -139,6 +144,9 @@ onMounted(() => {
</div>
</div>
</div>
<div class="justify-center">
<slot></slot>
</div>
</div>
</div>
</Card>
@ -159,6 +167,7 @@ onMounted(() => {
<div class="justify-center">
<h3>{{ title }}</h3>
<small>{{ date }}</small>
<slot></slot>
</div>
</div>
</div>
@ -179,6 +188,7 @@ onMounted(() => {
<div class="grid">
<div class="justify-center">
<h5>{{ title }}</h5>
<slot></slot>
</div>
</div>
</div>

10
content.config.ts Normal file
View file

@ -0,0 +1,10 @@
import { defineCollection, defineContentConfig } from "@nuxt/content";
export default defineContentConfig({
collections: {
content: defineCollection({
type: "page",
source: "**/**/**/*.md",
}),
},
});

View file

@ -15,4 +15,8 @@ tags: ['godot', 'curated']
::alert{type="note"}
If you find that this list is lacking or inaccurate, please open a GitHub issue or pull request.
::
::alert{type="info"}
This article is a work in progress - You can submit an issue or pull request to contribute.
::

View file

@ -1,6 +1,6 @@
---
title: LGBTQ+ Resources
description: A list of resources for LGBTQ+ individuals
description: A list of resources for LGBTQ+ (and adjacent) individuals
date: 2025-01-02
tags: ['lgbtq', 'resources']
---
@ -17,6 +17,10 @@ tags: ['lgbtq', 'resources']
I will try not to provide medical advice, for I am not a doctor. Please consult a medical professional for any medical advice.
::
::alert{type="info"}
This article is a work in progress - You can submit an issue or pull request to contribute.
::
If you find that this list is lacking or inaccurate, please open a GitHub issue or pull request.
## Table of Contents
@ -24,9 +28,10 @@ If you find that this list is lacking or inaccurate, please open a GitHub issue
- [LGBTQ+ Resources](#lgbtq-resources)
- [Table of Contents](#table-of-contents)
- [Some FAQs](#some-faqs)
- [Do I have to have dysphoria to be trans?](#do-i-have-to-have-dysphoria-to-be-trans)
- [Do I have to medically transition to be trans?](#do-i-have-to-medically-transition-to-be-trans)
- [Can I change course if I realize I'm something else? Like if I'm non-binary and realize I'm something else for instance?](#can-i-change-course-if-i-realize-im-something-else-like-if-im-non-binary-and-realize-im-something-else-for-instance)
- [What is plural?](#what-is-plural)
- [Can I change course if I realize I'm something else?](#can-i-change-course-if-i-realize-im-something-else)
- [What is plurality/multiplicity?](#what-is-pluralitymultiplicity)
- [What is a therian?](#what-is-a-therian)
- [LGBTQ+ Knowledge bases](#lgbtq-knowledge-bases)
- [LGBTQ+ Flashcards](#lgbtq-flashcards)
@ -41,22 +46,28 @@ If you find that this list is lacking or inaccurate, please open a GitHub issue
## Some FAQs
### Do I have to have dysphoria to be trans?
Nope. You don't need *any* dysphoria to be trans. If you want to be a gender? That's all you need.
### Do I have to medically transition to be trans?
No. You do not have to medically transition - The belief that you must transition is actually known as "gatekeeping" and is harmful - This specific gatekeeping is known as transmedicalism.
### Can I change course if I realize I'm something else? Like if I'm non-binary and realize I'm something else for instance?
### Can I change course if I realize I'm something else?
Yes. You can change course if you realize you're something else. It's okay to change your mind.
Yes. You can change course if you realize you're something else (or perhaps cis after all). It's okay to change your mind.
### What is plural?
### What is plurality/multiplicity?
Plural is a term used to describe systems of multiple people in one body.
Plural is a term used to describe systems of multiple people in one body, put simply.
While they *might* not be LGBTQ+ by default, they are often included in LGBTQ+ spaces due to the discrimination they face. It is important to respect their identities and pronouns.
I also include information on plurality in this list because it is important to understand and respect plural systems.
If you've watched Yu Yu Hakusho, think of Sensui and his multiple personalities.
Here's a site that contains some quick information: [MoreThanOne.info](http://morethanone.info/)
### What is a therian?
A therian is someone who identifies as a non-human animal on a spiritual or psychological level. They are not LGBTQ+ by default, but they are often included in LGBTQ+ spaces due to the discrimination they face. It's important to respect them as well.
@ -77,6 +88,15 @@ By flashcards, I mean something short that can be rapidly shared to provide info
> A site that provides information on plurality, myths, etiquette, terms, and more.
- [Pronouns.page](https://pronouns.page)
> A site that lets you create cards about you that you can share! You can also add friends and relatives on it.
- [Pronouns.cc](https://pronouns.cc)
> Similar to pronouns.page, however it is tailored to be able to better handle plural systems and so on.
> It is open source under the GNU AGPL.
## LGBTQ+ Content Creators and Artists
- [Chipflake](https://www.youtube.com/@chipflake)

View file

@ -0,0 +1,15 @@
---
title: Neurodiverse Resources
description: A list of neurodiversity resources
date: 2025-02-04
tags: ['neurodiversity', 'resources']
---
::alert{type="note"}
I will try not to provide medical advice, for I am not a doctor. Please consult a medical professional for any medical advice.
::
::alert{type="info"}
This article is a work in progress - You can submit an issue or pull request to contribute.
::

View file

@ -5,9 +5,9 @@ prop: true
# 🌙 Luna - She/They/Fae 🌙
<br>
<small>Profile picture by Chereverie on [Picrew](https://picrew.me/en/image_maker/100365)</small>
[`mrrpnya@proton.me`](mailto:thelunacy@proton.me)
<br>
<br>

View file

@ -0,0 +1,43 @@
---
title: About Me
prop: true
---
# 🌙 mi jan Luna 🌙
## She/They/Fae
<!--<small>Profile picture by Chereverie on [Picrew](https://picrew.me/en/image_maker/100365)</small>
<br>
<br>
##### toki!
###### 🏳️‍⚧️ I am exploring feminine presentation and identity, so this may change or I might go by different things elsewhere. 🏳️‍⚧️
###### Please respect what I set my info as in the context of it at least.
<br>
I'm a student, apprentice, hobbyist, and generally a nerd who does some coding every once and a while.
I've mainly been messing with C, C++, C# and have been learning Rust. I'm presently learning more about web development, particularly regarding frameworks and libraries.
I'm a bit shy in terms of socializing, but I'm often open to chat about most things, so long as it's respectful.
Please reach out to me first, I probably won't do so myself.
Follow if you enjoy. I'm not *too* active, but I'll try to post some things every once and a while. -->
<small>sitelen selo tan jan Chereverie. jan Chereverie li lon [Picrew](https://picrew.me/en/image_maker/100365).
</small>
<!-- Outer art (PFP) by jan Chereverie. jan Chereverie is on Picrew. -->
<br>
<br>
##### toki!
###### 🏳️‍⚧️ mi alasa lukin e sona pi mi meli. tenpo ali la ni ken ante, en mi ken jo e nimi ante lon ma ante 🏳️‍⚧️
<br>
mi pi kama sona. mi sitelen kepeken toki "C", "C++", en "C#" lon ilo. mi li kama sona e sitelen kepeken toki "Rust" lon ilo.

1714
deno.lock generated

File diff suppressed because it is too large Load diff

19
i18n.config.ts Normal file
View file

@ -0,0 +1,19 @@
export default defineI18nConfig(() => ({
legacy: false,
locale: "en",
fallbackLocale: "en",
messages: {
en: {
home: "Home",
articles: "Articles",
awesome: "Awesome",
badges: "Badges"
},
tp: {
home: "ma tomo",
articles: "sona sitelen",
awesome: "pona sitelen",
badges: "musi sitelen lili"
}
},
}));

2384
log.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,35 @@
import * as pages from '~/utils/page_updater/update_pagelist';
import * as pages from "./utils/page_updater/update_pagelist";
const blog_list: pages.PageList = (await import('./assets/meta/post_list.json')) as pages.PageList;
// Import the generated page list.
const blog_list_json = (await import("./assets/meta/post_list.json")).default;
// 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);
const blog_list: pages.PageList = pages.PageList.fromJSON(JSON.stringify(blog_list_json));
// Nitro expects a string array of routes.
const blog_nitro_routes: string[] = [];
// Iterate over each available language.
for (const [lang, langData] of Object.entries(blog_list.languages)) {
// For each category in this language.
for (const [categoryName, category] of Object.entries(langData.categories)) {
// For each post, use its canonical id to build the route.
for (const post of category.posts) {
// Get the canonical id (removes the language folder, e.g. "en/page.md" becomes "page.md")
const canonicalId = pages.PageList.getCanonicalId(post.id);
// Remove the file extension (e.g. "page.md" becomes "page")
const postSlug = canonicalId.replace(/\.md$/, '');
// Build the localized route (e.g. "/en/article/page")
blog_nitro_routes.push(`/${lang}/article/${postSlug}`);
}
}
}
console.log(blog_nitro_routes);
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2024-11-01',
ssr: true,
compatibilityDate: "2024-11-01",
ssr: false,
postcss: {
plugins: {
tailwindcss: {},
@ -23,32 +37,49 @@ export default defineNuxtConfig({
},
},
routeRules: {
'/article/:category:/:id': {
redirect: '/article/:category:/:id/index.html'
}
"/article/:category:/:id": {
redirect: "/article/:category:/:id/index.html",
},
},
app: {
pageTransition: {
name: 'page',
mode: 'out-in'
name: "page",
mode: "out-in",
},
},
modules: [
'nuxt-particles',
'@nuxt/content'
"nuxt-particles",
"@nuxt/test-utils/module",
"@nuxt/content",
"@nuxtjs/i18n",
],
i18n: {
vueI18n: './i18n.config.ts',
locales: [
{
code: 'en',
name: 'English'
},
{
code: 'tp',
name: 'Toki Pona'
}
]
},
content: {
// ... options
api: {
baseURL: '/api/_content'
}
},
nitro: {
prerender: {
routes: blog_nitro_routes,
autoSubfolderIndex: true
}
autoSubfolderIndex: true,
},
},
particles: {
mode: 'slim',
lazy: true
}
})
mode: "slim",
lazy: true,
},
});

View file

@ -19,20 +19,26 @@
"@mdit/plugin-tasklist": "^0.14.0",
"@mdit/plugin-tex": "^0.14.0",
"@nuxt/content": "^2.13.4",
"@nuxt/test-utils": "^3.15.4",
"@nuxtjs/i18n": "9.2.0",
"@popperjs/core": "^2.11.8",
"@tsparticles/slim": "^3.7.1",
"@types/markdown-it": "^14.1.2",
"@vue/test-utils": "^2.4.6",
"animejs": "^3.2.2",
"css-loader": "^7.1.2",
"happy-dom": "^17.1.0",
"markdown-it-checkbox": "^1.1.0",
"mathpix-markdown-it": "^2.0.9",
"nuxt": "^3.14.1592",
"ofetch": "^1.4.1",
"playwright-core": "^1.50.1",
"postcss-loader": "^8.1.1",
"sass-embedded": "^1.83.1",
"tex-to-svg": "^0.2.0",
"texsvg": "^2.2.2",
"tsparticles": "^3.7.1",
"vitest": "^3.0.5",
"vue": "latest",
"vue-loader": "^17.4.2",
"vue-router": "latest",

View file

@ -1,103 +1,152 @@
<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';
import siteConfig from '~/assets/config';
let route = useRoute()
console.log(route)
let route = useRoute();
const { locale, setLocale } = useI18n();
console.log(route);
const url: Ref<string> = ref("")
url.value = '/' + route.params.category.concat('/' + route.params.id as string) as string
const url: Ref<string> = ref('');
url.value = '/' + route.params.category.concat('/' + route.params.id as string) as string;
console.log(url.value)
console.log(url.value);
const loading: Ref<boolean> = ref(false)
const loading: Ref<boolean> = ref(false);
const tagFilter: Ref<string[]> = ref([])
tagFilter.value = []
const tagFilter: Ref<string[]> = ref([]);
tagFilter.value = [];
const markdown: Ref<any> = ref(null)
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("")
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('');
function tagsToString(tags: String[]): string {
var tagString = '';
for (let i = 0; i < tags.length; i++) {
tagString += tags[i];
}
return tagString;
}
function updateMetadata(data: ParsedContent) {
title.value = data.title ? data.title : ""
description.value = data.description ? data.description : ""
date.value = data.date ? new Date(data.date).toLocaleDateString() : ""
tags.value = data.tags ? data.tags : []
background.value = data.background ? data.background : ""
title.value = data.title ? data.title : '';
description.value = data.description ? data.description : '';
date.value = data.date ? new Date(data.date).toLocaleDateString() : '';
tags.value = data.tags ? data.tags : [];
background.value = data.background ? data.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
url.value = '/' + route.params.category.concat('/' + route.params.id as string) as string;
if (url.value) {
console.log("Fetching article")
loading.value = true
console.log('Fetching article');
loading.value = true;
try {
await fetchArticle(url.value)
}
finally {
loading.value = false
await fetchArticle(url.value, route.params.lang as string);
} finally {
loading.value = false;
}
}
}, {
immediate: true
});
// Fetch the article contents from the URL
async function fetchArticle(url: string): Promise<any> {
async function fetchArticle(url: string, region?: string): Promise<any> {
if (!url) {
return
console.error('fetchArticle: No URL provided');
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;
updateMetadata(markdown.value)
try {
// Trim the .md extension
var trimmedUrl = url.replace(/\.md$/, '');
console.log('[id].vue - Fetching article: ' + trimmedUrl);
return data.value
// Define the available languages in the order of preference (falling back on the next if one fails)
const languages = [region ? region : locale.value, 'en', 'fr', 'de']; // Add all available languages in order of preference
console.log(languages)
let dataFound = false;
let data: any = null;
for (let lang of languages) {
console.log(`Querying ${lang}${trimmedUrl}`)
const { data: languageData, error } = await useAsyncData(`${lang}/${trimmedUrl}`, () =>
queryContent(`${lang}${trimmedUrl}`).findOne()
);
if (error.value) {
console.warn(`Error fetching article for language ${lang}:`, error.value);
continue; // Try the next language
}
if (languageData.value) {
data = languageData.value;
dataFound = true;
break; // Break as soon as data is found
}
}
if (!dataFound) {
console.warn('fetchArticle: No data found for URL', trimmedUrl);
return;
}
console.log(data);
markdown.value = data;
updateMetadata(markdown.value);
// Update next/previous links based on language
const language = route.query.lang || 'en'; // Use the language from the route or default to 'en'
updateNavigationLinks(language as string);
return data;
} catch (err) {
console.error('fetchArticle: An unexpected error occurred', err);
}
}
function updateNavigationLinks(language: string) {
// Get the next and previous articles based on the current language
const postId = route.params.id as string;
// const langPosts = languageMap[language] || [];
// const postIndex = langPosts.indexOf(postId);
// if (postIndex !== -1) {
// Find the previous and next posts in the same language
// previous.value = langPosts[postIndex - 1] || '';
// next.value = langPosts[postIndex + 1] || '';
// }
}
function resetReadingPosition() {
window.scrollTo(0, 0)
window.scrollTo(0, 0);
}
const data = await fetchArticle(url.value)
updateMetadata(data)
const data = await fetchArticle(url.value);
updateMetadata(data);
console.log("Prefetching article")
console.log('Prefetching article');
onMounted(async () => {
console.log("Fetching article :3")
await fetchArticle(url.value)
})
const temp_url = route.query.post as string
console.log('Fetching article :3');
await fetchArticle(url.value);
});
const temp_url = route.query.post as string;
await fetchArticle(temp_url);
const fullTitle = data.title + " | " + siteConfig.siteTitle;
const fullTitle = data.title + ' | ' + siteConfig.siteTitle;
useHead({
title: fullTitle,
@ -122,7 +171,7 @@ useHead({
{ name: 'og:image', content: background },
{ name: 'og:image:alt', content: fullTitle }
]
})
});
useSeoMeta({
title: fullTitle,
@ -146,7 +195,7 @@ useSeoMeta({
<template>
<div class="relative z-50 flex w-full justify-center text-white">
<!-- Article Viewer -->
<!-- Article Viewer -->
<div class="mt-8 flex-col text-center">
<Transition name="list">
<div class="flex flex-col" :key="url">
@ -195,4 +244,4 @@ useSeoMeta({
</Transition>
</div>
</div>
</template>
</template>

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, watch, ref } from 'vue';
import { onMounted, watch, ref, computed } from 'vue';
import fm from 'front-matter';
import PostCard from '~/components/PostCard.vue';
@ -7,130 +7,154 @@ 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/post_list.json') as pages.PageList;
const article_list_data = await import('~/assets/meta/post_list.json');
const article_list: pages.PageList = pages.PageList.fromJSON(JSON.stringify(article_list_data));
let route = useRoute()
console.log(route)
const loading: Ref<boolean> = ref(false);
const view: Ref<string> = ref('list');
const categoryFilter: Ref<string[]> = ref([]);
const tagFilter: Ref<string[]> = ref([]);
const tagList: Ref<string[]> = ref([]);
const languageList: Ref<string[]> = ref([]);
const loading: Ref<boolean> = ref(false)
const view: Ref<string> = ref('list') // list, category
const listCategoryKeys: Ref<string[]> = ref([])
const tagList: Ref<string[]> = ref([])
const categoryFilter: Ref<string[]> = ref([])
const tagFilter: Ref<string[]> = ref([])
tagFilter.value = []
function resetReadingPosition() {
window.scrollTo(0, 0)
}
// Create a map for languages (using Record<string, string[]>)
const languageMap: Record<string, string[]> = {};
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)
let tags: Set<string> = new Set();
let languages: Set<string> = new Set();
// Populate the languageMap with languages from article_list
for (const lang in article_list.languages) {
if (article_list.languages.hasOwnProperty(lang)) {
// Initialize the array for each language in the map
languageMap[lang] = [];
languages.add(lang);
// Iterate through categories and posts to collect tags
for (const category in article_list.languages[lang].categories) {
for (const post of article_list.languages[lang].categories[category].posts) {
if (post.metadata.tags) {
post.metadata.tags.forEach(tag => tags.add(tag));
}
// Add the post ID or any other identifier to the language array
languageMap[lang].push(post.id);
}
}
}
}
tagList.value = tags
// Populate tagList and languageList
tagList.value = Array.from(tags);
languageList.value = Array.from(languages);
});
const filteredArticles = computed<pages.Page[]>(() => {
// Initialize allPosts as an empty array as a fallback.
let allPosts: pages.Page[] = [];
let uniquePostIds = new Set<string>(); // To keep track of unique post IDs
try {
// Safely access article_list.languages (fallbacks to empty object if undefined)
allPosts = Object.values(article_list?.languages || {}).map((lang) => {
// Safely access lang.categories (fallbacks to empty object if undefined)
return Object.values(lang?.categories || {}).filter(category => category.show).flatMap((category) =>
category?.posts || []
);
}).flat() // Flatten all post arrays into one single array.
.filter(post => {
// Check if post.id is already in the set, if it is, exclude it (deduplicate)
if (uniquePostIds.has(post.id)) {
return false; // Skip this post if already added
}
uniquePostIds.add(post.id); // Otherwise, add it to the set
return true; // Keep this post in the list
})
.sort((a, b) => {
// Ensure both `metadata.date` values are valid Date objects
const dateA = new Date(a?.metadata?.date ?? 0).getTime(); // Default to 0 if date is missing or invalid
const dateB = new Date(b?.metadata?.date ?? 0).getTime();
return dateB - dateA; // Sort by descending date
});
} catch (error) {
console.error("Error processing articles:", error);
}
// Apply tag filter if any filter is selected.
if (tagFilter.value?.length) {
// Filter posts based on matching tags
allPosts = allPosts.filter((post) => {
// Ensure post.metadata.tags exists before checking
return post?.metadata?.tags?.some((tag) => tagFilter.value.includes(tag));
});
}
// Return the filtered and deduplicated list of posts
return allPosts;
});
</script>
<template>
<div class="relative z-50 flex w-full justify-center text-white">
<!-- Metadata -->
<MetaSet title="Articles" description="Ramblings."
background="/images/me.png" tags="blog, personal, author" />
<!-- Main Content -->
<MetaSet title="Articles" description="Ramblings." background="/images/me.png"
:tags="['blog', 'personal', 'author']" />
<div class="mt-8 flex-col text-center">
<Transition name="list">
<div>
<!-- Article List -->
<h1>Articles</h1>
<!-- View mode switcher -->
<div class="flex justify-center">
<button @click="view = 'list'"
class="m-1 bg-black border-purple-400 border text-white p-1 rounded-md"
:class="view == 'list' ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">List</button>
:class="view == 'list' ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">
List
</button>
<button @click="view = 'category'"
class="m-1 bg-black border-purple-400 border text-white p-1 rounded-md"
:class="view == 'category' ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">Category</button>
:class="view == 'category' ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">
Category
</button>
</div>
<!-- Tag selection -->
<div class="flex justify-center" v-if="view == 'list'">
<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>
:class="tagFilter.includes(tag) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">
{{ tag }}
</button>
</div>
</div>
</div>
<!-- Category selection -->
<div class="flex justify-center" v-if="view == 'category'">
<div class="flex flex-wrap justify-center m-5 max-w-96">
<div v-for="category in Object.keys(article_list.categories)" :key="category"
class="m-1">
<button
@click="categoryFilter.includes(category) ? categoryFilter.splice(categoryFilter.indexOf(category), 1) : categoryFilter.push(category)"
class="text-xs bg-black border-purple-400 border text-white p-1 rounded-md"
:class="categoryFilter.includes(category) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">{{
category }}</button>
</div>
</div>
</div>
<!-- Category view -->
<div v-if="view == 'category'">
<div v-for="categoryKey in Object.keys(article_list.categories).filter((category) => categoryFilter.length == 0 ? true : categoryFilter.includes(category))" :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 mb-8">
<h2>{{ categoryKey }}</h2>
<div
v-for="post in article_list.categories[categoryKey].posts">
<PostCard class="lg:w-[48rem]" :url="post.url" :key="post.id"
:tagFilter="tagFilter" />
<div v-if="view == 'list'">
<div v-for="post in filteredArticles" :key="post.id">
<PostCard class="lg:w-[48rem]" :url="post.url" :tagFilter="tagFilter">
<!-- Show available languages for the post using languageMap -->
<div v-if="post.id">
<div class="text-sm text-gray-400">Available in:
<span v-for="(postIds, lang) in languageMap" :key="lang">
<!-- Check if the post.id is in the array of postIds for the current language -->
<span v-if="postIds.includes(post.id)">
<!-- Display the flag image for each available language -->
<img :src="`/images/flags/${lang}.svg`" :alt="lang"
class="inline-block w-6 h-4 mr-2"
/>
</span>
</span>
</div>
</div>
</div>
</div>
</div>
<!-- List view -->
<!-- Instead of grouping by category, just throw cards by order of appearance -->
<div v-else-if="view == 'list'">
<div
v-for="post in tagFilter.length == 0 ?
Object.values(article_list.categories)
.map((category) => category.posts)
.flat()
.sort((a, b) => new Date(b.metadata.date ?? 0).getTime() - new Date(a.metadata.date ?? 0).getTime())
: Object.values(article_list.categories)
.map((category) => category.posts)
.flat()
.filter((post) => post.metadata.tags ? post.metadata.tags.some((tag) => tagFilter.includes(tag)) : true)
.sort((a, b) => new Date(b.metadata.date ?? 0).getTime() - new Date(a.metadata.date ?? 0).getTime())
">
<PostCard class="lg:w-[48rem]" :url="post.url" :key="post.id" :tagFilter="tagFilter" />
</PostCard>
</div>
</div>
</div>
</Transition>
</div>
</div>
</template>
<style scoped></style>

View file

@ -2,25 +2,46 @@
import { ref } from 'vue';
import Markdown from '~/components/Markdown.vue';
import Card from '~/components/Card.vue';
import siteTitle from '../assets/config'
import siteTitle from '~/assets/config'
import * as pages from "~/utils/page_updater/update_pagelist";
const aboutMe = ref('');
const test = ref('');
const { data } = await useAsyncData('about_me', () => queryContent('/about_me').findOne())
let blog_list_json = (await import("~/assets/meta/post_list.json")).default;
let blog_list = pages.PageList.fromJSON(JSON.stringify(blog_list_json));
const { locale, setLocale } = useI18n();
let currentLocale = locale.value.toLowerCase().replace(/-/g, '_');
const aboutMePage = blog_list.languages[currentLocale]?.categories['Site']?.posts.find(
post => {
const canonicalId = pages.PageList.getCanonicalId(post.id);
return canonicalId && canonicalId.toLowerCase().includes('about_me');
}
);
if (!aboutMePage) {
throw new Error(`"about_me" page not found for locale ${currentLocale}. Available posts: ${JSON.stringify(blog_list.languages[currentLocale]?.categories['Site']?.posts.map(p => p.id))}`);
}
console.log("url:" + aboutMePage.url)
const { data } = await useAsyncData('about_me', () => queryContent(aboutMePage.url).findOne());
</script>
<template>
<div class="relative flex w-full justify-center text-white">
<!-- Metadata -->
<MetaSet title="Home" :description="siteTitle"
background="/images/me.png" tags="home, personal, author" />
<MetaSet title="Home" :description="siteTitle" background="/images/me.png" tags="home, personal, author" />
<div class="mt-8 flex-col text-center">
<div class="flex justify-center">
<div id="PFP" class="shadow-md rounded-full shadow-highlight">
<img class="transition-all w-40 h-40 md:w-56 md:h-56 rounded-full"
src="/images/me.png" alt="User PFP" />
<img class="transition-all w-40 h-40 md:w-56 md:h-56 rounded-full" src="/images/me.png"
alt="User PFP" />
</div>
</div>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 30" width="1200" height="600">
<clipPath id="s">
<path d="M0,0 v30 h60 v-30 z"/>
</clipPath>
<clipPath id="t">
<path d="M30,15 h30 v15 z v15 h-30 z h-30 v-15 z v-15 h30 z"/>
</clipPath>
<g clip-path="url(#s)">
<path d="M0,0 v30 h60 v-30 z" fill="#012169"/>
<path d="M0,0 L60,30 M60,0 L0,30" stroke="#fff" stroke-width="6"/>
<path d="M0,0 L60,30 M60,0 L0,30" clip-path="url(#t)" stroke="#C8102E" stroke-width="4"/>
<path d="M30,0 v30 M0,15 h60" stroke="#fff" stroke-width="10"/>
<path d="M30,0 v30 M0,15 h60" stroke="#C8102E" stroke-width="6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 657 B

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="600"
height="400"
version="1.1"
id="svg838"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs842" />
<rect
style="fill:#c8e1ed;fill-opacity:1.0;stroke-width:5.66929;stroke-linecap:round;stroke-linejoin:bevel;stop-color:#000000"
id="rect4684"
width="600"
height="400"
x="0"
y="0" />
<g
transform="matrix(0.33755415,0,0,-0.33755415,188.33065,342.42618)"
fill="#000099"
stroke="none"
id="g1364">
<g
id="g1755"
transform="matrix(1.0249506,0,0,1.0249506,-8.2541353,-9.3390257)">
<path
fill="#000099"
stroke="none"
d="m 302,838 c -14,-14 -16,-126 -3,-147 5,-8 16,-11 25,-8 12,5 16,21 16,71 0,89 -10,112 -38,84 z"
id="path1352"
style="fill:#11119a;fill-opacity:1" />
<path
fill="#000099"
stroke="none"
d="m 521,775 c -27,-57 -32,-108 -10,-113 18,-3 84,122 75,144 -11,30 -44,15 -65,-31 z"
id="path1354"
style="fill:#11119a;fill-opacity:1" />
<path
fill="#000099"
stroke="none"
d="M 34,797 C 26,775 93,639 110,643 148,650 99,810 59,810 48,810 37,804 34,797 Z"
id="path1356"
style="fill:#11119a;fill-opacity:1" />
<path
fill="#000099"
stroke="none"
d="M 254,590 C 204,583 126,538 79,490 -19,390 14,144 136,67 199,27 243,17 336,23 c 125,7 212,62 275,172 53,92 32,220 -51,317 -62,71 -170,99 -306,78 z"
id="path1358"
style="fill:#11119a;fill-opacity:1" />
<path
fill="#ffff63"
stroke="none"
d="M 443,539 C 490,526 555,469 581,419 605,371 607,272 584,229 562,186 502,121 467,104 330,33 190,49 116,145 77,197 65,237 65,320 c 1,77 19,113 82,161 80,63 198,86 296,58 z"
id="path1360"
style="fill:#ffff77;fill-opacity:1" />
<path
fill="#000099"
stroke="none"
d="m 462,367 c -5,-7 -15,-28 -21,-48 -21,-67 -100,-120 -144,-98 -30,15 -65,56 -88,102 -21,40 -51,48 -57,14 -5,-26 53,-111 96,-141 89,-62 204,-7 252,119 15,40 -15,81 -38,52 z"
id="path1362"
style="fill:#11119a;fill-opacity:1" />
</g>
</g>
</svg>
<!--
len pi toki pona
COPYRIGHT:
Spencer van der Meulen (jan Pensa) ©2021
Licence: Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
Based on:
- original logo design by Sonja Lang.
- Toki Pona logo vector image from https://commons.wikimedia.org/wiki/File:Toki_pona.svg by Eequor.
- colors from image on https://www.teepublic.com/user/toki-pona by Sonja Lang.
-->

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View file

@ -9,6 +9,7 @@ export interface PageLocation {
tags: string[];
map: string;
root: string;
show: boolean;
}
export interface PageInfoMetdata {

107
utils/page_updater/run.ts Normal file
View file

@ -0,0 +1,107 @@
import { type Page, postDirectories, generatePageCategory, PageList } from "./update_pagelist.ts"
import * as pages from "./pages.ts";
import * as fs from "node:fs";
import * as path from "node:path";
// ---------------------------------------------------------------------------
// Build the PageList
// ---------------------------------------------------------------------------
/**
* In the new structure, pages are stored under "content/[LANG]/[DIRECTORY]".
* The following code:
* 1. Reads the "content" directory to determine available language folders.
* 2. For each language, loops over the defined postDirectories.
* 3. If the expected subdirectory exists, it retrieves pages and adds them to the PageList.
*/
const contentRoot = "content";
// Read all items in the content root and filter for directories (languages)
const availableLangs = fs.readdirSync(contentRoot).filter((item) =>
fs.statSync(path.join(contentRoot, item)).isDirectory()
);
export const postList = new PageList();
for (const lang of availableLangs) {
// Ensure the language exists in our PageList.
if (!postList.languages[lang]) {
postList.languages[lang] = { categories: {} };
}
// Loop over each defined post category.
for (const postDirectory of postDirectories) {
// Construct the expected directory path for this language and category.
// E.g., "content/en/site" or "content/fr/collections"
const dirPath = path.join(contentRoot, lang, postDirectory.map);
// If the directory doesn't exist for this language, skip it.
if (!fs.existsSync(dirPath)) continue;
// Retrieve page info for this directory.
// We override the "root" with our computed directory path.
const pagesInfo = pages.getPagesInfo("", {
...postDirectory,
root: dirPath,
});
// If no pages are found, skip this category for the language.
if (Object.keys(pagesInfo).length === 0) continue;
// Initialize the category if it doesn't exist.
if (!postList.languages[lang].categories[postDirectory.title]) {
postList.languages[lang].categories[postDirectory.title] =
generatePageCategory({});
// Set category metadata.
postList.languages[lang].categories[postDirectory.title].title =
postDirectory.title;
postList.languages[lang].categories[postDirectory.title]
.description = postDirectory.description;
postList.languages[lang].categories[postDirectory.title].tags =
postDirectory.tags;
postList.languages[lang].categories[postDirectory.title].show =
postDirectory.show;
}
// Process each page in the retrieved pagesInfo.
for (const [filePath, page] of Object.entries(pagesInfo)) {
// Create a Page object.
const pageDict: Page = {
metadata: page.metadata,
id: page.local_path, // e.g. "en/page.md"
url: page.absolute_path.replace("content/", ""), // Remove "content/" prefix
hash: page.hash,
};
// Add the page to the appropriate language and category.
postList.languages[lang].categories[postDirectory.title].posts.push(
pageDict,
);
}
}
}
// ---------------------------------------------------------------------------
// Sort Pages by Date
// ---------------------------------------------------------------------------
/**
* For each language and category, sort the pages by date (most recent first).
*/
for (const lang of Object.keys(postList.languages)) {
for (const category of Object.values(postList.languages[lang].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();
});
}
}
// Output the resulting PageList as JSON.
console.log(postList.toJSONString());
// ---------------------------------------------------------------------------
// Save the PageList to File
// ---------------------------------------------------------------------------
fs.writeFileSync("assets/meta/post_list.json", postList.toJSONString());

View file

@ -1,107 +1,281 @@
import * as fs from 'node:fs';
import * as pages from './pages.ts';
import { format } from 'date-fns';
// Type for metadata and page info
import * as pages from "./pages.ts";
import { format } from "date-fns";
/**
* Interface representing a page's essential information.
*/
export interface Page {
/** Metadata for the page (e.g., title, date, etc.) */
metadata: pages.PageInfoMetdata;
/** The page's identifier, e.g. "en/page.md" */
id: string;
/** The URL for the page (relative to content) */
url: string;
/** A hash representing the page's content/version */
hash: string;
}
/**
* Interface representing a category that groups multiple pages.
*/
export interface PageCategory {
/** An array of pages that belong to this category */
posts: Page[];
/** The title of the category */
title: string;
/** A description for the category */
description: string;
/** Tags associated with the category */
tags: string[];
}
export interface PageList {
last_generated: string;
categories: Record<string, PageCategory>;
show: boolean;
}
// Function to generate the page list
function generatePageCategory(pagesInfo: Record<string, any>): PageCategory {
/**
* Class representing a collection of pages across multiple languages.
*
* The structure of a PageList is as follows:
* - last_generated: A timestamp for when the list was last updated.
* - languages: An object mapping language codes (e.g., "en", "es") to objects
* containing categories, which in turn map to PageCategory objects.
*/
export class PageList {
last_generated: string;
languages: {
[lang: string]: {
categories: Record<string, PageCategory>;
};
};
constructor() {
this.last_generated = format(new Date(), "yyyy-MM-dd HH:mm:ss");
this.languages = {};
}
/**
* Deserialize a JSON string to a PageList instance.
* @param json - JSON string representation of a PageList
* @returns A new PageList instance
*/
static fromJSON(json: string): PageList {
return Object.assign(new PageList(), JSON.parse(json));
}
/**
* Custom JSON serialization.
* This method returns a plain object representation, avoiding recursive calls.
* @returns A plain object representation of the PageList.
*/
toJSON(): object {
return {
last_generated: this.last_generated,
languages: this.languages,
};
}
/**
* Returns the JSON string representation of the PageList.
* @returns JSON string of the PageList.
*/
toJSONString(): string {
return JSON.stringify(this, null, 2);
}
/**
* Retrieve a specific category for a given language.
* @param lang - The language code (e.g., "en")
* @param categoryName - The name of the category
* @returns The PageCategory if it exists; otherwise, undefined.
*/
getCategory(lang: string, categoryName: string): PageCategory | undefined {
return this.languages[lang]?.categories[categoryName];
}
/**
* Retrieve all pages for a given language and category.
* @param lang - The language code (e.g., "en")
* @param categoryName - The name of the category
* @returns An array of pages, or an empty array if not found.
*/
getPagesByCategory(lang: string, categoryName: string): Page[] {
return this.languages[lang]?.categories[categoryName]?.posts || [];
}
/**
* Compute the canonical ID for a page by removing the language prefix.
* For example, "en/page.md" becomes "page.md".
* @param pageId - The full page id including language folder
* @returns The canonical page id
*/
static getCanonicalId(pageId: string): string {
return pageId.replace(/^[^/]+\//, "");
}
/**
* Retrieve a specific page by its canonical ID from a given language and category.
* @param lang - The language code (e.g., "en")
* @param categoryName - The category containing the page
* @param canonicalId - The canonical id of the page (language prefix removed)
* @returns The Page if found; otherwise, undefined.
*/
getPage(
lang: string,
categoryName: string,
canonicalId: string,
): Page | undefined {
const pagesArr = this.getPagesByCategory(lang, categoryName);
return pagesArr.find((page) =>
PageList.getCanonicalId(page.id) === canonicalId
);
}
/**
* Identify all languages in which a page (by its canonical ID) is available.
* @param canonicalId - The canonical id of the page (language prefix removed)
* @returns An array of language codes where the page exists.
*/
getAvailableLanguagesForPage(canonicalId: string): string[] {
const langs: string[] = [];
for (const lang of Object.keys(this.languages)) {
const categories = this.languages[lang].categories;
for (const categoryName of Object.keys(categories)) {
if (
categories[categoryName].posts.some((page) =>
PageList.getCanonicalId(page.id) === canonicalId
)
) {
langs.push(lang);
break; // Page found in this language; no need to check further categories.
}
}
}
return langs;
}
/**
* Enumerate unique pages across all languages.
* Each unique page (by canonical id) is listed along with:
* - An array of languages in which it is available.
* - A mapping of each language to its corresponding Page.
* @returns An array of objects representing unique pages.
*/
enumerateUniquePages(): Array<
{
canonicalId: string;
langs: string[];
pages: { [lang: string]: Page };
}
> {
const uniquePages: {
[canonicalId: string]: {
langs: Set<string>;
pages: { [lang: string]: Page };
};
} = {};
for (const lang of Object.keys(this.languages)) {
const categories = this.languages[lang].categories;
for (const categoryName of Object.keys(categories)) {
for (const page of categories[categoryName].posts) {
const canonicalId = PageList.getCanonicalId(page.id);
if (!uniquePages[canonicalId]) {
uniquePages[canonicalId] = {
langs: new Set(),
pages: {},
};
}
uniquePages[canonicalId].langs.add(lang);
uniquePages[canonicalId].pages[lang] = page;
}
}
}
return Object.entries(uniquePages).map(([canonicalId, data]) => ({
canonicalId,
langs: Array.from(data.langs),
pages: data.pages,
}));
}
}
/**
* Generate a PageCategory from the provided pagesInfo object.
*
* @param pagesInfo - An object where keys are paths (e.g., "content/[lang]/...") and values are page data.
* @returns A PageCategory object containing an array of Page objects.
*/
export function generatePageCategory(pagesInfo: Record<string, any>): PageCategory {
const pageList: Page[] = [];
for (const [path, page] of Object.entries(pagesInfo)) {
for (const [filePath, page] of Object.entries(pagesInfo)) {
// Expect the path in the form "content/[lang]/..."
const langMatch = filePath.match(/^content\/([^/]+)\//);
const lang = langMatch ? langMatch[1] : "default";
const pageDict: Page = {
metadata: page.metadata,
id: page.local_path,
url: page.absolute_path.replace("content", ""),
hash: page.hash
id: page.local_path, // e.g. "en/page.md"
url: page.absolute_path.replace("content/", ""), // Remove "content/" prefix
hash: page.hash,
};
pageList.push(pageDict);
}
pageList.forEach(page => {
// Format dates consistently.
pageList.forEach((page) => {
if (page.metadata.date) {
page.metadata.date = format(new Date(page.metadata.date), 'yyyy-MM-dd HH:mm:ss');
page.metadata.date = format(
new Date(page.metadata.date),
"yyyy-MM-dd HH:mm:ss",
);
}
});
const pageListDict: PageCategory = {
return {
posts: pageList,
title: "",
description: "",
tags: []
tags: [],
show: true
};
return pageListDict;
}
const postDirectories: pages.PageLocation[] = [
// ---------------------------------------------------------------------------
// Directory Definitions
// ---------------------------------------------------------------------------
/**
* An array of directories to scan for pages.
* Each directory is represented as a PageLocation from the `pages` module.
*
* Note: The 'map' property should correspond to the subdirectory name that
* exists within each language folder. For example, if articles for "Site"
* are stored in "content/en/site", "content/fr/site", etc., then map: "site".
*/
export const postDirectories: pages.PageLocation[] = [
{
title: "Site",
description: "Articles to test site functionality",
tags: ["site"],
map: "site",
root: "content/site"
root: "", // Not used in the new structure
show: false
},
{
title: "Collections",
description: "Articles that are collections of information: Lists, Awesome lists, etc.",
description:
"Articles that are collections of information: Lists, Awesome lists, etc.",
tags: ["collection"],
map: "collections",
root: "content/collections"
root: "",
show: true
},
{
title: "Guides",
description: "Guides and tutorials",
tags: ["guide"],
map: "guides",
root: "content/guides"
}
]
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);
postList.categories[postDirectory.title].title = postDirectory.title;
postList.categories[postDirectory.title].description = postDirectory.description;
postList.categories[postDirectory.title].tags = postDirectory.tags;
}
// 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)
fs.writeFileSync("assets/meta/post_list.json", JSON.stringify(postList, null, 2));
root: "",
show: true
},
];

8
vitest.config.ts Normal file
View file

@ -0,0 +1,8 @@
import { defineVitestConfig } from "@nuxt/test-utils/config";
export default defineVitestConfig({
// any custom Vitest config you require
test: {
environment: "nuxt",
},
});