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