Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/node_modules
/.next/
/out/
.DS_Store
*.pem
npm-debug.log*
.env*.local
.vercel
next-env.d.ts
15 changes: 15 additions & 0 deletions app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Metadata } from 'next'

export const metadata: Metadata = { title: 'About' }

export default function About() {
return (
<div className="prose dark:prose-invert max-w-none">
<h1>About Me</h1>
<p>
I am Erkin George, a Software Developer and Computer Science graduate.
Based out of Seattle, I love reading, video games, working out, software development and cats.
</p>
</div>
)
}
47 changes: 47 additions & 0 deletions app/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { getAllPosts, getPost } from '@/lib/posts'
import { MDXRemote } from 'next-mdx-remote/rsc'
import { notFound } from 'next/navigation'

type Props = { params: Promise<{ slug: string }> }

export function generateStaticParams() {
return getAllPosts().map(post => ({ slug: post.slug }))
}

export async function generateMetadata({ params }: Props) {
const { slug } = await params
try {
const post = getPost(slug)
return { title: post.title }
} catch {
return {}
}
}

export default async function BlogPost({ params }: Props) {
const { slug } = await params
let post
try {
post = getPost(slug)
} catch {
notFound()
}

return (
<article className="space-y-8">
<header className="space-y-2">
<h1 className="text-3xl font-bold tracking-tight">{post.title}</h1>
<time className="block text-sm text-slate-400 dark:text-slate-500">
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
</header>
<div className="prose dark:prose-invert max-w-none">
<MDXRemote source={post.content} />
</div>
</article>
)
}
40 changes: 40 additions & 0 deletions app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { getAllPosts } from '@/lib/posts'

export const metadata: Metadata = { title: 'Blog' }

export default function Blog() {
const posts = getAllPosts()

return (
<div className="space-y-8">
<h1 className="text-3xl font-bold tracking-tight">Blog</h1>
{posts.length === 0 ? (
<p className="text-slate-500">No posts yet.</p>
) : (
<ul className="divide-y divide-slate-200 dark:divide-slate-800">
{posts.map(post => (
<li key={post.slug} className="py-4 first:pt-0">
<Link
href={`/blog/${post.slug}`}
className="group flex justify-between items-baseline gap-4"
>
<span className="font-medium group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{post.title}
</span>
<span className="shrink-0 text-sm text-slate-400 dark:text-slate-500 tabular-nums">
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</span>
</Link>
</li>
))}
</ul>
)}
</div>
)
}
32 changes: 32 additions & 0 deletions app/contact/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Metadata } from 'next'

export const metadata: Metadata = { title: 'Contact' }

export default function Contact() {
return (
<div className="space-y-6">
<h1 className="text-3xl font-bold tracking-tight">Contact</h1>
<p className="text-slate-600 dark:text-slate-400">You can reach me at the following:</p>
<ul className="space-y-3">
<li>
<a
href="mailto:erkin.george@gmail.com"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
erkin.george@gmail.com
</a>
</li>
<li>
<a
href="https://www.linkedin.com/in/erkin-george/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 hover:underline"
>
LinkedIn Profile
</a>
</li>
</ul>
</div>
)
}
3 changes: 3 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
51 changes: 51 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import Link from 'next/link'
import './globals.css'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
title: {
default: 'Erkin George',
template: '%s | Erkin George',
},
description: 'Software Developer based in Seattle',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={`${inter.className} min-h-screen bg-white dark:bg-slate-950 text-slate-900 dark:text-slate-100 flex flex-col`}>
<header className="border-b border-slate-200 dark:border-slate-800">
<nav className="max-w-2xl mx-auto px-4 py-4 flex justify-between items-center">
<Link href="/" className="font-semibold tracking-tight hover:text-slate-600 dark:hover:text-slate-300 transition-colors">
Erkin George
</Link>
<div className="flex gap-6 text-sm text-slate-500 dark:text-slate-400">
<Link href="/about" className="hover:text-slate-900 dark:hover:text-slate-100 transition-colors">
About
</Link>
<Link href="/blog" className="hover:text-slate-900 dark:hover:text-slate-100 transition-colors">
Blog
</Link>
<Link href="/contact" className="hover:text-slate-900 dark:hover:text-slate-100 transition-colors">
Contact
</Link>
</div>
</nav>
</header>

<main className="max-w-2xl mx-auto px-4 py-12 w-full flex-1">
{children}
</main>

<footer className="border-t border-slate-200 dark:border-slate-800">
<div className="max-w-2xl mx-auto px-4 py-6 text-sm text-slate-400 dark:text-slate-600">
© {new Date().getFullYear()} Erkin George
</div>
</footer>
</body>
</html>
)
}
48 changes: 48 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Link from 'next/link'
import { getAllPosts } from '@/lib/posts'

export default function Home() {
const posts = getAllPosts()

return (
<div className="space-y-12">
<section>
<h1 className="text-3xl font-bold tracking-tight mb-3">Hi, I&apos;m Erkin George</h1>
<p className="text-lg text-slate-600 dark:text-slate-400">
Software Developer and Computer Science graduate based in Seattle.
</p>
</section>

<section>
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-400 dark:text-slate-500 mb-5">
Recent Posts
</h2>
{posts.length === 0 ? (
<p className="text-slate-500">No posts yet.</p>
) : (
<ul className="space-y-3">
{posts.map(post => (
<li key={post.slug}>
<Link
href={`/blog/${post.slug}`}
className="group flex justify-between items-baseline gap-4"
>
<span className="font-medium group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">
{post.title}
</span>
<span className="shrink-0 text-sm text-slate-400 dark:text-slate-500 tabular-nums">
{new Date(post.date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</span>
</Link>
</li>
))}
</ul>
)}
</section>
</div>
)
}
39 changes: 39 additions & 0 deletions content/posts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Writing blog posts

Each post is a single Markdown file in this folder. The filename (minus the
extension) becomes the URL slug, e.g. `my-first-post.md` → `/blog/my-first-post`.

## Weekly workflow

1. **Scaffold a new post** — this creates the file with today's date and a
`draft: true` flag so it stays hidden until you're ready:

```bash
npm run new-post "My Post Title"
```

2. **Write it** — open the generated file in `content/posts/` and write your
content in Markdown below the frontmatter.

3. **Preview locally**:

```bash
npm run dev
```

Then visit http://localhost:3000/blog

4. **Publish** — set `draft: false` in the frontmatter, then commit and push.
Vercel deploys automatically.

## Frontmatter reference

```yaml
---
title: "Your Title" # shown on the blog index and post page
date: 2026-06-07T12:00:00Z # used for sorting (newest first)
draft: false # true = hidden from the site
---
```

Drafts are excluded from the homepage and `/blog` index automatically.
9 changes: 9 additions & 0 deletions content/posts/my-first-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "My First Post"
date: 2022-11-20T09:03:20-08:00
draft: false
---

## Introduction

This is my first test post on this website. I plan to post more as time passes and I learn more about hosting and building a website.
55 changes: 55 additions & 0 deletions lib/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDir = path.join(process.cwd(), 'content/posts')

export type PostMeta = {
slug: string
title: string
date: string
}

export type Post = PostMeta & {
content: string
}

// gray-matter parses unquoted YAML dates into Date objects, so normalize to an
// ISO string for stable typing, sorting, and formatting.
function normalizeDate(value: unknown): string {
return value instanceof Date ? value.toISOString() : String(value)
}

// Resolve a slug to its file, supporting both .md and .mdx extensions.
function resolvePostPath(slug: string): string | null {
for (const ext of ['.md', '.mdx']) {
const filePath = path.join(postsDir, `${slug}${ext}`)
if (fs.existsSync(filePath)) return filePath
}
return null
}

export function getAllPosts(): PostMeta[] {
return fs
.readdirSync(postsDir)
.filter(f => /\.mdx?$/.test(f))
.flatMap(fileName => {
const slug = fileName.replace(/\.mdx?$/, '')
const { data } = matter(fs.readFileSync(path.join(postsDir, fileName), 'utf8'))
if (!data.title || data.draft) return []
return [{ slug, title: data.title as string, date: normalizeDate(data.date) }]
})
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
}

export function getPost(slug: string): Post {
const filePath = resolvePostPath(slug)
if (!filePath) throw new Error(`Post not found: ${slug}`)
const { data, content } = matter(fs.readFileSync(filePath, 'utf8'))
return {
slug,
title: data.title as string,
date: normalizeDate(data.date),
content,
}
}
5 changes: 5 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {}

export default nextConfig
Loading
Loading