Skip to content
View mo-gaafar's full-sized avatar
🏠
Working from home
🏠
Working from home

Highlights

  • Pro

Block or report mo-gaafar

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Maximum 250 characters. Please don’t include any personal information such as legal names or email addresses. Markdown is supported. This note will only be visible to you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
mo-gaafar/README.md

mngaafar.com — Portfolio (Next.js + PayloadCMS)

Mohamed N. Gaafar's portfolio, positioned as AI Engineer & Fractional CTO. Rebuilt from the previous Hugo site onto a modern, CMS-backed stack.

  • Framework: Next.js 15 (App Router, React 19)
  • CMS: PayloadCMS 3 (self-hosted, in-app admin at /admin)
  • Database: PostgreSQL
  • Styling: framework-free CSS with design tokens + light/dark themes
  • Deploy target: Coolify via Docker Compose

Quick start (local)

pnpm install
cp .env.example .env # set PAYLOAD_SECRET + DATABASE_URL# start a Postgres (any), point DATABASE_URL at it, then:
pnpm dev # http://localhost:3000 (admin: /admin)
pnpm seed # migrate the initial content into the DB

pnpm seed creates the admin user (SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD, defaults admin@mngaafar.com / changeme-please) and loads all content (experience, skills, education, certifications, services, case studies, projects, publications, blog posts, and the two globals). It is repeatable — it clears and re-inserts content collections each run.

Environment variables

VarPurpose
DATABASE_URLPostgres connection string
PAYLOAD_SECRETLong random string for Payload auth/encryption
NEXT_PUBLIC_SERVER_URLPublic site URL (no trailing slash) — used for CORS, canonical, OG
SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORDFirst admin user (seed only)

Scripts

ScriptWhat it does
pnpm devDev server
pnpm build / pnpm startProduction build / serve
pnpm seedMigrate/seed content
pnpm generate:typesRegenerate src/payload-types.ts after schema changes
pnpm generate:importmapRegenerate the admin import map

Deploying to Coolify

Postgres is a separate Coolify-managed database resource (Coolify runs it from the official Postgres image and handles its volume/backups). The app deploys on its own and connects via DATABASE_URL. docker-compose.yml is app-only; the bundled-Postgres file (docker-compose.local.yml) is for local runs only.

  1. In Coolify, create a PostgreSQL database resource. Copy its internal connection string (e.g. postgres://user:pass@<service>:5432/<db>).
  2. Create a Docker Compose resource pointing at this repo's docker-compose.yml.
  3. Set env vars in Coolify: DATABASE_URL (from step 1), PAYLOAD_SECRET, NEXT_PUBLIC_SERVER_URL (your domain), the S3_* (R2) vars, and optionally SEED_ADMIN_EMAIL / SEED_ADMIN_PASSWORD.
  4. Deploy. Schema is auto-synced on boot (push: true), so no manual migration step is needed against the fresh database.
  5. First run only — seed the content from the app container's terminal:
    pnpm seed
  6. Point your domain at the app service and set it as NEXT_PUBLIC_SERVER_URL.

Local prod-like run with a throwaway Postgres: docker compose -f docker-compose.local.yml up --build.

If the managed DB enforces TLS, append ?sslmode=require to DATABASE_URL.

Media storage (important) + Cloudflare

Container disks are ephemeral — CMS uploads must go to object storage or they vanish on redeploy. Set the S3_* env vars to a Cloudflare R2 bucket and Media uploads persist there (falls back to local disk only when S3_BUCKET is unset, for local dev).

The chosen hosting model is Cloudflare in front, Node behind: the app runs on Coolify, Cloudflare proxies the domain for CDN/TLS, and media lives in R2. Full setup (R2 bucket + token, DNS proxy, SSL Full-strict, cache rules) is in docs/09-cloudflare.md.

Project structure

src/
├── app/(frontend)/ # Public site (home, projects, blog, publications, resume)
├── app/(payload)/ # Payload admin + API (auto-wired)
├── collections/ # Payload collections
├── globals/ # SiteSettings, Home
├── components/ # Nav, Footer, ThemeToggle, Icon, RichText
├── fields/ # Reusable field helpers (slug)
├── lib/ # Payload client helpers
├── seed/ # Content migration script + source markdown
└── payload.config.ts # Payload config (Postgres, collections, globals)
docs/ # Site map + rebuild documentation
Dockerfile, docker-compose.yml # Coolify deployment

MCP (Model Context Protocol) support

The CMS is exposed to MCP clients via @payloadcms/plugin-mcp, so an AI client (Claude Desktop, Claude Code, Cursor, etc.) can read and edit portfolio content through tools.

  • Endpoint:POST/GET {NEXT_PUBLIC_SERVER_URL}/api/mcp (Streamable HTTP).
  • Auth:Authorization: Bearer <api-key>.
  • Turn off: set DISABLE_MCP=true.

Create an API key

  1. Open /adminMCP → API KeysCreate.
  2. Tick Enable API Key (the key is generated and shown once — copy it).
  3. Toggle the exact capabilities that key should have per collection/global (find / create / update). Keys are scoped: a client only sees tools for the capabilities its key grants, and only ever acts as the key's associated user.

Connect a client

Streamable-HTTP MCP client config (e.g. Claude Desktop mcpServers):

{
"mcpServers": {
"mngaafar-portfolio": {
"type": "http",
"url": "https://www.mngaafar.com/api/mcp",
"headers": { "Authorization": "Bearer YOUR_API_KEY" }
}
}
}

Exposed tools follow find* / create* / update* per enabled collection (e.g. findProjects, updateHome). Delete is disabled by default; globals support find/update only. Which collections/globals are available is configured in src/payload.config.ts (the mcpPlugin({...}) block).

Content model

Managed in /admin:

  • Collections: Projects, Case Studies, Publications, Blog Posts, Services, Testimonials, Media, Experience, Skill Groups, Education, Certifications, Users.
  • Globals: Site Settings (identity, contact, socials, analytics), Home (hero, proof metrics, section visibility).

See docs/ for the full map of the previous site, the design system, and the rebuild plan.

Notes

  • The previous Hugo source directories (content/, data/, themes/, static/, content-example/) are retired. Their content has been migrated into Payload (seed) and their assets copied to public/. They can be deleted.
  • Blog SEO redirects from old URLs are preserved via each post's redirectFrom field (handled in src/app/(frontend)/blog/[slug]/page.tsx).

Pinned Loading

  1. image-processing-toolboximage-processing-toolboxPublic

    A python based medical image processing toolbox, with image manipulation features ranging from histogram equalization, affine transformations, scaling to spatial and frequency domain filtering.

    Python 1

  2. mini-music-workstationmini-music-workstationPublic

    A miniature digital audio workstation that can modify and equalize musical signals or create its own music

    Python 3 4

  3. app-with-autodeploy-superpowersapp-with-autodeploy-superpowersPublic

    Capstone project for AWS cloud devops, has a fully automated CI/CD pipeline utilizing CircleCI and CloudFormation API

    TypeScript

  4. blind-assistance-deep-learningblind-assistance-deep-learningPublic

    Aiming to transform the lives of visually impaired individuals by enabling them to understand and interact with their environment more effectively.

    Jupyter Notebook

  5. CGM_Prediction_LSTMCGM_Prediction_LSTMPublic

    Forked from JakubDylag/CGM_Prediction_LSTM

    Machine Learning based Prediction of Glucose Levels on Type 1 Diabetes Patients with the use of Continuous Glucose Monitoring Data.

    Jupyter Notebook 1

  6. deep-retinal-classification-pythondeep-retinal-classification-pythonPublic

    Forked from mawady/DeepRetinalClassification

    3-Class Retinal Classification via Deep Network Features and SVM Classifier (Academic Research Use). Reimplemented using python

    MATLAB 2