feat: update schema, add ProjectCard
, readme
This commit is contained in:
parent
fbeab5a744
commit
b93eddea6b
24 changed files with 373 additions and 72 deletions
214
README.md
214
README.md
|
@ -4,12 +4,15 @@
|
||||||
|
|
||||||
## astro-erudite
|
## astro-erudite
|
||||||
|
|
||||||
|
![Stargazers]
|
||||||
|
[![License]](LICENSE)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
astro-erudite is an opinionated, no-frills 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).
|
astro-erudite is an opinionated, no-frills 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).
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
@ -27,3 +30,210 @@ This is a list of the various technologies used to build this website:
|
||||||
| Deployment | [Vercel](https://vercel.com) |
|
| Deployment | [Vercel](https://vercel.com) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [Astro](https://astro.build/)’s [Islands](https://docs.astro.build/en/concepts/islands/) architecture for partial/selective hydration and client-side interactivity while maintaining a fast-to-render static site.
|
||||||
|
- [shadcn/ui](https://ui.shadcn.com/)’s [Tailwind](https://tailwindcss.com/) color convention for automatic styling across both light and dark themes. Includes accessible, theme-aware UI components for navigation, buttons, etc.
|
||||||
|
- [rehype-pretty-code](https://rehype-pretty.pages.dev/) with [Shiki](https://github.com/shikijs/shiki) for advanced code block styling, highlighting, and code block titles/captions.
|
||||||
|
- Blog post authoring using [MDX](https://mdxjs.com/) for component-style content, alongside $\LaTeX$ rendering using [KaTeX](https://katex.org/).
|
||||||
|
- Astro [View Transitions](https://docs.astro.build/en/guides/view-transitions/) in <abbr title="Single Page Application">SPA</abbr> mode for smooth, opt-in animations during route switching.
|
||||||
|
- SEO optimization with fine-grained control over metadata and [Open Graph](https://ogp.me/) tags for each post.
|
||||||
|
- [RSS](https://en.wikipedia.org/wiki/RSS) feeds and sitemap generation!
|
||||||
|
- Supports author profiles (with a dedicated authors page) and adding multiple authors per post.
|
||||||
|
- Supports project tags (with a dedicated tags page) for easy post categorization and discovery.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Hit “Use this template” 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 see your blog in action. 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:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const SITE: Site = {
|
||||||
|
TITLE: 'astro-erudite',
|
||||||
|
DESCRIPTION:
|
||||||
|
'astro-erudite is a opinionated, no-frills blogging template—built with Astro, Tailwind, and shadcn/ui.',
|
||||||
|
EMAIL: 'jason@enscribe.dev',
|
||||||
|
NUM_POSTS_ON_HOMEPAGE: 2,
|
||||||
|
SITEURL: 'https://astro-erudite.vercel.app',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NAV_LINKS: Link[] = [
|
||||||
|
{ href: '/blog', label: 'blog' },
|
||||||
|
{ href: '/authors', label: 'authors' },
|
||||||
|
{ href: '/about', label: 'about' },
|
||||||
|
{ href: '/tags', label: 'tags' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const SOCIAL_LINKS: Link[] = [
|
||||||
|
{ href: 'https://github.com/jktrn', label: 'GitHub' },
|
||||||
|
{ href: 'https://twitter.com/enscry', label: 'Twitter' },
|
||||||
|
{ href: 'jason@enscribe.dev', label: 'Email' },
|
||||||
|
{ href: '/rss.xml', label: 'RSS' },
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
|
||||||
|
Colors are defined in `src/styles/global.css` in [<abbr title="Hue, Saturation, Lightness">HSL</abbr> format](https://en.wikipedia.org/wiki/HSL_and_HSV), using the [shadcn/ui](https://ui.shadcn.com/) convention:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
--primary: 0 0% 9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 0 0% 80.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
--muted: 0 0% 80.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
--accent: 0 0% 80.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
--additive: 112 50% 36%; /* Unique to astro-erudite */
|
||||||
|
--additive-foreground: 0 0% 98%; /* Unique to astro-erudite */
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
/* ... */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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` | Must be ≤60 characters. | Yes |
|
||||||
|
| `description` | `string` | Must be ≤155 characters. | Yes |
|
||||||
|
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
|
||||||
|
| `image` | `image()` | Must be exactly 1200px × 630px. | Optional |
|
||||||
|
| `tags` | `string[]` | Preferably use kebab-case for these. | Optional |
|
||||||
|
| `authors` | `string[]` | If the author has a profile, use the slug 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:
|
||||||
|
|
||||||
|
```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()` | Must be a valid URL. 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 |
|
||||||
|
|
||||||
|
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'
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
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()` | Must be exactly 1200px × 630px. | Yes |
|
||||||
|
| `link` | `string.url()` | Must be a valid URL. | Yes |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is open source and available under the [MIT License](LICENSE).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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
|
||||||
|
|
33
src/components/ProjectCard.astro
Normal file
33
src/components/ProjectCard.astro
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import Link from '@components/Link.astro'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
project: CollectionEntry<'projects'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { project } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50">
|
||||||
|
<Link href={project.data.link} class="block">
|
||||||
|
<Image
|
||||||
|
src={project.data.image}
|
||||||
|
alt={project.data.name}
|
||||||
|
width={400}
|
||||||
|
height={200}
|
||||||
|
class="w-full object-cover"
|
||||||
|
/>
|
||||||
|
<div class="p-4">
|
||||||
|
<h3 class="mb-2 text-lg font-semibold">{project.data.name}</h3>
|
||||||
|
<p class="mb-4 text-sm text-muted-foreground">{project.data.description}</p>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{project.data.tags.map((tag) => (
|
||||||
|
<Badge variant="secondary" showHash={false}>{tag}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
|
@ -25,12 +25,14 @@ const badgeVariants = cva(
|
||||||
|
|
||||||
export interface BadgeProps
|
export interface BadgeProps
|
||||||
extends React.HTMLAttributes<HTMLDivElement>,
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
VariantProps<typeof badgeVariants> {}
|
VariantProps<typeof badgeVariants> {
|
||||||
|
showHash?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
function Badge({ className, variant, showHash = true, ...props }: BadgeProps) {
|
||||||
return (
|
return (
|
||||||
<div className={cn(badgeVariants({ variant }), className)} {...props}>
|
<div className={cn(badgeVariants({ variant }), className)} {...props}>
|
||||||
<Hash className="size-3 -translate-x-0.5" />
|
{showHash && <Hash className="size-3 -translate-x-0.5" />}
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,7 +7,7 @@ const Card = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('bg-card text-card-foreground rounded-xl border', className)}
|
className={cn('bg-background rounded-xl border', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
|
@ -23,7 +23,16 @@ export function ModeToggle() {
|
||||||
theme === 'dark' ||
|
theme === 'dark' ||
|
||||||
(theme === 'system' &&
|
(theme === 'system' &&
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
|
||||||
|
document.documentElement.classList.add('disable-transitions')
|
||||||
|
|
||||||
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
|
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
|
||||||
|
|
||||||
|
window.getComputedStyle(document.documentElement).getPropertyValue('opacity')
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.documentElement.classList.remove('disable-transitions')
|
||||||
|
})
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const NAV_LINKS: Link[] = [
|
||||||
{ href: '/blog', label: 'blog' },
|
{ href: '/blog', label: 'blog' },
|
||||||
{ href: '/authors', label: 'authors' },
|
{ href: '/authors', label: 'authors' },
|
||||||
{ href: '/about', label: 'about' },
|
{ href: '/about', label: 'about' },
|
||||||
|
{ href: '/tags', label: 'tags' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SOCIAL_LINKS: Link[] = [
|
export const SOCIAL_LINKS: Link[] = [
|
||||||
|
|
BIN
src/content/blog/2022-post/2022.png
Normal file
BIN
src/content/blog/2022-post/2022.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: '2022 Post'
|
title: '2022 Post'
|
||||||
description: 'This a dummy post written in the year 2022.'
|
description: 'This a dummy post written in the year 2022.'
|
||||||
date: '2022-01-01'
|
date: 2022-01-01
|
||||||
tags: ['dummy', 'placeholder']
|
tags: ['dummy', 'placeholder']
|
||||||
image: '/static/1200x630.png'
|
image: './2022.png'
|
||||||
---
|
---
|
||||||
|
|
||||||
This is a dummy post written in the year 2022.
|
This is a dummy post written in the year 2022.
|
BIN
src/content/blog/2023-post/2023.png
Normal file
BIN
src/content/blog/2023-post/2023.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: '2023 Post'
|
title: '2023 Post'
|
||||||
description: 'This a dummy post written in the year 2023.'
|
description: 'This a dummy post written in the year 2023.'
|
||||||
date: '2023-01-01'
|
date: 2023-01-01
|
||||||
tags: ['dummy', 'placeholder']
|
tags: ['dummy', 'placeholder']
|
||||||
image: '/static/1200x630.png'
|
image: './2023.png'
|
||||||
authors: ['enscribe']
|
authors: ['enscribe']
|
||||||
---
|
---
|
||||||
|
|
BIN
src/content/blog/2024-post/2024.png
Normal file
BIN
src/content/blog/2024-post/2024.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: '2024 Post'
|
title: '2024 Post'
|
||||||
description: 'This a dummy post written in the year 2024 (with multiple authors).'
|
description: 'This a dummy post written in the year 2024 (with multiple authors).'
|
||||||
date: '2024-01-01'
|
date: 2024-01-01
|
||||||
tags: ['dummy', 'placeholder']
|
tags: ['dummy', 'placeholder']
|
||||||
image: '/static/1200x630.png'
|
image: './2024.png'
|
||||||
authors: ['enscribe', 'jktrn']
|
authors: ['enscribe', 'jktrn']
|
||||||
---
|
---
|
||||||
|
|
BIN
src/content/blog/the-state-of-static-blogs/1200x630.png
Normal file
BIN
src/content/blog/the-state-of-static-blogs/1200x630.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
|
@ -1,9 +1,9 @@
|
||||||
---
|
---
|
||||||
title: 'The State of Static Blogs in 2024'
|
title: 'The State of Static Blogs in 2024'
|
||||||
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
|
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
|
||||||
date: '2024-07-25'
|
date: 2024-07-25
|
||||||
tags: ['webdev', 'opinion']
|
tags: ['webdev', 'opinion']
|
||||||
image: '/static/1200x630.png'
|
image: './1200x630.png'
|
||||||
authors: ['enscribe']
|
authors: ['enscribe']
|
||||||
---
|
---
|
||||||
|
|
|
@ -2,14 +2,30 @@ import { defineCollection, z } from 'astro:content'
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
type: 'content',
|
type: 'content',
|
||||||
schema: z.object({
|
schema: ({ image }) =>
|
||||||
title: z.string(),
|
z.object({
|
||||||
description: z.string(),
|
title: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
60,
|
||||||
|
'Title should be 60 characters or less for optimal Open Graph display.',
|
||||||
|
),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.max(
|
||||||
|
155,
|
||||||
|
'Description should be 155 characters or less for optimal Open Graph display.',
|
||||||
|
),
|
||||||
date: z.coerce.date(),
|
date: z.coerce.date(),
|
||||||
draft: z.boolean().optional(),
|
image: image()
|
||||||
image: z.string().optional(),
|
.refine((img) => img.width === 1200 && img.height === 630, {
|
||||||
|
message:
|
||||||
|
'The image must be exactly 1200px × 630px for Open Graph requirements.',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
authors: z.array(z.string()).optional(),
|
authors: z.array(z.string()).optional(),
|
||||||
|
draft: z.boolean().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -20,13 +36,28 @@ const authors = defineCollection({
|
||||||
pronouns: z.string().optional(),
|
pronouns: z.string().optional(),
|
||||||
avatar: z.string().url(),
|
avatar: z.string().url(),
|
||||||
bio: z.string().optional(),
|
bio: z.string().optional(),
|
||||||
website: z.string().url().optional(),
|
|
||||||
twitter: z.string().optional(),
|
|
||||||
github: z.string().optional(),
|
|
||||||
linkedin: z.string().optional(),
|
|
||||||
mail: z.string().email().optional(),
|
mail: z.string().email().optional(),
|
||||||
discord: z.string().optional(),
|
website: z.string().url().optional(),
|
||||||
|
twitter: z.string().url().optional(),
|
||||||
|
github: z.string().url().optional(),
|
||||||
|
linkedin: z.string().url().optional(),
|
||||||
|
discord: z.string().url().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const collections = { blog, authors }
|
const projects = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: ({ image }) =>
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
image: image().refine((img) => img.width === 1200 && img.height === 630, {
|
||||||
|
message:
|
||||||
|
'The image must be exactly 1200px × 630px for Open Graph requirements.',
|
||||||
|
}),
|
||||||
|
link: z.string().url(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const collections = { blog, authors, projects }
|
||||||
|
|
7
src/content/projects/project-a.md
Normal file
7
src/content/projects/project-a.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
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: "../../../public/static/1200x630.png"
|
||||||
|
link: "https://example.com"
|
||||||
|
---
|
7
src/content/projects/project-b.md
Normal file
7
src/content/projects/project-b.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
name: "Project B"
|
||||||
|
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: "../../../public/static/1200x630.png"
|
||||||
|
link: "https://example.com"
|
||||||
|
---
|
7
src/content/projects/project-c.md
Normal file
7
src/content/projects/project-c.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
name: "Project C"
|
||||||
|
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: "../../../public/static/1200x630.png"
|
||||||
|
link: "https://example.com"
|
||||||
|
---
|
|
@ -1,8 +1,12 @@
|
||||||
---
|
---
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
|
import ProjectCard from '@components/ProjectCard.astro'
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
const projects = await getCollection('projects')
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="About" description={SITE.DESCRIPTION}>
|
<Layout title="About" description={SITE.DESCRIPTION}>
|
||||||
|
@ -12,10 +16,17 @@ import Layout from '@layouts/Layout.astro'
|
||||||
<section>
|
<section>
|
||||||
<div class="min-w-full">
|
<div class="min-w-full">
|
||||||
<h1 class="mb-4 text-3xl font-bold">Some more about us</h1>
|
<h1 class="mb-4 text-3xl font-bold">Some more about us</h1>
|
||||||
<p class="prose prose-neutral dark:prose-invert">
|
<p class="prose prose-neutral dark:prose-invert mb-8">
|
||||||
{SITE.TITLE} is an opinionated, no-frills static blogging template built
|
{SITE.TITLE} is an opinionated, no-frills static blogging template built
|
||||||
with Astro.
|
with Astro.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="mb-4 text-2xl font-semibold">Our Projects</h2>
|
||||||
|
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<ProjectCard project={project} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -11,6 +11,7 @@ const authors = await getCollection('authors')
|
||||||
<Layout title="Authors" description="A list of authors on this site.">
|
<Layout title="Authors" description="A list of authors on this site.">
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumbs items={[{ label: 'Authors' }]} />
|
<Breadcrumbs items={[{ label: 'Authors' }]} />
|
||||||
|
{authors.length > 0 ? (
|
||||||
<ul class="not-prose flex flex-col gap-4">
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
{
|
{
|
||||||
authors.map((author) => (
|
authors.map((author) => (
|
||||||
|
@ -20,5 +21,8 @@ const authors = await getCollection('authors')
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="text-center text-muted-foreground">No authors found.</p>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -57,7 +57,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
||||||
<Layout
|
<Layout
|
||||||
title={post.data.title}
|
title={post.data.title}
|
||||||
description={post.data.description}
|
description={post.data.description}
|
||||||
image={post.data.image ?? '/static/1200x630.png'}
|
image={post.data.image?.src ?? '/static/1200x630.png'}
|
||||||
>
|
>
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
|
|
|
@ -9,13 +9,8 @@ export async function GET(context: APIContext) {
|
||||||
(post) => !post.data.draft,
|
(post) => !post.data.draft,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Filter posts by tag 'rss-feed'
|
|
||||||
const filteredBlogs = blog.filter(
|
|
||||||
(post) => post.data.tags && post.data.tags.includes('rss-feed'),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Sort posts by date
|
// Sort posts by date
|
||||||
const items = [...filteredBlogs].sort(
|
const items = [...blog].sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
|
new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,12 +38,12 @@ export async function getStaticPaths() {
|
||||||
>
|
>
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumbs items={[{ href: '/tags', label: 'Tags' }, { label: tag }]} />
|
<Breadcrumbs items={[{ href: '/tags', label: 'Tags' }, { label: tag }]} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center flex-wrap gap-2">
|
||||||
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
||||||
<span
|
<span
|
||||||
class="flex items-center gap-x-1 rounded-full bg-secondary px-4 py-2 text-2xl font-medium"
|
class="flex items-center gap-x-1 rounded-full bg-secondary px-4 py-2 text-2xl font-bold"
|
||||||
>
|
>
|
||||||
<Hash className="size-6" />{tag}
|
<Hash className="size-6 -translate-x-0.5" strokeWidth={3} />{tag}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-y-4">
|
<div class="flex flex-col gap-y-4">
|
||||||
|
|
|
@ -6,10 +6,6 @@
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
--background: 0 0% 100%;
|
||||||
--foreground: 0 0% 3.9%;
|
--foreground: 0 0% 3.9%;
|
||||||
--card: 0 0% 100%;
|
|
||||||
--card-foreground: 0 0% 3.9%;
|
|
||||||
--popover: 0 0% 100%;
|
|
||||||
--popover-foreground: 0 0% 3.9%;
|
|
||||||
--primary: 0 0% 9%;
|
--primary: 0 0% 9%;
|
||||||
--primary-foreground: 0 0% 98%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--secondary: 0 0% 80.1%;
|
--secondary: 0 0% 80.1%;
|
||||||
|
@ -23,22 +19,11 @@
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 89.8%;
|
--border: 0 0% 89.8%;
|
||||||
--input: 0 0% 89.8%;
|
|
||||||
--ring: 0 0% 3.9%;
|
--ring: 0 0% 3.9%;
|
||||||
--chart-1: 12 76% 61%;
|
|
||||||
--chart-2: 173 58% 39%;
|
|
||||||
--chart-3: 197 37% 24%;
|
|
||||||
--chart-4: 43 74% 66%;
|
|
||||||
--chart-5: 27 87% 67%;
|
|
||||||
--radius: 0.5rem;
|
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--background: 0 0% 3.9%;
|
--background: 0 0% 3.9%;
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 3.9%;
|
|
||||||
--card-foreground: 0 0% 98%;
|
|
||||||
--popover: 0 0% 3.9%;
|
|
||||||
--popover-foreground: 0 0% 98%;
|
|
||||||
--primary: 0 0% 98%;
|
--primary: 0 0% 98%;
|
||||||
--primary-foreground: 0 0% 9%;
|
--primary-foreground: 0 0% 9%;
|
||||||
--secondary: 0 0% 14.9%;
|
--secondary: 0 0% 14.9%;
|
||||||
|
@ -52,13 +37,7 @@
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 0 0% 14.9%;
|
--border: 0 0% 14.9%;
|
||||||
--input: 0 0% 14.9%;
|
|
||||||
--ring: 0 0% 83.1%;
|
--ring: 0 0% 83.1%;
|
||||||
--chart-1: 220 70% 50%;
|
|
||||||
--chart-2: 160 60% 45%;
|
|
||||||
--chart-3: 30 80% 55%;
|
|
||||||
--chart-4: 280 65% 60%;
|
|
||||||
--chart-5: 340 75% 55%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
@ -80,6 +59,11 @@
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disable-transitions,
|
||||||
|
.disable-transitions * {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|
Loading…
Add table
Reference in a new issue