diff options
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/404.astro | 29 | ||||
| -rw-r--r-- | src/pages/about.astro | 47 | ||||
| -rw-r--r-- | src/pages/authors/[...id].astro | 58 | ||||
| -rw-r--r-- | src/pages/authors/index.astro | 27 | ||||
| -rw-r--r-- | src/pages/blog/[...id].astro | 299 | ||||
| -rw-r--r-- | src/pages/blog/[...page].astro | 58 | ||||
| -rw-r--r-- | src/pages/index.astro | 85 | ||||
| -rw-r--r-- | src/pages/robots.txt.ts | 13 | ||||
| -rw-r--r-- | src/pages/rss.xml.ts | 25 | ||||
| -rw-r--r-- | src/pages/tags/[...id].astro | 52 | ||||
| -rw-r--r-- | src/pages/tags/index.astro | 33 |
11 files changed, 726 insertions, 0 deletions
diff --git a/src/pages/404.astro b/src/pages/404.astro new file mode 100644 index 0000000..5591bc7 --- /dev/null +++ b/src/pages/404.astro @@ -0,0 +1,29 @@ +--- +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import Link from '@/components/Link.astro' +import PageHead from '@/components/PageHead.astro' +import { buttonVariants } from '@/components/ui/button' +import Layout from '@/layouts/Layout.astro' +import { cn } from '@/lib/utils' +--- + +<Layout class="max-w-3xl"> + <PageHead slot="head" title="404" /> + <Breadcrumbs items={[{ label: '???', icon: 'lucide:circle-help' }]} /> + + <section + class="flex flex-col items-center justify-center gap-y-4 text-center" + > + <div class="max-w-md"> + <h1 class="mb-4 text-3xl font-medium">404: Page not found</h1> + <p class="prose">Oops! The page you're looking for doesn't exist.</p> + </div> + <Link + href="/" + class={cn(buttonVariants({ variant: 'outline' }), 'flex gap-x-1.5 group')} + > + <span class="transition-transform group-hover:-translate-x-1">←</span + > Go to home page + </Link> + </section> +</Layout> diff --git a/src/pages/about.astro b/src/pages/about.astro new file mode 100644 index 0000000..680c1b2 --- /dev/null +++ b/src/pages/about.astro @@ -0,0 +1,47 @@ +--- +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import Link from '@/components/Link.astro' +import PageHead from '@/components/PageHead.astro' +import ProjectCard from '@/components/ProjectCard.astro' +import Layout from '@/layouts/Layout.astro' +import { getAllProjects } from '@/lib/data-utils' + +const projects = await getAllProjects() +--- + +<Layout class="max-w-3xl"> + <PageHead slot="head" title="About" /> + <Breadcrumbs items={[{ label: 'About', icon: 'lucide:info' }]} /> + + <section> + <div class="min-w-full"> + <div class="prose mb-8"> + <p class="mt-0"> + astro-erudite is an opinionated, unstyled static blogging template + that prioritizes simplicity and performance, built with <Link + href="https://astro.build" + external + underline>Astro</Link + >, <Link href="https://tailwindcss.com" external underline + >Tailwind</Link + >, and <Link href="https://ui.shadcn.com" external underline + >shadcn/ui</Link + >. It provides a clean foundation for your content while being + extremely easy to customize. + </p> + <p> + To learn more about the philosophy behind this template, check out the + following blog post: <Link + href="/blog/the-state-of-static-blogs" + underline>The State of Static Blogs in 2024</Link + >. + </p> + </div> + + <h2 class="mb-4 text-2xl font-medium">Example Projects Listing</h2> + <div class="flex flex-col gap-4"> + {projects.map((project) => <ProjectCard project={project} />)} + </div> + </div> + </section> +</Layout> diff --git a/src/pages/authors/[...id].astro b/src/pages/authors/[...id].astro new file mode 100644 index 0000000..d71fcf8 --- /dev/null +++ b/src/pages/authors/[...id].astro @@ -0,0 +1,58 @@ +--- +import AuthorCard from '@/components/AuthorCard.astro' +import BlogCard from '@/components/BlogCard.astro' +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import PageHead from '@/components/PageHead.astro' +import Layout from '@/layouts/Layout.astro' +import { getAllAuthors, getPostsByAuthor } from '@/lib/data-utils' + +export async function getStaticPaths() { + const authors = await getAllAuthors() + return authors.map((author) => ({ + params: { id: author.id }, + props: { author }, + })) +} + +const { author } = Astro.props +const authorPosts = await getPostsByAuthor(author.id) +--- + +<Layout class="max-w-3xl"> + <PageHead + slot="head" + title={`${author.data.name} (Author)`} + description={author.data.bio || `Profile of ${author.data.name}.`} + noindex + /> + <Breadcrumbs + items={[ + { href: '/authors', label: 'Authors', icon: 'lucide:users' }, + { label: author.data.name, icon: 'lucide:user' }, + ]} + /> + + <section> + <AuthorCard author={author} /> + </section> + <section class="flex flex-col gap-y-4"> + <h2 class="text-2xl font-medium">Posts by {author.data.name}</h2> + { + authorPosts.length > 0 ? ( + <ul class="flex flex-col gap-4"> + {authorPosts + .filter((post) => !post.data.draft) + .map((post) => ( + <li> + <BlogCard entry={post} /> + </li> + ))} + </ul> + ) : ( + <p class="text-muted-foreground"> + No posts available from this author. + </p> + ) + } + </section> +</Layout> diff --git a/src/pages/authors/index.astro b/src/pages/authors/index.astro new file mode 100644 index 0000000..ba311a0 --- /dev/null +++ b/src/pages/authors/index.astro @@ -0,0 +1,27 @@ +--- +import AuthorCard from '@/components/AuthorCard.astro' +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import PageHead from '@/components/PageHead.astro' +import Layout from '@/layouts/Layout.astro' +import { getAllAuthors } from '@/lib/data-utils' + +const authors = await getAllAuthors() +--- + +<Layout class="max-w-3xl"> + <PageHead slot="head" title="Authors" /> + <Breadcrumbs items={[{ label: 'Authors', icon: 'lucide:users' }]} /> + { + authors.length > 0 ? ( + <ul class="flex flex-col gap-4"> + {authors.map((author) => ( + <li> + <AuthorCard author={author} /> + </li> + ))} + </ul> + ) : ( + <p class="text-muted-foreground text-center">No authors found.</p> + ) + } +</Layout> diff --git a/src/pages/blog/[...id].astro b/src/pages/blog/[...id].astro new file mode 100644 index 0000000..5cc9eab --- /dev/null +++ b/src/pages/blog/[...id].astro @@ -0,0 +1,299 @@ +--- +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import Link from '@/components/Link.astro' +import PostHead from '@/components/PostHead.astro' +import PostNavigation from '@/components/PostNavigation.astro' +import SubpostsHeader from '@/components/SubpostsHeader.astro' +import SubpostsSidebar from '@/components/SubpostsSidebar.astro' +import TOCHeader from '@/components/TOCHeader.astro' +import TOCSidebar from '@/components/TOCSidebar.astro' +import { badgeVariants } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import Layout from '@/layouts/Layout.astro' +import { + getAdjacentPosts, + getAllPostsAndSubposts, + getCombinedReadingTime, + getParentId, + getParentPost, + getPostReadingTime, + getSubpostCount, + getTOCSections, + hasSubposts, + isSubpost, + parseAuthors, +} from '@/lib/data-utils' +import { formatDate } from '@/lib/utils' +import { Icon } from 'astro-icon/components' +import { Image } from 'astro:assets' +import { render } from 'astro:content' + +export async function getStaticPaths() { + const posts = await getAllPostsAndSubposts() + return posts.map((post) => ({ + params: { id: post.id }, + props: post, + })) +} + +const post = Astro.props +const currentPostId = Astro.params.id +const { Content, headings } = await render(post) +const authors = await parseAuthors(post.data.authors ?? []) + +const isCurrentSubpost = isSubpost(currentPostId) +const navigation = await getAdjacentPosts(currentPostId) +const parentPost = isCurrentSubpost ? await getParentPost(currentPostId) : null + +const hasChildPosts = await hasSubposts(currentPostId) +const subpostCount = !isCurrentSubpost + ? await getSubpostCount(currentPostId) + : 0 +const postReadingTime = await getPostReadingTime(currentPostId) +const combinedReadingTime = + hasChildPosts && !isCurrentSubpost + ? await getCombinedReadingTime(currentPostId) + : null + +const tocSections = await getTOCSections(currentPostId) +--- + +<Layout> + <PostHead slot="head" post={post} /> + { + (hasChildPosts || isCurrentSubpost) && ( + <SubpostsHeader + slot="subposts-navigation" + parentId={isCurrentSubpost ? getParentId(currentPostId) : currentPostId} + /> + ) + } + { + headings?.length > 0 && + !( + isCurrentSubpost && + headings.length === 1 && + headings[0].text === post.data.title + ) && <TOCHeader slot="table-of-contents" headings={headings} /> + } + + <section + class="grid grid-cols-[minmax(0px,1fr)_min(calc(var(--breakpoint-md)-2rem),100%)_minmax(0px,1fr)] gap-y-6" + > + <div class="col-start-2"> + <Breadcrumbs + items={[ + { href: '/blog', label: 'Blog', icon: 'lucide:library-big' }, + ...(isCurrentSubpost && parentPost + ? [ + { + href: `/blog/${parentPost.id}`, + label: parentPost.data.title, + icon: 'lucide:book-open', + }, + { + href: `/blog/${currentPostId}`, + label: post.data.title, + icon: 'lucide:file-text', + }, + ] + : [ + { + href: `/blog/${currentPostId}`, + label: post.data.title, + icon: 'lucide:book-open-text', + }, + ]), + ]} + /> + </div> + + { + post.data.image && ( + <Image + src={post.data.image} + alt={post.data.title} + width={1200} + height={630} + class="col-span-full mx-auto w-full max-w-5xl object-cover" + /> + ) + } + + <section class="col-start-2 flex flex-col gap-y-6 text-center"> + <div class="flex flex-col"> + <h1 + class="mb-2 scroll-mt-31 text-3xl leading-tight font-medium sm:text-4xl" + id="post-title" + > + {post.data.title} + </h1> + + <div + class="text-muted-foreground divide-border mb-4 flex flex-col items-center justify-center divide-y text-xs sm:flex-row sm:flex-wrap sm:divide-x sm:divide-y-0 sm:text-sm" + > + { + authors.length > 0 && ( + <div class="flex w-full items-center justify-center gap-x-2 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0"> + {authors.map((author) => ( + <div class="flex items-center gap-x-1.5"> + <Image + src={author.avatar} + alt={author.name} + width={20} + height={20} + class="rounded-full" + /> + {author.isRegistered ? ( + <Link + href={`/authors/${author.id}`} + underline + class="text-foreground" + > + <span>{author.name}</span> + </Link> + ) : ( + <span>{author.name}</span> + )} + </div> + ))} + </div> + ) + } + + <div + class="flex w-full items-center justify-center gap-2 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0" + > + <span>{formatDate(post.data.date)}</span> + </div> + + <div + class="flex w-full items-center justify-center gap-2 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0" + > + <span> + {postReadingTime} + { + combinedReadingTime && + combinedReadingTime !== postReadingTime && ( + <span class="text-muted-foreground"> + {' '} + ({combinedReadingTime} total) + </span> + ) + } + </span> + </div> + + { + subpostCount > 0 && ( + <div class="flex w-full items-center justify-center gap-1 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0"> + <Icon name="lucide:file-text" class="size-3" /> + {subpostCount} subpost{subpostCount === 1 ? '' : 's'} + </div> + ) + } + </div> + <div class="flex flex-wrap justify-center gap-2"> + { + post.data.tags && + post.data.tags.length > 0 && + post.data.tags.map((tag) => ( + <a + href={`/tags/${tag}`} + class={badgeVariants({ variant: 'muted' })} + > + <Icon name="lucide:hash" class="size-3" /> + {tag} + </a> + )) + } + </div> + </div> + + <PostNavigation + newerPost={navigation.newer} + olderPost={navigation.older} + parentPost={isCurrentSubpost ? navigation.parent : undefined} + /> + </section> + + { + tocSections.length > 0 && ( + <TOCSidebar sections={tocSections} currentPostId={currentPostId} /> + ) + } + + <article class="prose col-start-2 max-w-none"> + <Content /> + </article> + + { + (hasChildPosts || isCurrentSubpost) && ( + <SubpostsSidebar + parentId={ + isCurrentSubpost ? getParentId(currentPostId) : currentPostId + } + className="w-64" + /> + ) + } + + <PostNavigation + newerPost={navigation.newer} + olderPost={navigation.older} + parentPost={isCurrentSubpost ? navigation.parent : undefined} + /> + </section> + + <Button + variant="outline" + size="icon" + className="group fixed right-8 bottom-8 z-50 hidden" + id="scroll-to-top" + title="Scroll to top" + aria-label="Scroll to top" + > + <Icon + name="lucide:arrow-up" + class="mx-auto size-4 transition-all group-hover:-translate-y-0.5" + /> + </Button> + + <script> + document.addEventListener('astro:page-load', () => { + const scrollToTopButton = document.getElementById('scroll-to-top') + const footer = document.querySelector('footer') + + if (scrollToTopButton && footer) { + scrollToTopButton.addEventListener('click', () => { + window.scrollTo({ top: 0, behavior: 'smooth' }) + }) + + window.addEventListener('scroll', () => { + const footerRect = footer.getBoundingClientRect() + const isFooterVisible = footerRect.top <= window.innerHeight + + scrollToTopButton.classList.toggle( + 'hidden', + window.scrollY <= 300 || isFooterVisible, + ) + }) + } + }) + </script> +</Layout> + +<script> + const ensureKatexStyles = () => { + if (!document.querySelector('.katex')) return + if (document.querySelector('link[href*="katex.min.css"]')) return + + const link = document.createElement('link') + link.rel = 'stylesheet' + link.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css' + document.head.appendChild(link) + } + + document.addEventListener('astro:page-load', ensureKatexStyles) + document.addEventListener('astro:after-swap', ensureKatexStyles) +</script>
\ No newline at end of file diff --git a/src/pages/blog/[...page].astro b/src/pages/blog/[...page].astro new file mode 100644 index 0000000..eb196db --- /dev/null +++ b/src/pages/blog/[...page].astro @@ -0,0 +1,58 @@ +--- +import BlogCard from '@/components/BlogCard.astro' +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import PageHead from '@/components/PageHead.astro' +import PaginationComponent from '@/components/ui/pagination' +import { SITE } from '@/consts' +import Layout from '@/layouts/Layout.astro' +import { getAllPosts, groupPostsByYear } from '@/lib/data-utils' +import type { PaginateFunction } from 'astro' + +export async function getStaticPaths({ + paginate, +}: { + paginate: PaginateFunction +}) { + const allPosts = await getAllPosts() + return paginate(allPosts, { pageSize: SITE.postsPerPage }) +} + +const { page } = Astro.props + +const postsByYear = groupPostsByYear(page.data) +const years = Object.keys(postsByYear).sort((a, b) => parseInt(b) - parseInt(a)) +--- + +<Layout class="max-w-3xl"> + <PageHead slot="head" title="Blog" /> + <Breadcrumbs + items={[ + { label: 'Blog', href: '/blog', icon: 'lucide:library-big' }, + { label: `Page ${page.currentPage}`, icon: 'lucide:book-copy' }, + ]} + /> + + <div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8"> + { + years.map((year) => ( + <section class="flex flex-col gap-y-4"> + <div class="font-medium">{year}</div> + <ul class="flex flex-col gap-4"> + {postsByYear[year].map((post) => ( + <li> + <BlogCard entry={post} /> + </li> + ))} + </ul> + </section> + )) + } + </div> + + <PaginationComponent + currentPage={page.currentPage} + totalPages={page.lastPage} + baseUrl="/blog/" + client:load + /> +</Layout> diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..019283d --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,85 @@ +--- +import BlogCard from '@/components/BlogCard.astro' +import Link from '@/components/Link.astro' +import PageHead from '@/components/PageHead.astro' +import { buttonVariants } from '@/components/ui/button' +import { SITE } from '@/consts' +import Layout from '@/layouts/Layout.astro' +import { getRecentPosts } from '@/lib/data-utils' + +const blog = await getRecentPosts(SITE.featuredPostCount) +--- + +<Layout class="max-w-3xl"> + <PageHead slot="head" title="Home" /> + <section class="rounded-lg border"> + <div class="flex flex-col space-y-1.5 p-6"> + <h3 class="text-3xl leading-none font-medium">er·u·dite</h3> + <p class="text-muted-foreground text-sm"> + /ˈer(y)əˌdīt/ • <span class="font-medium">adjective</span> + </p> + </div> + <div class="p-6 pt-0"> + <p class="text-muted-foreground mb-2 text-sm"> + astro-erudite is an opinionated, unstyled static blogging template built + with <Link + href="https://astro.build" + class="text-foreground" + external + underline>Astro</Link + >, <Link + href="https://tailwindcss.com" + class="text-foreground" + external + underline>Tailwind</Link + >, and <Link + href="https://ui.shadcn.com" + class="text-foreground" + external + underline>shadcn/ui</Link + >. Extraordinarily loosely based on the <Link + href="https://astro-micro.vercel.app/" + class="text-foreground" + external + underline>Astro Micro</Link + > theme. + </p> + <p class="text-muted-foreground text-sm"> + To use this template, check out the <Link + href="https://github.com/jktrn/astro-erudite" + class="text-foreground" + underline + external>GitHub</Link + > repository. To learn more about why this template exists, read this blog + post: <Link + href="/blog/the-state-of-static-blogs" + class="text-foreground" + underline>The State of Static Blogs in 2024</Link + >. + </p> + </div> + </section> + <section class="flex flex-col gap-y-4"> + <h2 class="text-2xl font-medium">Latest posts</h2> + <ul class="flex flex-col gap-y-4"> + { + blog.map((post) => ( + <li> + <BlogCard entry={post} /> + </li> + )) + } + </ul> + <div class="flex justify-center"> + <Link + href="/blog" + class={buttonVariants({ variant: 'ghost' }) + ' group'} + > + See all posts <span + class="ml-1.5 transition-transform group-hover:translate-x-1" + >→</span + > + </Link> + </div> + </section> +</Layout> diff --git a/src/pages/robots.txt.ts b/src/pages/robots.txt.ts new file mode 100644 index 0000000..679a44d --- /dev/null +++ b/src/pages/robots.txt.ts @@ -0,0 +1,13 @@ +import type { APIRoute } from 'astro' + +const getRobotsTxt = (sitemapURL: URL) => ` +User-agent: * +Allow: / + +Sitemap: ${sitemapURL.href} +` + +export const GET: APIRoute = ({ site }) => { + const sitemapURL = new URL('sitemap-index.xml', site) + return new Response(getRobotsTxt(sitemapURL)) +} diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts new file mode 100644 index 0000000..fe7e9d2 --- /dev/null +++ b/src/pages/rss.xml.ts @@ -0,0 +1,25 @@ +import { SITE } from '@/consts' +import rss from '@astrojs/rss' +import type { APIContext } from 'astro' +import { getAllPosts } from '@/lib/data-utils' + +export async function GET(context: APIContext) { + try { + const posts = await getAllPosts() + + return rss({ + title: SITE.title, + description: SITE.description, + site: context.site ?? SITE.href, + items: posts.map((post) => ({ + title: post.data.title, + description: post.data.description, + pubDate: post.data.date, + link: `/blog/${post.id}/`, + })), + }) + } catch (error) { + console.error('Error generating RSS feed:', error) + return new Response('Error generating RSS feed', { status: 500 }) + } +} diff --git a/src/pages/tags/[...id].astro b/src/pages/tags/[...id].astro new file mode 100644 index 0000000..dfd4bb8 --- /dev/null +++ b/src/pages/tags/[...id].astro @@ -0,0 +1,52 @@ +--- +import BlogCard from '@/components/BlogCard.astro' +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import PageHead from '@/components/PageHead.astro' +import Layout from '@/layouts/Layout.astro' +import { getAllTags, getPostsByTag } from '@/lib/data-utils' + +export async function getStaticPaths() { + const tagMap = await getAllTags() + const uniqueTags = Array.from(tagMap.keys()) + + return Promise.all( + uniqueTags.map(async (tag) => { + const posts = await getPostsByTag(tag) + return { + params: { id: tag }, + props: { + tag, + posts, + }, + } + }), + ) +} + +const { tag, posts } = Astro.props +--- + +<Layout class="max-w-3xl"> + <PageHead + slot="head" + title={`Posts tagged with "${tag}"`} + description={`A collection of posts tagged with ${tag}.`} + noindex + /> + <Breadcrumbs + items={[ + { href: '/tags', label: 'Tags', icon: 'lucide:tags' }, + { label: tag, icon: 'lucide:tag' }, + ]} + /> + + <ul class="flex flex-col gap-y-4"> + { + posts.map((post) => ( + <li> + <BlogCard entry={post} /> + </li> + )) + } + </ul> +</Layout> diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro new file mode 100644 index 0000000..3d2c75f --- /dev/null +++ b/src/pages/tags/index.astro @@ -0,0 +1,33 @@ +--- +import Breadcrumbs from '@/components/Breadcrumbs.astro' +import Link from '@/components/Link.astro' +import PageHead from '@/components/PageHead.astro' +import { badgeVariants } from '@/components/ui/badge' +import Layout from '@/layouts/Layout.astro' +import { getSortedTags } from '@/lib/data-utils' +import { Icon } from 'astro-icon/components' + +const sortedTags = await getSortedTags() +--- + +<Layout class="max-w-3xl"> + <PageHead slot="head" title="Tags" /> + <Breadcrumbs items={[{ label: 'Tags', icon: 'lucide:tags' }]} /> + + <div class="flex flex-col gap-4"> + <div class="flex flex-wrap gap-2"> + { + sortedTags.map(({ tag, count }) => ( + <Link + href={`/tags/${tag}`} + class={badgeVariants({ variant: 'muted' })} + > + <Icon name="lucide:hash" class="size-3" /> + {tag} + <span class="text-muted-foreground ml-1.5">({count})</span> + </Link> + )) + } + </div> + </div> +</Layout> |
