Personal Website for @jaspermayone.com jaspermayone.com
0
fork

Configure Feed

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

og

+369 -73
+41
src/app/api/og/route.tsx
··· 1 + import { generateOGImage } from "@/lib/og-image"; 2 + import { getPageOGData } from "@/lib/og-utils"; 3 + import { NextRequest } from "next/server"; 4 + 5 + export const runtime = "edge"; 6 + 7 + export async function GET(request: NextRequest) { 8 + try { 9 + const { searchParams } = new URL(request.url); 10 + const path = searchParams.get("path") || "/"; 11 + const title = searchParams.get("title"); 12 + const subtitle = searchParams.get("subtitle"); 13 + const description = searchParams.get("description"); 14 + const type = searchParams.get("type") as 15 + | "default" 16 + | "portfolio" 17 + | "project" 18 + | "page" 19 + | null; 20 + 21 + // Use custom data if provided, otherwise fallback to defaults 22 + let data; 23 + if (title) { 24 + data = { 25 + title, 26 + subtitle: subtitle || undefined, 27 + description: description || undefined, 28 + type: type || "page", 29 + }; 30 + } else { 31 + data = getPageOGData(path); 32 + } 33 + 34 + return generateOGImage(data); 35 + } catch (e: any) { 36 + console.log(`${e.message}`); 37 + return new Response(`Failed to generate the image`, { 38 + status: 500, 39 + }); 40 + } 41 + }
+4 -73
src/app/opengraph-image.tsx
··· 1 - import { ImageResponse } from "@vercel/og"; 1 + import { generateOGImage } from "@/lib/og-image"; 2 + import { getPageOGData } from "@/lib/og-utils"; 2 3 3 4 export const runtime = "edge"; 4 5 export const alt = "Jasper Mayone"; ··· 9 10 export const contentType = "image/png"; 10 11 11 12 export default async function Image() { 12 - return new ImageResponse( 13 - ( 14 - <div 15 - style={{ 16 - height: "100%", 17 - width: "100%", 18 - display: "flex", 19 - flexDirection: "column", 20 - alignItems: "center", 21 - justifyContent: "center", 22 - backgroundColor: "#151922", // Updated to match site dark mode background 23 - padding: "40px", 24 - }} 25 - > 26 - <div 27 - style={{ 28 - display: "flex", 29 - flexDirection: "column", 30 - alignItems: "center", 31 - justifyContent: "center", 32 - border: "2px solid rgba(255, 255, 255, 0.1)", 33 - borderRadius: "10px", // Matching site's rounded corners 34 - padding: "40px", 35 - backgroundColor: "rgba(21, 25, 34, 0.7)", 36 - boxShadow: "0 10px 30px rgba(0, 0, 0, 0.3)", 37 - width: "90%", 38 - height: "80%", 39 - }} 40 - > 41 - <h1 42 - style={{ 43 - fontSize: "88px", 44 - fontWeight: "bold", 45 - color: "#ffffff", 46 - marginBottom: "20px", 47 - textAlign: "center", 48 - borderBottom: "4px solid #4299e1", // Using border instead of unsupported wavy underline 49 - paddingBottom: "10px", 50 - }} 51 - > 52 - Jasper Mayone 53 - </h1> 54 - <div 55 - style={{ 56 - fontSize: "32px", 57 - color: "#e0e0e0", 58 - textAlign: "center", 59 - maxWidth: "700px", 60 - lineHeight: 1.4, 61 - margin: "0 auto", 62 - }} 63 - > 64 - Circus Artist • Coder • Photographer 65 - </div> 66 - <div 67 - style={{ 68 - fontSize: "24px", 69 - color: "#4299e1", // Changed to site's primary blue 70 - marginTop: "30px", 71 - textAlign: "center", 72 - }} 73 - > 74 - jaspermayone.com 75 - </div> 76 - </div> 77 - </div> 78 - ), 79 - { 80 - width: 1200, 81 - height: 630, 82 - }, 83 - ); 13 + const data = getPageOGData("/"); 14 + return generateOGImage(data); 84 15 }
+6
src/app/portfolio/page.tsx
··· 7 7 import FOOTER from "@/components/FOOTER"; 8 8 import SquigglyLine from "@/components/SquigglyLine"; 9 9 import styles from "@/styles/Home.module.css"; 10 + import { generateOGMetadata } from "@/lib/og-utils"; 10 11 11 12 export const metadata: Metadata = { 12 13 title: "Portfolio", 13 14 description: "A showcase of my projects and accomplishments.", 15 + ...generateOGMetadata( 16 + "/portfolio", 17 + "Portfolio", 18 + "A showcase of my projects and accomplishments.", 19 + ), 14 20 }; 15 21 16 22 export default function Portfolio() {
+167
src/lib/og-image.tsx
··· 1 + import { ImageResponse } from "@vercel/og"; 2 + import { OGImageData } from "./og-utils"; 3 + 4 + export const runtime = "edge"; 5 + 6 + export const size = { 7 + width: 1200, 8 + height: 630, 9 + }; 10 + 11 + export const contentType = "image/png"; 12 + 13 + export function generateOGImage(data: OGImageData) { 14 + const { title, subtitle, description, type = "default" } = data; 15 + 16 + // Color schemes based on type 17 + const getColors = (type: string) => { 18 + switch (type) { 19 + case "portfolio": 20 + return { 21 + bg: "#151922", 22 + cardBg: "rgba(21, 25, 34, 0.8)", 23 + accent: "#10b981", // emerald 24 + border: "rgba(16, 185, 129, 0.2)", 25 + }; 26 + case "project": 27 + return { 28 + bg: "#1a1a2e", 29 + cardBg: "rgba(26, 26, 46, 0.8)", 30 + accent: "#f59e0b", // amber 31 + border: "rgba(245, 158, 11, 0.2)", 32 + }; 33 + case "page": 34 + return { 35 + bg: "#0f172a", 36 + cardBg: "rgba(15, 23, 42, 0.8)", 37 + accent: "#8b5cf6", // violet 38 + border: "rgba(139, 92, 246, 0.2)", 39 + }; 40 + default: 41 + return { 42 + bg: "#151922", 43 + cardBg: "rgba(21, 25, 34, 0.7)", 44 + accent: "#4299e1", // blue 45 + border: "rgba(66, 153, 225, 0.2)", 46 + }; 47 + } 48 + }; 49 + 50 + const colors = getColors(type); 51 + 52 + return new ImageResponse( 53 + ( 54 + <div 55 + style={{ 56 + height: "100%", 57 + width: "100%", 58 + display: "flex", 59 + flexDirection: "column", 60 + alignItems: "center", 61 + justifyContent: "center", 62 + backgroundColor: colors.bg, 63 + padding: "40px", 64 + fontFamily: 65 + 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', 66 + }} 67 + > 68 + <div 69 + style={{ 70 + display: "flex", 71 + flexDirection: "column", 72 + alignItems: "center", 73 + justifyContent: "center", 74 + border: `2px solid ${colors.border}`, 75 + borderRadius: "16px", 76 + padding: "50px", 77 + backgroundColor: colors.cardBg, 78 + boxShadow: "0 25px 50px rgba(0, 0, 0, 0.4)", 79 + width: "90%", 80 + height: "80%", 81 + textAlign: "center", 82 + }} 83 + > 84 + <h1 85 + style={{ 86 + fontSize: title.length > 20 ? "64px" : "80px", 87 + fontWeight: "bold", 88 + color: "#ffffff", 89 + marginBottom: "16px", 90 + textAlign: "center", 91 + borderBottom: `4px solid ${colors.accent}`, 92 + paddingBottom: "12px", 93 + maxWidth: "100%", 94 + lineHeight: 1.1, 95 + }} 96 + > 97 + {title} 98 + </h1> 99 + 100 + {subtitle && ( 101 + <div 102 + style={{ 103 + fontSize: "36px", 104 + color: "#e2e8f0", 105 + textAlign: "center", 106 + maxWidth: "800px", 107 + lineHeight: 1.3, 108 + marginBottom: description ? "16px" : "24px", 109 + }} 110 + > 111 + {subtitle} 112 + </div> 113 + )} 114 + 115 + {description && ( 116 + <div 117 + style={{ 118 + fontSize: "24px", 119 + color: "#94a3b8", 120 + textAlign: "center", 121 + maxWidth: "700px", 122 + lineHeight: 1.4, 123 + marginBottom: "24px", 124 + }} 125 + > 126 + {description} 127 + </div> 128 + )} 129 + 130 + <div 131 + style={{ 132 + fontSize: "20px", 133 + color: colors.accent, 134 + marginTop: "auto", 135 + textAlign: "center", 136 + display: "flex", 137 + alignItems: "center", 138 + gap: "8px", 139 + }} 140 + > 141 + <div 142 + style={{ 143 + width: "8px", 144 + height: "8px", 145 + borderRadius: "50%", 146 + backgroundColor: colors.accent, 147 + }} 148 + /> 149 + jaspermayone.com 150 + <div 151 + style={{ 152 + width: "8px", 153 + height: "8px", 154 + borderRadius: "50%", 155 + backgroundColor: colors.accent, 156 + }} 157 + /> 158 + </div> 159 + </div> 160 + </div> 161 + ), 162 + { 163 + width: 1200, 164 + height: 630, 165 + }, 166 + ); 167 + }
+151
src/lib/og-utils.ts
··· 1 + export interface OGImageData { 2 + title: string; 3 + subtitle?: string; 4 + description?: string; 5 + type?: "default" | "portfolio" | "project" | "page"; 6 + } 7 + 8 + export const getPageOGData = (path: string): OGImageData => { 9 + const pathMappings: Record<string, OGImageData> = { 10 + "/": { 11 + title: "Jasper Mayone", 12 + subtitle: "Circus Artist • Coder • Photographer", 13 + type: "default", 14 + }, 15 + "/portfolio": { 16 + title: "Portfolio", 17 + subtitle: "Projects & Work by Jasper Mayone", 18 + description: 19 + "Full-stack development, circus performance, and creative projects", 20 + type: "portfolio", 21 + }, 22 + "/contact": { 23 + title: "Contact", 24 + subtitle: "Get in Touch", 25 + description: 26 + "Reach out for collaborations, projects, or just to say hello", 27 + type: "page", 28 + }, 29 + "/uses": { 30 + title: "Uses", 31 + subtitle: "Tools & Setup", 32 + description: "Software, hardware, and gear I use daily", 33 + type: "page", 34 + }, 35 + "/now": { 36 + title: "Now", 37 + subtitle: "What I'm up to", 38 + description: "Current projects and focus areas", 39 + type: "page", 40 + }, 41 + "/changelog": { 42 + title: "Changelog", 43 + subtitle: "Site Updates", 44 + description: "Recent changes and improvements to the site", 45 + type: "page", 46 + }, 47 + "/colophon": { 48 + title: "Colophon", 49 + subtitle: "About This Site", 50 + description: "How this website was built and designed", 51 + type: "page", 52 + }, 53 + "/panera": { 54 + title: "Panera", 55 + subtitle: "Menu Explorer", 56 + description: "Interactive Panera menu browser", 57 + type: "project", 58 + }, 59 + "/green": { 60 + title: "Green", 61 + subtitle: "Sustainability Focus", 62 + description: "Environmental initiatives and green tech", 63 + type: "page", 64 + }, 65 + "/podroll": { 66 + title: "Podroll", 67 + subtitle: "Podcast Recommendations", 68 + description: "Curated list of podcasts worth listening to", 69 + type: "page", 70 + }, 71 + "/pfp": { 72 + title: "Profile Pictures", 73 + subtitle: "Avatar Collection", 74 + description: "Available profile pictures and avatars", 75 + type: "page", 76 + }, 77 + "/verify": { 78 + title: "Verify", 79 + subtitle: "Identity Verification", 80 + description: "Verify my identity across platforms", 81 + type: "page", 82 + }, 83 + }; 84 + 85 + return ( 86 + pathMappings[path] || { 87 + title: "Jasper Mayone", 88 + subtitle: path 89 + .replace("/", "") 90 + .replace("-", " ") 91 + .replace(/\b\w/g, (l) => l.toUpperCase()), 92 + type: "page", 93 + } 94 + ); 95 + }; 96 + 97 + // Helper function to generate OG image URL for any page 98 + export const getOGImageUrl = ( 99 + path: string, 100 + customData?: Partial<OGImageData>, 101 + ) => { 102 + const baseUrl = "/api/og"; 103 + const params = new URLSearchParams(); 104 + 105 + if (customData) { 106 + if (customData.title) params.set("title", customData.title); 107 + if (customData.subtitle) params.set("subtitle", customData.subtitle); 108 + if (customData.description) 109 + params.set("description", customData.description); 110 + if (customData.type) params.set("type", customData.type); 111 + } else { 112 + params.set("path", path); 113 + } 114 + 115 + return `${baseUrl}?${params.toString()}`; 116 + }; 117 + 118 + // Helper to generate complete OG metadata for any page 119 + export const generateOGMetadata = ( 120 + path: string, 121 + title: string, 122 + description: string, 123 + customData?: Partial<OGImageData>, 124 + ) => { 125 + const ogImageUrl = getOGImageUrl(path, customData); 126 + 127 + return { 128 + openGraph: { 129 + title: `${title} - Jasper Mayone`, 130 + description, 131 + images: [ 132 + { 133 + url: ogImageUrl, 134 + width: 1200, 135 + height: 630, 136 + alt: `${title} - Jasper Mayone`, 137 + }, 138 + ], 139 + }, 140 + twitter: { 141 + title: `${title} - Jasper Mayone`, 142 + description, 143 + images: [ 144 + { 145 + url: ogImageUrl, 146 + alt: `${title} - Jasper Mayone`, 147 + }, 148 + ], 149 + }, 150 + }; 151 + };