diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-03 10:56:21 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-03 10:56:21 +0300 |
| commit | 456cf011b36de91c9936994b1fa45703adcd309b (patch) | |
| tree | 8e60daf998f731ac50d100fa490eaecae1168042 /src/pages | |
Initial fork of chrismwilliams/astro-theme-cactus theme
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/404.astro | 13 | ||||
| -rw-r--r-- | src/pages/about.astro | 36 | ||||
| -rw-r--r-- | src/pages/index.astro | 61 | ||||
| -rw-r--r-- | src/pages/notes/[...page].astro | 63 | ||||
| -rw-r--r-- | src/pages/notes/[...slug].astro | 31 | ||||
| -rw-r--r-- | src/pages/notes/rss.xml.ts | 18 | ||||
| -rw-r--r-- | src/pages/og-image/[...slug].png.ts | 90 | ||||
| -rw-r--r-- | src/pages/posts/[...page].astro | 125 | ||||
| -rw-r--r-- | src/pages/posts/[...slug].astro | 24 | ||||
| -rw-r--r-- | src/pages/rss.xml.ts | 19 | ||||
| -rw-r--r-- | src/pages/tags/[tag]/[...page].astro | 79 | ||||
| -rw-r--r-- | src/pages/tags/index.astro | 35 |
12 files changed, 594 insertions, 0 deletions
diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..d02cca3 --- /dev/null +++ b/src/pages/404.astro @@ -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> diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..190bbd3 --- /dev/null +++ b/src/pages/about.astro @@ -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, I’m a starter Astro. I’m 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> diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..d953797 --- /dev/null +++ b/src/pages/index.astro @@ -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, I’m 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-6" 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-6" role="list"> + {latestNotes.map((note) => ( + <li> + <Note note={note} as="h3" isPreview /> + </li> + ))} + </ul> + </section> + ) + } +</PageLayout> diff --git a/src/pages/notes/[...page].astro b/src/pages/notes/[...page].astro new file mode 100644 index 0000000..fdc5af9 --- /dev/null +++ b/src/pages/notes/[...page].astro @@ -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> diff --git a/src/pages/notes/[...slug].astro b/src/pages/notes/[...slug].astro new file mode 100644 index 0000000..2ce847d --- /dev/null +++ b/src/pages/notes/[...slug].astro @@ -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> diff --git a/src/pages/notes/rss.xml.ts b/src/pages/notes/rss.xml.ts new file mode 100644 index 0000000..0f1f945 --- /dev/null +++ b/src/pages/notes/rss.xml.ts @@ -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}/`, + })), + }); +}; diff --git a/src/pages/og-image/[...slug].png.ts b/src/pages/og-image/[...slug].png.ts new file mode 100644 index 0000000..a4982d8 --- /dev/null +++ b/src/pages/og-image/[...slug].png.ts @@ -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, + }, + })); +} diff --git a/src/pages/posts/[...page].astro b/src/pages/posts/[...page].astro new file mode 100644 index 0000000..495fc7b --- /dev/null +++ b/src/pages/posts/[...page].astro @@ -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-6 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> diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro new file mode 100644 index 0000000..ca9c491 --- /dev/null +++ b/src/pages/posts/[...slug].astro @@ -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> diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts new file mode 100644 index 0000000..1c305af --- /dev/null +++ b/src/pages/rss.xml.ts @@ -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}/`, + })), + }); +}; diff --git a/src/pages/tags/[tag]/[...page].astro b/src/pages/tags/[tag]/[...page].astro new file mode 100644 index 0000000..56923fb --- /dev/null +++ b/src/pages/tags/[tag]/[...page].astro @@ -0,0 +1,79 @@ +--- +import { render } from "astro:content"; +import Pagination from "@/components/Paginator.astro"; +import PostPreview from "@/components/blog/PostPreview.astro"; +import { getAllPosts, getTagMeta, getUniqueTags } from "@/data/post"; +import PageLayout from "@/layouts/Base.astro"; +import { collectionDateSort } from "@/utils/date"; +import type { GetStaticPaths, InferGetStaticPropsType } from "astro"; +import { Icon } from "astro-icon/components"; + +export const getStaticPaths = (async ({ paginate }) => { + const allPosts = await getAllPosts(); + const sortedPosts = allPosts.sort(collectionDateSort); + const uniqueTags = getUniqueTags(sortedPosts); + + return uniqueTags.flatMap((tag) => { + const postsWithTag = sortedPosts.filter((post) => post.data.tags.includes(tag)); + return paginate(postsWithTag, { + pageSize: 10, + params: { tag }, + }); + }); +}) satisfies GetStaticPaths; + +type Props = InferGetStaticPropsType<typeof getStaticPaths>; + +const { page } = Astro.props as Props; +const { tag } = Astro.params; +const tagMeta = await getTagMeta(tag); + +const TagContent = tagMeta ? (await render(tagMeta)).Content : null; + +const meta = { + description: tagMeta?.data.description ?? `View all posts with the tag - ${tag}`, + title: tagMeta?.data.title ?? `Posts about ${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}> + <nav class="mb-8" aria-label="Breadcrumbs"> + <ul class="flex items-center"> + <li class="flex items-center"> + <a class="text-accent" href="/tags/">Tags</a> + <Icon aria-hidden="true" name="mdi:chevron-right" class="mx-1.5" /> + </li> + <li aria-current="page" class=""><span aria-hidden="true">#</span>{tag}</li> + </ul> + </nav> + <h1 class="title capitalize">{tagMeta?.data.title ?? `Posts about ${tag}`}</h1> + <div class="prose prose-sm prose-cactus mb-16 max-w-none"> + {tagMeta?.data.description && <p>{tagMeta.data.description}</p>} + {TagContent && <TagContent />} + </div> + <ul class="space-y-6"> + { + page.data.map((p) => ( + <li class="grid gap-2 sm:grid-cols-[auto_1fr]"> + <PostPreview as="h2" post={p} /> + </li> + )) + } + </ul> + <Pagination {...paginationProps} /> +</PageLayout> diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro new file mode 100644 index 0000000..df1f630 --- /dev/null +++ b/src/pages/tags/index.astro @@ -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-6"> + { + 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}`} + > + #{tag} + </a> + <span class="inline-block"> + - {val} Post{val > 1 && "s"} + </span> + </li> + )) + } + </ul> +</PageLayout> |
