refactor(all): complete rewrite

This commit is contained in:
z0x 2025-02-04 23:37:40 -05:00
parent 9f928f4786
commit 757d21f0e8
67 changed files with 4053 additions and 974 deletions

View file

@ -1,15 +1,37 @@
---
import Container from "@components/Container.astro";
import { SITE } from "@consts";
import Layout from "@layouts/Layout.astro";
import Breadcrumbs from '@/components/Breadcrumbs.astro'
import Container from '@/components/Container.astro'
import Link from '@/components/Link.astro'
import { buttonVariants } from '@/components/ui/button'
import { SITE } from '@/consts'
import Layout from '@/layouts/Layout.astro'
import { cn } from '@/lib/utils'
---
<Layout title="404" description={SITE.DESCRIPTION}>
<Container>
<div class="text-center">
<h4 class="text-2xl font-semibold text-black dark:text-white">
404: Page not found
</h4>
</div>
<Container class="flex grow flex-col gap-y-6">
<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-bold">404: Page not found</h1>
<p class="prose prose-neutral dark:prose-invert">
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"
>&larr;</span
> Go to home page
</Link>
</section>
</Container>
</Layout>

View file

@ -1,76 +1,145 @@
---
import Breadcrumbs from "@/components/Breadcrumbs.astro";
import PostNavigation from "@/components/PostNavigation.astro";
import TableOfContents from "@/components/TableOfContents.astro";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import Layout from "@/layouts/Layout.astro";
import { formatDate, readingTime } from "@/lib/utils";
import { Icon } from "astro-icon/components";
import { Image } from "astro:assets";
import { type CollectionEntry, getCollection, render } from "astro:content";
import Container from "@components/Container.astro";
import FormattedDate from "@components/FormattedDate.astro";
import PostNavigation from "@components/PostNavigation.astro";
import TableOfContents from "@components/TableOfContents.astro";
import Layout from "@layouts/Layout.astro";
import { readingTime } from "@lib/utils";
export async function getStaticPaths() {
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return posts.map((post) => ({
params: { id: post.id },
props: post,
}));
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return posts.map((post) => ({
params: { id: post.id },
props: post,
}));
}
type Props = CollectionEntry<"blog">;
const posts = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
function getNextPost() {
let postIndex;
for (const post of posts) {
if (post.id === Astro.params.id) {
postIndex = posts.indexOf(post);
return posts[postIndex + 1];
}
}
function getPostIndex(id: string): number {
return posts.findIndex((post) => post.id === id);
}
function getPrevPost() {
let postIndex;
for (const post of posts) {
if (post.id === Astro.params.id) {
postIndex = posts.indexOf(post);
return posts[postIndex - 1];
}
}
function getPrevPost(id: string): Props | null {
const postIndex = getPostIndex(id);
return postIndex !== -1 && postIndex < posts.length - 1
? posts[postIndex + 1]
: null;
}
const nextPost = getNextPost();
const prevPost = getPrevPost();
function getNextPost(id: string): Props | null {
const postIndex = getPostIndex(id);
return postIndex > 0 ? posts[postIndex - 1] : null;
}
const currentPostId = Astro.params.id;
const nextPost = getNextPost(currentPostId);
const prevPost = getPrevPost(currentPostId);
const post = Astro.props;
const { Content, headings } = await render(post);
---
<Layout title={post.data.title} description={post.data.description}>
<Container>
<div class="my-10 space-y-1">
<div class="flex items-center gap-1.5">
<div class="font-base text-sm">
<FormattedDate date={post.data.date} />
</div>
&bull;
{
post.body && (
<div class="font-base text-sm">{readingTime(post.body)}</div>
)
}
</div>
<h1 class="text-3xl font-semibold text-black dark:text-white">
{post.data.title}
</h1>
</div>
{headings.length > 0 && <TableOfContents headings={headings} />}
<article>
<Content />
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
</article>
</Container>
<Layout
title={post.data.title}
description={post.data.description}
image={post.data.image?.src ?? "/static/1200x630.png"}
>
<section
class="grid grid-cols-[minmax(0px,1fr)_min(768px,100%)_minmax(0px,1fr)] gap-y-6 *:px-4"
>
<Breadcrumbs
items={[
{ href: "/", label: "", icon: "lucide:archive" },
{ label: post.data.title, icon: "lucide:file-text" },
]}
class="col-start-2"
/>
{
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-[1000px] object-cover"
/>
)
}
<section class="col-start-2 flex flex-col gap-y-6 text-center">
<div class="flex flex-col gap-y-4">
<h1 class="text-4xl font-bold leading-tight sm:text-5xl">
{post.data.title}
</h1>
<div
class="flex flex-wrap items-center justify-center gap-2 text-sm text-muted-foreground"
>
<div class="flex items-center gap-2">
<span>{formatDate(post.data.date)}</span>
<Separator orientation="vertical" className="h-4" />
<span>{readingTime(post.body!)}</span>
</div>
</div>
</div>
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
</section>
{headings.length > 0 && <TableOfContents headings={headings} />}
<article
class="prose prose-neutral col-start-2 max-w-none dark:prose-invert"
>
<Content />
</article>
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
</section>
<Button
variant="outline"
size="icon"
className="group fixed bottom-8 right-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>

72
src/pages/[...page].astro Normal file
View file

@ -0,0 +1,72 @@
---
import BlogCard from "@/components/BlogCard.astro";
import Breadcrumbs from "@/components/Breadcrumbs.astro";
import Container from "@/components/Container.astro";
import PaginationComponent from "@/components/ui/pagination";
import { SITE } from "@/consts";
import Layout from "@/layouts/Layout.astro";
import type { PaginateFunction } from "astro";
import { type CollectionEntry, getCollection } from "astro:content";
export async function getStaticPaths({
paginate,
}: {
paginate: PaginateFunction;
}) {
const allPosts = await getCollection("blog", ({ data }) => !data.draft);
return paginate(
allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()),
{ pageSize: SITE.POSTS_PER_PAGE },
);
}
const { page } = Astro.props;
const postsByYear = page.data.reduce(
(acc: Record<string, CollectionEntry<"blog">[]>, post) => {
const year = post.data.date.getFullYear().toString();
(acc[year] ??= []).push(post);
return acc;
},
{},
);
const years = Object.keys(postsByYear).sort(
(a, b) => parseInt(b) - parseInt(a),
);
---
<Layout title="home" description={SITE.DESCRIPTION}>
<Container class="flex grow flex-col gap-y-6">
<Breadcrumbs
items={[
{ label: "", href: "/", icon: "lucide:archive" },
{ label: `Page ${page.currentPage}`, icon: "lucide:folder-open" },
]}
/>
<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-semibold">{year}</div>
<ul class="not-prose 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="/"
client:load
/>
</Container>
</Layout>

View file

@ -1,58 +0,0 @@
---
import { type CollectionEntry, getCollection } from "astro:content";
import ArrowCard from "@components/ArrowCard.astro";
import Container from "@components/Container.astro";
import { SITE } from "@consts";
import { HOME } from "@consts";
import Layout from "@layouts/Layout.astro";
const data = (await getCollection("blog"))
.filter((post) => !post.data.draft)
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
type Acc = {
[year: string]: CollectionEntry<"blog">[];
};
const posts = data.reduce((acc: Acc, post) => {
const year = post.data.date.getFullYear().toString();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(post);
return acc;
}, {});
const years = Object.keys(posts).sort(
(a, b) => Number.parseInt(b) - Number.parseInt(a),
);
---
<Layout title={HOME.TITLE} description={SITE.DESCRIPTION}>
<Container>
<aside>
<div class="space-y-10">
<div class="space-y-4">
{
years.map((year) => (
<section class="space-y-4">
<div class="font-semibold text-black dark:text-white">
{year}
</div>
<div>
<ul class="not-prose flex flex-col gap-4">
{posts[year].map((post) => (
<li>
<ArrowCard entry={post} />
</li>
))}
</ul>
</div>
</section>
))
}
</div>
</div>
</aside>
</Container>
</Layout>

62
src/pages/robots.txt.ts Normal file
View file

@ -0,0 +1,62 @@
import type { APIRoute } from 'astro'
const getRobotsTxt = (sitemapURL: URL) => `
User-agent: AI2Bot
User-agent: Ai2Bot-Dolma
User-agent: Amazonbot
User-agent: anthropic-ai
User-agent: Applebot
User-agent: Applebot-Extended
User-agent: Bytespider
User-agent: CCBot
User-agent: ChatGPT-User
User-agent: Claude-Web
User-agent: ClaudeBot
User-agent: cohere-ai
User-agent: cohere-training-data-crawler
User-agent: Crawlspace
User-agent: Diffbot
User-agent: DuckAssistBot
User-agent: FacebookBot
User-agent: FriendlyCrawler
User-agent: Google-Extended
User-agent: GoogleOther
User-agent: GoogleOther-Image
User-agent: GoogleOther-Video
User-agent: GPTBot
User-agent: iaskspider/2.0
User-agent: ICC-Crawler
User-agent: ImagesiftBot
User-agent: img2dataset
User-agent: ISSCyberRiskCrawler
User-agent: Kangaroo Bot
User-agent: Meta-ExternalAgent
User-agent: Meta-ExternalFetcher
User-agent: OAI-SearchBot
User-agent: omgili
User-agent: omgilibot
User-agent: PanguBot
User-agent: PerplexityBot
User-agent: PetalBot
User-agent: Scrapy
User-agent: SemrushBot-OCOB
User-agent: SemrushBot-SWA
User-agent: Sidetrade indexer bot
User-agent: Timpibot
User-agent: VelenPublicWebCrawler
User-agent: Webzio-Extended
User-agent: YouBot
Disallow: /
DisallowAITraining: /
# Block any non-specified AI crawlers
User-Agent: *
DisallowAITraining: /within
Sitemap: ${sitemapURL.href}
`
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL('sitemap-index.xml', site)
return new Response(getRobotsTxt(sitemapURL))
}

View file

@ -1,23 +0,0 @@
import { getCollection } from "astro:content";
import rss from "@astrojs/rss";
import { SITE } from "@consts";
export async function GET(context) {
const blog = (await getCollection("blog")).filter((post) => !post.data.draft);
const items = [...blog].sort(
(a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
);
return rss({
title: SITE.TITLE,
description: SITE.DESCRIPTION,
site: context.site,
items: items.map((item) => ({
title: item.data.title,
description: item.data.description,
pubDate: item.data.date,
link: `/${item.id}/`,
})),
});
}

32
src/pages/rss.xml.ts Normal file
View file

@ -0,0 +1,32 @@
import { SITE } from '@/consts'
import rss from '@astrojs/rss'
import type { APIContext } from 'astro'
import { getCollection } from 'astro:content'
export async function GET(context: APIContext) {
try {
const blog = (await getCollection('blog')).filter(
(post) => !post.data.draft,
)
const items = [...blog].sort(
(a, b) =>
new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
)
return rss({
title: SITE.TITLE,
description: SITE.DESCRIPTION,
site: context.site ?? SITE.SITEURL,
items: items.map((item) => ({
title: item.data.title,
description: item.data.description,
pubDate: item.data.date,
link: `/${item.id}/`,
})),
})
} catch (error) {
console.error('Error generating RSS feed:', error)
return new Response('Error generating RSS feed', { status: 500 })
}
}