Skip to content
View ninadpathak's full-sized avatar

Block or report ninadpathak

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.

Content in all repositories owned by your account will be closed.
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
ninadpathak/README.md

ninadpathak.com

Personal portfolio and blog. Built with a custom Python static site generator — no frameworks, no CMS, ~4 dependencies.

Stack

  • Generator: build.py — single Python script
  • Templates: Jinja2
  • Markdown: python-markdown + pygments for syntax highlighting
  • CSS: Vanilla, no build step
  • JS: Vanilla, no frameworks
  • Hosting: Cloudflare Pages
  • Serverless tools: Cloudflare Pages Functions

Setup

pip install -r requirements.txt
python3 build.py

Output goes to output/. That's the folder to deploy.

Every build runs seo_audit.py before it exits. Deployment stops if a generated page has a broken internal link, missing metadata, duplicate canonical, invalid JSON-LD, missing sitemap entry, missing llms.txt target, or malformed Cloudflare redirect. It also fails on internal links that would take a redirect hop (a slashless page path or a redirected URL), redirect chains, redirects to missing or live pages, and a redirect source without its bare or trailing-slash twin, since Cloudflare matches sources exactly.

To preview locally:

python3 build.py --serve
# → http://localhost:8000

Writing blog posts

Write in Obsidian (or any editor). Add frontmatter:

---
title: "Your Post Title"
date: 2026-03-08
description: "One sentence summary."
tags: [ai, devtools]
status: published
---

Post content here...

Set obsidian_path in config.toml to your Obsidian folder. Posts with status: published are included on the next build.

If obsidian_path is empty, posts are read from content/posts/.

Every prose paragraph is limited to two sentences. The build stops when a published post exceeds that limit; headings, lists, tables, blockquotes, and code are excluded.

Articles are written as a personal blog, not anonymous SEO copy. Use first person when it adds a real opinion, decision, or experience, but never to satisfy a numeric rule or repeat what the section heading already says.

The voice should feel relaxed and spoken, while the technical claims stay exact. Use natural connectors such as "if you look at," "so," "which means," and "the thing is" when they help one thought lead into the next. Context can carry the subject; do not rewrite every sentence as a standalone claim or force specificity where a pronoun would sound more natural.

Open with tension, not background. Challenge a decision the reader has already made, show the cost of getting it wrong, or give them a test they may be uncomfortable running. Avoid generic scene-setting that could introduce any article in the category.

Keep the opening short: one concrete moment from the reader’s work, then one paragraph that names the problem and hands them the article’s promise. The detail belongs in the body once the reader has decided to continue.

Use only details the reader can reasonably recognize across teams. Do not invent page counts, dates, folder names, navigation labels, release cadences, or other specifics to make an opening feel vivid.

Do not build paragraphs around a contrast formula such as "not X, but Y" or "rather than X, do Y." Stay with the situation, name what happens next, and let the reader follow the reasoning without a rhetorical reversal.

Project structure

build.py              # The generator
config.toml           # Site config + Obsidian path
requirements.txt

templates/            # Jinja2 templates
  base.html
  index.html
  blog_list.html
  post.html
  work.html
  work_single.html
  portfolio.html
  about.html
  contact.html

static/
  css/main.css
  js/main.js

functions/
  api/discover-site.js # Sitemap and page metadata scanner

content/
  about.md
  portfolio.yaml
  work/              # Case study markdown files
  posts/             # Blog posts (fallback if no Obsidian path)

output/              # Generated site (gitignored)

Design system

The shared design primitives live in :root at the top of static/css/main.css. Use the existing color, typography, spacing, radius, shadow, and focus tokens instead of adding raw values to a component. Reuse base classes such as .container, .prose-container, .label, .btn, .section-row, and .arrow-link before creating a new structural class.

Keep page-specific CSS out of the global bundle. The linter owns linter.css, while article syntax highlighting and visual-embed styles are included only when the rendered article needs them. Static decision trees use the reusable classes in flowcharts.css; the build includes that stylesheet only for articles containing a .flowchart.

Tokens cover color, font size, weight (--weight-*), line height (--leading-*), tracking (--tracking-*), spacing (--space-*), rules (--rule, --rule-strong, --rule-accent, --rule-accent-thin), durations (--t-*), and page stacking layers (--z-*). A handful of single-use values stay raw because snapping them would visibly move a component; tests/test_design_tokens.py names each one, so a new one-off fails the build.

Templates, articles, and site scripts never set style=. Use a class, or a state class toggled from JavaScript such as .scroll-locked, .is-active, or .copy-failed. There are three exceptions. The critical <style> in base.html paints the themed background before main.css loads. The reading-progress bar sets its per-scroll width inline, and post.html sets an embedded visual's height from the height the visual reports.

The iframe visuals in static/visuals/ are standalone documents that cannot inherit main.css. They link /static/css/visual-embed.css, which build.py generates from the main.css palette, so a color is edited in one place. Visuals follow the reader's OS color scheme through prefers-color-scheme. Each visual keeps only its own geometry, chart colors, and any token it deliberately overrides. The Three.js scenes with fixed artwork colors do not link the shared palette.

Visual markup has no style= either. A utility used by two or more visuals lives once in static/css/visual-utilities.css. That file loads before each visual's own <style>, so a utility repeats its class until it outranks every rule that matches the same element. A utility used by one visual stays in that visual. Script state changes toggle classes that sit last in the visual's sheet. The listed visuals in tests/test_design_tokens.py still write computed geometry at runtime, such as a data-driven bar width or a tooltip position. tests/test_script_syntax.py parses every inline visual script and every file in static/js/ with node --check.

Deploying to Cloudflare Pages

Set the build command to python3 build.py and the output directory to output.

Page dates

An article's sitemap lastmod, its dateModified and its visible "Updated" date come from content/lastmod.json, not from frontmatter. After editing a post, its figure sources in content/diagrams/<slug>/, or its video, run this before you commit, and commit the manifest with the change:

python3 lastmod.py

Cloudflare Pages clones shallow, so the deploy build can't read dates from git. The manifest records each page's date with a hash of what its sources say, ignoring whitespace, line endings and frontmatter a reader never sees. If a push to main skips the refresh, as a Hermes publish does, the "Refresh page dates" workflow runs it with full history and commits the manifest as github-actions[bot]. Until then the build uses frontmatter dates for that page and CI shows a warning. Frontmatter date and updated still work as a floor: a page is never dated earlier than either.

Or push manually:

python3 build.py
# deploy output/ to Cloudflare

Pinned Loading

  1. Python-Projects Python-Projects Public

    Some basic python projects

    Python

  2. Data-Mapping Data-Mapping Public

    Simple code examples which demonstrate matplotlib and other libraries (which I would come across) for visualizing data

    Python

  3. Machine_Learning_Beginner Machine_Learning_Beginner Public

    Just some random machine learning code to get a feel of how things work

    Python

  4. Bulk-Organizer Bulk-Organizer Public

    Organize files where filenames contain the date in yyyymmdd format

    Python

  5. CLI-Address-Book CLI-Address-Book Public

    A basic CLI addressbook program that stores data in a file using pickle in python. You may find it helpful in case you want to see the usage of "pickle"

    Python