diff --git a/src/components/helper/authors/Info.astro b/src/components/helper/authors/Info.astro
new file mode 100644
index 0000000..806a143
--- /dev/null
+++ b/src/components/helper/authors/Info.astro
@@ -0,0 +1,29 @@
+---
+import {Image} from "astro:assets";
+import {getEntry} from "astro:content";
+import {siteConfig} from "../../../config";
+
+const { id } = Astro.props;
+
+// Get author data
+const authorData = await getEntry('authors', id || '');
+const authorAvatar = authorData?.data.mcplayerid ? `/images/avatars/${id}.png` : null;
+const authorName = authorData ? authorData.data.name : null;
+---
+{(siteConfig.displayAvatar && authorData) &&
+ <>
+ {authorAvatar && }
+ {authorName} @
+ >
+ }
+
\ No newline at end of file
diff --git a/src/config.ts b/src/config.ts
index 709cb5d..c5618a8 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -26,6 +26,8 @@ export const siteConfig = {
// search
// This only works when noClientJavaScript is enabled
searchEngine: 'bing', // 'google', 'duckduckgo', 'bing'(broken until M1cr0$0ft get support for it), defaults to 'google'
+ // content
+ displayAvatar: true, // display author avatar in the article list and info line of article page
// footer
// yes you can write html safely here
customFooter: 'I have no mouth, and I must SCREAM',
diff --git a/src/content/config.ts b/src/content/config.ts
index d305fc5..b250015 100644
--- a/src/content/config.ts
+++ b/src/content/config.ts
@@ -27,6 +27,7 @@ const authorsData = defineCollection({
schema: z.object({
name: z.string().default(siteConfig.defaultAuthor.name),
email: z.string().email().default(siteConfig.defaultAuthor.email),
+ mcplayerid: z.string().optional(),
social: z.object({
twitter: z.string().optional(),
fediverse: z.string().optional(),
diff --git a/src/content/posts/_schemas.ts b/src/content/posts/_schemas.ts
index bc668d1..15806c1 100644
--- a/src/content/posts/_schemas.ts
+++ b/src/content/posts/_schemas.ts
@@ -5,6 +5,8 @@ export const posts = ({ image }) => z.object({
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
+ categories: z.array(z.string()).optional().default(['uncategorized']),
+ tags: z.array(z.string()).optional().default([]),
cover: image().optional(),
author: z.string().optional(),
});
\ No newline at end of file
diff --git a/src/content/posts/minimalism/index.md b/src/content/posts/minimalism/index.md
index c29b2bd..24c9388 100644
--- a/src/content/posts/minimalism/index.md
+++ b/src/content/posts/minimalism/index.md
@@ -3,6 +3,10 @@ title: 'The Art of Minimalism'
description: 'Thoughts on minimalism in design and code'
pubDate: '2025-06-05'
author: 'Wheatley'
+tags:
+ - 'design'
+ - 'code'
+ - 'minimalism'
---
Minimalism isn't just about having less, it's about making room for what matters.
diff --git a/src/data/authors.yaml b/src/data/authors.yaml
index 613201f..5cf773a 100644
--- a/src/data/authors.yaml
+++ b/src/data/authors.yaml
@@ -1,6 +1,7 @@
Wheatley: # the key name (id) of the author, which is used in the front matter
name: "Wheatley" # the display name of the author
email: "hello@example.org" # the email address of the author
+ mcplayerid: "Wheatley" # the Minecraft player ID of the author, if applicable
social: # the social media accounts of the author, if any (there is no reference for this yet except for the twitter handle)
twitter: "@wheatley"
fediverse: "@"
diff --git a/src/pages/categories.astro b/src/pages/categories.astro
new file mode 100644
index 0000000..bae76c3
--- /dev/null
+++ b/src/pages/categories.astro
@@ -0,0 +1,17 @@
+---
+import {getCollection} from "astro:content";
+import Layout from "../layouts/Layout.astro";
+const allPosts = await getCollection('posts');
+const uniqueCategories = [...new Set(allPosts.map((post: any) => post.data.categories ? post.data.categories : []).flat())];
+---
+
+ ~/blog/categories
+
+
ls -l categories/
+
+ {uniqueCategories.map((tag) => (
+
{tag}
+ ))}
+
+
+
diff --git a/src/pages/categories/[...category].astro b/src/pages/categories/[...category].astro
new file mode 100644
index 0000000..4270589
--- /dev/null
+++ b/src/pages/categories/[...category].astro
@@ -0,0 +1,33 @@
+---
+import Layout from '../../layouts/Layout.astro';
+import {getCollection} from "astro:content";
+import {categoryLabel} from "astro/client/dev-toolbar/apps/audit/rules";
+
+export async function getStaticPaths() {
+ const allPosts = await getCollection('posts');
+ console.log(allPosts)
+ const uniqueCategories = [...new Set(allPosts.map((post: any) => post.data.categories ? post.data.categories : []).flat())];
+ return uniqueCategories.map((category) => {
+ const filteredPosts = allPosts.filter((post: any) => post.data.categories?.includes(category));
+ return {
+ params: { category },
+ props: { posts: filteredPosts },
+ };
+ });
+}
+
+const { category } = Astro.params;
+
+const { posts } = Astro.props;
+---
+
+ ls ~/blog | grep "{category}"
+
+
\ No newline at end of file
diff --git a/src/pages/images/avatars/[author].png.js b/src/pages/images/avatars/[author].png.js
new file mode 100644
index 0000000..57360b4
--- /dev/null
+++ b/src/pages/images/avatars/[author].png.js
@@ -0,0 +1,125 @@
+import { getCollection } from 'astro:content';
+
+export async function getStaticPaths() {
+ const authorsData = await getCollection('authors');
+ return authorsData.map(author => ({
+ params: { author: author.id },
+ props: { author }
+ }));
+}
+
+export async function GET({ props }) {
+ const { author } = props;
+
+ if (!author.data.mcplayerid) {
+ return new Response(null, { status: 404 });
+ }
+
+ const username = author.data.mcplayerid;
+
+ try {
+ // get Minecraft profile by username
+ const profileResponse = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`);
+
+ if (!profileResponse.ok) {
+ return new Response('Player not found', { status: 404 });
+ }
+
+ const profile = await profileResponse.json();
+ const uuid = profile.id;
+
+ // get skin data from session server
+ const sessionResponse = await fetch(`https://sessionserver.mojang.com/session/minecraft/profile/${uuid}`);
+ const sessionData = await sessionResponse.json();
+
+ const texturesProperty = sessionData.properties.find((prop) => prop.name === 'textures');
+ const texturesData = JSON.parse(atob(texturesProperty.value));
+ const skinUrl = texturesData.textures.SKIN?.url;
+ if (!skinUrl) {
+ return new Response('Skin not found', { status: 404 });
+ }
+
+ // get skin image from the URL
+ const skinResponse = await fetch(skinUrl);
+ const skinBuffer = await skinResponse.arrayBuffer();
+
+ // render the Minecraft head image
+ const headImage = await renderMinecraftHead(new Uint8Array(skinBuffer));
+
+ return new Response(headImage, {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Cache-Control': 'public, max-age=3600', // 缓存1小时
+ },
+ });
+
+ } catch (error) {
+ console.error('Error fetching Minecraft head:', error);
+ return new Response('Internal server error', { status: 500 });
+ }
+}
+
+async function renderMinecraftHead(skinData) {
+ // Use sharp library to process images
+ const sharp = (await import('sharp')).default;
+
+ // Load the skin image
+ const skinImage = sharp(skinData);
+ const metadata = await skinImage.metadata();
+ const { width, height } = metadata;
+
+ // Determine skin format (64x32 old format or 64x64 new format)
+ const isNewFormat = height === 64;
+ const headSize = 8; // Head is 8x8 pixels
+ const scale = 8; // Scale factor, final output is 64x64
+
+ // 3D-like effect: slightly offset hat layer
+ // TODO: real 3D effect, which would require more complex rendering
+ const offset = -1; // Negative value moves up/left (creates 3D effect)
+
+ // Extract head base layer (8x8 pixels)
+ const headBase = await skinImage
+ .clone() // Clone to avoid modifying original
+ .extract({ left: 8, top: 8, width: headSize, height: headSize })
+ .png()
+ .toBuffer();
+
+ let finalHead = sharp(headBase).resize(headSize * scale, headSize * scale, {
+ kernel: 'nearest' // Keep pixel art style
+ });
+
+ // If new format and has hat layer, composite hat layer
+ if (isNewFormat) {
+ try {
+ // Check if we're in bounds before extracting hat layer
+ if (width >= 48 && height >= 16) {
+ // Extract hat layer (8x8 pixels)
+ const hatLayer = await skinImage
+ .clone() // Clone to avoid modifying original
+ .extract({ left: 40, top: 8, width: headSize, height: headSize })
+ .png()
+ .toBuffer();
+
+ // Resize hat layer
+ const hatResized = await sharp(hatLayer)
+ .resize(headSize * scale, headSize * scale, { kernel: 'nearest' })
+ .png()
+ .toBuffer();
+
+ // Composite base layer and hat layer with offset for 3D effect
+ finalHead = finalHead.composite([{
+ input: hatResized,
+ left: offset * scale, // Apply scaled offset horizontally
+ top: offset * scale, // Apply scaled offset vertically
+ blend: 'over'
+ }]);
+ }
+ } catch (error) {
+ // If hat layer processing fails, just use base layer
+ console.warn('Failed to process hat layer:', error);
+ }
+ }
+
+ const result = await finalHead.png().toBuffer();
+ return new Uint8Array(result);
+}
\ No newline at end of file
diff --git a/src/pages/post/[...slug].astro b/src/pages/post/[...slug].astro
index ae5d4db..e430eed 100644
--- a/src/pages/post/[...slug].astro
+++ b/src/pages/post/[...slug].astro
@@ -6,6 +6,7 @@ import {getImage} from "astro:assets";
import {siteConfig} from "../../config";
import ReplyViaEmail from "../../components/ReplyViaEmail.astro";
import { ExtractFirstImage } from '../../plugins/extract-images';
+import AuthorInfo from "../../components/helper/authors/Info.astro";
export async function getStaticPaths() {
const blogEntries = await getCollection('posts');
@@ -45,6 +46,7 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
author={authorInfo.name}
>
{entry.data.title}
+
{new Date(entry.data.pubDate).toISOString().split('T')[0]}
diff --git a/src/pages/tags.astro b/src/pages/tags.astro
new file mode 100644
index 0000000..340f690
--- /dev/null
+++ b/src/pages/tags.astro
@@ -0,0 +1,17 @@
+---
+import {getCollection} from "astro:content";
+import Layout from "../layouts/Layout.astro";
+const allPosts = await getCollection('posts');
+const uniqueTags = [...new Set(allPosts.map((post: any) => post.data.tags ? post.data.tags : []).flat())];
+---
+
+ ~/blog/tags
+
+
ls -l tags/
+
+ {uniqueTags.map((tag) => (
+
{tag}
+ ))}
+
+
+
diff --git a/src/pages/tags/[...tag].astro b/src/pages/tags/[...tag].astro
new file mode 100644
index 0000000..3b834c1
--- /dev/null
+++ b/src/pages/tags/[...tag].astro
@@ -0,0 +1,31 @@
+---
+import Layout from '../../layouts/Layout.astro';
+import {getCollection} from "astro:content";
+
+export async function getStaticPaths() {
+ const allPosts = await getCollection('posts');
+ const uniqueTags = [...new Set(allPosts.map((post: any) => post.data.tags ? post.data.tags : []).flat())];
+ return uniqueTags.map((tag) => {
+ const filteredPosts = allPosts.filter((post: any) => post.data.tags?.includes(tag));
+ return {
+ params: { tag },
+ props: { posts: filteredPosts },
+ };
+ });
+}
+
+const { tag } = Astro.params;
+
+const { posts } = Astro.props;
+---
+
+ ls ~/blog | grep "{tag}"
+
+
\ No newline at end of file