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.

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.

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.

Deploying to Cloudflare Pages

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

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