Initial commit

This commit is contained in:
KazooTTT
2025-02-05 14:07:58 +08:00
committed by GitHub
commit ea645368e9
85 changed files with 11210 additions and 0 deletions

13
src/pages/404.astro Normal file
View File

@ -0,0 +1,13 @@
---
import PageLayout from "@/layouts/Base.astro";
const meta = {
description: "Oops! It looks like this page is lost in space!",
title: "Oops! You found a missing page!",
};
---
<PageLayout meta={meta}>
<h1 class="title mb-6">404 | Oops something went wrong</h1>
<p class="mb-8">Please use the navigation to find your way back</p>
</PageLayout>

36
src/pages/about.astro Normal file
View File

@ -0,0 +1,36 @@
---
import PageLayout from "@/layouts/Base.astro";
const meta = {
description: "I'm a starter theme for Astro.build",
title: "About",
};
---
<PageLayout meta={meta}>
<h1 class="title mb-6">About</h1>
<div class="prose prose-sm prose-cactus max-w-none">
<p>
Hi, Im a starter Astro. Im particularly great for getting you started with your own blogging
website.
</p>
<p>Here are my some of my awesome built in features:</p>
<ul class="list-inside list-disc" role="list">
<li>I'm ultra fast as I'm a static site</li>
<li>I'm fully responsive</li>
<li>I come with a light and dark mode</li>
<li>I'm easy to customise and add additional content</li>
<li>I have Tailwind CSS styling</li>
<li>Shiki code syntax highlighting</li>
<li>Satori for auto generating OG images for blog posts</li>
</ul>
<p>
Clone or fork my <a
class="cactus-link inline-block"
href="https://github.com/chrismwilliams/astro-cactus"
rel="noreferrer"
target="_blank">repo</a
> if you like me!
</p>
</div>
</PageLayout>

61
src/pages/index.astro Normal file
View File

@ -0,0 +1,61 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import SocialList from "@/components/SocialList.astro";
import PostPreview from "@/components/blog/PostPreview.astro";
import Note from "@/components/note/Note.astro";
import { getAllPosts } from "@/data/post";
import PageLayout from "@/layouts/Base.astro";
import { collectionDateSort } from "@/utils/date";
// Posts
const MAX_POSTS = 10;
const allPosts = await getAllPosts();
const allPostsByDate = allPosts
.sort(collectionDateSort)
.slice(0, MAX_POSTS) as CollectionEntry<"post">[];
// Notes
const MAX_NOTES = 5;
const allNotes = await getCollection("note");
const latestNotes = allNotes.sort(collectionDateSort).slice(0, MAX_NOTES);
---
<PageLayout meta={{ title: "Home" }}>
<section>
<h1 class="title mb-6">Hello World!</h1>
<p class="mb-4">
Hi, Im a theme for Astro, a simple starter that you can use to create your website or blog.
If you want to know more about how you can customise me, add more posts, and make it your own,
click on the GitHub icon link below and it will take you to my repo.
</p>
<SocialList />
</section>
<section class="mt-16">
<h2 class="title text-accent mb-6 text-xl"><a href="/posts/">Posts</a></h2>
<ul class="space-y-4" role="list">
{
allPostsByDate.map((p) => (
<li class="grid gap-2 sm:grid-cols-[auto_1fr]">
<PostPreview post={p} />
</li>
))
}
</ul>
</section>
{
latestNotes.length > 0 && (
<section class="mt-16">
<h2 class="title text-accent mb-6 text-xl">
<a href="/notes/">Notes</a>
</h2>
<ul class="space-y-4" role="list">
{latestNotes.map((note) => (
<li>
<Note note={note} as="h3" isPreview />
</li>
))}
</ul>
</section>
)
}
</PageLayout>

View File

@ -0,0 +1,63 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import Pagination from "@/components/Paginator.astro";
import Note from "@/components/note/Note.astro";
import PageLayout from "@/layouts/Base.astro";
import { collectionDateSort } from "@/utils/date";
import type { GetStaticPaths, Page } from "astro";
import { Icon } from "astro-icon/components";
export const getStaticPaths = (async ({ paginate }) => {
const MAX_NOTES_PER_PAGE = 10;
const allNotes = await getCollection("note");
return paginate(allNotes.sort(collectionDateSort), { pageSize: MAX_NOTES_PER_PAGE });
}) satisfies GetStaticPaths;
interface Props {
page: Page<CollectionEntry<"note">>;
uniqueTags: string[];
}
const { page } = Astro.props;
const meta = {
description: "Read my collection of notes",
title: "Notes",
};
const paginationProps = {
...(page.url.prev && {
prevUrl: {
text: "← Previous Page",
url: page.url.prev,
},
}),
...(page.url.next && {
nextUrl: {
text: "Next Page →",
url: page.url.next,
},
}),
};
---
<PageLayout meta={meta}>
<section>
<h1 class="title mb-6 flex items-center gap-3">
Notes <a class="text-accent" href="/notes/rss.xml" target="_blank">
<span class="sr-only">RSS feed</span>
<Icon aria-hidden="true" class="h-6 w-6" focusable="false" name="mdi:rss" />
</a>
</h1>
<ul class="mt-6 space-y-8 text-start">
{
page.data.map((note) => (
<li class="">
<Note note={note} as="h2" isPreview />
</li>
))
}
</ul>
<Pagination {...paginationProps} />
</section>
</PageLayout>

View File

@ -0,0 +1,31 @@
---
import { getCollection } from "astro:content";
import Note from "@/components/note/Note.astro";
import PageLayout from "@/layouts/Base.astro";
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
export const getStaticPaths = (async () => {
const allNotes = await getCollection("note");
return allNotes.map((note) => ({
params: { slug: note.id },
props: { note },
}));
}) satisfies GetStaticPaths;
export type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { note } = Astro.props;
const meta = {
description:
note.data.description ||
`Read about my note posted on: ${note.data.publishDate.toLocaleDateString()}`,
title: note.data.title,
};
---
<PageLayout meta={meta}>
<Note as="h1" note={note} />
</PageLayout>

View File

@ -0,0 +1,18 @@
import { getCollection } from "astro:content";
import { siteConfig } from "@/site.config";
import rss from "@astrojs/rss";
export const GET = async () => {
const notes = await getCollection("note");
return rss({
title: siteConfig.title,
description: siteConfig.description,
site: import.meta.env.SITE,
items: notes.map((note) => ({
title: note.data.title,
pubDate: note.data.publishDate,
link: `notes/${note.id}/`,
})),
});
};

View File

@ -0,0 +1,90 @@
import RobotoMonoBold from "@/assets/roboto-mono-700.ttf";
import RobotoMono from "@/assets/roboto-mono-regular.ttf";
import { getAllPosts } from "@/data/post";
import { siteConfig } from "@/site.config";
import { getFormattedDate } from "@/utils/date";
import { Resvg } from "@resvg/resvg-js";
import type { APIContext, InferGetStaticPropsType } from "astro";
import satori, { type SatoriOptions } from "satori";
import { html } from "satori-html";
const ogOptions: SatoriOptions = {
// debug: true,
fonts: [
{
data: Buffer.from(RobotoMono),
name: "Roboto Mono",
style: "normal",
weight: 400,
},
{
data: Buffer.from(RobotoMonoBold),
name: "Roboto Mono",
style: "normal",
weight: 700,
},
],
height: 630,
width: 1200,
};
const markup = (title: string, pubDate: string) =>
html`<div tw="flex flex-col w-full h-full bg-[#1d1f21] text-[#c9cacc]">
<div tw="flex flex-col flex-1 w-full p-10 justify-center">
<p tw="text-2xl mb-6">${pubDate}</p>
<h1 tw="text-6xl font-bold leading-snug text-white">${title}</h1>
</div>
<div tw="flex items-center justify-between w-full p-10 border-t border-[#2bbc89] text-xl">
<div tw="flex items-center">
<svg height="60" fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 272 480">
<path
d="M181.334 93.333v-40L226.667 80v40l-45.333-26.667ZM136.001 53.333 90.667 26.667v426.666L136.001 480V53.333Z"
fill="#B04304"
></path>
<path
d="m136.001 119.944 45.333-26.667 45.333 26.667-45.333 26.667-45.333-26.667ZM90.667 26.667 136.001 0l45.333 26.667-45.333 26.666-45.334-26.666ZM181.334 53.277l45.333-26.666L272 53.277l-45.333 26.667-45.333-26.667ZM0 213.277l45.333-26.667 45.334 26.667-45.334 26.667L0 213.277ZM136 239.944l-45.333-26.667v53.333L136 239.944Z"
fill="#FF5D01"
></path>
<path
d="m136 53.333 45.333-26.666v120L226.667 120V80L272 53.333V160l-90.667 53.333v240L136 480V306.667L45.334 360V240l45.333-26.667v53.334L136 240V53.333Z"
fill="#53C68C"
></path>
<path d="M45.334 240 0 213.334v120L45.334 360V240Z" fill="#B04304"></path>
</svg>
<p tw="ml-3 font-semibold">${siteConfig.title}</p>
</div>
<p>by ${siteConfig.author}</p>
</div>
</div>`;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
export async function GET(context: APIContext) {
const { pubDate, title } = context.props as Props;
const postDate = getFormattedDate(pubDate, {
month: "long",
weekday: "long",
});
const svg = await satori(markup(title, postDate), ogOptions);
const png = new Resvg(svg).render().asPng();
return new Response(png, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
"Content-Type": "image/png",
},
});
}
export async function getStaticPaths() {
const posts = await getAllPosts();
return posts
.filter(({ data }) => !data.ogImage)
.map((post) => ({
params: { slug: post.id },
props: {
pubDate: post.data.updatedDate ?? post.data.publishDate,
title: post.data.title,
},
}));
}

View File

@ -0,0 +1,125 @@
---
import type { CollectionEntry } from "astro:content";
import Pagination from "@/components/Paginator.astro";
import PostPreview from "@/components/blog/PostPreview.astro";
import { getAllPosts, getUniqueTags, groupPostsByYear } from "@/data/post";
import PageLayout from "@/layouts/Base.astro";
import { collectionDateSort } from "@/utils/date";
import type { GetStaticPaths, Page } from "astro";
import { Icon } from "astro-icon/components";
export const getStaticPaths = (async ({ paginate }) => {
const MAX_POSTS_PER_PAGE = 10;
const MAX_TAGS = 7;
const allPosts = await getAllPosts();
const uniqueTags = getUniqueTags(allPosts).slice(0, MAX_TAGS);
return paginate(allPosts.sort(collectionDateSort), {
pageSize: MAX_POSTS_PER_PAGE,
props: { uniqueTags },
});
}) satisfies GetStaticPaths;
interface Props {
page: Page<CollectionEntry<"post">>;
uniqueTags: string[];
}
const { page, uniqueTags } = Astro.props;
const meta = {
description: "Read my collection of posts and the things that interest me",
title: "Posts",
};
const paginationProps = {
...(page.url.prev && {
prevUrl: {
text: "← Previous Page",
url: page.url.prev,
},
}),
...(page.url.next && {
nextUrl: {
text: "Next Page →",
url: page.url.next,
},
}),
};
const groupedByYear = groupPostsByYear(page.data);
const descYearKeys = Object.keys(groupedByYear).sort((a, b) => +b - +a);
---
<PageLayout meta={meta}>
<div class="mb-6 flex items-center gap-3">
<h1 class="title">Posts</h1>
<a class="text-accent" href="/rss.xml" target="_blank">
<span class="sr-only">RSS feed</span>
<Icon aria-hidden="true" class="h-6 w-6" focusable="false" name="mdi:rss" />
</a>
</div>
<div class="grid sm:grid-cols-[3fr_1fr] sm:gap-x-8 sm:gap-y-16">
<div>
{
descYearKeys.map((yearKey) => (
<section aria-labelledby={`year-${yearKey}`}>
<h2 id={`year-${yearKey}`} class="title text-lg">
<span class="sr-only">Posts in</span>
{yearKey}
</h2>
<ul class="mt-5 mb-16 space-y-4 text-start">
{groupedByYear[yearKey]?.map((p) => (
<li class="grid gap-2 sm:grid-cols-[auto_1fr] sm:[&_q]:col-start-2">
<PostPreview post={p} />
</li>
))}
</ul>
</section>
))
}
<Pagination {...paginationProps} />
</div>
{
!!uniqueTags.length && (
<aside>
<h2 class="title mb-4 flex items-center gap-2 text-lg">
Tags
<svg
aria-hidden="true"
class="h-6 w-6"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M0 0h24v24H0z" fill="none" stroke="none" />
<path d="M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z" />
<path d="M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116" />
<path d="M6 9h-.01" />
</svg>
</h2>
<ul class="flex flex-wrap gap-2">
{uniqueTags.map((tag) => (
<li>
<a class="cactus-link flex items-center justify-center" href={`/tags/${tag}/`}>
<span aria-hidden="true">#</span>
<span class="sr-only">View all posts with the tag</span>
{tag}
</a>
</li>
))}
</ul>
<span class="mt-4 block sm:text-end">
<a class="hover:text-link" href="/tags/">
View all <span aria-hidden="true">→</span>
<span class="sr-only">blog tags</span>
</a>
</span>
</aside>
)
}
</div>
</PageLayout>

View File

@ -0,0 +1,24 @@
---
import { render } from "astro:content";
import { getAllPosts } from "@/data/post";
import PostLayout from "@/layouts/BlogPost.astro";
import type { GetStaticPaths, InferGetStaticPropsType } from "astro";
// if you're using an adaptor in SSR mode, getStaticPaths wont work -> https://docs.astro.build/en/guides/routing/#modifying-the-slug-example-for-ssr
export const getStaticPaths = (async () => {
const blogEntries = await getAllPosts();
return blogEntries.map((post) => ({
params: { slug: post.id },
props: { post },
}));
}) satisfies GetStaticPaths;
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
const { post } = Astro.props;
const { Content } = await render(post);
---
<PostLayout post={post}>
<Content />
</PostLayout>

19
src/pages/rss.xml.ts Normal file
View File

@ -0,0 +1,19 @@
import { getAllPosts } from "@/data/post";
import { siteConfig } from "@/site.config";
import rss from "@astrojs/rss";
export const GET = async () => {
const posts = await getAllPosts();
return rss({
title: siteConfig.title,
description: siteConfig.description,
site: import.meta.env.SITE,
items: posts.map((post) => ({
title: post.data.title,
description: post.data.description,
pubDate: post.data.publishDate,
link: `posts/${post.id}/`,
})),
});
};

View File

@ -0,0 +1,72 @@
---
import type { CollectionEntry } from "astro:content";
import Pagination from "@/components/Paginator.astro";
import PostPreview from "@/components/blog/PostPreview.astro";
import { getAllPosts, getUniqueTags } from "@/data/post";
import PageLayout from "@/layouts/Base.astro";
import { collectionDateSort } from "@/utils/date";
import type { GetStaticPaths, Page } from "astro";
export const getStaticPaths: GetStaticPaths = async ({ paginate }) => {
const allPosts = await getAllPosts();
const sortedPosts = allPosts.sort(collectionDateSort);
const uniqueTags = getUniqueTags(sortedPosts);
return uniqueTags.flatMap((tag) => {
const filterPosts = sortedPosts.filter((post) => post.data.tags.includes(tag));
return paginate(filterPosts, {
pageSize: 10,
params: { tag },
});
});
};
interface Props {
page: Page<CollectionEntry<"post">>;
}
const { page } = Astro.props;
const { tag } = Astro.params;
const meta = {
description: `View all posts with the tag - ${tag}`,
title: `Tag: ${tag}`,
};
const paginationProps = {
...(page.url.prev && {
prevUrl: {
text: "← Previous Tags",
url: page.url.prev,
},
}),
...(page.url.next && {
nextUrl: {
text: "Next Tags →",
url: page.url.next,
},
}),
};
---
<PageLayout meta={meta}>
<div class="mb-6 flex items-center">
<h1 class="sr-only">Posts with the tag {tag}</h1>
<a class="title text-accent" href="/tags/"><span class="sr-only">All {" "}</span>Tags</a>
<span aria-hidden="true" class="ms-2 me-3 text-xl">→</span>
<span aria-hidden="true" class="text-xl">#{tag}</span>
</div>
<section aria-labelledby={`tags-${tag}`}>
<h2 id={`tags-${tag}`} class="sr-only">Post List</h2>
<ul class="space-y-4">
{
page.data.map((p) => (
<li class="grid gap-2 sm:grid-cols-[auto_1fr]">
<PostPreview as="h2" post={p} />
</li>
))
}
</ul>
<Pagination {...paginationProps} />
</section>
</PageLayout>

View File

@ -0,0 +1,35 @@
---
import { getAllPosts, getUniqueTagsWithCount } from "@/data/post";
import PageLayout from "@/layouts/Base.astro";
const allPosts = await getAllPosts();
const allTags = getUniqueTagsWithCount(allPosts);
const meta = {
description: "A list of all the topics I've written about in my posts",
title: "All Tags",
};
---
<PageLayout meta={meta}>
<h1 class="title mb-6">Tags</h1>
<ul class="space-y-4">
{
allTags.map(([tag, val]) => (
<li class="flex items-center gap-x-2">
<a
class="cactus-link inline-block"
data-astro-prefetch
href={`/tags/${tag}/`}
title={`View posts with the tag: ${tag}`}
>
&#35;{tag}
</a>
<span class="inline-block">
- {val} Post{val > 1 && "s"}
</span>
</li>
))
}
</ul>
</PageLayout>