This commit is contained in:
parent
15f2688ca3
commit
edbce48613
33 changed files with 4943 additions and 623 deletions
|
@ -4,6 +4,7 @@ export default {
|
||||||
siteDescription: "Luna's rambling place!",
|
siteDescription: "Luna's rambling place!",
|
||||||
siteUrl: 'https://mrrpnya.github.io',
|
siteUrl: 'https://mrrpnya.github.io',
|
||||||
siteImage: '',
|
siteImage: '',
|
||||||
|
siteDefaultLocale: 'en_us',
|
||||||
|
|
||||||
// Site personalization
|
// Site personalization
|
||||||
siteColor: '#550077',
|
siteColor: '#550077',
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"last_generated": "2025-01-04 12:52:32",
|
"last_generated": "2025-02-16 14:47:00",
|
||||||
|
"languages": {
|
||||||
|
"en": {
|
||||||
"categories": {
|
"categories": {
|
||||||
"Site": {
|
"Site": {
|
||||||
"posts": [
|
"posts": [
|
||||||
|
@ -7,44 +9,83 @@
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "Styling Test",
|
"title": "Styling Test",
|
||||||
"description": "A test post to see how the site styling looks",
|
"description": "A test post to see how the site styling looks",
|
||||||
"date": "2024-12-31 16:00:00",
|
"date": "2025-01-01T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"meta",
|
"meta",
|
||||||
"web"
|
"web"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"id": "site/styling_test",
|
"id": "site/styling_test",
|
||||||
"url": "/site/styling_test",
|
"url": "en/site/styling_test",
|
||||||
"hash": "0ff9f34321a27f462ca26656a1dc5024c0e800ea1e176ff36316b158ab4606c9"
|
"hash": "e581ca6fef00cdc54a660744b295ff83ce05c2d75561a43695917dde2aa2d06f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"title": "About Me",
|
||||||
|
"prop": true
|
||||||
|
},
|
||||||
|
"id": "site/about_me",
|
||||||
|
"url": "en/site/about_me",
|
||||||
|
"hash": "85cd293e18e1f11b8f49c3858c78b98d7cb3dfcc31b347d76db1be7d8c400b81"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Site",
|
"title": "Site",
|
||||||
"description": "Articles to test site functionality",
|
"description": "Articles to test site functionality",
|
||||||
"tags": [
|
"tags": [
|
||||||
"site"
|
"site"
|
||||||
]
|
],
|
||||||
|
"show": false
|
||||||
},
|
},
|
||||||
"Collections": {
|
"Collections": {
|
||||||
"posts": [
|
"posts": [
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "LGBTQ+ Resources",
|
"title": "Neurodiverse Resources",
|
||||||
"description": "A list of resources for LGBTQ+ individuals",
|
"description": "A list of neurodiversity resources",
|
||||||
"date": "2025-01-01 16:00:00",
|
"date": "2025-02-04T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"lgbtq+",
|
"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"
|
"resources"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"id": "collections/lgbtq_resources",
|
"id": "collections/lgbtq_resources",
|
||||||
"url": "/collections/lgbtq_resources",
|
"url": "en/collections/lgbtq_resources",
|
||||||
"hash": "3da76064aa95cc06937bde01128ed44aafb850f35a43bd214ce0cd89a875c674"
|
"hash": "2f1f9c04ef62313bccecf7e5f22b95e862a36fce260b34ca64286e684a453196"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "Badges!",
|
"title": "Badges!",
|
||||||
"description": "A collection of 88x31 badges for various things",
|
"description": "Some 88x31 badges for various things",
|
||||||
"date": "2024-12-20 16:00:00",
|
"date": "2024-12-21T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"badges",
|
"badges",
|
||||||
"retro",
|
"retro",
|
||||||
|
@ -52,29 +93,30 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"id": "collections/badges",
|
"id": "collections/badges",
|
||||||
"url": "/collections/badges",
|
"url": "en/collections/badges",
|
||||||
"hash": "338ccfecc6523dff93708330a8b43af715f1e80d55e1cc3bea2d1a7306fc4f00"
|
"hash": "7fc0dbfff6dfba66b5a6e93ba4394a2034ab3935ae6acaf2b5ac4a815116d24e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "Awesome",
|
"title": "Awesome",
|
||||||
"description": "A curated list of awesome stuff I like",
|
"description": "A curated list of awesome stuff I like",
|
||||||
"date": "2024-11-25 16:00:00",
|
"date": "2024-11-26T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"awesome",
|
"awesome",
|
||||||
"curated"
|
"curated"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"id": "collections/awesome",
|
"id": "collections/awesome",
|
||||||
"url": "/collections/awesome",
|
"url": "en/collections/awesome",
|
||||||
"hash": "0632400858006b93f2f36d87953538c2a400bacc75aaa29928aee226e8b343b1"
|
"hash": "43704f5de68e422ca3187cda0e34084d6ad3b930b4238bdd1b80535c3013c191"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"title": "Collections",
|
"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": [
|
"tags": [
|
||||||
"collection"
|
"collection"
|
||||||
]
|
],
|
||||||
|
"show": true
|
||||||
},
|
},
|
||||||
"Guides": {
|
"Guides": {
|
||||||
"posts": [
|
"posts": [
|
||||||
|
@ -82,7 +124,7 @@
|
||||||
"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": "2024-12-31 16:00:00",
|
"date": "2025-01-01T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"3ds",
|
"3ds",
|
||||||
"programming",
|
"programming",
|
||||||
|
@ -94,14 +136,14 @@
|
||||||
"next": "old3ds_touchscreen.md"
|
"next": "old3ds_touchscreen.md"
|
||||||
},
|
},
|
||||||
"id": "guides/old3ds_romfs",
|
"id": "guides/old3ds_romfs",
|
||||||
"url": "/guides/old3ds_romfs",
|
"url": "en/guides/old3ds_romfs",
|
||||||
"hash": "34062b79909f5b18a647b484687cf862e779c08da9fc6052c4ebab3eef67151c"
|
"hash": "f518b6cdf7a5eb0d72f86c305089df5ee42a4c4aae1589c7abace33368dd4ede"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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": "2024-12-31 16:00:00",
|
"date": "2025-01-01T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"3ds",
|
"3ds",
|
||||||
"programming",
|
"programming",
|
||||||
|
@ -112,14 +154,14 @@
|
||||||
"previous": "old3ds_romfs.md"
|
"previous": "old3ds_romfs.md"
|
||||||
},
|
},
|
||||||
"id": "guides/old3ds_touchscreen",
|
"id": "guides/old3ds_touchscreen",
|
||||||
"url": "/guides/old3ds_touchscreen",
|
"url": "en/guides/old3ds_touchscreen",
|
||||||
"hash": "c026e506fb60c8ed9943f5806e8adf611a382a7de34e30fc2a72f4578d66899e"
|
"hash": "c026e506fb60c8ed9943f5806e8adf611a382a7de34e30fc2a72f4578d66899e"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"title": "3DS Programming - Hello World",
|
"title": "3DS Programming - Hello World",
|
||||||
"description": "A guide to creating a simple Hello, World program for the 3DS. (Old)",
|
"description": "A guide to creating a simple Hello, World program for the 3DS. (Old)",
|
||||||
"date": "2024-12-31 16:00:00",
|
"date": "2025-01-01T00:00:00.000Z",
|
||||||
"tags": [
|
"tags": [
|
||||||
"3ds",
|
"3ds",
|
||||||
"programming",
|
"programming",
|
||||||
|
@ -130,7 +172,7 @@
|
||||||
"next": "old3ds_romfs.md"
|
"next": "old3ds_romfs.md"
|
||||||
},
|
},
|
||||||
"id": "guides/old3ds_helloworld",
|
"id": "guides/old3ds_helloworld",
|
||||||
"url": "/guides/old3ds_helloworld",
|
"url": "en/guides/old3ds_helloworld",
|
||||||
"hash": "77a21a1201a35d6a85cb2305166cfb20a0a45546fea1f73fd620b2b84ec70fda"
|
"hash": "77a21a1201a35d6a85cb2305166cfb20a0a45546fea1f73fd620b2b84ec70fda"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -138,7 +180,33 @@
|
||||||
"description": "Guides and tutorials",
|
"description": "Guides and tutorials",
|
||||||
"tags": [
|
"tags": [
|
||||||
"guide"
|
"guide"
|
||||||
]
|
],
|
||||||
|
"show": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -9,6 +9,10 @@ html {
|
||||||
@apply min-h-screen bg-slate-950;
|
@apply min-h-screen bg-slate-950;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6, p {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-family: 'Lobster', cursive, 'Courier New', Courier, monospace;
|
font-family: 'Lobster', cursive, 'Courier New', Courier, monospace;
|
||||||
@apply text-3xl font-bold;
|
@apply text-3xl font-bold;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import siteConfig from '../assets/config';
|
import siteConfig from '~/assets/config';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
|
|
|
@ -1,17 +1,37 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import Card from './Card.vue';
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex justify-center">
|
<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">
|
<div class="flex-col transition-[margin] justify-center md:rounded-md max-md:w-screen lg:mt-3 w-fit-content">
|
||||||
<Card>
|
<Card>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center items-center gap-4">
|
||||||
<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 :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 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 :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 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 :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 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>
|
<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>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -67,10 +67,12 @@ async function fetchData(url: string) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData(url.value)
|
fetchData(url.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const localePath = useLocalePath();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink :href="'/article' + url">
|
<NuxtLink :to="localePath('/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">
|
||||||
|
@ -97,6 +99,9 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="justify-center">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -139,6 +144,9 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="justify-center">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -159,6 +167,7 @@ onMounted(() => {
|
||||||
<div class="justify-center">
|
<div class="justify-center">
|
||||||
<h3>{{ title }}</h3>
|
<h3>{{ title }}</h3>
|
||||||
<small>{{ date }}</small>
|
<small>{{ date }}</small>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,6 +188,7 @@ onMounted(() => {
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="justify-center">
|
<div class="justify-center">
|
||||||
<h5>{{ title }}</h5>
|
<h5>{{ title }}</h5>
|
||||||
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
10
content.config.ts
Normal file
10
content.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineCollection, defineContentConfig } from "@nuxt/content";
|
||||||
|
|
||||||
|
export default defineContentConfig({
|
||||||
|
collections: {
|
||||||
|
content: defineCollection({
|
||||||
|
type: "page",
|
||||||
|
source: "**/**/**/*.md",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
|
@ -16,3 +16,7 @@ tags: ['godot', 'curated']
|
||||||
::alert{type="note"}
|
::alert{type="note"}
|
||||||
If you find that this list is lacking or inaccurate, please open a GitHub issue or pull request.
|
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.
|
||||||
|
::
|
|
@ -88,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.
|
> 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
|
## LGBTQ+ Content Creators and Artists
|
||||||
|
|
||||||
- [Chipflake](https://www.youtube.com/@chipflake)
|
- [Chipflake](https://www.youtube.com/@chipflake)
|
15
content/en/collections/neurodiverse_resources.md
Normal file
15
content/en/collections/neurodiverse_resources.md
Normal 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.
|
||||||
|
::
|
||||||
|
|
|
@ -5,9 +5,9 @@ prop: true
|
||||||
|
|
||||||
# 🌙 Luna - She/They/Fae 🌙
|
# 🌙 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>
|
<br>
|
||||||
|
|
43
content/tp/site/about_me.md
Normal file
43
content/tp/site/about_me.md
Normal 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.
|
19
i18n.config.ts
Normal file
19
i18n.config.ts
Normal 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"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
|
@ -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_list: pages.PageList = pages.PageList.fromJSON(JSON.stringify(blog_list_json));
|
||||||
const blog_nitro_routes: any = [];
|
|
||||||
// key value
|
// Nitro expects a string array of routes.
|
||||||
for (let [key, category] of Object.entries(blog_list.categories)) {
|
const blog_nitro_routes: string[] = [];
|
||||||
for (let post of category.posts) {
|
|
||||||
blog_nitro_routes.push('/article' + post.url);
|
// 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);
|
console.log(blog_nitro_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: false,
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
|
@ -23,32 +37,49 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/article/:category:/:id': {
|
"/article/:category:/:id": {
|
||||||
redirect: '/article/:category:/:id/index.html'
|
redirect: "/article/:category:/:id/index.html",
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
pageTransition: {
|
pageTransition: {
|
||||||
name: 'page',
|
name: "page",
|
||||||
mode: 'out-in'
|
mode: "out-in",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modules: [
|
modules: [
|
||||||
'nuxt-particles',
|
"nuxt-particles",
|
||||||
'@nuxt/content'
|
"@nuxt/test-utils/module",
|
||||||
|
"@nuxt/content",
|
||||||
|
"@nuxtjs/i18n",
|
||||||
],
|
],
|
||||||
|
i18n: {
|
||||||
|
vueI18n: './i18n.config.ts',
|
||||||
|
locales: [
|
||||||
|
{
|
||||||
|
code: 'en',
|
||||||
|
name: 'English'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'tp',
|
||||||
|
name: 'Toki Pona'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
content: {
|
content: {
|
||||||
// ... options
|
// ... options
|
||||||
|
api: {
|
||||||
|
baseURL: '/api/_content'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
nitro: {
|
nitro: {
|
||||||
prerender: {
|
prerender: {
|
||||||
routes: blog_nitro_routes,
|
routes: blog_nitro_routes,
|
||||||
autoSubfolderIndex: true
|
autoSubfolderIndex: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
particles: {
|
particles: {
|
||||||
mode: 'slim',
|
mode: "slim",
|
||||||
lazy: true
|
lazy: true,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
|
@ -19,20 +19,26 @@
|
||||||
"@mdit/plugin-tasklist": "^0.14.0",
|
"@mdit/plugin-tasklist": "^0.14.0",
|
||||||
"@mdit/plugin-tex": "^0.14.0",
|
"@mdit/plugin-tex": "^0.14.0",
|
||||||
"@nuxt/content": "^2.13.4",
|
"@nuxt/content": "^2.13.4",
|
||||||
|
"@nuxt/test-utils": "^3.15.4",
|
||||||
|
"@nuxtjs/i18n": "9.2.0",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"@tsparticles/slim": "^3.7.1",
|
"@tsparticles/slim": "^3.7.1",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
"animejs": "^3.2.2",
|
"animejs": "^3.2.2",
|
||||||
"css-loader": "^7.1.2",
|
"css-loader": "^7.1.2",
|
||||||
|
"happy-dom": "^17.1.0",
|
||||||
"markdown-it-checkbox": "^1.1.0",
|
"markdown-it-checkbox": "^1.1.0",
|
||||||
"mathpix-markdown-it": "^2.0.9",
|
"mathpix-markdown-it": "^2.0.9",
|
||||||
"nuxt": "^3.14.1592",
|
"nuxt": "^3.14.1592",
|
||||||
"ofetch": "^1.4.1",
|
"ofetch": "^1.4.1",
|
||||||
|
"playwright-core": "^1.50.1",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
"sass-embedded": "^1.83.1",
|
"sass-embedded": "^1.83.1",
|
||||||
"tex-to-svg": "^0.2.0",
|
"tex-to-svg": "^0.2.0",
|
||||||
"texsvg": "^2.2.2",
|
"texsvg": "^2.2.2",
|
||||||
"tsparticles": "^3.7.1",
|
"tsparticles": "^3.7.1",
|
||||||
|
"vitest": "^3.0.5",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-loader": "^17.4.2",
|
"vue-loader": "^17.4.2",
|
||||||
"vue-router": "latest",
|
"vue-router": "latest",
|
||||||
|
|
|
@ -1,103 +1,152 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, watch, ref } from 'vue';
|
import { onMounted, watch, ref } from 'vue';
|
||||||
import fm from 'front-matter';
|
import fm from 'front-matter';
|
||||||
|
|
||||||
import PostCard from '~/components/PostCard.vue';
|
import PostCard from '~/components/PostCard.vue';
|
||||||
import * as pages from '~/utils/page_updater/update_pagelist';
|
import * as pages from '~/utils/page_updater/update_pagelist';
|
||||||
import type { PageInfo, PageInfoMetdata } from '~/utils/page_updater/pages';
|
import type { PageInfo, PageInfoMetdata } from '~/utils/page_updater/pages';
|
||||||
import type { ParsedContent } from '@nuxt/content';
|
import type { ParsedContent } from '@nuxt/content';
|
||||||
import siteConfig from '~/assets/config';
|
import siteConfig from '~/assets/config';
|
||||||
|
|
||||||
let route = useRoute()
|
let route = useRoute();
|
||||||
console.log(route)
|
const { locale, setLocale } = useI18n();
|
||||||
|
console.log(route);
|
||||||
|
|
||||||
const url: Ref<string> = ref("")
|
const url: Ref<string> = ref('');
|
||||||
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;
|
||||||
|
|
||||||
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([])
|
const tagFilter: Ref<string[]> = ref([]);
|
||||||
tagFilter.value = []
|
tagFilter.value = [];
|
||||||
|
|
||||||
const markdown: Ref<any> = ref(null)
|
const markdown: Ref<any> = ref(null);
|
||||||
|
|
||||||
const title: Ref<string> = ref("")
|
const title: Ref<string> = ref('');
|
||||||
const description: Ref<string> = ref("")
|
const description: Ref<string> = ref('');
|
||||||
const date: Ref<string> = ref("")
|
const date: Ref<string> = ref('');
|
||||||
const tags: Ref<string[]> = ref([])
|
const tags: Ref<string[]> = ref([]);
|
||||||
const background: Ref<string> = ref("")
|
const background: Ref<string> = ref('');
|
||||||
const next: Ref<string> = ref("")
|
const next: Ref<string> = ref('');
|
||||||
const previous: Ref<string> = ref("")
|
const previous: Ref<string> = ref('');
|
||||||
|
|
||||||
function tagsToString(tags: String[]): string {
|
function tagsToString(tags: String[]): string {
|
||||||
var tagString = '';
|
var tagString = '';
|
||||||
for (let i = 0; i < tags.length; i++) {
|
for (let i = 0; i < tags.length; i++) {
|
||||||
tagString += tags[i];
|
tagString += tags[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
return tagString;
|
return tagString;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateMetadata(data: ParsedContent) {
|
function updateMetadata(data: ParsedContent) {
|
||||||
title.value = data.title ? data.title : ""
|
title.value = data.title ? data.title : '';
|
||||||
description.value = data.description ? data.description : ""
|
description.value = data.description ? data.description : '';
|
||||||
date.value = data.date ? new Date(data.date).toLocaleDateString() : ""
|
date.value = data.date ? new Date(data.date).toLocaleDateString() : '';
|
||||||
tags.value = data.tags ? data.tags : []
|
tags.value = data.tags ? data.tags : [];
|
||||||
background.value = data.background ? data.background : ""
|
background.value = data.background ? data.background : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// watch the params of the route to fetch the data again
|
// watch the params of the route to fetch the data again
|
||||||
watch(route, async () => {
|
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) {
|
if (url.value) {
|
||||||
console.log("Fetching article")
|
console.log('Fetching article');
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await fetchArticle(url.value)
|
await fetchArticle(url.value, route.params.lang as string);
|
||||||
}
|
} finally {
|
||||||
finally {
|
loading.value = false;
|
||||||
loading.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
immediate: true
|
immediate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch the article contents from the URL
|
async function fetchArticle(url: string, region?: string): Promise<any> {
|
||||||
async function fetchArticle(url: string): Promise<any> {
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
console.error('fetchArticle: No URL provided');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
// Trim the .md extension
|
// Trim the .md extension
|
||||||
var url = url.replace(/\.md$/, "")
|
var trimmedUrl = url.replace(/\.md$/, '');
|
||||||
console.log("Fetching article: " + url)
|
console.log('[id].vue - Fetching article: ' + trimmedUrl);
|
||||||
const { data } = await useAsyncData(url, () => queryContent(url).findOne())
|
|
||||||
console.log(data)
|
|
||||||
|
|
||||||
markdown.value = data.value;
|
// Define the available languages in the order of preference (falling back on the next if one fails)
|
||||||
updateMetadata(markdown.value)
|
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;
|
||||||
|
|
||||||
return data.value
|
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() {
|
function resetReadingPosition() {
|
||||||
window.scrollTo(0, 0)
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fetchArticle(url.value)
|
const data = await fetchArticle(url.value);
|
||||||
updateMetadata(data)
|
updateMetadata(data);
|
||||||
|
|
||||||
console.log("Prefetching article")
|
console.log('Prefetching article');
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log("Fetching article :3")
|
console.log('Fetching article :3');
|
||||||
await fetchArticle(url.value)
|
await fetchArticle(url.value);
|
||||||
})
|
});
|
||||||
const temp_url = route.query.post as string
|
|
||||||
|
const temp_url = route.query.post as string;
|
||||||
await fetchArticle(temp_url);
|
await fetchArticle(temp_url);
|
||||||
|
|
||||||
const fullTitle = data.title + " | " + siteConfig.siteTitle;
|
const fullTitle = data.title + ' | ' + siteConfig.siteTitle;
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: fullTitle,
|
title: fullTitle,
|
||||||
|
@ -122,7 +171,7 @@ useHead({
|
||||||
{ name: 'og:image', content: background },
|
{ name: 'og:image', content: background },
|
||||||
{ name: 'og:image:alt', content: fullTitle }
|
{ name: 'og:image:alt', content: fullTitle }
|
||||||
]
|
]
|
||||||
})
|
});
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: fullTitle,
|
title: fullTitle,
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, watch, ref } from 'vue';
|
import { onMounted, watch, ref, computed } from 'vue';
|
||||||
import fm from 'front-matter';
|
import fm from 'front-matter';
|
||||||
|
|
||||||
import PostCard from '~/components/PostCard.vue';
|
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 { PageInfo, PageInfoMetdata } from '~/utils/page_updater/pages';
|
||||||
import type { ParsedContent } from '@nuxt/content';
|
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.
|
const article_list_data = await import('~/assets/meta/post_list.json');
|
||||||
// This file is generated by a script in the utils/pageupdater folder.
|
const article_list: pages.PageList = pages.PageList.fromJSON(JSON.stringify(article_list_data));
|
||||||
const article_list: pages.PageList = await import('~/assets/meta/post_list.json') as pages.PageList;
|
|
||||||
|
|
||||||
let route = useRoute()
|
const loading: Ref<boolean> = ref(false);
|
||||||
console.log(route)
|
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)
|
// Create a map for languages (using Record<string, string[]>)
|
||||||
const view: Ref<string> = ref('list') // list, category
|
const languageMap: Record<string, string[]> = {};
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Extract the tags from each post in each category
|
let tags: Set<string> = new Set();
|
||||||
var tags: string[] = []
|
let languages: Set<string> = new Set();
|
||||||
for (const category in article_list.categories) {
|
|
||||||
console.log("Category: " + category)
|
// Populate the languageMap with languages from article_list
|
||||||
for (const post of article_list.categories[category].posts) {
|
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) {
|
if (post.metadata.tags) {
|
||||||
for (const tag of post.metadata.tags) {
|
post.metadata.tags.forEach(tag => tags.add(tag));
|
||||||
if (!tags.includes(tag)) {
|
}
|
||||||
console.log("Adding tag: " + tag)
|
|
||||||
tags.push(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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative z-50 flex w-full justify-center text-white">
|
<div class="relative z-50 flex w-full justify-center text-white">
|
||||||
<!-- Metadata -->
|
<MetaSet title="Articles" description="Ramblings." background="/images/me.png"
|
||||||
<MetaSet title="Articles" description="Ramblings."
|
:tags="['blog', 'personal', 'author']" />
|
||||||
background="/images/me.png" tags="blog, personal, author" />
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="mt-8 flex-col text-center">
|
<div class="mt-8 flex-col text-center">
|
||||||
<Transition name="list">
|
<Transition name="list">
|
||||||
<div>
|
<div>
|
||||||
<!-- Article List -->
|
|
||||||
<h1>Articles</h1>
|
<h1>Articles</h1>
|
||||||
|
|
||||||
<!-- View mode switcher -->
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button @click="view = 'list'"
|
<button @click="view = 'list'"
|
||||||
class="m-1 bg-black border-purple-400 border text-white p-1 rounded-md"
|
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'"
|
<button @click="view = 'category'"
|
||||||
class="m-1 bg-black border-purple-400 border text-white p-1 rounded-md"
|
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>
|
</div>
|
||||||
<!-- Tag selection -->
|
|
||||||
<div class="flex justify-center" v-if="view == 'list'">
|
<div class="flex justify-center" v-if="view == 'list'">
|
||||||
<div class="flex flex-wrap justify-center m-5 max-w-96">
|
<div class="flex flex-wrap justify-center m-5 max-w-96">
|
||||||
<div v-for="tag in tagList" :key="tag" class="m-1">
|
<div v-for="tag in tagList" :key="tag" class="m-1">
|
||||||
<button
|
<button
|
||||||
@click="tagFilter.includes(tag) ? tagFilter.splice(tagFilter.indexOf(tag), 1) : tagFilter.push(tag)"
|
@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="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'">{{
|
:class="tagFilter.includes(tag) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">
|
||||||
tag }}</button>
|
{{ tag }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Category selection -->
|
<div v-if="view == 'list'">
|
||||||
<div class="flex justify-center" v-if="view == 'category'">
|
<div v-for="post in filteredArticles" :key="post.id">
|
||||||
<div class="flex flex-wrap justify-center m-5 max-w-96">
|
<PostCard class="lg:w-[48rem]" :url="post.url" :tagFilter="tagFilter">
|
||||||
<div v-for="category in Object.keys(article_list.categories)" :key="category"
|
|
||||||
class="m-1">
|
<!-- Show available languages for the post using languageMap -->
|
||||||
<button
|
<div v-if="post.id">
|
||||||
@click="categoryFilter.includes(category) ? categoryFilter.splice(categoryFilter.indexOf(category), 1) : categoryFilter.push(category)"
|
<div class="text-sm text-gray-400">Available in:
|
||||||
class="text-xs bg-black border-purple-400 border text-white p-1 rounded-md"
|
<span v-for="(postIds, lang) in languageMap" :key="lang">
|
||||||
:class="categoryFilter.includes(category) ? 'border-2 border-white bg-slate-700' : 'border-2 bg-black text-white'">{{
|
<!-- Check if the post.id is in the array of postIds for the current language -->
|
||||||
category }}</button>
|
<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>
|
||||||
|
|
||||||
|
</PostCard>
|
||||||
</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>
|
|
||||||
</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" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -2,25 +2,46 @@
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Markdown from '~/components/Markdown.vue';
|
import Markdown from '~/components/Markdown.vue';
|
||||||
import Card from '~/components/Card.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 aboutMe = ref('');
|
||||||
const test = 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>
|
</script>
|
||||||
|
|
||||||
<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="Home" :description="siteTitle"
|
<MetaSet title="Home" :description="siteTitle" background="/images/me.png" tags="home, personal, author" />
|
||||||
background="/images/me.png" 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">
|
||||||
<div id="PFP" class="shadow-md rounded-full shadow-highlight">
|
<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"
|
<img class="transition-all w-40 h-40 md:w-56 md:h-56 rounded-full" src="/images/me.png"
|
||||||
src="/images/me.png" alt="User PFP" />
|
alt="User PFP" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
16
public/images/flags/en.svg
Normal file
16
public/images/flags/en.svg
Normal 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 |
76
public/images/flags/tp.svg
Normal file
76
public/images/flags/tp.svg
Normal 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 |
|
@ -9,6 +9,7 @@ export interface PageLocation {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
map: string;
|
map: string;
|
||||||
root: string;
|
root: string;
|
||||||
|
show: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageInfoMetdata {
|
export interface PageInfoMetdata {
|
||||||
|
|
107
utils/page_updater/run.ts
Normal file
107
utils/page_updater/run.ts
Normal 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());
|
|
@ -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 {
|
export interface Page {
|
||||||
|
/** Metadata for the page (e.g., title, date, etc.) */
|
||||||
metadata: pages.PageInfoMetdata;
|
metadata: pages.PageInfoMetdata;
|
||||||
|
/** The page's identifier, e.g. "en/page.md" */
|
||||||
id: string;
|
id: string;
|
||||||
|
/** The URL for the page (relative to content) */
|
||||||
url: string;
|
url: string;
|
||||||
|
/** A hash representing the page's content/version */
|
||||||
hash: string;
|
hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface representing a category that groups multiple pages.
|
||||||
|
*/
|
||||||
export interface PageCategory {
|
export interface PageCategory {
|
||||||
|
/** An array of pages that belong to this category */
|
||||||
posts: Page[];
|
posts: Page[];
|
||||||
|
/** The title of the category */
|
||||||
title: string;
|
title: string;
|
||||||
|
/** A description for the category */
|
||||||
description: string;
|
description: string;
|
||||||
|
/** Tags associated with the category */
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
show: boolean;
|
||||||
export interface PageList {
|
|
||||||
last_generated: string;
|
|
||||||
categories: Record<string, PageCategory>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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[] = [];
|
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 = {
|
const pageDict: Page = {
|
||||||
metadata: page.metadata,
|
metadata: page.metadata,
|
||||||
id: page.local_path,
|
id: page.local_path, // e.g. "en/page.md"
|
||||||
url: page.absolute_path.replace("content", ""),
|
url: page.absolute_path.replace("content/", ""), // Remove "content/" prefix
|
||||||
hash: page.hash
|
hash: page.hash,
|
||||||
};
|
};
|
||||||
pageList.push(pageDict);
|
pageList.push(pageDict);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageList.forEach(page => {
|
// Format dates consistently.
|
||||||
|
pageList.forEach((page) => {
|
||||||
if (page.metadata.date) {
|
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,
|
posts: pageList,
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
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",
|
title: "Site",
|
||||||
description: "Articles to test site functionality",
|
description: "Articles to test site functionality",
|
||||||
tags: ["site"],
|
tags: ["site"],
|
||||||
map: "site",
|
map: "site",
|
||||||
root: "content/site"
|
root: "", // Not used in the new structure
|
||||||
|
show: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Collections",
|
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"],
|
tags: ["collection"],
|
||||||
map: "collections",
|
map: "collections",
|
||||||
root: "content/collections"
|
root: "",
|
||||||
|
show: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Guides",
|
title: "Guides",
|
||||||
description: "Guides and tutorials",
|
description: "Guides and tutorials",
|
||||||
tags: ["guide"],
|
tags: ["guide"],
|
||||||
map: "guides",
|
map: "guides",
|
||||||
root: "content/guides"
|
root: "",
|
||||||
}
|
show: true
|
||||||
]
|
},
|
||||||
|
];
|
||||||
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));
|
|
8
vitest.config.ts
Normal file
8
vitest.config.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineVitestConfig } from "@nuxt/test-utils/config";
|
||||||
|
|
||||||
|
export default defineVitestConfig({
|
||||||
|
// any custom Vitest config you require
|
||||||
|
test: {
|
||||||
|
environment: "nuxt",
|
||||||
|
},
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue