From f1d413afaeb1c8af6cacf10d7e48379a3ccbf4a2 Mon Sep 17 00:00:00 2001 From: libr Date: Fri, 28 Nov 2025 17:20:08 +0800 Subject: [PATCH] fix --- .dockerignore | 3 + Dockerfile | 14 ++ README.md | 295 ----------------------------------- astro.config.ts | 7 +- src/lib/data-utils.ts | 116 +++----------- src/pages/blog/[...id].astro | 17 +- 6 files changed, 49 insertions(+), 403 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile delete mode 100644 README.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..262e83b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.astro/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6d154b9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM oven/bun:latest AS build + +ADD * /app/ +WORKDIR /app +RUN bun install && \ + bun run astro build + +FROM node:22-alpine +WORKDIR /app +COPY --from=build /app/dist ./dist +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/node_modules ./node_modules +EXPOSE 3000 +CMD ["node", "dist/server/entry.mjs"] \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index e88a09c..0000000 --- a/README.md +++ /dev/null @@ -1,295 +0,0 @@ -![Showcase Card](/public/static/twitter-card.png) - -
- -## astro-erudite - -![Stargazers] -[![License]](LICENSE) - -
- -astro-erudite is an opinionated, unstyled static blogging template built with [Astro](https://astro.build/), [Tailwind](https://tailwindcss.com/), and [shadcn/ui](https://ui.shadcn.com/). Extraordinarily loosely based off the [Astro Micro](https://astro-micro.vercel.app/) theme by [trevortylerlee](https://github.com/trevortylerlee). - -| ![Preview 1](/public/static/preview-1.png) | ![Preview 2](/public/static/preview-2.png) | -| ------------------------------------------ | ------------------------------------------ | -| ![Preview 3](/public/static/preview-3.png) | ![Preview 4](/public/static/preview-4.png) | - -> [!NOTE] -> To learn more about why this template exists, read [The State of Static Blogs in 2024](https://astro-erudite.vercel.app/blog/the-state-of-static-blogs), where I share my take on what constitutes a great blogging template and my goals while developing this one. - ---- - -## Community Examples - -Below are some fantastic examples of websites based on this template. If you wish to add your site to this list, open a pull request! - -| Site | Author | Description/Features | Source | -| ---------------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | -| [enscribe.dev](https://enscribe.dev) | [@jktrn](https://github.com/jktrn) | Heavily modified bento-style homepage with client interactivity, with custom MDX components! | [→](https://github.com/jktrn/enscribe.dev) | -| [emile.sh](https://emile.sh) | [@echoghi](https://github.com/echoghi) | A minimalist personal blog using the [flexoki](https://stephango.com/flexoki) theme | [→](https://github.com/echoghi/v5) | -| [decentparadox.me](https://decentparadox.me) | [@decentparadox](https://github.com/decentparadox) | A heavily customized personal portfolio with a sci-fi theme! | [→](https://github.com/decentparadox/decentparadox.me) | -| [flocto.github.io](https://flocto.github.io/) | [@flocto](https://github.com/flocto) | A slightly modified personal blog | [→](https://github.com/flocto/flocto.github.io) | -| [dumbprism.me](https://www.dumbprism.me/) | [@dumbprism](https://github.com/dumbprism) | A customized portfolio inspired by enscribe's bento grid style adding my gist of UI | [→](https://github.com/dumbprism/dumbprism-portfolio) | -| [hyuki.dev](https://hyuki.dev/) | [@snow0406](https://github.com/snow0406) | A minimalist blog with a blue color scheme, focusing on simplicity! | [→](https://github.com/Snow0406/hyuki.dev) | -| [ldd.cc](https://ldd.cc/) | [@xJoyLu](https://github.com/xjoylu) | The cream of the idlers. | [→](https://ldd.cc/) | -| [rezarezvan.com](https://rezarezvan.com/) | [@rezaarezvan](https://github.com/rezaarezvan) | A academic blog with personal touches :). | [→](https://rezarezvan.com/) | -| [blog.z0x.ca](https://blog.z0x.ca/) | [@z0x](https://z0x.ca) | _Very_ minimal version of erudite, stripping it down to the bare essentials | [→](https://git.z0x.ca/z0x/blog.z0x.ca/) | -| [angelaytchan.net](https://angelaytchan.net/) | [@wispyplant](https://github.com/wispyplant) | An artist portfolio and activities archive | [→](https://github.com/wispyplant/wispyplant.github.io) | -| [kaezr.xyz](https://kaezr.xyz/) | [@kaezrr](https://github.com/kaezrr) | A minimal porfolio and blog website with slight tweaks to the original. | [→](https://github.com/kaezrr/webfolio) | -| [worldwidewong](https://worldwidewong.vercel.app) | [@brendanwong-web](https://github.com/brendanwong-web) | A slightly funky portfolio, blog, and resume site with an added photo gallery. | [→](https://github.com/brendanwong-web/worldwidewong) | -| [bgajjala.dev](https://bgajjala.dev) | [@bgajjala8](https://github.com/bgajjala8) | A minimal blog featuring a paper-color inspired color scheme | [→](https://github.com/bgajjala8/bgajjala.dev) | -| [ankitz007.vercel.app](https://ankitz007.vercel.app) | [@ankitz007](https://github.com/ankitz007) | A personal blog with a few modifications and updates to the original. | [→](https://github.com/ankitz007/webfolio) | -| [sadman.ca](https://sadman.ca) | [@sadmanca](https://github.com/sadmanca) | A customized personal blog with: Goodreads reading progress tracker, SVG thumbnails, custom heading styles, and dynamic media grids (books, movies, etc.) | [→](https://github.com/sadmanca/blogv3) | -| [marcel-to.vercel.app](https://marcel-to.vercel.app) | [@Marcel-TO](https://github.com/Marcel-TO) | A content‑driven personal portfolio showcasing software projects, deep–dive blog series and multi-part project documentation. | [→](https://github.com/Marcel-TO/marcel-to-website) | -| [merox.dev](https://merox.dev) | [@meroxdotdev](https://github.com/meroxdotdev) | A technical blog focused on DevOps automation and homelab infrastructure | [→](https://github.com/meroxdotdev/merox) | - -## Features - -- [Astro](https://astro.build/)'s [Islands](https://docs.astro.build/en/concepts/islands/) architecture for selective hydration and client-side interactivity while maintaining fast static site rendering. -- [shadcn/ui](https://ui.shadcn.com/) with [Tailwind](https://tailwindcss.com/) color conventions for automatic light and dark theme styling. Features accessible, theme-aware UI components for navigation, buttons, and more. -- [Expressive Code](https://expressive-code.com/) for enhanced code block styling, syntax highlighting, and code block titles. -- Blog authoring with [MDX](https://mdxjs.com/) for component-rich content and $\LaTeX$ math rendering via [KaTeX](https://katex.org/). -- Astro [View Transitions](https://docs.astro.build/en/guides/view-transitions/) in SPA mode for smooth route animations. -- SEO optimization with granular metadata and [Open Graph](https://ogp.me/) tag control for each post. -- [RSS](https://en.wikipedia.org/wiki/RSS) feed and sitemap generation. -- Subpost support for breaking long content into digestible parts and organizing related series. -- Author profiles with a dedicated authors page and multi-author post support. -- Project tags with a dedicated tags page for post categorization and discovery. -- Custom Callout component variants for enhanced technical writing. - -## Technology Stack - -This is a list of the various technologies used to build this template: - -| Category | Technology Name | -| ---------- | ------------------------------------------------------------------------------------------ | -| Framework | [Astro](https://astro.build/) | -| Styling | [Tailwind](https://tailwindcss.com) | -| Components | [shadcn/ui](https://ui.shadcn.com/) | -| Content | [MDX](https://mdxjs.com/) | -| Codeblocks | [Expressive Code](https://expressive-code.com/), [Shiki](https://github.com/shikijs/shiki) | -| Graphics | [Figma](https://www.figma.com/) | -| Deployment | [Vercel](https://vercel.com) | - -## Getting Started - -1. Hit “Use this template”, the big green button on the top right, to create a new repository in your own GitHub account with this template. - -2. Clone the repository: - - ```bash - git clone https://github.com/[YOUR_USERNAME]/[YOUR_REPO_NAME].git - cd [YOUR_REPO_NAME] - ``` - -3. Install dependencies: - - ```bash - npm install - ``` - -4. Start the development server: - - ```bash - npm run dev - ``` - -5. Open your browser and visit `http://localhost:1234` to get started. The following commands are also available: - - | Command | Description | - | ------------------ | --------------------------------------------------------------- | - | `npm run start` | Alias for `npm run dev` | - | `npm run build` | Run type checking and build the project | - | `npm run preview` | Previews the built project | - | `npm run astro` | Run Astro CLI commands | - | `npm run prettier` | Blanket format all files using [Prettier](https://prettier.io/) | - -## Customization - -### Site Configuration - -Edit the `src/consts.ts` file to update your site's metadata, navigation links, and social links: - -```ts -export const SITE: Site = { - title: 'astro-erudite', - description: // ... - href: 'https://astro-erudite.vercel.app', - featuredPostCount: 2, - postsPerPage: 3, -} - -export const NAV_LINKS: SocialLink[] = [ - { - href: '/blog', - label: 'blog', - }, - // ... -] - -export const SOCIAL_LINKS: SocialLink[] = [ - { - href: 'https://github.com/jktrn', - label: 'GitHub', - }, - // ... -] -``` - -### Color Palette - -Colors are defined in `src/styles/global.css` in [OKLCH format](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch), using the [shadcn/ui](https://ui.shadcn.com/) convention: - -```css -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); -} - -[data-theme='dark'] { - /* ... */ -} -``` - -### Favicons - -Favicons are generated using [RealFaviconGenerator](https://realfavicongenerator.net/). To adjust the favicons, replace the files in the `public/` directory (such as `favicon.ico`, `favicon.svg`, `apple-touch-icon.png`, etc.) with your own. After updating the favicon files, you'll also need to adjust the references in `src/components/Favicons.astro` to match your new favicon filenames and paths: - -```html - - - - - - - -``` - -## Adding Content - -### Blog Posts - -Add new blog posts as MDX files in the `src/content/blog/` directory. Use the following frontmatter structure: - -```yml ---- -title: 'Your Post Title' -description: 'A brief description of your post!' -date: 2024-01-01 -tags: ['tag1', 'tag2'] -image: './image.png' -authors: ['author1', 'author2'] -draft: false ---- -``` - -The blog post schema is defined as follows: - -| Field | Type (Zod) | Requirements | Required | -| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| `title` | `string` | Should be ≤60 characters. | Yes | -| `description` | `string` | Should be ≤155 characters. | Yes | -| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes | -| `order` | `number` | Sort order for subposts with the same `date`. Defaults to `0` if not provided. | Optional | -| `image` | `image()` | Should be exactly 1200px × 630px. | Optional | -| `tags` | `string[]` | Preferably use kebab-case for these. | Optional | -| `authors` | `string[]` | If the author has a profile, use the id associated with their Markdown file in `src/content/authors/` (e.g. if their file is named `jane-doe.md`, use `jane-doe` in the array). | Optional | -| `draft` | `boolean` | Defaults to `false` if not provided. | Optional | - -### Authors - -Add author information in `src/content/authors/` as Markdown files. A file named `[author-name].md` can be associated with a blog post if `"author-name"` (the id) is added to the `authors` field: - -```yml ---- -name: 'enscribe' -pronouns: 'he/him' -avatar: 'https://gravatar.com/avatar/9bfdc4ec972793cf05cb91efce5f4aaaec2a0da1bf4ec34dad0913f1d845faf6.webp?size=256' -bio: 'd(-_-)b' -website: 'https://enscribe.dev' -twitter: 'https://twitter.com/enscry' -github: 'https://github.com/jktrn' -mail: 'jason@enscribe.dev' ---- -``` - -The author schema is defined as follows: - -| Field | Type (Zod) | Requirements | Required | -| ---------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | -| `name` | `string` | n/a | Yes | -| `pronouns` | `string` | n/a | Optional | -| `avatar` | `string.url()` or `string.startsWith('/')` | Should be either a valid URL or a path starting with `/`. Preferably use [Gravatar](https://en.gravatar.com/site/implement/images/) with the `?size=256` size parameter. | Yes | -| `bio` | `string` | n/a | Optional | -| `mail` | `string.email()` | Must be a valid email address. | Optional | -| `website` | `string.url()` | Must be a valid URL. | Optional | -| `twitter` | `string.url()` | Must be a valid URL. | Optional | -| `github` | `string.url()` | Must be a valid URL. | Optional | -| `linkedin` | `string.url()` | Must be a valid URL. | Optional | -| `discord` | `string.url()` | Must be a valid URL. | Optional | - -> [!TIP] -> You can add as many social media links as you want, as long as you adjust the schema! Make sure you also support the new field in the `src/components/SocialIcons.astro` component. - -### Projects - -Add projects in `src/content/projects/` as Markdown files: - -```yml ---- -name: 'Project A' -description: 'This is an example project description! You should replace this with a description of your own project.' -tags: ['Framework A', 'Library B', 'Tool C', 'Resource D'] -image: '/static/1200x630.png' -link: 'https://example.com' -startDate: '2024-01-01' -endDate: '2024-01-01' ---- -``` - -The project schema is defined as follows: - -| Field | Type (Zod) | Requirements | Required | -| ------------- | --------------- | --------------------------------------- | -------- | -| `name` | `string` | n/a | Yes | -| `description` | `string` | n/a | Yes | -| `tags` | `string[]` | n/a | Yes | -| `image` | `image()` | Should be exactly 1200px × 630px. | Yes | -| `link` | `string.url()` | Must be a valid URL. | Yes | -| `startDate` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Optional | -| `endDate` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Optional | - -## License - -This project is open source and available under the [MIT License](LICENSE). - ---- - -### Star History - - - - - - Star History Chart - - - ---- - -Built with ♥ by [enscribe](https://enscribe.dev)! - -[Stargazers]: https://img.shields.io/github/stars/jktrn/astro-erudite?color=fafafa&logo=github&logoColor=fff&style=for-the-badge -[License]: https://img.shields.io/github/license/jktrn/astro-erudite?color=0a0a0a&logo=github&logoColor=fff&style=for-the-badge diff --git a/astro.config.ts b/astro.config.ts index 282dfae..df589a9 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -20,8 +20,6 @@ import tailwindcss from '@tailwindcss/vite' import node from '@astrojs/node'; -import vercel from '@astrojs/vercel'; - export default defineConfig({ site: 'https://nvme0n1p.dev', output: 'server', @@ -114,5 +112,8 @@ export default defineConfig({ remarkPlugins: [remarkMath, remarkEmoji], }, - adapter: vercel(), + adapter: node({ + mode: 'standalone', + + }), }) \ No newline at end of file diff --git a/src/lib/data-utils.ts b/src/lib/data-utils.ts index 301e02a..1576f2d 100644 --- a/src/lib/data-utils.ts +++ b/src/lib/data-utils.ts @@ -70,34 +70,8 @@ export async function getAllAuthors(): Promise[]> { return getCollectionSafe('authors') } -export const POSTS_API_URL = 'https://notion-api.nvme0n1p.dev/v2/posts' - -export async function getAllPosts(): Promise[]> { - const fallback = async () => { - const posts = await getCollectionSafe('blog') - return posts - .filter((post) => !post.data.draft && !isSubpost(post.id)) - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) - .map((post) => ({ - ...post, - data: { - ...post.data, - banner: post.data.banner ?? post.data.image, - authors: [DEFAULT_AUTHOR_ID], - }, - })) - } - - try { - const res = await fetch(POSTS_API_URL) - if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`) - const payload = (await res.json()) as { posts?: NotionPost[] } - const posts = (payload.posts ?? []).filter( - (post) => post.Published ?? true, - ) - - const normalized = posts.map((post) => { - const dateString = +export function normalizePost(post: NotionPost): CollectionEntry<'blog'> { + const dateString = post['Published Date'] ?? post.created_time ?? new Date().toISOString() const date = new Date(dateString) const id = post.slug || post.id @@ -114,37 +88,39 @@ export async function getAllPosts(): Promise[]> { tags: Array.isArray(post.Tags) ? post.Tags : [], draft: !(post.Published ?? true), authors: [DEFAULT_AUTHOR_ID], - banner, image, }, body: '', } - }) +} + +export async function getAllPosts(): Promise[]> { + + try { + const res = await fetch("https://notion-api.nvme0n1p.dev/v2/posts") + if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`) + const payload = (await res.json()) as { posts?: NotionPost[] } + const posts = (payload.posts ?? []).filter( + (post) => post.Published ?? true, + ) + + const normalized = posts.map(normalizePost) return normalized .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) .filter((post) => !isSubpost(post.id)) as unknown as CollectionEntry<'blog'>[] } catch (error) { console.error('getAllPosts remote fetch failed, using fallback:', error) - return fallback() + return [] } } export async function getAllPostsAndSubposts(): Promise< CollectionEntry<'blog'>[] > { - // 远程源不区分子文章,直接沿用 getAllPosts 结果 return getAllPosts() } -export async function getAllProjects(): Promise[]> { - const projects = await getCollectionSafe('projects') - return projects.sort((a, b) => { - const dateA = a.data.startDate?.getTime() || 0 - const dateB = b.data.startDate?.getTime() || 0 - return dateB - dateA - }) -} export async function getAllTags(): Promise> { const posts = await getAllPosts() @@ -163,41 +139,6 @@ export async function getAdjacentPosts(currentId: string): Promise<{ }> { const allPosts = await getAllPosts() - if (isSubpost(currentId)) { - const parentId = getParentId(currentId) - const allPosts = await getAllPosts() - const parent = allPosts.find((post) => post.id === parentId) || null - - const posts = await getCollectionSafe('blog') - const subposts = posts - .filter( - (post) => - isSubpost(post.id) && - getParentId(post.id) === parentId && - !post.data.draft, - ) - .sort((a, b) => { - const dateDiff = a.data.date.valueOf() - b.data.date.valueOf() - if (dateDiff !== 0) return dateDiff - - const orderA = a.data.order ?? 0 - const orderB = b.data.order ?? 0 - return orderA - orderB - }) - - const currentIndex = subposts.findIndex((post) => post.id === currentId) - if (currentIndex === -1) { - return { newer: null, older: null, parent } - } - - return { - newer: - currentIndex < subposts.length - 1 ? subposts[currentIndex + 1] : null, - older: currentIndex > 0 ? subposts[currentIndex - 1] : null, - parent, - } - } - const parentPosts = allPosts.filter((post) => !isSubpost(post.id)) const currentIndex = parentPosts.findIndex((post) => post.id === currentId) @@ -287,15 +228,13 @@ export function groupPostsByYear( } export async function hasSubposts(postId: string): Promise { - const subposts = await getSubpostsForParent(postId) - return subposts.length > 0 + return false } export function isSubpost(postId: string): boolean { - return postId.includes('/') + return false } -export const FRIENDS_API_URL = 'https://notion-api.nvme0n1p.dev/v2/links' export async function fetchRemotePost( slug: string, ): Promise { @@ -314,16 +253,11 @@ export async function fetchRemotePostContent( slug: string, ): Promise { try { - const res = await fetch(`${POSTS_API_URL}/${encodeURI(slug)}`) + const res = await fetch(`https://notion-api.nvme0n1p.dev/v2/posts/${encodeURI(slug)}`) if (!res.ok) throw new Error(`Failed to fetch post content: ${res.status}`) const data = (await res.json()) as Partial if (!data.post || !data.blockMap) return null - - const post = data.post as any - if (post?.banner && !post?.image) { - post.image = post.banner - } - + data.post = normalizePost(data.post) return data as RemotePostPayload } catch (error) { console.error(`fetchRemotePostContent error for slug "${slug}":`, error) @@ -332,28 +266,24 @@ export async function fetchRemotePostContent( } export async function getFriendLinks(): Promise { - const fallback: LinkEntry[] = [] try { - const res = await fetch(FRIENDS_API_URL) + const res = await fetch("https://notion-api.nvme0n1p.dev/v2/links") if (!res.ok) throw new Error(`Failed to fetch links: ${res.status}`) const data = (await res.json()) as LinkEntry[] - if (!Array.isArray(data)) return fallback + if (!Array.isArray(data)) throw new Error('Invalid links data format') return data } catch (error) { console.error('getFriendLinks error:', error) - return fallback + return [] } } export async function getParentPost( subpostId: string, ): Promise | null> { - if (!isSubpost(subpostId)) { - return null - } const parentId = getParentId(subpostId) const allPosts = await getAllPosts() diff --git a/src/pages/blog/[...id].astro b/src/pages/blog/[...id].astro index c0fb61f..3baf6d7 100644 --- a/src/pages/blog/[...id].astro +++ b/src/pages/blog/[...id].astro @@ -38,25 +38,18 @@ export async function getStaticPaths() { })) } -const post = Astro.props const currentPostId = Astro.params.id -const isRemotePost = post.body === '' let Content: any = null let headings let remoteContent = null -if (isRemotePost) { - const remote = await fetchRemotePostContent(currentPostId) - remoteContent = remote ? renderRemoteBlockMap(remote.blockMap, remote.post.id) : null - headings = remoteContent?.headings ?? [] -} else { - const rendered = await render(post) - Content = rendered.Content - headings = rendered.headings -} -const authors = await parseAuthors(post.data.authors ?? []) +const remote = await fetchRemotePostContent(currentPostId) +remoteContent = remote ? renderRemoteBlockMap(remote.blockMap, remote.post.id) : null +headings = remoteContent?.headings ?? [] +const post = remote?.post +const authors = await parseAuthors(post.authors ?? []) const isCurrentSubpost = isSubpost(currentPostId) const navigation = await getAdjacentPosts(currentPostId)