Move to astro
All checks were successful
Build & Depoly / Depoly blog (push) Successful in 1m52s

This commit is contained in:
thislight 2025-04-11 21:50:48 +08:00
parent 729704984c
commit 4bd975796e
148 changed files with 4865 additions and 27561 deletions

20
src/pages/[year].astro Normal file
View file

@ -0,0 +1,20 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import { shouldBeVisible } from "~/utils/posts";
export const getStaticPaths = (async () => {
const posts = (await getCollection("posts")).filter(shouldBeVisible);
const years = new Set(
posts.map((x) => x.data.date.getUTCFullYear().toString())
);
return years
.values()
.map((y) => ({ params: { year: y }, props: { year: y } }))
.toArray();
}) satisfies GetStaticPaths;
const { year } = Astro.props;
return Astro.rewrite(`/${year}/page/1`);
---

View file

@ -0,0 +1,109 @@
---
import "~/material/content.css";
import type { GetStaticPaths } from "astro";
import { render } from "astro:content";
import { getCollection } from "astro:content";
import { format } from "date-fns";
import TimeDistanceToNow from "~/components/TimeDistanceToNow/index.astro";
import Regular from "~/layouts/Regular.astro";
import { getPostParam } from "~/utils/posts";
export const getStaticPaths = (async () => {
const posts = await getCollection("posts");
return posts.map((x) => ({
params: getPostParam(x),
props: {
post: x,
},
}));
}) satisfies GetStaticPaths;
const { post } = Astro.props;
const { Content } = await render(post);
---
<Regular title={post.data.title}>
<div id="_layout">
<div></div>
<main class="content">
<h1 transition:name={`post:${post.id}:title`}>{post.data.title}</h1>
<div class="page-metadata">
<span>
<TimeDistanceToNow datetime={post.data.date.toISOString()}
>{format(post.data.date, "yyyy/MM/dd")}</TimeDistanceToNow
>
创建
</span>
{
post.data.updated && (
<span>
<TimeDistanceToNow datetime={post.data.updated.toISOString()}>
{format(post.data.updated, "yyyy/MM/dd")}
</TimeDistanceToNow>
更新
</span>
)
}
{post.data.visibility === "draft" && <span>This is a draft</span>}
</div>
<Content />
</main>
<div></div>
</div>
</Regular>
<style>
:global(:root) {
background-color: var(--palette-grey-200);
}
#_layout {
display: grid;
grid-template-columns: 1fr auto 1fr;
margin: auto;
& > :global(*) {
overflow: hidden;
word-wrap: normal;
}
}
.page-metadata {
display: grid;
justify-content: flex-end;
margin-inline: 16px;
gap: 4px;
color: var(--palette-grey-700);
> * {
display: flex;
gap: 2px;
justify-content: flex-end;
}
}
main {
max-width: 70rem;
margin-top: 32px;
margin-bottom: calc(env(safe-area-insets-bottom, 16px) + 16px);
background-color: var(--palette-grey-50);
padding-block: 16px;
border-radius: 2px;
}
</style>
<script>
import { wrapElementsInClass } from "~/utils/dom";
import { renderAdvancedTablesOn } from "~/utils/table";
document.addEventListener("astro:page-load", () => {
wrapElementsInClass(document.querySelectorAll(".content > table"), [
"table-responsive",
]);
renderAdvancedTablesOn(
document.querySelectorAll(".content > .table-responsive > table")
);
});
</script>

View file

@ -0,0 +1,68 @@
---
import type { GetStaticPaths } from "astro";
import type { CollectionEntry } from "astro:content";
import { getCollection } from "astro:content";
import AuthorCard from "~/components/AuthorCard.astro";
import Pager from "~/components/Pager.astro";
import PostList from "~/components/PostList.astro";
import TagListCard from "~/components/TagListCard.astro";
import IndexLayout from "~/layouts/IndexLayout.astro";
import { getAllTags, getHotTags, shouldBeVisible } from "~/utils/posts";
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection("posts"))
.filter(shouldBeVisible)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const groupByYear = new Map<string, CollectionEntry<"posts">[]>();
for (const post of posts) {
const year = post.data.date.getUTCFullYear().toString();
const collection = groupByYear.get(year);
if (collection) {
collection.push(post);
} else {
groupByYear.set(year, [post]);
}
}
return groupByYear
.entries()
.flatMap(([year, postsOfYear]) => {
return paginate(postsOfYear, {
params: {
year,
},
props: {
year,
},
});
})
.toArray();
}) satisfies GetStaticPaths;
const { year, page } = Astro.props;
const authors = (await getCollection("authors")).filter((a) => a.data.default);
const posts = (await getCollection("posts")).filter(
(p) => p.data.date.getUTCFullYear().toString() === year
);
const tags = getAllTags(posts);
const hotTags = getHotTags(posts, tags);
---
<IndexLayout title={`${year}年`}>
<main>
<PostList posts={page.data} />
<div style="display: flex; justify-content: center; margin: 16px;">
<Pager {...page} urlForPage={(n) => `/page/${n}`} />
</div>
</main>
<div
style="display: flex; flex-flow: column nowrap; gap: 16px; height: fit-content; position: sticky; top: 0;"
>
{authors.map((item) => <AuthorCard {...item.data} />)}
<TagListCard title={`${year}年的标签`} tags={tags} hotTags={hotTags} />
</div>
</IndexLayout>

58
src/pages/_headers.ts Normal file
View file

@ -0,0 +1,58 @@
import type { APIRoute } from "astro";
import { getCollection } from "astro:content";
import { postParamlink } from "~/utils/posts";
export const GET: APIRoute = async () => {
const headers = new Map<string, Headers>();
headers.set(
"/*",
new Headers({
"Cache-Control": "public, max-age=1440, must-revalidate",
}),
);
headers.set(
"/_astro/*",
new Headers({
"Cache-Control": "public, max-age=31536000, immutable",
}),
);
const posts = await getCollection("posts");
for (const post of posts) {
const etagContent = JSON.stringify({
id: post.id,
body: post.body,
data: post.data,
});
const etag = `"${new Bun.SHA256().update(etagContent).digest("hex")}"`;
const matcher = postParamlink(post);
headers.set(
matcher,
new Headers({
ETag: etag,
}),
);
headers.set(
`${matcher}index.html`,
new Headers({
ETag: etag,
}),
);
}
const lines = [] as string[];
for (const [k, entries] of headers) {
lines.push(k);
for (const [name, value] of entries) {
lines.push(` ${name}: ${value}`);
}
}
return new Response(lines.join("\n"));
};

82
src/pages/archives.astro Normal file
View file

@ -0,0 +1,82 @@
---
import { getCollection } from "astro:content";
import Regular from "~/layouts/Regular.astro";
import { shouldBeVisible } from "~/utils/posts";
const posts = (await getCollection("posts")).filter(shouldBeVisible);
const postCountByYear = new Map<string, number>();
for (const post of posts) {
const year = post.data.date.getUTCFullYear().toString();
const ocount = postCountByYear.get(year) ?? 0;
postCountByYear.set(year, ocount + 1);
}
const counts = postCountByYear
.entries()
.toArray()
.sort(([y0], [y1]) => Number(y1) - Number(y0));
---
<Regular title="所有归档">
<main id="_layout">
<ul class="archive-list">
{
counts.map(([year, count]) => {
return (
<li class="archive-list-item">
<a class="archive-list-link" href={`/${year}/`}>
{year}
</a>
<span class="archive-list-count">{count}</span>
</li>
);
})
}
</ul>
</main>
</Regular>
<style>
#_layout {
display: grid;
margin-inline: 60px;
padding-block: 24px;
}
@media (max-width: 720px) {
#_layout {
margin-inline: 8px;
}
}
#_layout > :first-child {
max-width: 560px;
}
.archive-list {
display: flex;
flex-flow: column nowrap;
gap: 12px;
padding: 0;
}
.archive-list-item {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--palette-grey-500);
}
a.archive-list-link {
font: var(--typ-title);
line-height: 2;
padding-right: 48px;
padding-left: 16px;
text-align: start;
}
</style>

99
src/pages/atom.xml.ts Normal file
View file

@ -0,0 +1,99 @@
import type { APIRoute } from "astro";
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { loadRenderers } from "astro:container";
import { getContainerRenderer as solidContainerRenderer } from "@astrojs/solid-js";
import { getContainerRenderer as mdxContainerRenderer } from "@astrojs/mdx";
import { getCollection, render } from "astro:content";
import { x } from "xastscript";
import { postParamlink, shouldBeVisible } from "~/utils/posts";
import { toXml } from "xast-util-to-xml";
export const GET: APIRoute = async () => {
const renderers = await loadRenderers([
solidContainerRenderer(),
mdxContainerRenderer(),
]);
const cont = await AstroContainer.create({
renderers: renderers,
});
const posts = (await getCollection("posts"))
.filter(shouldBeVisible)
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
.slice(0, 10);
const authors = await getCollection("authors");
const tr = x(
"feed",
{ xmlns: "http://www.w3.org/2005/Atom" },
x("title", { type: "text" }, "Rubicon's Rubicon"),
x("id", {}, "https://rubicon.lightstands.xyz/"),
x(
"updated",
{},
posts.length > 0
? posts[0].data.date.toISOString()
: new Date().toISOString(),
),
x("link", {
rel: "alternate",
type: "text/html",
hreflang: "zh",
href: "https://rubicon.lightstands.xyz",
}),
x("link", {
rel: "alternate",
type: "application/feed+json",
hreflang: "zh",
href: "https://rubicon.lightstands.xyz/feed.json",
}),
x("link", {
rel: "self",
type: "application/atom+xml",
href: "https://rubicon.lightstands.xyz/atom.xml",
}),
...(await Promise.all(
posts.map(async (item) => {
const href = new URL(
postParamlink(item),
"https://rubicon.lightstands.xyz",
).toString();
const { Content } = await render(item);
return x(
"entry",
{},
x("title", {}, item.data.title),
x("id", {}, href),
x("published", {}, item.data.date.toISOString()),
x(
"updated",
{},
item.data.updated
? item.data.updated.toISOString()
: item.data.date.toISOString(),
),
x("link", {
rel: "alternate",
type: "application/xhtml+html",
href,
}),
...authors.map((author) => {
return x(
"author",
{},
x("name", {}, author.data.name),
(author.data.links?.length ?? 0 > 0)
? x("uri", {}, author.data.links![0].href)
: undefined,
);
}),
x(
"content",
{ type: "text/html", "xml:base": href },
await cont.renderToString(Content),
),
);
}),
)),
);
return new Response(toXml(tr, { allowDangerousXml: true }));
};

View file

@ -0,0 +1,146 @@
---
import "~/material/content.css";
import { getCollection } from "astro:content";
import AuthorCard from "~/components/AuthorCard.astro";
import Wikipedia from "~/components/Wikipedia.astro";
import Regular from "~/layouts/Regular.astro";
const defaultAuthors = (await getCollection("authors")).filter(
(x) => x.data.default
);
---
<Regular title="External Resource Usage" lang="en">
<main>
<section>
<h2>Contract Infomation</h2>
<div class="cols-2">
<div>
<p>
Please tell me if you found unaccpectable traffic coming from this
website.
</p>
<p>
Please check if the traffic actually coming from this site
<a href="https://rubicon.lightstands.xyz">rubicon.lightstands.xyz</a
>. The source code of this site is open source - anyone can use it
and its user agent string to create abuse traffic.
</p>
</div>
<div>
{defaultAuthors.map((author) => <AuthorCard {...author.data} />)}
</div>
</div>
</section>
<section>
<h2>Fetching from Wikipedia</h2>
<div class="cols-2" style="align-items: center;">
<div class="content">
<Wikipedia page="Wikipedia" />
</div>
<p>
This website uses Wikipedia's content to explain terms and ideas to
the visitors, mostly in the form of blockquote, which automatically
fetched by the software depending on the argument provided by the
author.
</p>
</div>
<p>
To achive the best user experience, for every such blockquote, this
software does 1 fetch as they appear in the post. Possibly additional 1
fetch for every shown of the blockquote. Each fetch is assocatiated 1 to
3 requests to variaous Wikipedia instance.
</p>
<p>
The first required fetch is the baking fetch, which appears on the
machine building the software. This software, is being built at once for
every published website change, bakes the requested content in the page.
</p>
<p>
<span style="font-weight: bold;">In the building process,</span> every requested
blockquote will trigger 1 or 2 requests to various instance of Wikipedia.
Due to technology limit, there is not implemented limitation for the advised
200reqs/min. I think that's acceptable because the building is rare, usually
less than 10 times each months, and in one-at-a-time basis, and each building
unlikely create requests more than 5000 . As the written of this page, 2024/04/06,
there just are less than 20 requests.
</p>
<p>
In this matter, the user agent string includes the text "on Server",
indicates this fetch happens on server.
</p>
<p>
Worth noting that one post may be rendered multiple times, so the
blockquotes technically may appear multiple times. This software
provides a whole site JSON feed and a recent Atom feed in addition to
the normal HTML pages. So each blockquote might be fetch 2 - 3 times for
the baking.
</p>
<p>
<span style="font-weight: bold;">The requests happen on client</span> when
the user agent string includes "on Client". They appear when the client-side
script trys to enhance the experience by swap the content with the user accepted
languages. The page content can remain untouched if this enhancement process
failed.
</p>
<p>
Just like the baked fetch, those enhancement fetch is without
implemented limitation for the advised 200reqs/min. We think that's
highly unlikely to have 100 blockquotes from Wikipedia in a post.
</p>
</section>
</main>
</Regular>
<style>
main {
display: grid;
place-items: center;
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 32px);
}
h2 {
margin-top: 36px;
margin-bottom: 24px;
margin-inline: 8px;
}
p {
max-width: 120ch;
margin-inline: 16px;
line-height: 1.75;
}
p + p {
margin-top: 16px;
}
.cols-2 {
display: flex;
flex-flow: row wrap;
gap: 8px;
> * {
max-width: 60ch;
min-width: 300px;
}
> :first-child {
flex: 1;
}
> :last-child {
flex: 1;
}
}
</style>

5
src/pages/feed.json.ts Normal file
View file

@ -0,0 +1,5 @@
import type { APIRoute } from "astro";
export const GET: APIRoute = ({rewrite}) => {
return rewrite("/feeds/1.json")
}

View file

@ -0,0 +1,58 @@
import type { APIRoute, GetStaticPaths, Page } from "astro";
import { getCollection, render, type CollectionEntry } from "astro:content";
import { experimental_AstroContainer as AstroContainer } from "astro/container";
import { loadRenderers } from "astro:container";
import { getContainerRenderer as solidContainerRenderer } from "@astrojs/solid-js";
import { getContainerRenderer as mdxContainerRenderer } from "@astrojs/mdx";
import { postParamlink, shouldBeVisible } from "~/utils/posts";
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection("posts")).filter(shouldBeVisible).sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime(),
);
return paginate(posts, {pageSize: 5});
}) satisfies GetStaticPaths;
export const GET: APIRoute<{ page: Page<CollectionEntry<"posts">> }> = async ({
props: { page },
}) => {
const renderers = await loadRenderers([
solidContainerRenderer(),
mdxContainerRenderer(),
]);
const cont = await AstroContainer.create({
renderers: renderers,
});
const authors = await getCollection("authors");
const hasNextPage = page.currentPage < page.start + 1;
return new Response(
JSON.stringify({
version: "https://jsonfeed.org/version/1.1",
title: "Rubicon's Rubicon",
home_page_url: "https://rubicon.lightstands.xyz/",
feed_url: "https://rubicon.lightstands.xyz/feed.json",
next_url: hasNextPage
? `https://rubicon.lightstands.xyz/feeds/${page.currentPage + 1}.json`
: undefined,
authors: authors.map((item) => {
return {
name: item.data.name,
url: item.data.links?.[0].href,
};
}),
items: await Promise.all(
page.data.map(async (item) => {
const { Content } = await render(item);
return {
id: item.id,
context_html: await cont.renderToString(Content),
url: new URL(postParamlink(item), "https://rubicon.lightstands.xyz").toString(),
};
}),
),
}, undefined, 2),
);
};

3
src/pages/index.astro Normal file
View file

@ -0,0 +1,3 @@
---
return Astro.rewrite("/page/1")
---

View file

@ -0,0 +1,73 @@
---
import type { GetStaticPathsOptions } from "astro";
import { Image } from "astro:assets";
import { getCollection } from "astro:content";
import AuthorCard from "~/components/AuthorCard.astro";
import Pager from "~/components/Pager.astro";
import PostList from "~/components/PostList.astro";
import TagListCard from "~/components/TagListCard.astro";
import IndexLayout from "~/layouts/IndexLayout.astro";
import { getAllTags, getHotTags, shouldBeVisible } from "~/utils/posts";
import JSONFeedIcon from "~/assets/jsonfeed.png"
import { Icon } from "astro-icon/components";
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
const posts = (await getCollection("posts")).filter(shouldBeVisible).sort((a, b) => {
return b.data.date.getTime() - a.data.date.getTime();
});
return paginate(posts, {});
}
const { page } = Astro.props;
const posts = await getCollection("posts");
const tags = getAllTags(posts);
const hotTags = getHotTags(posts, tags);
const defaultAuthors = (await getCollection("authors")).filter(
(x) => x.data.default
)!;
---
<IndexLayout title="Rubicon's Rubicon">
<link
slot="head"
rel="alternate"
title="Rubicon's Rubicon Feed"
type="application/feed+json"
href="/feed.json"
/>
<main>
<PostList posts={page.data} />
<div style="display: flex; justify-content: center; margin: 16px;">
<Pager {...page} urlForPage={(n) => `/page/${n}`} />
</div>
</main>
<div
style="display: flex; flex-flow: column nowrap; gap: 16px; height: fit-content; position: sticky; top: 0;"
>
{defaultAuthors.map((item) => <AuthorCard {...item.data} />)}
<TagListCard title="所有标签" tags={tags} hotTags={hotTags} />
<div style="display: flex; gap: 8px; flex-flow: row wrap;">
<a href="/feed.json" style="display: inline-flex; align-items: center; gap: 8px" data-astro-prefetch="false">
使用JSON Feed订阅此网站
<Image
src={JSONFeedIcon}
alt="JSON Feed图标"
height={16}
width={16}
style={{width: "1em", height: "1em"}}
/>
</a>
<a href="/atom.xml" style="display: inline-flex; align-items: center; gap: 8px" data-astro-prefetch="false">
使用Atom订阅此网站
<Icon name="mdi:rss-feed"/>
</a>
<a href="/external-resource-usage/">
External Resource Usage
</a>
</div>
</div>
</IndexLayout>

View file

@ -0,0 +1,18 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import { getAllTags } from "~/utils/posts";
export const getStaticPaths = (async () => {
const posts = await getCollection("posts");
const tags = getAllTags(posts);
return tags.map((t) => ({
params: { name: t },
props: { name: t },
}));
}) satisfies GetStaticPaths;
const { name } = Astro.props;
return Astro.rewrite(`/tags/${name}/1`);
---

View file

@ -0,0 +1,49 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
import AuthorCard from "~/components/AuthorCard.astro";
import Pager from "~/components/Pager.astro";
import PostList from "~/components/PostList.astro";
import TagListCard from "~/components/TagListCard.astro";
import IndexLayout from "~/layouts/IndexLayout.astro";
import { getAllTags, getHotTags, shouldBeVisible } from "~/utils/posts";
export const getStaticPaths = (async ({ paginate }) => {
const posts = (await getCollection("posts")).filter(shouldBeVisible);
const tags = getAllTags(posts);
const postsByTag = tags.map((t) => {
return posts.filter((p) => p.data.tags?.includes(t));
});
return tags.flatMap((name, idx) => {
return paginate(postsByTag[idx], {
params: { name },
props: { name },
});
});
}) satisfies GetStaticPaths;
const { page, name } = Astro.props;
const posts = (await getCollection("posts")).filter(shouldBeVisible);
const tags = getAllTags(posts);
const hotTags = getHotTags(posts, tags);
const defaultAuthors = (await getCollection("authors")).filter(
(x) => x.data.default
)!;
---
<IndexLayout title={`标记为“${name}”的文章`}>
<main>
<PostList posts={page.data} />
<div style="display: flex; justify-content: center; margin: 16px;">
<Pager {...page} urlForPage={(n) => `/page/${n}`} />
</div>
</main>
<div
style="display: flex; flex-flow: column nowrap; gap: 16px; height: fit-content; position: sticky; top: 0;"
>
{defaultAuthors.map((item) => <AuthorCard {...item.data} />)}
<TagListCard title="所有标签" tags={tags} hotTags={hotTags} />
</div>
</IndexLayout>