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 && {`avatar} + {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}"

+
    + {posts.map((post: any) => +

    + {new Date(post.data.pubDate).toISOString().split('T')[0]} + {post.data.title} +

    + )} +
+
\ No newline at end of file