| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A personal website, portfolio, and blog for a software engineer, built as a single Next.js application with Payload CMS providing the admin panel and content API.
The public site renders live content from the CMS: a bio, a work-experience timeline, portfolio projects with image galleries, and a Markdown blog.
| Layer | Technology |
|---|---|
| CMS | Payload CMS 3 (admin panel, REST + GraphQL API, Local API) |
| Framework | Next.js 16 (App Router) with React 19 |
| Runtime & package manager | Bun |
| Database | SQLite via @payloadcms/db-sqlite (libSQL + Drizzle) |
| Styling | Tailwind CSS v4 + @tailwindcss/typography |
| Rich text | Lexical (@payloadcms/richtext-lexical) |
| Markdown | react-markdown + remark-gfm |
| Unit / integration tests | Vitest + React Testing Library |
| End-to-end tests | Playwright |
| Tooling | TypeScript, ESLint, Prettier |
src/ ├── app/ │ ├── (frontend)/ Public website │ │ ├── layout.tsx Fixed nav + footer, global styles │ │ ├── page.tsx Home: bio → experience → curated projects → latest posts │ │ ├── styles.css Tailwind entrypoint (CSS-first config) │ │ ├── projects/ │ │ │ ├── page.tsx Project grid with pagination │ │ │ └── [slug]/page.tsx Project detail with image carousel │ │ └── blog/ │ │ ├── page.tsx Post list with year filter and pagination │ │ └── [slug]/page.tsx Post with rendered Markdown │ ├── (payload)/ Payload-generated admin panel and API routes │ └── my-route/ Example custom route handler ├── collections/ Users, Media, WorkExperience, Skills, Projects, Blog ├── globals/ Bio, Footer ├── components/ Presentational React components ├── lib/ Local API queries and pure helpers ├── payload.config.ts Payload configuration └── payload-types.ts Generated types — do not edit by hand tests/ ├── int/ Vitest specs (*.int.spec.ts / .tsx) ├── e2e/ Playwright specs (*.e2e.spec.ts) └── helpers/ Shared fixtures, seeding and test utilities
Globals
| Global | Slug | Purpose |
|---|---|---|
| Bio | bio | Name, subtitle, tagline and a rich text "About me" |
| Footer | footer | Copyright line and social profile links |
| Home Page Projects | home-page-projects | Which projects the home page shows, in the order they appear |
| Project Page Projects | project-page-projects | Which projects /projects lists, in the order they appear |
Both project globals are curations: only what they list is shown, in the order it is listed, and an empty list means that the page shows no projects at all.
Collections
| Collection | Slug | Purpose |
|---|---|---|
| Users | users | Admin panel authentication |
| Media | media | Uploaded images, stored in /media |
| Work Experience | work-experience | Roles in the home page timeline (empty end year renders as "Present") |
| Skills | skills | Technology tags, created inline from the Projects form |
| Projects | projects | Portfolio projects with skills and images |
| Blog | blog | Markdown posts with a publication date |
git clone <repository-url>
cd code-art-website
bun install
cp .env.example .envThen edit .env:
DATABASE_URL=file:./code-art-website.db
PAYLOAD_SECRET=<a long random string>Generate a secret with openssl rand -hex 32. The SQLite file is created automatically on the first run — there is no separate database server to install.
bun run devThe first visit to /admin prompts you to create an admin user. After that, fill in the Bio global — until it has a name, the home page shows a placeholder instead of the real content.
For a production build:
bun run build
bun run startbun run build:staticWrites a self-contained static copy of the site to out/: HTML, CSS, JavaScript and the uploaded images only — no database, no admin panel and no API routes. Nothing in the output needs a server, so it can be dropped onto any static host.
There is nothing for Next to export directly because every page reads live content from Payload. Instead, the script builds the app into a throwaway .next-static directory, starts it, crawls every page the site links to and saves the HTML it gets back, along with the assets and uploads those pages reference. Whatever the CMS holds at that moment is baked in, so re-run it after editing content.
Static hosts ignore the query string, so the paginated and filtered list pages become real paths — /blog?year=2026&page=2 is written to out/blog/year/2026/page/2/index.html, and the links in the exported HTML point there. out/404.html holds the not-found page.
The same command publishes what it built. The repository the site is served from — ../code-art-static by default — has everything except its .git directory replaced with the new out/, and the difference is committed and pushed. A build identical to what is already published leaves no commit behind, and the clear refuses to run anywhere that is not the root of a repository of its own.
bun run build:static # build, publish and push
bun run build:static --no-push # build and commit, review before pushing
bun run build:static --no-publish # build out/ and stopSTATIC_PORT (default 4321) changes the port the crawl runs against, STATIC_OUT_DIR the output directory, and STATIC_PUBLISH_DIR the repository published into — set it empty to build without publishing.
bun run test:int # Vitest — unit and integration
bun run test:e2e # Playwright — end to end
bun run test # both suitesUnit / integration tests (tests/int/) run in jsdom and cover two things: Payload schema configuration (field types, required flags, validators, slug hooks) and React components rendered with React Testing Library. Components are presentational and take plain props, so no database is needed to test them.
End-to-end tests (tests/e2e/) start a dev server automatically and drive Chromium. They seed content through the Payload Local API before each suite and clean it up afterward — globals are snapshotted and restored, and seeded rows use test- prefixed slugs that are deleted in afterAll, so your own content is left intact.
Playwright browsers are installed with:
bunx playwright install chromiumBoth suites run serially on purpose (fileParallelism: false in vitest.config.mts, workers: 1 in playwright.config.ts): they share a single SQLite file with the dev server, and parallel runs raced Payload's dev schema push. The SQLite adapter is configured with WAL mode and a busy timeout for the same reason.
bun run generate:types # Regenerate src/payload-types.ts from the config
bun run generate:importmap # Regenerate src/app/(payload)/admin/importMap.js
bun run payload <command> # Any other Payload CLI commandRun both generators after every change to a collection, global or field, and commit their output. src/payload-types.ts and src/app/(payload)/admin/importMap.js are generated and must never be edited by hand.
The import map is not only for custom components. The admin panel resolves richText fields — the Lexical editor and each of its toolbar features — through it, so a stale map makes those fields render as nothing at all, with no error anywhere in the admin UI.
The schema is applied automatically in development (Payload pushes it to SQLite on startup), so no migration step is needed for local work. bun run devsafe clears the .next cache first if the dev server gets into a bad state.
bunx prettier --write . # Format
bun run lint # ESLint
bun run build # Also type-checks src/ and tests/Frontend styling is Tailwind utility classes only. Tailwind v4 is configured CSS-first in src/app/(frontend)/styles.css — there is no tailwind.config.js. Every surface supports light and dark color schemes.
Project-specific architecture notes, conventions, and gotchas live in .claude/skills/code-art-website/SKILL.md, with a general Payload reference in .claude/skills/payload/SKILL.md.
CLAUDE.md contains a block that next dev regenerates automatically; commit it as-is.
| Back | FazBrowse Home | New Git URL |