refactor(all): complete rewrite
This commit is contained in:
parent
9f928f4786
commit
757d21f0e8
67 changed files with 4053 additions and 974 deletions
|
@ -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"
|
||||
>←</span
|
||||
> Go to home page
|
||||
</Link>
|
||||
</section>
|
||||
</Container>
|
||||
</Layout>
|
||||
|
|
|
@ -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>
|
||||
•
|
||||
{
|
||||
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
72
src/pages/[...page].astro
Normal 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>
|
|
@ -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
62
src/pages/robots.txt.ts
Normal 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))
|
||||
}
|
|
@ -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
32
src/pages/rss.xml.ts
Normal 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 })
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue