My personal site. theclashfruit.me
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: blog seo stuff

+432 -2
+1
.drizzle/0001_late_agent_zero.sql
··· 1 + ALTER TABLE "posts" ADD COLUMN "excerpt" text;
+271
.drizzle/meta/0001_snapshot.json
··· 1 + { 2 + "id": "2cbd9483-c81e-45f3-8892-b9539e29f8c1", 3 + "prevId": "7a7dc3ca-7ef1-47fb-b857-9f130f2b2287", 4 + "version": "7", 5 + "dialect": "postgresql", 6 + "tables": { 7 + "public.comments": { 8 + "name": "comments", 9 + "schema": "", 10 + "columns": { 11 + "id": { 12 + "name": "id", 13 + "type": "bigint", 14 + "primaryKey": true, 15 + "notNull": true 16 + }, 17 + "post_id": { 18 + "name": "post_id", 19 + "type": "bigint", 20 + "primaryKey": false, 21 + "notNull": false 22 + }, 23 + "parent_id": { 24 + "name": "parent_id", 25 + "type": "bigint", 26 + "primaryKey": false, 27 + "notNull": false 28 + }, 29 + "author_id": { 30 + "name": "author_id", 31 + "type": "bigint", 32 + "primaryKey": false, 33 + "notNull": false 34 + }, 35 + "display_name": { 36 + "name": "display_name", 37 + "type": "varchar(64)", 38 + "primaryKey": false, 39 + "notNull": false 40 + }, 41 + "content": { 42 + "name": "content", 43 + "type": "text", 44 + "primaryKey": false, 45 + "notNull": true 46 + } 47 + }, 48 + "indexes": {}, 49 + "foreignKeys": { 50 + "comments_post_id_posts_id_fk": { 51 + "name": "comments_post_id_posts_id_fk", 52 + "tableFrom": "comments", 53 + "tableTo": "posts", 54 + "columnsFrom": [ 55 + "post_id" 56 + ], 57 + "columnsTo": [ 58 + "id" 59 + ], 60 + "onDelete": "no action", 61 + "onUpdate": "no action" 62 + }, 63 + "comments_parent_id_comments_id_fk": { 64 + "name": "comments_parent_id_comments_id_fk", 65 + "tableFrom": "comments", 66 + "tableTo": "comments", 67 + "columnsFrom": [ 68 + "parent_id" 69 + ], 70 + "columnsTo": [ 71 + "id" 72 + ], 73 + "onDelete": "no action", 74 + "onUpdate": "no action" 75 + }, 76 + "comments_author_id_users_id_fk": { 77 + "name": "comments_author_id_users_id_fk", 78 + "tableFrom": "comments", 79 + "tableTo": "users", 80 + "columnsFrom": [ 81 + "author_id" 82 + ], 83 + "columnsTo": [ 84 + "id" 85 + ], 86 + "onDelete": "no action", 87 + "onUpdate": "no action" 88 + } 89 + }, 90 + "compositePrimaryKeys": {}, 91 + "uniqueConstraints": {}, 92 + "policies": {}, 93 + "checkConstraints": {}, 94 + "isRLSEnabled": false 95 + }, 96 + "public.posts": { 97 + "name": "posts", 98 + "schema": "", 99 + "columns": { 100 + "id": { 101 + "name": "id", 102 + "type": "bigint", 103 + "primaryKey": true, 104 + "notNull": true 105 + }, 106 + "author_id": { 107 + "name": "author_id", 108 + "type": "bigint", 109 + "primaryKey": false, 110 + "notNull": false 111 + }, 112 + "slug": { 113 + "name": "slug", 114 + "type": "varchar(128)", 115 + "primaryKey": false, 116 + "notNull": true 117 + }, 118 + "title": { 119 + "name": "title", 120 + "type": "varchar(256)", 121 + "primaryKey": false, 122 + "notNull": true 123 + }, 124 + "excerpt": { 125 + "name": "excerpt", 126 + "type": "text", 127 + "primaryKey": false, 128 + "notNull": false 129 + }, 130 + "content": { 131 + "name": "content", 132 + "type": "text", 133 + "primaryKey": false, 134 + "notNull": true 135 + }, 136 + "published_at": { 137 + "name": "published_at", 138 + "type": "timestamp", 139 + "primaryKey": false, 140 + "notNull": true, 141 + "default": "now()" 142 + }, 143 + "updated_at": { 144 + "name": "updated_at", 145 + "type": "timestamp", 146 + "primaryKey": false, 147 + "notNull": true, 148 + "default": "now()" 149 + } 150 + }, 151 + "indexes": {}, 152 + "foreignKeys": { 153 + "posts_author_id_users_id_fk": { 154 + "name": "posts_author_id_users_id_fk", 155 + "tableFrom": "posts", 156 + "tableTo": "users", 157 + "columnsFrom": [ 158 + "author_id" 159 + ], 160 + "columnsTo": [ 161 + "id" 162 + ], 163 + "onDelete": "no action", 164 + "onUpdate": "no action" 165 + } 166 + }, 167 + "compositePrimaryKeys": {}, 168 + "uniqueConstraints": { 169 + "posts_slug_unique": { 170 + "name": "posts_slug_unique", 171 + "nullsNotDistinct": false, 172 + "columns": [ 173 + "slug" 174 + ] 175 + } 176 + }, 177 + "policies": {}, 178 + "checkConstraints": {}, 179 + "isRLSEnabled": false 180 + }, 181 + "public.users": { 182 + "name": "users", 183 + "schema": "", 184 + "columns": { 185 + "id": { 186 + "name": "id", 187 + "type": "bigint", 188 + "primaryKey": true, 189 + "notNull": true 190 + }, 191 + "username": { 192 + "name": "username", 193 + "type": "varchar(32)", 194 + "primaryKey": false, 195 + "notNull": true 196 + }, 197 + "display_name": { 198 + "name": "display_name", 199 + "type": "varchar(64)", 200 + "primaryKey": false, 201 + "notNull": false 202 + }, 203 + "email": { 204 + "name": "email", 205 + "type": "varchar(320)", 206 + "primaryKey": false, 207 + "notNull": true 208 + }, 209 + "password": { 210 + "name": "password", 211 + "type": "varchar(72)", 212 + "primaryKey": false, 213 + "notNull": true 214 + }, 215 + "avatar": { 216 + "name": "avatar", 217 + "type": "varchar(40)", 218 + "primaryKey": false, 219 + "notNull": false 220 + }, 221 + "permissions": { 222 + "name": "permissions", 223 + "type": "integer", 224 + "primaryKey": false, 225 + "notNull": true, 226 + "default": 0 227 + } 228 + }, 229 + "indexes": {}, 230 + "foreignKeys": {}, 231 + "compositePrimaryKeys": {}, 232 + "uniqueConstraints": { 233 + "users_username_unique": { 234 + "name": "users_username_unique", 235 + "nullsNotDistinct": false, 236 + "columns": [ 237 + "username" 238 + ] 239 + }, 240 + "users_email_unique": { 241 + "name": "users_email_unique", 242 + "nullsNotDistinct": false, 243 + "columns": [ 244 + "email" 245 + ] 246 + }, 247 + "users_password_unique": { 248 + "name": "users_password_unique", 249 + "nullsNotDistinct": false, 250 + "columns": [ 251 + "password" 252 + ] 253 + } 254 + }, 255 + "policies": {}, 256 + "checkConstraints": {}, 257 + "isRLSEnabled": false 258 + } 259 + }, 260 + "enums": {}, 261 + "schemas": {}, 262 + "sequences": {}, 263 + "roles": {}, 264 + "policies": {}, 265 + "views": {}, 266 + "_meta": { 267 + "columns": {}, 268 + "schemas": {}, 269 + "tables": {} 270 + } 271 + }
+7
.drizzle/meta/_journal.json
··· 8 8 "when": 1773845940841, 9 9 "tag": "0000_silent_cammi", 10 10 "breakpoints": true 11 + }, 12 + { 13 + "idx": 1, 14 + "version": "7", 15 + "when": 1774691481484, 16 + "tag": "0001_late_agent_zero", 17 + "breakpoints": true 11 18 } 12 19 ] 13 20 }
+116
app/(main)/post/[slug]/opengraph-image.tsx
··· 1 + import { db } from '@/lib/db/drizzle'; 2 + import { postsTable, usersTable } from '@/lib/db/schema'; 3 + import { eq } from 'drizzle-orm'; 4 + import { ImageResponse } from 'next/og'; 5 + import { readFile } from 'node:fs/promises'; 6 + import { join } from 'node:path'; 7 + 8 + export const alt = 9 + "A simple image with leaves in the background and text in the foreground stating the post's name and author."; 10 + export const size = { 11 + width: 1280, 12 + height: 640 13 + }; 14 + 15 + export const contentType = 'image/png'; 16 + 17 + export default async function Image({ 18 + params 19 + }: { 20 + params: Promise<{ slug: string }>; 21 + }) { 22 + const { slug } = await params; 23 + const { posts: post, users: user } = ( 24 + await db 25 + .select() 26 + .from(postsTable) 27 + .leftJoin(usersTable, eq(postsTable.author, usersTable.id)) 28 + .where(eq(postsTable.slug, slug)) 29 + .limit(1) 30 + )[0]; 31 + 32 + const bg = await readFile( 33 + join(process.cwd(), 'public', 'img', 'social.png'), 34 + 'base64' 35 + ); 36 + 37 + return new ImageResponse( 38 + <div 39 + style={{ 40 + background: 'white', 41 + width: '100%', 42 + height: '100%', 43 + display: 'flex', 44 + flexDirection: 'column', 45 + alignItems: 'center', 46 + justifyContent: 'center', 47 + backgroundImage: `url(data:image/png;base64,${bg})` 48 + }} 49 + > 50 + <div 51 + style={{ 52 + display: 'flex', 53 + flexDirection: 'column', 54 + width: '70%', 55 + gap: '6px' 56 + }} 57 + > 58 + <h1 59 + style={{ 60 + fontWeight: 500, 61 + fontSize: '48px', 62 + margin: 0 63 + }} 64 + > 65 + {post.title} 66 + </h1> 67 + <h2 68 + style={{ 69 + fontWeight: 400, 70 + fontSize: '24px', 71 + margin: 0 72 + }} 73 + > 74 + By {user?.displayName ?? user?.username} 75 + </h2> 76 + </div> 77 + </div>, 78 + { 79 + ...size, 80 + fonts: [ 81 + { 82 + name: 'Cantarell', 83 + data: await readFile( 84 + join(process.cwd(), 'fonts/otf/Cantarell-Light.otf') 85 + ), 86 + style: 'normal', 87 + weight: 300 88 + }, 89 + { 90 + name: 'Cantarell', 91 + data: await readFile( 92 + join(process.cwd(), 'fonts/otf/Cantarell-Regular.otf') 93 + ), 94 + style: 'normal', 95 + weight: 400 96 + }, 97 + { 98 + name: 'Cantarell', 99 + data: await readFile( 100 + join(process.cwd(), 'fonts/otf/Cantarell-Bold.otf') 101 + ), 102 + style: 'normal', 103 + weight: 500 104 + }, 105 + { 106 + name: 'Cantarell', 107 + data: await readFile( 108 + join(process.cwd(), 'fonts/otf/Cantarell-ExtraBold.otf') 109 + ), 110 + style: 'normal', 111 + weight: 600 112 + } 113 + ] 114 + } 115 + ); 116 + }
+34
app/(main)/post/[slug]/page.tsx
··· 12 12 13 13 import Comment, { CommentWithReplies } from '@/components/Comment'; 14 14 15 + import type { Metadata } from 'next'; 16 + 15 17 const options: MDXRemoteOptions = { 16 18 mdxOptions: { 17 19 rehypePlugins: [rehypeStarryNight], 18 20 remarkPlugins: [remarkGfm] 19 21 } 20 22 }; 23 + 24 + export async function generateMetadata({ 25 + params 26 + }: { 27 + params: Promise<{ slug: string }>; 28 + }): Promise<Metadata> { 29 + const slug = (await params).slug; 30 + 31 + const { posts: post, users: user } = await fetchPostData(slug); 32 + 33 + return { 34 + title: post.title, 35 + description: 36 + post.excerpt ?? 37 + 'A fluffy dragon smashing buttons on a keyboard and drawing lines on paper.', 38 + openGraph: { 39 + type: 'article', 40 + 41 + siteName: 'TheClashFruit', 42 + title: post.title, 43 + 44 + publishedTime: post.publishedAt.toISOString(), 45 + modifiedTime: post.updatedAt.toISOString(), 46 + 47 + description: 48 + post.excerpt ?? 49 + 'A fluffy dragon smashing buttons on a keyboard and drawing lines on paper.', 50 + 51 + authors: [user!.displayName ?? user!.username] 52 + } 53 + }; 54 + } 21 55 22 56 export default async function Post({ 23 57 params
+2 -2
app/layout.tsx
··· 21 21 default: 'TheClashFruit', 22 22 template: 'TheClashFruit • %s' 23 23 }, 24 - description: 'A fluffy dragon smashing buttons on a keyboard and darwing lines on paper.', 24 + description: 'A fluffy dragon smashing buttons on a keyboard and drawing lines on paper.', 25 25 openGraph: { 26 26 type: 'website', 27 27 siteName: 'TheClashFruit', 28 - description: 'A fluffy dragon smashing buttons on a keyboard and darwing lines on paper.', 28 + description: 'A fluffy dragon smashing buttons on a keyboard and drawing lines on paper.', 29 29 url: 'https://theclashfruit.me' 30 30 } 31 31 };
fonts/otf/Cantarell-Bold.otf

This is a binary file and will not be displayed.

fonts/otf/Cantarell-ExtraBold.otf

This is a binary file and will not be displayed.

fonts/otf/Cantarell-Light.otf

This is a binary file and will not be displayed.

fonts/otf/Cantarell-Regular.otf

This is a binary file and will not be displayed.

fonts/otf/Cantarell-Thin.otf

This is a binary file and will not be displayed.

fonts/otf/Cantarell-VF.otf

This is a binary file and will not be displayed.

+1
lib/db/schema.ts
··· 36 36 slug: varchar({ length: 128 }).unique().notNull(), 37 37 title: varchar({ length: 256 }).notNull(), 38 38 39 + excerpt: text(), 39 40 content: text().notNull(), 40 41 41 42 publishedAt: timestamp().notNull().defaultNow(),
public/img/social.png

This is a binary file and will not be displayed.