refactor: update site metadata structure
This commit is contained in:
parent
71d1df3bd7
commit
931bf7277c
15 changed files with 123 additions and 114 deletions
38
README.md
38
README.md
|
@ -94,28 +94,30 @@ This is a list of the various technologies used to build this template:
|
||||||
|
|
||||||
Edit the `src/consts.ts` file to update your site's metadata, navigation links, and social links:
|
Edit the `src/consts.ts` file to update your site's metadata, navigation links, and social links:
|
||||||
|
|
||||||
```typescript
|
```ts
|
||||||
export const SITE: Site = {
|
export const SITE: Site = {
|
||||||
TITLE: 'astro-erudite',
|
title: 'astro-erudite',
|
||||||
DESCRIPTION:
|
description:
|
||||||
'astro-erudite is a opinionated, unstyled blogging template—built with Astro, Tailwind, and shadcn/ui.',
|
'astro-erudite is a opinionated, unstyled blogging template—built with Astro, Tailwind, and shadcn/ui.',
|
||||||
EMAIL: 'jason@enscribe.dev',
|
href: 'https://astro-erudite.vercel.app',
|
||||||
NUM_POSTS_ON_HOMEPAGE: 2,
|
featuredPostCount: 2,
|
||||||
SITEURL: 'https://astro-erudite.vercel.app',
|
postsPerPage: 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NAV_LINKS: Link[] = [
|
export const NAV_LINKS: SocialLink[] = [
|
||||||
{ href: '/blog', label: 'blog' },
|
{
|
||||||
{ href: '/authors', label: 'authors' },
|
href: '/blog',
|
||||||
{ href: '/about', label: 'about' },
|
label: 'blog',
|
||||||
{ href: '/tags', label: 'tags' },
|
},
|
||||||
|
// ...
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SOCIAL_LINKS: Link[] = [
|
export const SOCIAL_LINKS: SocialLink[] = [
|
||||||
{ href: 'https://github.com/jktrn', label: 'GitHub' },
|
{
|
||||||
{ href: 'https://twitter.com/enscry', label: 'Twitter' },
|
href: 'https://github.com/jktrn',
|
||||||
{ href: 'jason@enscribe.dev', label: 'Email' },
|
label: 'GitHub',
|
||||||
{ href: '/rss.xml', label: 'RSS' },
|
},
|
||||||
|
// ...
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -185,8 +187,8 @@ The blog post schema is defined as follows:
|
||||||
|
|
||||||
| Field | Type (Zod) | Requirements | Required |
|
| Field | Type (Zod) | Requirements | Required |
|
||||||
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
| ------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
||||||
| `title` | `string` | Must be ≤60 characters. | Yes |
|
| `title` | `string` | Should be ≤60 characters. | Yes |
|
||||||
| `description` | `string` | Must be ≤155 characters. | Yes |
|
| `description` | `string` | Should be ≤155 characters. | Yes |
|
||||||
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
|
| `date` | `coerce.date()` | Must be in `YYYY-MM-DD` format. | Yes |
|
||||||
| `image` | `image()` | Should be exactly 1200px × 630px. | Optional |
|
| `image` | `image()` | Should be exactly 1200px × 630px. | Optional |
|
||||||
| `tags` | `string[]` | Preferably use kebab-case for these. | Optional |
|
| `tags` | `string[]` | Preferably use kebab-case for these. | Optional |
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
import Link from '@/components/Link.astro'
|
import Link from '@/components/Link.astro'
|
||||||
import AvatarComponent from '@/components/ui/avatar'
|
import AvatarComponent from '@/components/ui/avatar'
|
||||||
import type { Link as SocialLink } from '@/consts'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import type { SocialLink } from '@/types'
|
||||||
import type { CollectionEntry } from 'astro:content'
|
import type { CollectionEntry } from 'astro:content'
|
||||||
import SocialIcons from './SocialIcons.astro'
|
import SocialIcons from './SocialIcons.astro'
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ const { title, description, image = '/static/twitter-card.png' } = Astro.props
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="title" content={title} />
|
<meta name="title" content={title} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<meta name="author" content={SITE.TITLE} />
|
<meta name="author" content={SITE.title} />
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
@ -44,7 +44,7 @@ const { title, description, image = '/static/twitter-card.png' } = Astro.props
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content={Astro.url} />
|
<meta property="og:url" content={Astro.url} />
|
||||||
<meta property="og:site_name" content={SITE.TITLE} />
|
<meta property="og:site_name" content={SITE.title} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:image" content={new URL(image, Astro.url)} />
|
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||||
|
|
|
@ -19,7 +19,7 @@ import logo from '../../public/static/logo.svg'
|
||||||
class="hover:text-primary flex shrink-0 items-center gap-2 text-xl font-medium transition-colors duration-300"
|
class="hover:text-primary flex shrink-0 items-center gap-2 text-xl font-medium transition-colors duration-300"
|
||||||
>
|
>
|
||||||
<Image src={logo} alt="Logo" class="size-8" />
|
<Image src={logo} alt="Logo" class="size-8" />
|
||||||
{SITE.TITLE}
|
{SITE.title}
|
||||||
</Link>
|
</Link>
|
||||||
<div class="flex items-center gap-2 md:gap-4">
|
<div class="flex items-center gap-2 md:gap-4">
|
||||||
<nav class="hidden items-center gap-4 text-sm sm:gap-6 md:flex">
|
<nav class="hidden items-center gap-4 text-sm sm:gap-6 md:flex">
|
||||||
|
|
|
@ -1,51 +1,37 @@
|
||||||
---
|
---
|
||||||
import Link from '@/components/Link.astro'
|
import Link from '@/components/Link.astro'
|
||||||
import { buttonVariants } from '@/components/ui/button'
|
import { buttonVariants } from '@/components/ui/button'
|
||||||
import type { Link as SocialLink } from '@/consts'
|
import { ICON_MAP } from '@/consts'
|
||||||
import { cn } from '@/lib/utils'
|
import type { SocialLink } from '@/types'
|
||||||
import { Icon } from 'astro-icon/components'
|
import { Icon } from 'astro-icon/components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
links: SocialLink[]
|
links: SocialLink[]
|
||||||
className?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { links, className } = Astro.props
|
const { links } = Astro.props
|
||||||
|
|
||||||
const iconMap = {
|
|
||||||
Website: 'lucide:globe',
|
|
||||||
GitHub: 'lucide:github',
|
|
||||||
LinkedIn: 'lucide:linkedin',
|
|
||||||
Twitter: 'lucide:twitter',
|
|
||||||
Email: 'lucide:mail',
|
|
||||||
RSS: 'lucide:rss',
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSocialLink = ({ href, label }: SocialLink) => ({
|
|
||||||
href: label === 'Email' ? `mailto:${href}` : href,
|
|
||||||
ariaLabel: label,
|
|
||||||
iconName:
|
|
||||||
iconMap[label as keyof typeof iconMap] || 'lucide:message-circle-question',
|
|
||||||
})
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<ul class={cn('flex flex-wrap gap-2', className)} role="list">
|
<ul class="flex flex-wrap gap-2" role="list">
|
||||||
{
|
{
|
||||||
links.map((link) => {
|
links.map(({ href, label }) => (
|
||||||
const { href, ariaLabel, iconName } = getSocialLink(link)
|
<li>
|
||||||
return (
|
<Link
|
||||||
<li>
|
href={href}
|
||||||
<Link
|
aria-label={label}
|
||||||
href={href}
|
title={label}
|
||||||
aria-label={ariaLabel}
|
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
||||||
title={ariaLabel}
|
external
|
||||||
class={buttonVariants({ variant: 'outline', size: 'icon' })}
|
>
|
||||||
external
|
<Icon
|
||||||
>
|
name={
|
||||||
<Icon name={iconName} class="size-4" />
|
ICON_MAP[label as keyof typeof ICON_MAP] ||
|
||||||
</Link>
|
'lucide:message-circle-question'
|
||||||
</li>
|
}
|
||||||
)
|
class="size-4"
|
||||||
})
|
/>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -1,37 +1,53 @@
|
||||||
export type Site = {
|
import type { IconMap, SocialLink, Site } from '@/types'
|
||||||
TITLE: string
|
|
||||||
DESCRIPTION: string
|
|
||||||
EMAIL: string
|
|
||||||
NUM_POSTS_ON_HOMEPAGE: number
|
|
||||||
POSTS_PER_PAGE: number
|
|
||||||
SITEURL: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Link = {
|
|
||||||
href: string
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SITE: Site = {
|
export const SITE: Site = {
|
||||||
TITLE: 'astro-erudite',
|
title: 'astro-erudite',
|
||||||
DESCRIPTION:
|
description:
|
||||||
'astro-erudite is a opinionated, unstyled blogging template—built with Astro, Tailwind, and shadcn/ui.',
|
'astro-erudite is a opinionated, unstyled blogging template—built with Astro, Tailwind, and shadcn/ui.',
|
||||||
EMAIL: 'jason@enscribe.dev',
|
href: 'https://astro-erudite.vercel.app',
|
||||||
NUM_POSTS_ON_HOMEPAGE: 2,
|
featuredPostCount: 2,
|
||||||
POSTS_PER_PAGE: 3,
|
postsPerPage: 3,
|
||||||
SITEURL: 'https://astro-erudite.vercel.app',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NAV_LINKS: Link[] = [
|
export const NAV_LINKS: SocialLink[] = [
|
||||||
{ href: '/blog', label: 'blog' },
|
{
|
||||||
{ href: '/authors', label: 'authors' },
|
href: '/blog',
|
||||||
{ href: '/about', label: 'about' },
|
label: 'blog',
|
||||||
{ href: '/tags', label: 'tags' },
|
},
|
||||||
|
{
|
||||||
|
href: '/authors',
|
||||||
|
label: 'authors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/about',
|
||||||
|
label: 'about',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const SOCIAL_LINKS: Link[] = [
|
export const SOCIAL_LINKS: SocialLink[] = [
|
||||||
{ href: 'https://github.com/jktrn', label: 'GitHub' },
|
{
|
||||||
{ href: 'https://twitter.com/enscry', label: 'Twitter' },
|
href: 'https://github.com/jktrn',
|
||||||
{ href: 'jason@enscribe.dev', label: 'Email' },
|
label: 'GitHub',
|
||||||
{ href: '/rss.xml', label: 'RSS' },
|
},
|
||||||
|
{
|
||||||
|
href: 'https://twitter.com/enscry',
|
||||||
|
label: 'Twitter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'mailto:jason@enscribe.dev',
|
||||||
|
label: 'Email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: '/rss.xml',
|
||||||
|
label: 'RSS',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
export const ICON_MAP: IconMap = {
|
||||||
|
Website: 'lucide:globe',
|
||||||
|
GitHub: 'lucide:github',
|
||||||
|
LinkedIn: 'lucide:linkedin',
|
||||||
|
Twitter: 'lucide:twitter',
|
||||||
|
Email: 'lucide:mail',
|
||||||
|
RSS: 'lucide:rss',
|
||||||
|
}
|
||||||
|
|
|
@ -5,18 +5,8 @@ const blog = defineCollection({
|
||||||
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
|
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
|
||||||
schema: ({ image }) =>
|
schema: ({ image }) =>
|
||||||
z.object({
|
z.object({
|
||||||
title: z
|
title: z.string(),
|
||||||
.string()
|
description: 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(),
|
||||||
image: image().optional(),
|
image: image().optional(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
|
|
|
@ -17,7 +17,7 @@ const { title, description, image } = Astro.props
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<Head
|
<Head
|
||||||
title={`${title} | ${SITE.TITLE}`}
|
title={`${title} | ${SITE.title}`}
|
||||||
description={description}
|
description={description}
|
||||||
image={image}
|
image={image}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Layout from '@/layouts/Layout.astro'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404" description={SITE.DESCRIPTION}>
|
<Layout title="404" description={SITE.description}>
|
||||||
<Container class="flex grow flex-col gap-y-6">
|
<Container class="flex grow flex-col gap-y-6">
|
||||||
<Breadcrumbs items={[{ label: '???', icon: 'lucide:circle-help' }]} />
|
<Breadcrumbs items={[{ label: '???', icon: 'lucide:circle-help' }]} />
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { getCollection } from 'astro:content'
|
||||||
const projects = await getCollection('projects')
|
const projects = await getCollection('projects')
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="About" description={SITE.DESCRIPTION}>
|
<Layout title="About" description={SITE.description}>
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<Breadcrumbs items={[{ label: 'About', icon: 'lucide:info' }]} />
|
<Breadcrumbs items={[{ label: 'About', icon: 'lucide:info' }]} />
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ export async function getStaticPaths({
|
||||||
const allPosts = await getCollection('blog', ({ data }) => !data.draft)
|
const allPosts = await getCollection('blog', ({ data }) => !data.draft)
|
||||||
return paginate(
|
return paginate(
|
||||||
allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()),
|
allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()),
|
||||||
{ pageSize: SITE.POSTS_PER_PAGE },
|
{ pageSize: SITE.postsPerPage },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,10 @@ import { getCollection } from 'astro:content'
|
||||||
const blog = (await getCollection('blog'))
|
const blog = (await getCollection('blog'))
|
||||||
.filter((post) => !post.data.draft)
|
.filter((post) => !post.data.draft)
|
||||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||||
.slice(0, SITE.NUM_POSTS_ON_HOMEPAGE)
|
.slice(0, SITE.featuredPostCount)
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Home" description={SITE.DESCRIPTION}>
|
<Layout title="Home" description={SITE.description}>
|
||||||
<Container class="flex flex-col gap-y-6">
|
<Container class="flex flex-col gap-y-6">
|
||||||
<section>
|
<section>
|
||||||
<div class="rounded-lg border">
|
<div class="rounded-lg border">
|
||||||
|
|
|
@ -9,9 +9,9 @@ export async function GET(context: APIContext) {
|
||||||
posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||||
|
|
||||||
return rss({
|
return rss({
|
||||||
title: SITE.TITLE,
|
title: SITE.title,
|
||||||
description: SITE.DESCRIPTION,
|
description: SITE.description,
|
||||||
site: context.site ?? SITE.SITEURL,
|
site: context.site ?? SITE.href,
|
||||||
items: posts.map((post) => ({
|
items: posts.map((post) => ({
|
||||||
title: post.data.title,
|
title: post.data.title,
|
||||||
description: post.data.description,
|
description: post.data.description,
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Geist';
|
font-family: 'Geist';
|
||||||
src: url('/fonts/GeistVF.woff2') format('woff2-variations');
|
src: url('/fonts/GeistVF.woff2') format('woff2-variations');
|
||||||
font-weight: 100 500;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Geist Mono';
|
font-family: 'Geist Mono';
|
||||||
src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations');
|
src: url('/fonts/GeistMonoVF.woff2') format('woff2-variations');
|
||||||
font-weight: 100 600;
|
font-weight: 100 900;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
@ -94,11 +94,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
color-scheme: light;
|
@apply bg-background text-foreground scheme-light;
|
||||||
@apply bg-background text-foreground;
|
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
color-scheme: dark;
|
@apply scheme-dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-corner {
|
::-webkit-scrollbar-corner {
|
||||||
|
|
16
src/types.ts
Normal file
16
src/types.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export type Site = {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
href: string
|
||||||
|
featuredPostCount: number
|
||||||
|
postsPerPage: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SocialLink = {
|
||||||
|
href: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IconMap = {
|
||||||
|
[key: string]: string
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue