|
| 1 | +import Head from 'next/head'; |
| 2 | +import Link from 'next/link'; |
| 3 | +import Image from 'next/legacy/image'; |
| 4 | +import Header from '@/components/Header'; |
| 5 | +import Footer from '@/components/Footer'; |
| 6 | + |
| 7 | +export interface BlogPost { |
| 8 | + slug: string; |
| 9 | + title: string; |
| 10 | + description: string; |
| 11 | + date: string; |
| 12 | + coverImage?: string; |
| 13 | + content: string; |
| 14 | +} |
| 15 | + |
| 16 | +interface BlogLayoutProps { |
| 17 | + post: BlogPost; |
| 18 | +} |
| 19 | + |
| 20 | +const BASE_URL = 'https://notion-avatar.app'; |
| 21 | + |
| 22 | +/** |
| 23 | + * Format a date string for display |
| 24 | + */ |
| 25 | +function formatDate(dateString: string): string { |
| 26 | + if (!dateString) return ''; |
| 27 | + |
| 28 | + const date = new Date(dateString); |
| 29 | + return date.toLocaleDateString('en-US', { |
| 30 | + year: 'numeric', |
| 31 | + month: 'long', |
| 32 | + day: 'numeric', |
| 33 | + }); |
| 34 | +} |
| 35 | + |
| 36 | +export default function BlogLayout({ post }: BlogLayoutProps) { |
| 37 | + const canonicalUrl = `${BASE_URL}/blog/${post.slug}`; |
| 38 | + const coverImageUrl = post.coverImage |
| 39 | + ? `${BASE_URL}${post.coverImage}` |
| 40 | + : `${BASE_URL}/social.png`; |
| 41 | + |
| 42 | + return ( |
| 43 | + <> |
| 44 | + <Head> |
| 45 | + {/* Basic Meta Tags */} |
| 46 | + <title>{post.title} | Notion Avatar Blog</title> |
| 47 | + <meta name="description" content={post.description} /> |
| 48 | + <meta name="author" content="Notion Avatar" /> |
| 49 | + |
| 50 | + {/* Open Graph */} |
| 51 | + <meta property="og:type" content="article" /> |
| 52 | + <meta property="og:title" content={post.title} /> |
| 53 | + <meta property="og:description" content={post.description} /> |
| 54 | + <meta property="og:url" content={canonicalUrl} /> |
| 55 | + <meta property="og:image" content={coverImageUrl} /> |
| 56 | + <meta property="og:site_name" content="Notion Avatar Blog" /> |
| 57 | + <meta property="article:published_time" content={post.date} /> |
| 58 | + |
| 59 | + {/* Twitter Card */} |
| 60 | + <meta name="twitter:card" content="summary_large_image" /> |
| 61 | + <meta name="twitter:title" content={post.title} /> |
| 62 | + <meta name="twitter:description" content={post.description} /> |
| 63 | + <meta name="twitter:image" content={coverImageUrl} /> |
| 64 | + <meta name="twitter:site" content="@phillzou" /> |
| 65 | + |
| 66 | + {/* Canonical URL */} |
| 67 | + <link rel="canonical" href={canonicalUrl} /> |
| 68 | + |
| 69 | + {/* JSON-LD Structured Data */} |
| 70 | + <script |
| 71 | + type="application/ld+json" |
| 72 | + dangerouslySetInnerHTML={{ |
| 73 | + __html: JSON.stringify({ |
| 74 | + '@context': 'https://schema.org', |
| 75 | + '@type': 'BlogPosting', |
| 76 | + headline: post.title, |
| 77 | + description: post.description, |
| 78 | + image: coverImageUrl, |
| 79 | + datePublished: post.date, |
| 80 | + author: { |
| 81 | + '@type': 'Organization', |
| 82 | + name: 'Notion Avatar', |
| 83 | + url: BASE_URL, |
| 84 | + }, |
| 85 | + publisher: { |
| 86 | + '@type': 'Organization', |
| 87 | + name: 'Notion Avatar', |
| 88 | + url: BASE_URL, |
| 89 | + logo: { |
| 90 | + '@type': 'ImageObject', |
| 91 | + url: `${BASE_URL}/logo.gif`, |
| 92 | + }, |
| 93 | + }, |
| 94 | + mainEntityOfPage: { |
| 95 | + '@type': 'WebPage', |
| 96 | + '@id': canonicalUrl, |
| 97 | + }, |
| 98 | + }), |
| 99 | + }} |
| 100 | + /> |
| 101 | + </Head> |
| 102 | + |
| 103 | + <Header /> |
| 104 | + |
| 105 | + <main className="min-h-screen py-8 px-4 sm:px-6 lg:px-8"> |
| 106 | + <article className="max-w-3xl mx-auto"> |
| 107 | + {/* Back to Blog */} |
| 108 | + <Link |
| 109 | + href="/blog" |
| 110 | + className="inline-flex items-center text-sm font-bold text-black hover:text-gray-600 transition-colors mb-8 group" |
| 111 | + > |
| 112 | + <svg |
| 113 | + xmlns="http://www.w3.org/2000/svg" |
| 114 | + className="h-4 w-4 mr-2 group-hover:-translate-x-1 transition-transform" |
| 115 | + fill="none" |
| 116 | + viewBox="0 0 24 24" |
| 117 | + stroke="currentColor" |
| 118 | + strokeWidth={2} |
| 119 | + > |
| 120 | + <path |
| 121 | + strokeLinecap="round" |
| 122 | + strokeLinejoin="round" |
| 123 | + d="M15 19l-7-7 7-7" |
| 124 | + /> |
| 125 | + </svg> |
| 126 | + Back to Blog |
| 127 | + </Link> |
| 128 | + |
| 129 | + {/* Article Header */} |
| 130 | + <header className="mb-8"> |
| 131 | + <time |
| 132 | + dateTime={post.date} |
| 133 | + className="text-sm text-gray-500 font-medium" |
| 134 | + > |
| 135 | + {formatDate(post.date)} |
| 136 | + </time> |
| 137 | + <h1 className="text-3xl sm:text-4xl font-bold text-gray-900 mt-2 mb-4 leading-tight"> |
| 138 | + {post.title} |
| 139 | + </h1> |
| 140 | + <p className="text-lg text-gray-600 leading-relaxed"> |
| 141 | + {post.description} |
| 142 | + </p> |
| 143 | + </header> |
| 144 | + |
| 145 | + {/* Cover Image */} |
| 146 | + {post.coverImage && ( |
| 147 | + <div className="relative w-full h-64 sm:h-80 md:h-96 mb-8 rounded-lg overflow-hidden border-3 border-black"> |
| 148 | + <Image |
| 149 | + src={post.coverImage} |
| 150 | + alt={post.title} |
| 151 | + layout="fill" |
| 152 | + objectFit="cover" |
| 153 | + priority |
| 154 | + /> |
| 155 | + </div> |
| 156 | + )} |
| 157 | + |
| 158 | + {/* Article Content */} |
| 159 | + <div |
| 160 | + className="prose prose-lg max-w-none |
| 161 | + prose-headings:font-bold prose-headings:text-gray-900 |
| 162 | + prose-h2:text-2xl prose-h2:mt-8 prose-h2:mb-4 |
| 163 | + prose-h3:text-xl prose-h3:mt-6 prose-h3:mb-3 |
| 164 | + prose-p:text-gray-700 prose-p:leading-relaxed prose-p:mb-4 |
| 165 | + prose-a:text-black prose-a:font-semibold prose-a:underline prose-a:underline-offset-2 hover:prose-a:text-gray-600 |
| 166 | + prose-strong:text-gray-900 prose-strong:font-bold |
| 167 | + prose-ul:my-4 prose-ul:pl-6 prose-li:text-gray-700 prose-li:mb-2 |
| 168 | + prose-ol:my-4 prose-ol:pl-6 |
| 169 | + prose-blockquote:border-l-4 prose-blockquote:border-black prose-blockquote:pl-4 prose-blockquote:italic prose-blockquote:text-gray-600 |
| 170 | + prose-code:bg-gray-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-sm prose-code:font-mono |
| 171 | + prose-pre:bg-gray-900 prose-pre:text-gray-100 prose-pre:rounded-lg prose-pre:border-3 prose-pre:border-black prose-pre:overflow-x-auto |
| 172 | + prose-img:rounded-lg prose-img:border-3 prose-img:border-black |
| 173 | + prose-table:border-3 prose-table:border-black prose-table:rounded-lg prose-table:overflow-hidden |
| 174 | + prose-th:px-4 prose-th:py-3 prose-th:text-left prose-th:font-bold prose-th:text-gray-900 |
| 175 | + prose-td:px-4 prose-td:py-3 prose-td:border-t prose-td:border-gray-200" |
| 176 | + dangerouslySetInnerHTML={{ __html: post.content }} |
| 177 | + /> |
| 178 | + |
| 179 | + {/* Footer Navigation */} |
| 180 | + <div className="mt-12 pt-8 border-t-3 border-black"> |
| 181 | + <Link |
| 182 | + href="/blog" |
| 183 | + className="inline-flex items-center px-6 py-3 bg-black text-white font-bold rounded-full hover:bg-gray-800 transition-colors" |
| 184 | + > |
| 185 | + <svg |
| 186 | + xmlns="http://www.w3.org/2000/svg" |
| 187 | + className="h-5 w-5 mr-2" |
| 188 | + fill="none" |
| 189 | + viewBox="0 0 24 24" |
| 190 | + stroke="currentColor" |
| 191 | + strokeWidth={2} |
| 192 | + > |
| 193 | + <path |
| 194 | + strokeLinecap="round" |
| 195 | + strokeLinejoin="round" |
| 196 | + d="M15 19l-7-7 7-7" |
| 197 | + /> |
| 198 | + </svg> |
| 199 | + View All Posts |
| 200 | + </Link> |
| 201 | + </div> |
| 202 | + </article> |
| 203 | + </main> |
| 204 | + |
| 205 | + <Footer /> |
| 206 | + </> |
| 207 | + ); |
| 208 | +} |
0 commit comments