mirror of
https://github.com/lbr77/blog-astro.git
synced 2026-04-10 09:39:11 +00:00
Initial commit
This commit is contained in:
29
src/pages/404.astro
Normal file
29
src/pages/404.astro
Normal file
@@ -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>
|
||||
47
src/pages/about.astro
Normal file
47
src/pages/about.astro
Normal file
@@ -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>
|
||||
58
src/pages/authors/[...id].astro
Normal file
58
src/pages/authors/[...id].astro
Normal file
@@ -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>
|
||||
27
src/pages/authors/index.astro
Normal file
27
src/pages/authors/index.astro
Normal file
@@ -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>
|
||||
296
src/pages/blog/[...id].astro
Normal file
296
src/pages/blog/[...id].astro
Normal file
@@ -0,0 +1,296 @@
|
||||
---
|
||||
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>
|
||||
if (document.querySelector('.katex')) {
|
||||
if (!document.querySelector('link[href*="katex.min.css"]')) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
58
src/pages/blog/[...page].astro
Normal file
58
src/pages/blog/[...page].astro
Normal file
@@ -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>
|
||||
85
src/pages/index.astro
Normal file
85
src/pages/index.astro
Normal file
@@ -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>
|
||||
13
src/pages/robots.txt.ts
Normal file
13
src/pages/robots.txt.ts
Normal file
@@ -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))
|
||||
}
|
||||
25
src/pages/rss.xml.ts
Normal file
25
src/pages/rss.xml.ts
Normal file
@@ -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 })
|
||||
}
|
||||
}
|
||||
52
src/pages/tags/[...id].astro
Normal file
52
src/pages/tags/[...id].astro
Normal file
@@ -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>
|
||||
33
src/pages/tags/index.astro
Normal file
33
src/pages/tags/index.astro
Normal file
@@ -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>
|
||||
Reference in New Issue
Block a user