Site App
Public portfolio and blog application documentation
Site App
The site app is the public-facing portfolio and blog at apps/site.
Overview
URL: http://localhost:3000 (development)
The site app showcases your portfolio, blog posts, projects, and provides contact functionality.
Features
- 🏠 Portfolio homepage with hero section
- 📝 Blog posts sourced from Hashnode
- 🚀 Projects showcase with case studies
- 📬 Contact form with email integration
- 🎨 Dark mode support
- ⚡ Optimized performance with caching
- 🔍 SEO optimized
Directory Structure
Key Pages
Homepage
// apps/site/app/page.tsx
import { Hero } from '@/components/Hero'
import { FeaturedProjects } from '@/components/FeaturedProjects'
import { RecentPosts } from '@/components/RecentPosts'
export default async function HomePage() {
return (
<>
<Hero />
<FeaturedProjects />
<RecentPosts />
</>
)
}
Blog Listing
// apps/site/app/blog/page.tsx
import { getAllPostsCached } from '@mindware-blog/hashnode'
export default async function BlogPage() {
const posts = await getAllPostsCached()
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
)
}
Project Page
// apps/site/app/projects/[slug]/page.tsx
import { getProjectBySlug } from '@/lib/projects'
export default async function ProjectPage({
params
}: {
params: { slug: string }
}) {
const project = await getProjectBySlug(params.slug)
return (
<article>
<h1>{project.title}</h1>
<div>{project.description}</div>
{/* Project details */}
</article>
)
}
Configuration
Next.js Config
// apps/site/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['cdn.hashnode.com'],
},
experimental: {
serverActions: true,
},
}
module.exports = nextConfig
Tailwind Config
// apps/site/tailwind.config.js
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'../../packages/ui/src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
// Custom theme
},
},
plugins: [],
}
export default config
API Routes
Contact Form
// apps/site/app/api/contact/route.ts
import { NextResponse } from 'next/server'
import { Resend } from 'resend'
const resend = new Resend(process.env.RESEND_API_KEY)
export async function POST(request: Request) {
const { name, email, message } = await request.json()
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
to: process.env.RESEND_TO_EMAIL!,
subject: `Contact from ${name}`,
html: `<p>${message}</p><p>From: ${email}</p>`
})
return NextResponse.json({ success: true })
}
Caching Strategy
// apps/site/lib/cache.ts
import { Redis } from '@upstash/redis'
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
})
export async function getCached<T>(
key: string,
fetcher: () => Promise<T>,
ttl: number = 3600
): Promise<T> {
const cached = await redis.get<T>(key)
if (cached) return cached
const data = await fetcher()
await redis.setex(key, ttl, JSON.stringify(data))
return data
}
SEO & Metadata
// apps/site/app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params
}: {
params: { slug: string }
}): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
return {
title: post.title,
description: post.brief,
openGraph: {
title: post.title,
description: post.brief,
images: [post.coverImage?.url],
},
}
}
Development
# Start dev server
cd apps/site
pnpm dev
# Run tests
pnpm test
# Build
pnpm build
# Start production server
pnpm start
Environment Variables
# apps/site/.env.local
NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST=yourblog.hashnode.dev
OPENAI_API_KEY=sk-...
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=noreply@yourdomain.com
RESEND_TO_EMAIL=you@yourdomain.com
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...
Deployment
The site app deploys to Vercel:
# Deploy to production
vercel --prod
# Deploy to preview
vercel
Next Steps
- Dashboard App - Admin interface
- Shared Packages - Package details
- API Reference - API documentation