feat: opengraph design

This commit is contained in:
enscribe 2024-09-12 16:34:38 -07:00
parent 0b430e5d43
commit c410c499e1
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
38 changed files with 179 additions and 66 deletions

View file

@ -1 +1,9 @@
# erudite
![Showcase Card](/public/static/twitter-card.png)
<div align="center">
## astro-erudite
astro-erudite is an opinionated, no-frills static blogging template built with [Astro](https://astro.build/) and [Tailwind](https://tailwindcss.com/). Extraordinarily loosely based on the [Astro Micro](https://astro-micro.vercel.app/) theme by [trevortylerlee](https://github.com/trevortylerlee).
</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/favicons/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 598 B

BIN
public/favicons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 4420 l0 -700 2320 0 2320 0 0 -230 0 -230 -2320 0 -2320 0 0 -700
0 -700 2320 0 2320 0 0 -230 0 -230 -2320 0 -2320 0 0 -700 0 -700 2560 0
2560 0 0 235 0 235 -2320 0 -2320 0 0 230 0 230 2320 0 2320 0 0 700 0 700
-2320 0 -2320 0 0 230 0 230 2320 0 2320 0 0 700 0 700 -2320 0 -2320 0 0 230
0 230 2320 0 2320 0 0 235 0 235 -2560 0 -2560 0 0 -700z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 877 B

View file

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/favicons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#121212",
"background_color": "#121212",
"display": "standalone"
}

BIN
public/static/1200x630.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
public/static/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

20
public/static/logo.svg Normal file
View file

@ -0,0 +1,20 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_271_118)">
<rect width="512" height="47" fill="#CCCCCC"/>
<rect y="93" width="512" height="47" fill="#CCCCCC"/>
<rect y="186" width="512" height="47" fill="#CCCCCC"/>
<rect y="279" width="512" height="47" fill="#CCCCCC"/>
<rect y="372" width="512" height="47" fill="#CCCCCC"/>
<rect y="465" width="512" height="47" fill="#CCCCCC"/>
<rect y="47" width="48" height="46" fill="#CCCCCC"/>
<rect x="464" y="140" width="48" height="46" fill="#CCCCCC"/>
<rect y="233" width="48" height="46" fill="#CCCCCC"/>
<rect x="464" y="326" width="48" height="46" fill="#CCCCCC"/>
<rect y="419" width="48" height="46" fill="#CCCCCC"/>
</g>
<defs>
<clipPath id="clip0_271_118">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,7 +1,7 @@
---
import type { CollectionEntry } from 'astro:content'
import { Image } from 'astro:assets'
import Link from '@components/Link.astro'
import AvatarComponent from '@/components/ui/avatar'
type Props = {
author: CollectionEntry<'authors'>
@ -15,12 +15,12 @@ const { name, avatar, bio } = author.data
class="rounded-xl border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
>
<Link href={`/authors/${author.slug}`} class="flex flex-wrap gap-4">
<Image
<AvatarComponent
client:load
src={avatar}
alt={`Avatar of ${name}`}
width={128}
height={128}
class="rounded-xl object-cover"
fallback={name[0]}
className="size-32 rounded-md"
/>
<div class="flex-grow">
<h3 class="mb-1 text-lg font-semibold">{name}</h3>

View file

@ -4,6 +4,7 @@ import { formatDate, readingTime, parseAuthors } from '@lib/utils'
import { Image } from 'astro:assets'
import { Badge } from '@/components/ui/badge'
import { Separator } from '@/components/ui/separator'
import AvatarComponent from '@/components/ui/avatar'
import Link from './Link.astro'
type Props = {
@ -54,12 +55,12 @@ const authors = await parseAuthors(entry.data.authors ?? [])
<>
{authors.map((author) => (
<div class="flex items-center gap-x-1.5">
<Image
<AvatarComponent
client:load
src={author.avatar}
alt={author.name}
width={18}
height={18}
class="rounded-full"
fallback={author.name[0]}
className="size-5 rounded-full"
/>
<span>{author.name}</span>
</div>

View file

@ -15,7 +15,7 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site)
const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props
const { title, description, image = '/static/twitter-card.png' } = Astro.props
---
<meta charset="utf-8" />
@ -28,6 +28,30 @@ const { title, description, image = '/blog-placeholder-1.jpg' } = Astro.props
<meta name="title" content={title} />
<meta name="description" content={description} />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicons/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicons/favicon-16x16.png"
/>
<link rel="manifest" href="/favicons/site.webmanifest" />
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="shortcut icon" href="/favicons/favicon.ico" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="msapplication-config" content="/favicons/browserconfig.xml" />
<meta name="theme-color" content="#121212" />
<meta property="og:type" content="website" />
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />

View file

@ -3,6 +3,8 @@ import Container from '@components/Container.astro'
import Link from '@components/Link.astro'
import { SITE } from '@consts'
import { ModeToggle } from '@/components/ui/mode-toggle'
import { Image } from 'astro:assets'
import logo from '../../public/static/logo.svg'
const items = [
{ href: '/blog', label: 'blog' },
@ -16,11 +18,12 @@ const items = [
transition:persist
>
<Container>
<div class="flex items-center justify-between py-4">
<div class="flex flex-wrap items-center justify-between gap-x-4 py-4">
<Link
href="/"
class="text-xl font-semibold transition-colors duration-300 hover:text-primary"
class="flex flex-shrink-0 items-center gap-2 text-xl font-semibold transition-colors duration-300 hover:text-primary"
>
<Image src={logo} alt="Logo" class="size-8 rounded-sm" />
{SITE.TITLE}
</Link>
<div class="flex items-center gap-4">

View file

@ -10,7 +10,7 @@ const Avatar = React.forwardRef<
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
'relative flex h-10 w-10 shrink-0 overflow-hidden',
className,
)}
{...props}
@ -37,7 +37,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
'flex h-full w-full items-center justify-center bg-muted',
className,
)}
{...props}
@ -45,4 +45,25 @@ const AvatarFallback = React.forwardRef<
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarFallback, AvatarImage }
interface AvatarComponentProps {
src?: string
alt?: string
fallback?: string
className?: string
}
const AvatarComponent: React.FC<AvatarComponentProps> = ({
src,
alt,
fallback,
className,
}) => {
return (
<Avatar className={className}>
<AvatarImage src={src} alt={alt} />
<AvatarFallback>{fallback}</AvatarFallback>
</Avatar>
)
}
export default AvatarComponent

View file

@ -5,7 +5,7 @@ import * as React from 'react'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring focus:ring-ring',
{
variants: {
variant: {

View file

@ -8,7 +8,7 @@ const Card = React.forwardRef<
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-xl border bg-card text-card-foreground', className)}
className={cn('bg-card text-card-foreground rounded-xl border', className)}
{...props}
/>
))

View file

@ -41,7 +41,7 @@ export function ModeToggle() {
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end" className="bg-background">
<DropdownMenuItem onClick={() => setThemeState('theme-light')}>
<Sun className="mr-2 size-4" />
<span>Light</span>

View file

@ -3,7 +3,7 @@ title: '2022 Post'
description: 'This a dummy post written in the year 2022.'
date: '2022-01-01'
tags: ['dummy', 'placeholder']
image: '/1200x630.png'
image: '/static/1200x630.png'
---
This is a dummy post written in the year 2022.

View file

@ -3,7 +3,7 @@ title: '2023 Post'
description: 'This a dummy post written in the year 2023.'
date: '2023-01-01'
tags: ['dummy', 'placeholder']
image: '/1200x630.png'
image: '/static/1200x630.png'
authors: ['enscribe']
---

View file

@ -3,7 +3,7 @@ title: '2024 Post'
description: 'This a dummy post written in the year 2024 (with multiple authors).'
date: '2024-01-01'
tags: ['dummy', 'placeholder']
image: '/1200x630.png'
image: '/static/1200x630.png'
authors: ['enscribe', 'jktrn']
---

View file

@ -3,7 +3,7 @@ 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'
tags: ['webdev', 'opinion']
image: '/1200x630.png'
image: '/static/1200x630.png'
authors: ['enscribe']
---
@ -19,7 +19,7 @@ astro-erudite is written in Astro, a framework hyperoptimized for static content
This is a non-exhaustive list of features I believe are essential for a frictionless developer experience:
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers. However, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-neutral-50 text-neutral-900 dark:bg-neutral-900 dark:text-neutral-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utiliy colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
- [shadcn/ui](https://ui.shadcn.com) is a pretty controversial component library. I love it. I don't care much for the components themselves as they are literally [Radix](https://www.radix-ui.com/) primitive wrappers. However, the best part is arguably its take on [theming](https://ui.shadcn.com/docs/theming), which introduces a convention involving CSS colors such as `background` and `foreground` into your Tailwind configuration so that styling is a breeze. These classes also automatically adapt to the user's selected theme, and as such you don't need to worry about adding an equivalent `dark:` style to all of your theming. shadcn/ui turns `"bg-stone-50 text-stone-900 dark:bg-stone-900 dark:text-stone-50"` into `"bg-background text-foreground"`, both more semantic and easier to blanket edit (if you wanted to change all your blues in your site to indigos, you would need to go around every single class and change it rather than editing a single CSS variable). Other utiliy colors such as `secondary`, `muted`, `accent`, and `destructive` also exist and are very self-explanatory in name (and also have an equivalent `-foreground` class, e.g. `secondary-foreground`, which you can apply to text on top of these colors).
- [Tailwind Typography](https://github.com/tailwindlabs/tailwindcss-typography) is a plugin that automatically styles any content surrounded by an `<article>{:html}` tag in a way which makes it readable and blog-post-friendly. It does this via a `prose` class which you can wrap anything with to style the interior content. This is especially useful for HTML you don't control, e.g. a post rendered from Markdown. Although your control over the rendering is a bit less fine-grained, you're also already using Tailwind so this right has long been forsaken.
- [Shiki](https://github.com/shikijs/shiki) is a syntax highlighter for code blocks. Although Astro code blocks utilize Shiki under the hood, I've actually disabled the default code blocks in this template so that they don't collide with my preferred library [rehype-pretty-code](https://rehype-pretty.pages.dev), which is _also_ powered by Shiki but allows for line numbers, line highlighting, inline code snytax highlighting, and a transformers API for advanced customization such as manual `diff` visualization and line blurring. This library does not ship with any CSS, and it's up to you to style the code blocks and code block titles as you see fit. I've provided styles in `src/styles/global.css` within the `@layer components{:css}` directive if you wish to fiddle with them. The following code block is an example of how to style code blocks using rehype-pretty-code, and was generated with the following Markdown code:
@ -115,5 +115,5 @@ Within the blog itself (as in the layout, appearance, and navigation) are featur
- You really don't need a comments section via [Giscus](https://giscus.app). This opens up a can of worms involving the ability to spam comments and the necessity to moderate them. If you want organic discussion about your blog posts to happen, then share on social media and let people discuss there.
- Speaking of sharing on social media, let's get rid of the share buttons. Please inform me of a single time you have used a share button on a blog post.
- You really don't need a <abbr title="Content Management System">CMS</abbr> unless you have thousands of posts and/or are willing to navigate through a clunky management interface. Markdown and folders is really all you need, which you can organize to your preference via folder or file naming conventions.
- If you have literally anything involving an `.env` file in a blog post, please reconsider what you are doing.
- If you have literally anything involving an `.env` file in a blogging site, please think about what you are doing very carefully.
- Please do not override the browser's <kbd>Ctrl</kbd> + <kbd>K</kbd> functionality to open up a command palette. There should not be a single reason why a user would use a small context menu to browse your blog over the `/blog` route. Most of the time, command palettes on sites do nothing more than regurgitate shortcuts that are already on the same page you're hiding with the palette's modal.

View file

@ -7,23 +7,30 @@ import { SITE } from '@consts'
type Props = {
title: string
description: string
image?: string
}
const { title, description } = Astro.props
const { title, description, image } = Astro.props
---
<!doctype html>
<html lang="en">
<head>
<Head title={`${title} | ${SITE.TITLE}`} description={description} />
<Head
title={`${title} | ${SITE.TITLE}`}
description={description}
image={image}
/>
</head>
<body
class="box-border flex h-fit min-h-screen flex-col gap-y-6 bg-background px-4 font-sans text-foreground antialiased"
>
<Header />
<main class="flex-grow">
<slot />
</main>
<Footer />
<body>
<div
class="box-border flex h-fit min-h-screen flex-col gap-y-6 bg-background px-4 font-sans text-foreground antialiased"
>
<Header />
<main class="flex-grow">
<slot />
</main>
<Footer />
</div>
</body>
</html>

View file

@ -29,14 +29,14 @@ export async function parseAuthors(authors: string[]) {
const author = await getEntry('authors', slug)
return {
name: author?.data?.name || slug,
avatar: author?.data?.avatar || '/512x512.png',
avatar: author?.data?.avatar || '/static/512x512.png',
isRegistered: !!author,
}
} catch (error) {
console.error(`Error fetching author with slug ${slug}:`, error)
return {
name: slug,
avatar: '/512x512.png',
avatar: '/static/512x512.png',
isRegistered: false,
}
}

View file

@ -39,7 +39,7 @@ const authorPosts = allPosts
<Layout
title={`${author.data.name} - Author`}
description={author.data.bio || `Profile of ${author.data.name}`}
description={author.data.bio || `Profile of ${author.data.name}.`}
>
<Container>
<Breadcrumb className="mb-6">

View file

@ -16,7 +16,7 @@ import { HomeIcon } from 'lucide-react'
const authors = await getCollection('authors')
---
<Layout title="Authors" description="Authors">
<Layout title="Authors" description="A list of authors on this site.">
<Container>
<Breadcrumb className="mb-6">
<BreadcrumbList>

View file

@ -62,7 +62,11 @@ const { Content, headings } = await post.render()
const authors = await parseAuthors(post.data.authors ?? [])
---
<Layout title={post.data.title} description={post.data.description}>
<Layout
title={post.data.title}
description={post.data.description}
image={post.data.image ?? '/static/1200x630.png'}
>
<Container>
<Breadcrumb className="mb-6">
<BreadcrumbList>
@ -89,7 +93,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
alt={post.data.title}
width={1200}
height={630}
class="mb-8 rounded-xl object-cover shadow-lg"
class="mb-8 rounded-xl object-cover"
/>
)
}
@ -162,7 +166,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
{headings.length > 0 && <TableOfContents headings={headings} />}
<article class="prose prose-neutral max-w-none dark:prose-invert">
<article class="prose prose-stone max-w-none dark:prose-invert">
<Content />
</article>
@ -170,7 +174,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
</Container>
<Button
variant="secondary"
variant="outline"
size="icon"
className="group fixed bottom-8 right-8 z-50 hidden"
id="scroll-to-top"

View file

@ -20,7 +20,7 @@ const blog = (await getCollection('blog'))
.slice(0, SITE.NUM_POSTS_ON_HOMEPAGE)
---
<Layout title="Home" description="Home">
<Layout title={SITE.TITLE} description={SITE.DESCRIPTION}>
<Container class="flex flex-col gap-y-6">
<section>
<Card>

View file

@ -112,7 +112,7 @@
/* Code block titles */
[data-rehype-pretty-code-title] {
@apply rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium !text-foreground;
@apply rounded-t-xl border-x border-t px-4 py-2 text-sm font-medium text-foreground;
/* Remove top margin from code block if a title is present */
& + pre {

View file

@ -10,22 +10,9 @@ const config: Config = {
sans: ['Geist Sans', ...defaultTheme.fontFamily.sans],
mono: ['Geist Mono', ...defaultTheme.fontFamily.mono],
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))',
@ -51,15 +38,7 @@ const config: Config = {
foreground: 'hsl(var(--destructive-foreground))',
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))',
},
},
typography: {
DEFAULT: {