Everything in your site that isn't hand-coded HTML is now generated by Python scripts that run automatically on every commit.
your repo (dipidup/Missing_Palette on GitHub)
┌────────────────────────────────────────────┐
│ │
┌──────────┴──────────┐ ┌──────────────────────┐ │
│ img/gallery/*.jpg │ │ Google Sheets │ │
│ (you upload) │───►│ (gallery rows) │────┤
└─────────────────────┘ └──────────────────────┘ │
│
┌─────────────────────┐ │
│ posts/*.md │────────────────────────────────┤
│ (you write) │ │
└─────────────────────┘ │
│
┌─────────────────────┐ │
│ img/blog/*.jpg │────────────────────────────────┤
│ (you upload) │ │
└─────────────────────┘ │
▼
┌──────────────────────┐
│ GitHub Actions │
│ runs all 3 builders │
└──────────┬───────────┘
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ gallery.html │ │ blog/<slug>.html │ │ blog.html │
│ (cards regen'd) │ │ (one per post) │ │ (cards regen'd) │
└────────────────────┘ └────────────────────┘ └────────────────────┘
posts/
Ghat.md
hostel4.html
watercolor-portrait-guide.md
(your future posts go here as .md files)
scripts/
build_post.py - converts posts/*.md -> blog/*.html
build_blog.py - regenerates the cards in blog.html
build_gallery.py - regenerates the gallery grid in gallery.html
sync_layout.py - propagates <nav> + <footer> from index.html
into every other page (run after editing nav/footer)
convert_gallery_images.py - emits .webp + .avif siblings for img/gallery/*.jpg
(run after dropping in new gallery images)
cleanup_pass.py - one-off site-wide content sanitizer
_add_blog_markers.py - one-time setup (adds AUTO:POSTS markers)
_add_gallery_markers.py - one-time setup (adds AUTO:GALLERY markers)
_convert_existing_html_to_md.py - one-time setup (already run for you)
.github/workflows/
build-site.yml - runs all builds on every commit + hourly
README.md - this file
You also get the rebuilt:
blog/Ghat.html
blog/hostel4.html
blog/watercolor-portrait-guide.html
blog.html (with AUTO:POSTS markers + freshly generated cards)
These should be committed alongside the .md files - they're the deployed artifacts users see, and the build pipeline keeps regenerating them.
In dipidup/Missing_Palette, place each file at this path:
posts/Ghat.md -> repo root /posts/
posts/hostel4.md -> repo root /posts/
posts/watercolor-portrait-guide.md -> repo root /posts/
scripts/build_post.py -> repo root /scripts/
scripts/build_blog.py -> repo root /scripts/
scripts/_add_blog_markers.py -> repo root /scripts/
.github/workflows/build-site.yml -> create the folders
blog.html -> replace the existing one (has markers now)
blog/Ghat.html -> replace
blog/hostel4.html -> replace
blog/watercolor-portrait-guide.html -> replace
If you already have .github/workflows/build-gallery.yml from the older setup,
DELETE IT - the new build-site.yml does everything (gallery + blog).
In your repo root, create an empty img/blog/ folder. Future blog post images
will live here. (Existing posts use images from img/gallery/, which is fine.)
git add posts scripts .github blog blog.html
git commit -m "Add blog build system"
git pushGitHub Actions will run the workflow once. Check the Actions tab to confirm it completes green. If anything fails, the log will tell you what.
You write Markdown, GitHub does the rest.
- Make a new file
posts/your-slug.md(e.g.posts/monsoon-2026.md). - Copy the frontmatter block from one of the existing .md files and edit it.
- Drop any images into
img/blog/(or reference existing ones inimg/gallery/). - Write the post body in Markdown.
git add posts/your-slug.md img/blog/* && git commit -m "New post" && git push- Wait ~60 seconds. The post is live at
https://missingpalette.com/blog/your-slug.htmland a card automatically appears on the journal index.
---
title: "Your title (with *emphasis* if you want)"
slug: "your-slug"
subtitle: "One-line subtitle below the h1."
category: "Memoir"
description: "One sentence. Used for SEO and social previews."
hero_image: "/img/blog/hero.jpg"
hero_caption: "Caption shown under the hero."
read_time: "6 min read"
status: "published"
---
This is the lead paragraph. It renders larger and italic.
A regular paragraph. Use **bold** and *italic* freely.
Inline links: [text](/gallery.html), [external](https://example.com).
## A section heading with *emphasis*
H2 emphasis renders crimson italic.
### A sub-heading
H3 for sub-points.
> A pull-quote. The single sentence you want a reader to remember.
- **Bold lead** - the rest is the supporting text
- **Same shape** - keep bullets parallel

---
Final paragraph. Pull the thread together.Inline:
The painting is called <span class="hi">भण्डि बाबा</span>.Inside a figure caption (use raw HTML for the figure if Hindi is needed in the caption, since markdown image syntax doesn't preserve span classes):
<figure class="post-image">
<img src="/img/blog/photo.jpg" alt="">
<figcaption class="post-image-caption">
<span class="hi">बनारसी सुबह</span> · Subah - dawn at the ghat.
</figcaption>
</figure>Set status: "draft" in frontmatter to skip a post in the build. The .md file
stays in the repo but no HTML is generated and no card appears in the index.
What you can do from phone (GitHub mobile app or any web editor):
- Toggle a post's
statusfromdrafttopublishedor back - Change a post's
hero_image(point to a different image already inimg/) - Reorder posts on the index (rename slugs, OR switch to sheet-driven mode - see "Sheet mode" below)
- Edit small typos in any post body
What is painful from phone (don't try this):
- Writing a new long-form post (markdown editor on phone is not fun)
- Uploading images that need to live in img/blog/
Default mode reads posts/*.md frontmatter for ordering (alphabetical by slug).
If you want to control ORDER from a phone via Google Sheets, switch to sheet mode. Useful for promoting a recent post to the top, or hiding a post without deleting the file.
- Add a "Posts" tab to your existing gallery sheet with these columns:
| slug | order | featured |
|---|---|---|
| Ghat | 1 | yes |
| hostel4 | 2 | no |
| watercolor-portrait-guide | 3 | no |
-
Publish the sheet to web as CSV. Get the URL of the Posts tab specifically (it'll have a
gid=parameter for the tab). -
In your repo, edit
.github/workflows/build-site.yml. In the "Build blog index" step, change to:
- name: Build blog index (cards in blog.html)
env:
BLOG_SOURCE: sheet
BLOG_SHEET_CSV_URL: https://docs.google.com/spreadsheets/d/YOUR_SHEET_ID/export?format=csv&gid=YOUR_TAB_GID
run: python3 scripts/build_blog.py- Commit. Now the sheet is the source of truth for blog index order.
In sheet mode, an unlisted slug = hidden. Adding a row to the sheet for a slug
that has a posts/<slug>.md file = visible in the index.
"Build failed in GitHub Actions" Open the Actions tab on github.com, click the failed run, find the red step. Most common cause: a typo in YAML frontmatter (forgot a closing quote, etc). The error message will tell you which file.
"My post isn't showing up on the index"
- Did you set
status: "published"(not "draft")? - Did
posts/your-slug.mdget committed? - Did the action complete successfully?
- Check
blog.htmlafter the build: did your card get added between the<!-- AUTO:POSTS-START -->and<!-- AUTO:POSTS-END -->markers?
"Image not loading on the post"
- Check the path. If your image lives at
img/blog/foo.jpg, the markdown reference should be- leading slash matters. - Spaces in filenames work but get URL-encoded automatically.
"I want to edit the post layout / template"
Edit POST_TEMPLATE.html. The build script uses it as the wrapper. Run
python3 scripts/build_post.py to rebuild every post against the new template.
"My em-dash and the AI tells came back in the rebuilt HTML" The build script uses POST_TEMPLATE.html as the wrapper. If POST_TEMPLATE has LLM-template phrases in it, every post inherits them. Edit POST_TEMPLATE.html once, rerun build_post.py, all posts update.
Top-level pages (hand-coded HTML, edit directly):
index.html - homepage (hero + about + commissions + contact)
gallery.html - full artwork grid (auto-regenerated; see AUTO:GALLERY markers)
blog.html - journal index (auto-regenerated; see AUTO:POSTS markers)
events.html - workshops & private sessions (Plan an Event)
exhibitions.html - exhibitions, press, awards
products.html - art materials I recommend (Amazon affiliate links)
register.html - class registration (embedded form)
legal.html - terms, copyright, DMCA
privacy.html - privacy policy
Blog posts live in blog/ and are generated from posts/*.md. Don't hand-edit
the blog/*.html files — re-run scripts/build_post.py instead.
POST_TEMPLATE.html is the wrapper used by the build script; edit it to change
the look of every post at once.
The whole site shares three CSS files (missingpalette.css, _v2, _v4) and
one JS file (missingpalette.js).
index.html is the source of truth for the <nav> and <footer> blocks.
After editing either one, run python scripts/sync_layout.py to propagate
the change into every other page in one go. Add --check to dry-run (exits
non-zero if anything is out of sync — useful in CI).
404.html is a stripped-down page (no footer) so GitHub Pages can serve it
on any unknown URL. It's intentionally excluded from sync_layout.py.
Serve the site over http://localhost so root-relative paths (/img/...,
/css/...) resolve. From the repo root:
# Python 3 (no install needed)
python -m http.server 8000
# then visit http://localhost:8000The build scripts need Python 3 and a couple of small libs:
pip install pyyaml markdown
pip install Pillow # for scripts/convert_gallery_images.py
pip install pillow-avif-plugin # optional, enables AVIF outputAfter dropping new images into img/gallery/, run:
python scripts/convert_gallery_images.pyThis generates .webp and .avif siblings next to each .jpg. The
<picture> tags in gallery.html automatically serve the modern format to
browsers that support it (most do), and fall back to the JPEG on the rest.
robots.txt blocks the major AI training crawlers (GPTBot, ClaudeBot, CCBot,
Google-Extended, PerplexityBot, Bytespider, Diffbot, FacebookBot, etc.). If you
notice a new one in your access logs, add it as another User-agent: <name> /
Disallow: / block.
sitemap.xml is hand-maintained for top-level pages and includes per-page
<image:image> annotations for SEO. When you add a new gallery image, update
the corresponding entries here too — there is no automation for the sitemap
yet.
Archive 01/holds the previous version of the site. Don't edit; it's kept for reference only and is excluded fromrobots.txt..DS_Storefiles (macOS metadata) get re-created automatically on macOS. Add them to.gitignoreif you haven't already.thumb.jpgis the canonical social/open-graph image referenced from every page. Replace it (keep the same filename) to update social previews site-wide.