Compare commits
No commits in common. "00115ad5a0f2e043b639d9e92cab410aaadd0ece" and "c7edf035c114ac977e34ea9385b565670faeaec0" have entirely different histories.
00115ad5a0
...
c7edf035c1
12 changed files with 0 additions and 264 deletions
|
@ -1,29 +0,0 @@
|
||||||
---
|
|
||||||
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 && <Image src={authorAvatar} alt={`avatar of ${authorName}`} width=16 height=16 />}
|
|
||||||
<span>{authorName} @ </span>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<style>
|
|
||||||
img + span {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
img {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -26,8 +26,6 @@ export const siteConfig = {
|
||||||
// search
|
// search
|
||||||
// This only works when noClientJavaScript is enabled
|
// This only works when noClientJavaScript is enabled
|
||||||
searchEngine: 'bing', // 'google', 'duckduckgo', 'bing'(broken until M1cr0$0ft get support for it), defaults to 'google'
|
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
|
// footer
|
||||||
// yes you can write html safely here
|
// yes you can write html safely here
|
||||||
customFooter: '<i>I have no mouth, and I must SCREAM</i>',
|
customFooter: '<i>I have no mouth, and I must SCREAM</i>',
|
||||||
|
|
|
@ -27,7 +27,6 @@ const authorsData = defineCollection({
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
name: z.string().default(siteConfig.defaultAuthor.name),
|
name: z.string().default(siteConfig.defaultAuthor.name),
|
||||||
email: z.string().email().default(siteConfig.defaultAuthor.email),
|
email: z.string().email().default(siteConfig.defaultAuthor.email),
|
||||||
mcplayerid: z.string().optional(),
|
|
||||||
social: z.object({
|
social: z.object({
|
||||||
twitter: z.string().optional(),
|
twitter: z.string().optional(),
|
||||||
fediverse: z.string().optional(),
|
fediverse: z.string().optional(),
|
||||||
|
|
|
@ -5,8 +5,6 @@ export const posts = ({ image }) => z.object({
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
pubDate: z.coerce.date(),
|
pubDate: z.coerce.date(),
|
||||||
updatedDate: z.coerce.date().optional(),
|
updatedDate: z.coerce.date().optional(),
|
||||||
categories: z.array(z.string()).optional().default(['uncategorized']),
|
|
||||||
tags: z.array(z.string()).optional().default([]),
|
|
||||||
cover: image().optional(),
|
cover: image().optional(),
|
||||||
author: z.string().optional(),
|
author: z.string().optional(),
|
||||||
});
|
});
|
|
@ -3,10 +3,6 @@ title: 'The Art of Minimalism'
|
||||||
description: 'Thoughts on minimalism in design and code'
|
description: 'Thoughts on minimalism in design and code'
|
||||||
pubDate: '2025-06-05'
|
pubDate: '2025-06-05'
|
||||||
author: 'Wheatley'
|
author: 'Wheatley'
|
||||||
tags:
|
|
||||||
- 'design'
|
|
||||||
- 'code'
|
|
||||||
- 'minimalism'
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Minimalism isn't just about having less, it's about making room for what matters.
|
Minimalism isn't just about having less, it's about making room for what matters.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
Wheatley: # the key name (id) of the author, which is used in the front matter
|
Wheatley: # the key name (id) of the author, which is used in the front matter
|
||||||
name: "Wheatley" # the display name of the author
|
name: "Wheatley" # the display name of the author
|
||||||
email: "hello@example.org" # the email address 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)
|
social: # the social media accounts of the author, if any (there is no reference for this yet except for the twitter handle)
|
||||||
twitter: "@wheatley"
|
twitter: "@wheatley"
|
||||||
fediverse: "@"
|
fediverse: "@"
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
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())];
|
|
||||||
---
|
|
||||||
<Layout title="Categoies" description="List all categories used in the blog posts.">
|
|
||||||
<h1 class="title">~/blog/categories</h1>
|
|
||||||
<div style="margin-top: 2rem;">
|
|
||||||
<span class="command">ls -l categories/</span>
|
|
||||||
<div style="margin-top: 1rem; margin-left: 1rem;">
|
|
||||||
{uniqueCategories.map((tag) => (
|
|
||||||
<p><a href={`/categories/${tag}`}>{tag}</a></p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
|
@ -1,33 +0,0 @@
|
||||||
---
|
|
||||||
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;
|
|
||||||
---
|
|
||||||
<Layout title={`posts tagged with ${category}`} description={`Posts tagged with ${category}`}>
|
|
||||||
<h1 class="title">ls ~/blog | grep "{category}"</h1>
|
|
||||||
<ul>
|
|
||||||
{posts.map((post: any) =>
|
|
||||||
<p>
|
|
||||||
<span class="list-date">{new Date(post.data.pubDate).toISOString().split('T')[0]}</span>
|
|
||||||
<a href={`/post/${post.slug}`}>{post.data.title}</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</Layout>
|
|
|
@ -1,125 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import {getImage} from "astro:assets";
|
||||||
import {siteConfig} from "../../config";
|
import {siteConfig} from "../../config";
|
||||||
import ReplyViaEmail from "../../components/ReplyViaEmail.astro";
|
import ReplyViaEmail from "../../components/ReplyViaEmail.astro";
|
||||||
import { ExtractFirstImage } from '../../plugins/extract-images';
|
import { ExtractFirstImage } from '../../plugins/extract-images';
|
||||||
import AuthorInfo from "../../components/helper/authors/Info.astro";
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const blogEntries = await getCollection('posts');
|
const blogEntries = await getCollection('posts');
|
||||||
|
@ -46,7 +45,6 @@ const cover = customFeaturedImage || matchedImage_src?.src || firstImageURL || `
|
||||||
author={authorInfo.name}
|
author={authorInfo.name}
|
||||||
>
|
>
|
||||||
<h1 class="title">{entry.data.title}</h1>
|
<h1 class="title">{entry.data.title}</h1>
|
||||||
<AuthorInfo id={authorId} />
|
|
||||||
<span class="date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
|
<span class="date">{new Date(entry.data.pubDate).toISOString().split('T')[0]}</span>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Content />
|
<Content />
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
---
|
|
||||||
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())];
|
|
||||||
---
|
|
||||||
<Layout title="Tags" description="List all tags used in the blog posts.">
|
|
||||||
<h1 class="title">~/blog/tags</h1>
|
|
||||||
<div style="margin-top: 2rem;">
|
|
||||||
<span class="command">ls -l tags/</span>
|
|
||||||
<div style="margin-top: 1rem; margin-left: 1rem;">
|
|
||||||
{uniqueTags.map((tag) => (
|
|
||||||
<p><a href={`/tags/${tag}`}>{tag}</a></p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
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;
|
|
||||||
---
|
|
||||||
<Layout title={`posts tagged with ${tag}`} description={`Posts tagged with ${tag}`}>
|
|
||||||
<h1 class="title">ls ~/blog | grep "{tag}"</h1>
|
|
||||||
<ul>
|
|
||||||
{posts.map((post: any) =>
|
|
||||||
<p>
|
|
||||||
<span class="list-date">{new Date(post.data.pubDate).toISOString().split('T')[0]}</span>
|
|
||||||
<a href={`/post/${post.slug}`}>{post.data.title}</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</Layout>
|
|
Loading…
Add table
Add a link
Reference in a new issue