Skip to content

Commit 8fc9c96

Browse files
authored
Merge pull request #51 from Mayandev/feat/ai
feat: add blog for tips, tutorials, and updates about creating Notion-style avatars
2 parents f05f66c + 977a095 commit 8fc9c96

15 files changed

Lines changed: 1717 additions & 14 deletions

File tree

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "notion-avatar",
3-
"description": "An online tool for making notion-style avatars.",
3+
"description": "AI-powered online tool for making notion-style avatars.",
44
"version": "1.0.0",
55
"private": true,
66
"author": "mayandev<phillzou@gmail.com> (@phillzou)",
@@ -32,10 +32,12 @@
3232
"@google/genai": "1.34.0",
3333
"@supabase/ssr": "0.8.0",
3434
"@supabase/supabase-js": "2.89.0",
35+
"@tailwindcss/typography": "0.5.19",
3536
"@tanstack/react-query": "5.90.14",
3637
"chrome-aws-lambda": "8.0.2",
3738
"copy-to-clipboard": "3.3.1",
3839
"dayjs": "1.10.7",
40+
"gray-matter": "4.0.3",
3941
"html2canvas": "1.3.2",
4042
"next": "13.0.0",
4143
"next-i18next": "8.8.0",
@@ -45,6 +47,9 @@
4547
"react": "18.2.0",
4648
"react-dom": "18.2.0",
4749
"react-hot-toast": "2.5.2",
50+
"remark": "15.0.1",
51+
"remark-gfm": "4.0.1",
52+
"remark-html": "16.0.1",
4853
"stripe": "20.1.0"
4954
},
5055
"devDependencies": {

public/sw.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Blog/BlogCard.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import Link from 'next/link';
2+
import Image from 'next/legacy/image';
3+
4+
export interface BlogPostMeta {
5+
slug: string;
6+
title: string;
7+
description: string;
8+
date: string;
9+
coverImage?: string;
10+
}
11+
12+
interface BlogCardProps {
13+
post: BlogPostMeta;
14+
}
15+
16+
/**
17+
* Format a date string for display
18+
*/
19+
function formatDate(dateString: string): string {
20+
if (!dateString) return '';
21+
22+
const date = new Date(dateString);
23+
return date.toLocaleDateString('en-US', {
24+
year: 'numeric',
25+
month: 'long',
26+
day: 'numeric',
27+
});
28+
}
29+
30+
export default function BlogCard({ post }: BlogCardProps) {
31+
return (
32+
<Link href={`/blog/${post.slug}`} className="group block">
33+
<article className="bg-white border-3 border-black rounded-lg overflow-hidden transition-all duration-300 hover:translate-x-1 hover:shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] flex flex-col sm:flex-row">
34+
{/* Cover Image */}
35+
{post.coverImage && (
36+
<div className="relative w-full sm:w-48 md:w-56 h-48 sm:h-auto flex-shrink-0 overflow-hidden border-b-3 sm:border-b-0 sm:border-r-3 border-black">
37+
<Image
38+
src={post.coverImage}
39+
alt={post.title}
40+
layout="fill"
41+
objectFit="cover"
42+
className="transition-transform duration-300 group-hover:scale-105"
43+
/>
44+
</div>
45+
)}
46+
47+
{/* Content */}
48+
<div className="p-5 flex-1 flex flex-col justify-center">
49+
{/* Date */}
50+
<time
51+
dateTime={post.date}
52+
className="text-sm text-gray-500 font-medium"
53+
>
54+
{formatDate(post.date)}
55+
</time>
56+
57+
{/* Title */}
58+
<h2 className="text-xl font-bold text-gray-900 mt-2 mb-3 line-clamp-2 group-hover:text-gray-700 transition-colors">
59+
{post.title}
60+
</h2>
61+
62+
{/* Description */}
63+
<p className="text-gray-600 text-sm line-clamp-2 leading-relaxed">
64+
{post.description}
65+
</p>
66+
67+
{/* Read More Indicator */}
68+
<div className="mt-4 flex items-center text-sm font-bold text-black group-hover:translate-x-1 transition-transform">
69+
<span>Read more</span>
70+
<svg
71+
xmlns="http://www.w3.org/2000/svg"
72+
className="h-4 w-4 ml-1"
73+
fill="none"
74+
viewBox="0 0 24 24"
75+
stroke="currentColor"
76+
strokeWidth={2}
77+
>
78+
<path
79+
strokeLinecap="round"
80+
strokeLinejoin="round"
81+
d="M9 5l7 7-7 7"
82+
/>
83+
</svg>
84+
</div>
85+
</div>
86+
</article>
87+
</Link>
88+
);
89+
}

src/components/Blog/BlogLayout.tsx

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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+
}

src/components/Footer/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default function Footer() {
88
return (
99
<footer className="flex flex-col items-center pb-4">
1010
<Image src="/icon/line.svg" width="125" height="54" />
11-
<div className="flex justify-center mt-10">
11+
<div className="flex flex-wrap justify-center mt-10 gap-x-1 gap-y-2">
1212
<a className="transition hover:underline" href={t(`coffeeUrl`)}>
1313
{t(`coffee`)}
1414
</a>
@@ -17,6 +17,10 @@ export default function Footer() {
1717
{t(`twitter`)}
1818
</a>
1919
<span className="mx-2">·</span>
20+
<Link className="transition hover:underline" href="/blog">
21+
Blog
22+
</Link>
23+
<span className="mx-2">·</span>
2024
<Link className="transition hover:underline" href="/privacy-policy">
2125
{t(`privacyPolicy`)}
2226
</Link>

0 commit comments

Comments
 (0)