website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


20f0db9 — pyrossh 2026-05-24T17:56:52+05:30
make everything server side

Generated files not shown: bun.lock

package.json CHANGED
@@ -22,10 +22,16 @@
22
22
  "astro-color-scheme": "^1.1.6",
23
23
  "astro-expressive-code": "^0.42.0",
24
24
  "astro-icon": "^1.1.5",
25
+ "gray-matter": "^4.0.3",
25
26
  "pretty-bytes": "^7.1.0",
27
+ "rehype-expressive-code": "^0.42.0",
28
+ "rehype-stringify": "^10.0.1",
29
+ "remark-parse": "^11.0.0",
30
+ "remark-rehype": "^11.1.2",
26
31
  "simple-git": "^3.27.0",
27
32
  "spamscanner": "^6.1.5",
28
- "timeago.js": "^4.0.2"
33
+ "timeago.js": "^4.0.2",
34
+ "unified": "^11.0.5"
29
35
  },
30
36
  "devDependencies": {
31
37
  "@astrojs/rss": "^4.0.18",
src/content.config.ts DELETED
@@ -1,51 +0,0 @@
1
- import fs from "fs/promises";
2
- import { glob } from 'astro/loaders';
3
- import { defineCollection, z } from 'astro:content';
4
- import { REPOS } from './consts';
5
- import { checkFileExists } from "./utils/files";
6
-
7
- export const collections = {
8
- repos: defineCollection({
9
- loader: {
10
- name: "loader",
11
- load: async ({ renderMarkdown, store }) => {
12
- for (const r of REPOS) {
13
- const repoPath = `/Users/pyrossh/Code/pyrossh/${r.title}`;
14
- const readmePath = repoPath + "/README.md";
15
- const readmeContent = await checkFileExists(readmePath) ? await renderMarkdown(await fs.readFile(readmePath, 'utf-8')) : {
16
- html: '',
17
- metadata: {},
18
- }
19
- store.set({
20
- id: r.title,
21
- data: {
22
- title: r.title,
23
- description: r.description,
24
- tags: r.tags,
25
- absolutePath: repoPath,
26
- },
27
- rendered: readmeContent,
28
- });
29
- }
30
- },
31
- },
32
- schema: z.object({
33
- title: z.string(),
34
- description: z.string().max(80),
35
- tags: z.array(z.string()),
36
- absolutePath: z.string(),
37
- }),
38
- }),
39
- content: defineCollection({
40
- loader: glob({ base: './src/content', pattern: '**/*.{md,mdx}' }),
41
- // Type-check frontmatter using a schema
42
- schema: z.object({
43
- title: z.string(),
44
- description: z.string(),
45
- // Transform string to Date object
46
- pubDate: z.coerce.date(),
47
- updatedDate: z.coerce.date().optional(),
48
- heroImage: z.string().optional(),
49
- }),
50
- }),
51
- };
src/layouts/FileLayout.astro CHANGED
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  import { Icon } from "astro-icon/components";
3
- import { type CollectionEntry } from "astro:content";
4
3
  import allIcons from "@iconify-json/vscode-icons/icons.json" assert { type: "json" };
5
4
  import RepoLayout from "@/layouts/RepoLayout.astro";
5
+ import type { RuntimeRepo } from "@/utils/content";
6
6
  import { resolveFileIcon, type FileNode } from "@/utils/files";
7
7
 
8
8
  type Props = {
9
- repo: CollectionEntry<"repos">;
9
+ repo: RuntimeRepo;
10
10
  file: FileNode;
11
11
  };
12
12
 
src/layouts/RepoLayout.astro CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
- import { type CollectionEntry } from "astro:content";
3
2
  import { Icon } from "astro-icon/components";
4
3
  import Layout from "@/layouts/BaseLayout.astro";
4
+ import type { RuntimeRepo } from "@/utils/content";
5
5
 
6
6
  type Props = {
7
- repo: CollectionEntry<"repos">;
7
+ repo: RuntimeRepo;
8
8
  };
9
9
 
10
10
  const { repo } = Astro.props;
src/pages/posts/[...postId]/index.astro CHANGED
@@ -1,11 +1,9 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
- import { render } from "astro:content";
4
2
  import Layout from "@/layouts/BaseLayout.astro";
5
3
  import FormattedDate from "@/components/FormattedDate.astro";
4
+ import { getPost } from "@/utils/content";
6
5
 
7
- const posts = await getCollection("content");
6
+ const post = await getPost(Astro.params.postId);
8
- const post = posts.find((item) => item.id === Astro.params.postId);
9
7
  if (!post) {
10
8
  return Astro.redirect("/404");
11
9
  }
@@ -13,7 +11,6 @@ if (!post) {
13
11
  const {
14
12
  data: { title, description, pubDate },
15
13
  } = post;
16
- const { Content } = await render(post);
17
14
  ---
18
15
 
19
16
  <Layout title={title} description={description}>
@@ -22,7 +19,7 @@ const { Content } = await render(post);
22
19
  <h2>{description}</h2>
23
20
  <FormattedDate date={pubDate} />
24
21
  <hr />
25
- <Content />
22
+ <div set:html={post.html} />
26
23
  </article>
27
24
  </Layout>
28
25
  <style>
src/pages/posts/index.astro CHANGED
@@ -1,11 +1,9 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
2
  import FormattedDate from "../../components/FormattedDate.astro";
4
3
  import Layout from "@/layouts/BaseLayout.astro";
4
+ import { getPosts } from "@/utils/content";
5
5
 
6
- const posts = (await getCollection("content")).sort(
6
+ const posts = await getPosts();
7
- (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
8
- );
9
7
  ---
10
8
 
11
9
  <Layout title="Posts" description="Posts">
src/pages/repos/[...repoId]/commits/[...hash]/index.astro CHANGED
@@ -1,12 +1,11 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
2
  import RepoLayout from "@/layouts/RepoLayout.astro";
4
3
  import Commit from "@/components/Commit.astro";
5
4
  import { generateHTMLDiff, getCommits } from "@/utils/commit";
5
+ import { getRepo } from "@/utils/content";
6
6
  import "diff2html/bundles/css/diff2html.min.css";
7
7
 
8
- const repos = await getCollection("repos");
9
- const repo = repos.find((item) => item.id === Astro.params.repoId);
8
+ const repo = getRepo(Astro.params.repoId);
10
9
  if (!repo) {
11
10
  return Astro.redirect("/404");
12
11
  }
src/pages/repos/[...repoId]/commits/index.astro CHANGED
@@ -1,11 +1,10 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
2
  import RepoLayout from "@/layouts/RepoLayout.astro";
4
3
  import Commit from "@/components/Commit.astro";
5
4
  import { getCommits } from "@/utils/commit";
5
+ import { getRepo } from "@/utils/content";
6
6
 
7
- const repos = await getCollection("repos");
8
- const repo = repos.find((item) => item.id === Astro.params.repoId);
7
+ const repo = getRepo(Astro.params.repoId);
9
8
  if (!repo) {
10
9
  return Astro.redirect("/404");
11
10
  }
src/pages/repos/[...repoId]/files/[...file]/blame/index.astro CHANGED
@@ -1,10 +1,9 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
2
  import FileLayout from "@/layouts/FileLayout.astro";
4
3
  import { getFileHistory, getFiles } from "@/utils/commit";
4
+ import { getRepo } from "@/utils/content";
5
5
 
6
- const repos = await getCollection("repos");
7
- const repo = repos.find((item) => item.id === Astro.params.repoId);
6
+ const repo = getRepo(Astro.params.repoId);
8
7
  if (!repo) {
9
8
  return Astro.redirect("/404");
10
9
  }
src/pages/repos/[...repoId]/files/[...file]/history/index.astro CHANGED
@@ -1,11 +1,10 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
2
  import FileLayout from "@/layouts/FileLayout.astro";
4
3
  import Commit from "@/components/Commit.astro";
5
4
  import { getFileHistory, getFiles } from "@/utils/commit";
5
+ import { getRepo } from "@/utils/content";
6
6
 
7
- const repos = await getCollection("repos");
8
- const repo = repos.find((item) => item.id === Astro.params.repoId);
7
+ const repo = getRepo(Astro.params.repoId);
9
8
  if (!repo) {
10
9
  return Astro.redirect("/404");
11
10
  }
src/pages/repos/[...repoId]/files/[...file]/index.astro CHANGED
@@ -1,12 +1,11 @@
1
1
  ---
2
2
  import fs from "fs/promises";
3
- import { getCollection } from "astro:content";
4
3
  import { Code } from "astro-expressive-code/components";
5
4
  import FileLayout from "@/layouts/FileLayout.astro";
6
5
  import { getFiles } from "@/utils/commit";
6
+ import { getRepo } from "@/utils/content";
7
7
 
8
- const repos = await getCollection("repos");
9
- const repo = repos.find((item) => item.id === Astro.params.repoId);
8
+ const repo = getRepo(Astro.params.repoId);
10
9
  if (!repo) {
11
10
  return Astro.redirect("/404");
12
11
  }
src/pages/repos/[...repoId]/files/index.astro CHANGED
@@ -1,12 +1,11 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
2
  import RepoLayout from "@/layouts/RepoLayout.astro";
4
3
  import RecursiveList from "@/components/RecursiveList.astro";
5
4
  import { getFiles } from "@/utils/commit";
5
+ import { getRepo } from "@/utils/content";
6
6
  import { buildFileTree, sortChildren } from "@/utils/files";
7
7
 
8
- const repos = await getCollection("repos");
9
- const repo = repos.find((item) => item.id === Astro.params.repoId);
8
+ const repo = getRepo(Astro.params.repoId);
10
9
  if (!repo) {
11
10
  return Astro.redirect("/404");
12
11
  }
src/pages/repos/[...repoId]/index.astro CHANGED
@@ -1,20 +1,18 @@
1
1
  ---
2
- import { getCollection } from "astro:content";
3
- import { render } from "astro:content";
4
2
  import RepoLayout from "@/layouts/RepoLayout.astro";
3
+ import { getRepo, getRepoReadmeHtml } from "@/utils/content";
5
4
 
6
- const repos = await getCollection("repos");
7
- const repo = repos.find((item) => item.id === Astro.params.repoId);
5
+ const repo = getRepo(Astro.params.repoId);
8
6
  if (!repo) {
9
7
  return Astro.redirect("/404");
10
8
  }
11
9
 
12
- const { Content } = await render(repo);
10
+ const readmeHtml = await getRepoReadmeHtml(repo);
13
11
  ---
14
12
 
15
13
  <RepoLayout repo={repo}>
16
14
  <article>
17
- <Content />
15
+ <div set:html={readmeHtml} />
18
16
  </article>
19
17
  </RepoLayout>
20
18
  <style>
src/pages/repos/[...repoId]/issues/[...issueId]/index.astro CHANGED
@@ -1,22 +1,15 @@
1
1
  ---
2
- import { type CollectionEntry, getCollection } from "astro:content";
3
2
  import { actions } from "astro:actions";
4
3
  import RepoLayout from "@/layouts/RepoLayout.astro";
5
4
  import Issue from "@/components/Issue.astro";
6
5
  import TimeAgo from "@/components/TimeAgo.astro";
6
+ import { getRepo } from "@/utils/content";
7
7
  import {
8
8
  getGitBugIssue,
9
9
  getGitBugIssues,
10
- type GitBugIssue,
11
10
  } from "@/utils/gitBug";
12
11
 
13
- type Props = {
14
- repo: CollectionEntry<"repos">;
15
- issue: GitBugIssue;
16
- };
17
-
18
- const repos = await getCollection("repos");
19
- const repo = repos.find((item) => item.id === Astro.params.repoId);
12
+ const repo = getRepo(Astro.params.repoId);
20
13
  if (!repo) {
21
14
  return Astro.redirect("/404");
22
15
  }
src/pages/repos/[...repoId]/issues/index.astro CHANGED
@@ -1,16 +1,11 @@
1
1
  ---
2
- import { type CollectionEntry, getCollection } from "astro:content";
3
2
  import { actions } from "astro:actions";
4
3
  import RepoLayout from "@/layouts/RepoLayout.astro";
5
4
  import Issue from "@/components/Issue.astro";
5
+ import { getRepo } from "@/utils/content";
6
6
  import { getGitBugIssues } from "@/utils/gitBug";
7
7
 
8
- type Props = {
9
- repo: CollectionEntry<"repos">;
10
- };
11
-
12
- const repos = await getCollection("repos");
13
- const repo = repos.find((item) => item.id === Astro.params.repoId);
8
+ const repo = getRepo(Astro.params.repoId);
14
9
  if (!repo) {
15
10
  return Astro.redirect("/404");
16
11
  }
src/pages/rss.xml.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import type { APIRoute } from 'astro';
2
- import { getCollection } from 'astro:content';
3
2
  import rss from '@astrojs/rss';
4
3
  import { SITE_TITLE, SITE_DESCRIPTION } from '../consts';
4
+ import { getPosts } from '../utils/content';
5
5
 
6
6
  export const GET: APIRoute = async ({ site }) => {
7
- const posts = await getCollection('content');
7
+ const posts = await getPosts();
8
8
  return rss({
9
9
  title: SITE_TITLE,
10
10
  description: SITE_DESCRIPTION,
src/utils/content.ts ADDED
@@ -0,0 +1,127 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import matter from "gray-matter";
4
+ import rehypeExpressiveCode from "rehype-expressive-code";
5
+ import rehypeStringify from "rehype-stringify";
6
+ import remarkParse from "remark-parse";
7
+ import remarkRehype from "remark-rehype";
8
+ import { unified } from "unified";
9
+ import { REPOS } from "@/consts";
10
+ import ecConfig from "../../ec.config.mjs";
11
+
12
+ export interface RuntimeRepo {
13
+ id: string;
14
+ data: {
15
+ title: string;
16
+ description: string;
17
+ tags: string[];
18
+ absolutePath: string;
19
+ };
20
+ }
21
+
22
+ export interface RuntimePost {
23
+ id: string;
24
+ data: {
25
+ title: string;
26
+ description: string;
27
+ pubDate: Date;
28
+ updatedDate?: Date;
29
+ heroImage?: string;
30
+ };
31
+ body: string;
32
+ html: string;
33
+ }
34
+
35
+ const contentDir = path.resolve("src/content");
36
+
37
+ const renderMarkdown = async (markdown: string) => {
38
+ const file = await unified()
39
+ .use(remarkParse)
40
+ .use(remarkRehype)
41
+ .use(rehypeExpressiveCode, ecConfig)
42
+ .use(rehypeStringify)
43
+ .process(markdown);
44
+
45
+ return String(file);
46
+ };
47
+
48
+ const toDate = (value: unknown) => {
49
+ if (value instanceof Date) {
50
+ return value;
51
+ }
52
+ return new Date(String(value));
53
+ };
54
+
55
+ const readMarkdownFiles = async (dir: string): Promise<string[]> => {
56
+ const entries = await fs.readdir(dir, { withFileTypes: true });
57
+ const files = await Promise.all(
58
+ entries.map(async (entry) => {
59
+ const itemPath = path.join(dir, entry.name);
60
+ if (entry.isDirectory()) {
61
+ return await readMarkdownFiles(itemPath);
62
+ }
63
+ return /\.(md|mdx)$/.test(entry.name) ? [itemPath] : [];
64
+ }),
65
+ );
66
+ return files.flat();
67
+ };
68
+
69
+ export const getRepos = (): RuntimeRepo[] =>
70
+ REPOS.map((repo) => ({
71
+ id: repo.title,
72
+ data: {
73
+ ...repo,
74
+ absolutePath: `/Users/pyrossh/Code/pyrossh/${repo.title}`,
75
+ },
76
+ }));
77
+
78
+ export const getRepo = (repoId?: string) =>
79
+ getRepos().find((repo) => repo.id === repoId);
80
+
81
+ export const getRepoReadmeHtml = async (repo: RuntimeRepo) => {
82
+ try {
83
+ const readme = await fs.readFile(
84
+ path.join(repo.data.absolutePath, "README.md"),
85
+ "utf-8",
86
+ );
87
+ return await renderMarkdown(readme);
88
+ } catch {
89
+ return "";
90
+ }
91
+ };
92
+
93
+ export const getPosts = async (): Promise<RuntimePost[]> => {
94
+ const files = await readMarkdownFiles(contentDir);
95
+ const posts = await Promise.all(
96
+ files.map(async (filePath) => {
97
+ const source = await fs.readFile(filePath, "utf-8");
98
+ const parsed = matter(source);
99
+ const relativePath = path.relative(contentDir, filePath);
100
+ const id = relativePath.replace(/\.(md|mdx)$/, "");
101
+
102
+ return {
103
+ id,
104
+ data: {
105
+ title: String(parsed.data.title ?? ""),
106
+ description: String(parsed.data.description ?? ""),
107
+ pubDate: toDate(parsed.data.pubDate),
108
+ updatedDate: parsed.data.updatedDate
109
+ ? toDate(parsed.data.updatedDate)
110
+ : undefined,
111
+ heroImage: parsed.data.heroImage
112
+ ? String(parsed.data.heroImage)
113
+ : undefined,
114
+ },
115
+ body: parsed.content,
116
+ html: await renderMarkdown(parsed.content),
117
+ };
118
+ }),
119
+ );
120
+
121
+ return posts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
122
+ };
123
+
124
+ export const getPost = async (postId?: string) => {
125
+ const posts = await getPosts();
126
+ return posts.find((post) => post.id === postId);
127
+ };