diff --git a/README.md b/README.md index 08082ff..c1c2aa0 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,15 @@ ## astro-erudite +![Stargazers] +[![License]](LICENSE) + + + 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. - - --- ## Technology Stack @@ -27,3 +30,210 @@ This is a list of the various technologies used to build this website: | 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 SPA 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 [HSL 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 diff --git a/src/components/ProjectCard.astro b/src/components/ProjectCard.astro new file mode 100644 index 0000000..6c65ebf --- /dev/null +++ b/src/components/ProjectCard.astro @@ -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 +--- + +
+ + {project.data.name} +
+

{project.data.name}

+

{project.data.description}

+
+ {project.data.tags.map((tag) => ( + {tag} + ))} +
+
+ +
\ No newline at end of file diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index dd46946..b94f006 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -25,12 +25,14 @@ const badgeVariants = cva( export interface BadgeProps extends React.HTMLAttributes, - VariantProps {} + VariantProps { + showHash?: boolean +} -function Badge({ className, variant, ...props }: BadgeProps) { +function Badge({ className, variant, showHash = true, ...props }: BadgeProps) { return (
- + {showHash && } {props.children}
) diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 7fa252c..d820e8f 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -7,7 +7,7 @@ const Card = React.forwardRef< >(({ className, ...props }, ref) => (
)) diff --git a/src/components/ui/mode-toggle.tsx b/src/components/ui/mode-toggle.tsx index 591f617..064a7e5 100644 --- a/src/components/ui/mode-toggle.tsx +++ b/src/components/ui/mode-toggle.tsx @@ -23,7 +23,16 @@ export function ModeToggle() { theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) + + document.documentElement.classList.add('disable-transitions') + document.documentElement.classList[isDark ? 'add' : 'remove']('dark') + + window.getComputedStyle(document.documentElement).getPropertyValue('opacity') + + requestAnimationFrame(() => { + document.documentElement.classList.remove('disable-transitions') + }) }, [theme]) return ( @@ -56,4 +65,4 @@ export function ModeToggle() { ) -} +} \ No newline at end of file diff --git a/src/consts.ts b/src/consts.ts index d911d6c..64a20a4 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -24,6 +24,7 @@ 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[] = [ diff --git a/src/content/blog/2022-post/2022.png b/src/content/blog/2022-post/2022.png new file mode 100644 index 0000000..3872e8d Binary files /dev/null and b/src/content/blog/2022-post/2022.png differ diff --git a/src/content/blog/2022.mdx b/src/content/blog/2022-post/index.mdx similarity index 77% rename from src/content/blog/2022.mdx rename to src/content/blog/2022-post/index.mdx index 4bfeb48..b02b3e1 100644 --- a/src/content/blog/2022.mdx +++ b/src/content/blog/2022-post/index.mdx @@ -1,9 +1,9 @@ --- title: '2022 Post' description: 'This a dummy post written in the year 2022.' -date: '2022-01-01' +date: 2022-01-01 tags: ['dummy', 'placeholder'] -image: '/static/1200x630.png' +image: './2022.png' --- This is a dummy post written in the year 2022. diff --git a/src/content/blog/2023-post/2023.png b/src/content/blog/2023-post/2023.png new file mode 100644 index 0000000..67b860c Binary files /dev/null and b/src/content/blog/2023-post/2023.png differ diff --git a/src/content/blog/2023.mdx b/src/content/blog/2023-post/index.mdx similarity index 79% rename from src/content/blog/2023.mdx rename to src/content/blog/2023-post/index.mdx index 41e3845..816cee8 100644 --- a/src/content/blog/2023.mdx +++ b/src/content/blog/2023-post/index.mdx @@ -1,9 +1,9 @@ --- title: '2023 Post' description: 'This a dummy post written in the year 2023.' -date: '2023-01-01' +date: 2023-01-01 tags: ['dummy', 'placeholder'] -image: '/static/1200x630.png' +image: './2023.png' authors: ['enscribe'] --- diff --git a/src/content/blog/2024-post/2024.png b/src/content/blog/2024-post/2024.png new file mode 100644 index 0000000..e684679 Binary files /dev/null and b/src/content/blog/2024-post/2024.png differ diff --git a/src/content/blog/2024.mdx b/src/content/blog/2024-post/index.mdx similarity index 81% rename from src/content/blog/2024.mdx rename to src/content/blog/2024-post/index.mdx index feb461d..3a411e2 100644 --- a/src/content/blog/2024.mdx +++ b/src/content/blog/2024-post/index.mdx @@ -1,9 +1,9 @@ --- title: '2024 Post' description: 'This a dummy post written in the year 2024 (with multiple authors).' -date: '2024-01-01' +date: 2024-01-01 tags: ['dummy', 'placeholder'] -image: '/static/1200x630.png' +image: './2024.png' authors: ['enscribe', 'jktrn'] --- diff --git a/src/content/blog/the-state-of-static-blogs/1200x630.png b/src/content/blog/the-state-of-static-blogs/1200x630.png new file mode 100644 index 0000000..933b94b Binary files /dev/null and b/src/content/blog/the-state-of-static-blogs/1200x630.png differ diff --git a/src/content/blog/the-state-of-static-blogs.mdx b/src/content/blog/the-state-of-static-blogs/index.mdx similarity index 99% rename from src/content/blog/the-state-of-static-blogs.mdx rename to src/content/blog/the-state-of-static-blogs/index.mdx index a6c0378..bf0e261 100644 --- a/src/content/blog/the-state-of-static-blogs.mdx +++ b/src/content/blog/the-state-of-static-blogs/index.mdx @@ -1,9 +1,9 @@ --- 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.' -date: '2024-07-25' +date: 2024-07-25 tags: ['webdev', 'opinion'] -image: '/static/1200x630.png' +image: './1200x630.png' authors: ['enscribe'] --- diff --git a/src/content/config.ts b/src/content/config.ts index 084ff0e..c3ce8d2 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -2,15 +2,31 @@ import { defineCollection, z } from 'astro:content' const blog = defineCollection({ type: 'content', - schema: z.object({ - title: z.string(), - description: z.string(), - date: z.coerce.date(), - draft: z.boolean().optional(), - image: z.string().optional(), - tags: z.array(z.string()).optional(), - authors: z.array(z.string()).optional(), - }), + schema: ({ image }) => + z.object({ + 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(), + image: image() + .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(), + authors: z.array(z.string()).optional(), + draft: z.boolean().optional(), + }), }) const authors = defineCollection({ @@ -20,13 +36,28 @@ const authors = defineCollection({ pronouns: z.string().optional(), avatar: z.string().url(), 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(), - 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 } diff --git a/src/content/projects/project-a.md b/src/content/projects/project-a.md new file mode 100644 index 0000000..2bf6faa --- /dev/null +++ b/src/content/projects/project-a.md @@ -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" +--- \ No newline at end of file diff --git a/src/content/projects/project-b.md b/src/content/projects/project-b.md new file mode 100644 index 0000000..08bebc5 --- /dev/null +++ b/src/content/projects/project-b.md @@ -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" +--- \ No newline at end of file diff --git a/src/content/projects/project-c.md b/src/content/projects/project-c.md new file mode 100644 index 0000000..e63aafe --- /dev/null +++ b/src/content/projects/project-c.md @@ -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" +--- \ No newline at end of file diff --git a/src/pages/about.astro b/src/pages/about.astro index 132f177..a001145 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -1,8 +1,12 @@ --- import Breadcrumbs from '@/components/Breadcrumbs.astro' import Container from '@components/Container.astro' +import ProjectCard from '@components/ProjectCard.astro' import { SITE } from '@consts' import Layout from '@layouts/Layout.astro' +import { getCollection } from 'astro:content' + +const projects = await getCollection('projects') --- @@ -12,11 +16,18 @@ import Layout from '@layouts/Layout.astro'

Some more about us

-

+

{SITE.TITLE} is an opinionated, no-frills static blogging template built with Astro.

+ +

Our Projects

+
+ {projects.map((project) => ( + + ))} +
-
+ \ No newline at end of file diff --git a/src/pages/authors/index.astro b/src/pages/authors/index.astro index ecbd855..fbc62ed 100644 --- a/src/pages/authors/index.astro +++ b/src/pages/authors/index.astro @@ -11,14 +11,18 @@ const authors = await getCollection('authors') -
    - { - authors.map((author) => ( -
  • - -
  • - )) - } -
+ {authors.length > 0 ? ( +
    + { + authors.map((author) => ( +
  • + +
  • + )) + } +
+ ) : ( +

No authors found.

+ )}
diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 3463053..6f47e69 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -57,7 +57,7 @@ const authors = await parseAuthors(post.data.authors ?? []) !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 - const items = [...filteredBlogs].sort( + const items = [...blog].sort( (a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(), ) diff --git a/src/pages/tags/[...slug].astro b/src/pages/tags/[...slug].astro index 5cef238..d58fcdc 100644 --- a/src/pages/tags/[...slug].astro +++ b/src/pages/tags/[...slug].astro @@ -38,12 +38,12 @@ export async function getStaticPaths() { > -
+

Posts tagged with

- {tag} + {tag}
diff --git a/src/styles/global.css b/src/styles/global.css index 040bcdf..ab75f1e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -6,10 +6,6 @@ :root { --background: 0 0% 100%; --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-foreground: 0 0% 98%; --secondary: 0 0% 80.1%; @@ -23,22 +19,11 @@ --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; --border: 0 0% 89.8%; - --input: 0 0% 89.8%; --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 { --background: 0 0% 3.9%; --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-foreground: 0 0% 9%; --secondary: 0 0% 14.9%; @@ -52,13 +37,7 @@ --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; --border: 0 0% 14.9%; - --input: 0 0% 14.9%; --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; } } + + .disable-transitions, + .disable-transitions * { + transition: none !important; + } } @layer components {