Personal site - blog, projects, small tools, and an interactive OSCP cheat sheet. Built as a statically generated React app and deployed to Vercel.
- React 19 + TypeScript, routed with react-router and pre-rendered to static HTML by vite-react-ssg.
- Vite 8 build; Tailwind CSS v4 (via
@tailwindcss/vite). - radix-ui primitives, lucide-react icons, @xyflow/react for the
OSCP graph, react-markdown +
remark-gfm/rehype-rawfor content. - Vitest + Testing Library for tests, ESLint + Prettier for lint/format.
- pnpm for package management, release-please for versioning & releases.
- Node 24.x (see
enginesinpackage.json) - pnpm 11.x (
packageManageris pinned)
pnpm install
pnpm dev # dev server (vite-react-ssg dev)| Command | What it does |
|---|---|
pnpm dev |
Start the local dev server. |
pnpm build |
Static build to dist/ (SSG). |
pnpm preview |
Serve the built dist/ locally. |
pnpm lint |
ESLint over the repo. |
pnpm format |
Prettier write (sorted imports, Tailwind class sort). |
pnpm test |
Run the Vitest suite once. |
pnpm test:watch |
Vitest in watch mode. |
pnpm test:coverage |
Coverage report (covers components/tools/**). |
components/ Reusable UI (radix-based ui/, tools/, oscp/, icons/)
content/ Markdown collections: blog/ and projects/
data/ Typed data sources (projectData, toolsData, oscp/)
lib/ Framework-agnostic helpers (posts, projects, oscp-graph, utils)
src/
routes.tsx Route table (feeds SSG static path generation)
layouts.tsx Section layouts (blog / projects / tools)
pages/ Page components per section
App.tsx Root layout (header/footer)
globals.css Tailwind entry + design tokens
types/ Shared type declarations (e.g. oscp.d.ts)
vite-plugin-content.ts Build-time markdown loader (gray-matter, Node-only)
vite-plugin-seo.ts Generates sitemap.xml after the SSG build
The @ import alias resolves to the repo root (see vite.config.ts).
Routes live in src/routes.tsx. Dynamic sections (blog/:slug,
projects/:slug, tools/:slug) expose getStaticPaths so vite-react-ssg
pre-renders one HTML file per entry. After the build, vite-plugin-seo.ts
writes sitemap.xml into dist/. robots.txt is a static file in public/
(it declares the Sitemap: URL and welcomes AI/search crawlers).
/military is intentionally unlisted: no nav link and absent from the sitemap
(it also sits outside the App layout). /cheatsheet has no nav link yet but is
included in the sitemap.
- Blog post - add
content/blog/<slug>.mdwith frontmatter (title,date,description,read,author). Markdown is parsed at build time byvite-plugin-content.ts, so nothing ships gray-matter to the client. - Project - add
content/projects/<slug>.md(frontmatter incl.status,projectSlug,featuredmetadata indata/projectData.ts). Onlyfeaturedprojects get a pre-rendered detail page. - Tool - register it in
data/toolsData.ts; the page renders via the dynamic tool loader undersrc/pages/tools/. - OSCP cheat sheet - see the section below.
Vitest runs under happy-dom with Testing Library. Config lives in
vite.config.ts (test block) and vitest.setup.ts. Coverage is scoped to
components/tools/**; the OSCP content graph has its own integrity tests in
tests/data/oscp.test.ts.
Static build (pnpm build) deployed to Vercel (vercel.json). Releases and the
changelog are handled by release-please (.github/workflows/release.yml) from
Conventional Commit messages: it maintains a release PR that bumps the version
and CHANGELOG.md, and on merge tags the commit and cuts a GitHub release.
The interactive OSCP cheat sheet at /cheatsheet is a DAG built from plain
TypeScript data. Nodes are the things you find, do, or reach on a box; edges are
the conditional transitions between them. To add your own material you only edit
the data files - no component changes needed.
types/oscp.d.ts- the data model (OscpNode,OscpEdge,OscpContent).data/oscp/*.ts- one module per phase, each a default-exportedOscpContent({ nodes, edges }).data/oscp/index.ts- merges every module, defines the phases, and runs an integrity check.
Each phase has its own module: enumeration.ts, service-enum.ts, web.ts,
passwords.ts, shells.ts, linux-privesc.ts, windows-privesc.ts,
pivoting.ts, ad-enum.ts, ad-attacks.ts, ad-lateral.ts. Add your entry to
the file matching its phase. (New phase? Add it to OscpPhase in
types/oscp.d.ts, add a PhaseMeta row to PHASES in index.ts, create the
module, and import it into the CONTENT array.)
Append an OscpNode to that module's nodes array. The id is a stable slug
that edges reference - keep it unique across all files.
{
id: 'smb-writable-share', // unique slug, referenced by edges
title: 'Writable SMB share', // short label shown on the node
type: 'finding', // 'finding' | 'technique' | 'state'
phase: 'service-enum', // must match this module's phase
os: 'windows', // 'linux' | 'windows' | 'ad' | 'agnostic'
description:
'A share you can write to. Drop a payload, a .scf/.url file for a ' +
'hash grab, or a webshell if it maps to a web root.',
commands: [ // optional, copyable blocks
{
label: 'Confirm write access',
code: 'smbcacls //<target>/<share> -N',
note: 'Optional one-line caveat shown under the block.',
},
],
references: [ // optional external links
{ label: 'PayloadsAllTheThings - SMB', url: 'https://...' },
],
tags: ['smb', 'share', 'writable'], // optional, matched by search only
}Node-type guide: finding = something you observed, technique = an action you
run, state = a position you reached (a shell, creds, DA). Reuse a shared state
node (e.g. foothold-windows) by pointing an edge at it rather than duplicating
it.
Every node must sit on at least one edge (no orphans allowed). Add OscpEdge
entries to the module's edges array. The label is the condition that makes
the transition true.
{ from: 'smb-enum', to: 'smb-writable-share', label: 'share is writable' },
{ from: 'smb-writable-share', to: 'foothold-windows', label: 'dropped a payload' },from/to may reference nodes in any file - cross-phase edges are fine and
expected (that's how a finding leads to a foothold in a later phase).
If the node is a realistic "here's what I found" starting point, add its id to
ENTRY_POINTS in data/oscp/index.ts so it shows up in guided mode.
pnpm test # checks for duplicate ids, dangling edges, and orphan nodes
pnpm dev # DEV build also warns in the console on content issuesThe test suite (tests/data/oscp.test.ts) enforces: unique ids, every edge
endpoint resolves to a real node, no orphan nodes, and that a fresh target still
chains through to Domain Admin. Fix any reported issue before committing.