-
\ No newline at end of file
+
diff --git a/components/PostCard.vue b/components/PostCard.vue
index 7104e1c..4bca493 100644
--- a/components/PostCard.vue
+++ b/components/PostCard.vue
@@ -67,10 +67,12 @@ async function fetchData(url: string) {
onMounted(() => {
fetchData(url.value)
})
+
+const localePath = useLocalePath();
-
+
@@ -97,6 +99,9 @@ onMounted(() => {
+
+
+
{{ description }}
@@ -139,6 +144,9 @@ onMounted(() => {
+
+
+
@@ -159,6 +167,7 @@ onMounted(() => {
{{ title }}
{{ date }}
+
@@ -179,6 +188,7 @@ onMounted(() => {
{{ title }}
+
diff --git a/content.config.ts b/content.config.ts
new file mode 100644
index 0000000..892b91a
--- /dev/null
+++ b/content.config.ts
@@ -0,0 +1,10 @@
+import { defineCollection, defineContentConfig } from "@nuxt/content";
+
+export default defineContentConfig({
+ collections: {
+ content: defineCollection({
+ type: "page",
+ source: "**/**/**/*.md",
+ }),
+ },
+});
diff --git a/content/collections/awesome.md b/content/en/collections/awesome.md
similarity index 100%
rename from content/collections/awesome.md
rename to content/en/collections/awesome.md
diff --git a/content/collections/badges.md b/content/en/collections/badges.md
similarity index 100%
rename from content/collections/badges.md
rename to content/en/collections/badges.md
diff --git a/content/collections/godot.md b/content/en/collections/godot.md
similarity index 77%
rename from content/collections/godot.md
rename to content/en/collections/godot.md
index c69d741..5126ff9 100644
--- a/content/collections/godot.md
+++ b/content/en/collections/godot.md
@@ -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.
::
\ No newline at end of file
diff --git a/content/collections/lgbtq_resources.md b/content/en/collections/lgbtq_resources.md
similarity index 96%
rename from content/collections/lgbtq_resources.md
rename to content/en/collections/lgbtq_resources.md
index 21c3e92..fb0f176 100644
--- a/content/collections/lgbtq_resources.md
+++ b/content/en/collections/lgbtq_resources.md
@@ -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.
+- [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)
diff --git a/content/en/collections/neurodiverse_resources.md b/content/en/collections/neurodiverse_resources.md
new file mode 100644
index 0000000..eff2952
--- /dev/null
+++ b/content/en/collections/neurodiverse_resources.md
@@ -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.
+::
+
diff --git a/content/guides/old3ds_helloworld.md b/content/en/guides/old3ds_helloworld.md
similarity index 100%
rename from content/guides/old3ds_helloworld.md
rename to content/en/guides/old3ds_helloworld.md
diff --git a/content/guides/old3ds_romfs.md b/content/en/guides/old3ds_romfs.md
similarity index 100%
rename from content/guides/old3ds_romfs.md
rename to content/en/guides/old3ds_romfs.md
diff --git a/content/guides/old3ds_touchscreen.md b/content/en/guides/old3ds_touchscreen.md
similarity index 100%
rename from content/guides/old3ds_touchscreen.md
rename to content/en/guides/old3ds_touchscreen.md
diff --git a/content/about_me.md b/content/en/site/about_me.md
similarity index 90%
rename from content/about_me.md
rename to content/en/site/about_me.md
index 90cc361..20e59e8 100644
--- a/content/about_me.md
+++ b/content/en/site/about_me.md
@@ -5,9 +5,9 @@ prop: true
# 🌙 Luna - She/They/Fae 🌙
-
+Profile picture by Chereverie on [Picrew](https://picrew.me/en/image_maker/100365)
-[`mrrpnya@proton.me`](mailto:thelunacy@proton.me)
+
-
-
\ No newline at end of file
diff --git a/pages/index.vue b/pages/index.vue
index 81f85ba..0fb2e36 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -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());
-
+
-
+
diff --git a/public/images/flags/en.svg b/public/images/flags/en.svg
new file mode 100644
index 0000000..9131483
--- /dev/null
+++ b/public/images/flags/en.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/public/images/flags/tp.svg b/public/images/flags/tp.svg
new file mode 100644
index 0000000..d0bea78
--- /dev/null
+++ b/public/images/flags/tp.svg
@@ -0,0 +1,76 @@
+
+
+
+
diff --git a/public/images/me.png b/public/images/me.png
index 482acc9..fd277de 100644
Binary files a/public/images/me.png and b/public/images/me.png differ
diff --git a/utils/page_updater/pages.ts b/utils/page_updater/pages.ts
index 3fba65b..9938aef 100644
--- a/utils/page_updater/pages.ts
+++ b/utils/page_updater/pages.ts
@@ -9,6 +9,7 @@ export interface PageLocation {
tags: string[];
map: string;
root: string;
+ show: boolean;
}
export interface PageInfoMetdata {
diff --git a/utils/page_updater/run.ts b/utils/page_updater/run.ts
new file mode 100644
index 0000000..a6b69c3
--- /dev/null
+++ b/utils/page_updater/run.ts
@@ -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());
diff --git a/utils/page_updater/update_pagelist.ts b/utils/page_updater/update_pagelist.ts
index 1c7b1c6..2887eed 100644
--- a/utils/page_updater/update_pagelist.ts
+++ b/utils/page_updater/update_pagelist.ts
@@ -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;
+ show: boolean;
}
-// Function to generate the page list
-function generatePageCategory(pagesInfo: Record): 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;
+ };
+ };
+
+ 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;
+ 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): 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));
\ No newline at end of file
+ root: "",
+ show: true
+ },
+];
\ No newline at end of file
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..5413b8c
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineVitestConfig } from "@nuxt/test-utils/config";
+
+export default defineVitestConfig({
+ // any custom Vitest config you require
+ test: {
+ environment: "nuxt",
+ },
+});