Skip to content
kabartayPublic

About

Portfolio

Resources

Contributing

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

Mukharbek Organokov - Portfolio

Release License: GPL v3 Release

A fast, static portfolio site for AI/ML & MLOps work. Built with plain HTML/CSS/JS, deployed as a Static Site on Render with pretty URLs, SEO basics, and a no-backend contact form via Formspree. Helped to setup your own website? You can buy me a coffee to support my work ☕️.

  • © 2025–2026 Mukharbek Organokov
  • 🌐 Website: www.organokov.com
  • 📜 License: GNU General Public License v3.0

Documentation

Deeper developer docs live in docs/:

  • docs/ARCHITECTURE.md — how the site works: the dual-page model, the homepage aggregate loader, and the two-axis theming system.
  • docs/CONTRIBUTING.md — local dev, the branch/PR workflow, and step-by-step guides (add a page, blog post, or palette).

Content

├── README.md                      # Project overview, setup, deploy, and domain setup
├── .github/workflows/             # GitHub Actions CI
│  └── release.yml                 # Creates a GitHub Release + zip on every v*.*.* tag
├── docs/                          # Developer documentation
│  ├── README.md                   # Docs index
│  ├── ARCHITECTURE.md             # How the site works (dual-page model, loader, theming)
│  └── CONTRIBUTING.md             # Local dev, PR workflow, how-to guides, checklist
├── package-lock.json              # Exact, locked dependency versions for reproducible installs
├── package.json                   # Project metadata + dev scripts (e.g., "dev" with `serve`)
├── public/                        # All files served to the browser (site root)
│  ├── assets/                     # Images, icons, downloadable files
│  │  ├── cv.pdf                   # Downloadable CV
│  │  ├── favicon.svg              # Site favicon
│  │  ├── og-card.png              # Open Graph / social share card (1200×630; referenced by every page)
│  │  ├── og-card.svg              # Older vector version of the share card (not referenced)
│  │  ├── logos/                   # Company and university logos, 192px copies used on Experience/Education
│  │  └── projects/                # Project logos used on the Projects page
│  ├── about.html                  # About page (also reachable at /about via Render rewrites)
│  ├── experience.html             # Experience/Resume page
│  ├── education.html              # Education page
│  ├── projects.html               # Projects page: logo, summary, links (and DOI where one exists)
│  ├── achievements.html           # Achievements: headline figures, patents, publications, Kaggle results
│  ├── blog.html                   # Blog landing page: cards that link to the post pages
│  ├── blog/                       # One page per post (/blog/<slug>), with newer/older links
│  ├── contact.html                # Contact page (Formspree form lives here)
│  ├── privacy.html                # Privacy notice (linked from the contact form)
│  ├── index.html                  # Current homepage (also used as “shell” for aggregate loader)
│  ├── 404.html                    # Custom not-found page (shown on broken routes)
│  ├── robots.txt                  # Crawler rules; points to sitemap.xml
│  ├── sitemap.xml                 # SEO sitemap of canonical “pretty” URLs
│  └── src/                        # Versioned static assets (cache-busted by path)
│     ├── css/                     # Stylesheets
│     │  ├── style.css             # Site-wide layout + component styles (imports theme.css)
│     │  └── theme.css             # Colour palettes (single source of truth for colours)
│     └── js/                      # JavaScript for UX and dynamic assembly
│        ├── aggregate.js          # Fetches each page, extracts main sections, and mounts them
│        ├── contact-form.js       # Formspree submit handler (loaded by contact.html and the homepage)
│        ├── main.js               # Global UI on standalone pages (nav, theme, animations)
│        └── theme-config.js       # Theme behaviour: default palette, toggle on/off, palette list
├── assets-src/logos/              # Full-size logo originals (not deployed; make 192px copies for public/)
├── render.yml                     # Render Static Site blueprint (routes/rewrites + headers/caching)
└── serve.json                     # Local dev config for `npx serve` (headers/spa/caching; optional)

Features

  • Static, zero-backend (served from /public)
  • Pretty URLs (/about, /experience, /blog etc.) via Render rewrites
  • Dynamic “aggregate” loader that assembles sections into a single page
  • Dark mode with system-preference detection and a navbar toggle (persisted to localStorage, applied before first paint to avoid a flash)
  • Config-driven colour palettes — switch between Castleton Green and Indigo from the navbar; palettes and behaviour are defined in theme.css + theme-config.js
  • Contact form wired to Formspree (no server code by decision)
  • Resilient UX: custom 404.html, smooth scrolling, mobile nav
  • Accessibility: semantic landmarks, aria-labels on controls, keyboard-friendly nav
  • SEO essentials: sitemap.xml, robots.txt, canonical tags, Open Graph/Twitter cards
  • Security & perf headers via render.yml (HSTS, caching)

Quick Start (Local)

Requirements: Node 18+ (any static server works)

# install deps (if needed)
npm ci

# serve /public locally at http://localhost:3000
npx serve public -l 3000

Any static server is fine (python -m http.server, VS Code Live Server, etc.). The site is entirely static — no build step required.

Contact Form

Used Formspree for simplicity - no server code.

  • Obtain your own https://formspree.io/f/YOUR-KEY from Formspree;
  • Target Email: set in the form’s Settings → Email Notifications;
  • (Optional) Project → Settings → Restrict to Domain: organokov.com (set your domain);
  • DevTools → Network to verify: POST https://formspree.io/f/YOUR-KEY returns 200 {"ok":true};
  • Check Submissions if emails don’t arrive; also inspect spam/junk.

Form action in public/contact.html:

    <form id="contactForm" action="https://formspree.io/f/YOUR-KEY" method="POST">
    <!-- name / email / message fields ... -->
    </form>
    <p id="formStatus" aria-live="polite"></p>

Delegated JS handler is bound globally in public/src/js/main.js, so it works even when the form is injected by the section loader:

    if (!window.__contactDelegatedBound) {
    window.__contactDelegatedBound = true;
    document.addEventListener('submit', async (e) => {
        const form = e.target;
        if (!form.matches('#contactForm')) return;
        e.preventDefault();

        const statusEl = document.getElementById('formStatus');
        if (statusEl) statusEl.textContent = 'Sending...';

        try {
        const res = await fetch(form.action, {
            method: form.method || 'POST',
            body: new FormData(form),
            headers: { Accept: 'application/json' }
        });
        const txt = await res.text();
        console.log('Formspree status:', res.status, txt);

        if (res.ok) {
            form.reset();
            statusEl && (statusEl.textContent = 'Thanks! I’ll get back to you shortly.');
        } else {
            let msg = txt; try { msg = JSON.parse(txt)?.errors?.[0]?.message || txt; } catch {}
            statusEl && (statusEl.textContent = 'Error: ' + msg);
        }
        } catch (err) {
        console.error(err);
        statusEl && (statusEl.textContent = 'Network error. Please try again.');
        }
    });
    }

Pretty URLs & Routing

Pages live as separate HTML files:

  • about.html, experience.html, education.html, projects.html, achievements.html
  • blog.html and one page per post in blog/ (served at /blog/<slug>)
  • contact.html, privacy.html

but are served at clean paths (/about, /blog/<slug>, etc.) using Render rewrites (see render.yml). Every new page needs its own rewrite and, if public, a sitemap.xml entry.

Use root-relative links in HTML:

<a href="/about">About</a>
<a href="/experience">Experience</a>
<a href="/blog">Blog</a>
<a href="/contact">Contact</a>

Theming (Dark Mode & Colour Palettes)

Two independent axes control appearance, both set on the root <html> element before first paint (an inline IIFE + theme-config.js in every <head>), so there's no colour flash on load:

  • Light / dark → data-theme="dark" (toggled by the 🌙 button; defaults to the OS preference)
  • Colour palette → data-palette="…" (toggled by the navbar colour swatch)

Colours and behaviour live in two files:

File Responsibility
public/src/css/theme.css All colour values — one CSS block per palette (:root = Indigo, [data-palette="alt"] = Castleton Green, plus a few commented-out experimental greens). style.css imports it first.
public/src/js/theme-config.js All colour behaviour — which palettes exist, the default, and whether the switch is shown.

Edit the window.SITE_THEME object in theme-config.js:

window.SITE_THEME = {
  palettes: [
    { key: 'green',  label: 'Castleton Green', attr: 'alt'  },
    { key: 'indigo', label: 'Indigo',          attr: null   }, // null = :root default
  ],
  defaultPalette: 'green',     // shown to first-time visitors
  enablePaletteToggle: true,   // false hides the navbar swatch entirely
};

To add a palette: copy a block in theme.css (e.g. [data-palette="alt"]), rename the selector, tweak the tokens, then add a matching { key, label, attr } entry to the list above. To lock the site to one colour, set enablePaletteToggle: false.

Deployment

Deployed on Render (Static Site) using render.yml. Render is configured to watch the main branch — every merge to main triggers an automatic deploy with no manual steps needed.

Local

Use npm run dev for local development. Be aware that start is only used if you run a Node server; Render static sites don't need it but it’s handy for testing.

{
  "scripts": {
    "dev": "npx serve public -l 3000",
    "start": "npx serve -s public -l $PORT"
  }
}

Headers & Caching

Deployment file render.yml sets security + caching headers:

Security:

  • X-Frame-Options: DENY
  • X-Content-Type-Options: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Strict-Transport-Security: max-age=31536000; includeSubDomains

Caching:

  • Short cache for *.html
  • Long cache for /src/** and /assets/**

Domains

Custom Domain:

  • For example, www.organokov.com→ CNAME to your Render subdomain
  • organokov.com → A → YOUR_IP (or ALIAS/ANAME to the Render subdomain)
  • TLS certificates are issued automatically by Render.

SEO

Basic SEO setup, please extend by your wish.

  • Each page includes a <link rel="canonical" …> tag (e.g., /about).
  • public/sitemap.xml lists canonical, pretty URLs.
  • robots.txt points to the sitemap.
  • 404.html should include <meta name="robots" content="noindex">.

About

Portfolio

Resources

Contributing

Stars

3 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages