A fictional blog built as an example portfolio project for my Upwork freelancer profile. It uses Next.js 15 (App Router + Server Components) and is backed by Wisp CMS, styled with Tailwind CSS + shadcn/ui, and includes dark mode.
- Upwork profile:
https://www.upwork.com/freelancers/~01b2bbaad5280577ea
- Framework: Next.js 15 (App Router, React Server Components)
- CMS: Wisp (
@wisp-cms/client) - Styling/UI: Tailwind CSS, shadcn/ui (Radix UI),
@tailwindcss/typography,tailwindcss-animate - Dark mode:
next-themes(class-based) - SEO: per-route metadata, JSON-LD on blog posts
- Extras: RSS utilities, signed OG image endpoint
- Wisp-powered content: posts + related posts fetched server-side
- Blog post pages:
/blog/[slug]with metadata + JSON-LD - Pagination: homepage uses Wisp pagination (
?page=1, etc.) - Signed OG images: dynamic OG image route with request signing/verification
- Dark mode: system/default theme with Tailwind
dark:styles - Sitemap:
/blog/sitemap.xml
- Home (paginated feed):
/ - About page:
/about - Blog post:
/blog/[slug] - Blog sitemap:
/blog/sitemap.xml - OG image:
/api/og-image?title=...&label=...&brand=...&s=...
npm installCopy the example env file and fill in values:
cp .env.example .env.localRequired:
NEXT_PUBLIC_BLOG_ID: your Wisp Blog ID (from your Wisp project setup page)
Recommended:
NEXT_PUBLIC_BASE_URL: e.g.http://localhost:3000(used for sitemap/canonical URLs)NEXT_PUBLIC_BLOG_DISPLAY_NAME: site name (default:Travel.)NEXT_DEFAULT_METADATA_DEFAULT_TITLE: default SEO titleNEXT_PUBLIC_BLOG_DESCRIPTION: default SEO descriptionNEXT_PUBLIC_BLOG_COPYRIGHT: footer copyright label
Optional (but recommended for production):
OG_IMAGE_SECRET: secret used to sign and verify OG image URLs
npm run devOpen http://localhost:3000.
This project is designed to deploy cleanly on Vercel.
- Set the same env vars in your Vercel project settings (especially
NEXT_PUBLIC_BLOG_IDandNEXT_PUBLIC_BASE_URL) - Build command:
npm run build - Output: Next.js default
- Content is fetched using
@wisp-cms/clientconfigured insrc/lib/wisp.ts. - The Wisp Blog ID is read from
NEXT_PUBLIC_BLOG_ID(seesrc/config.ts).
npm run dev # next dev --turbopack
npm run build # next build
npm run start # next start
npm run lint # next lintThis is a fictional portfolio blog meant to demonstrate modern Next.js patterns (App Router + Server Components), CMS integration, SEO fundamentals, and UI polish (Tailwind + shadcn/ui + dark mode). It’s not intended to represent a real publication.