Turns a Notion Content Calendar entry into a branded hush_hush_ltd post
preview (PNG), and publishes it to a per-client calendar page your clients
can click through, no Canva, no manual upload, triggered from n8n.
One shared repo serves every client. Adding a client is just a new
client_slug in the data n8n sends, nothing to duplicate here.
templates/ the HTML preview template + the client calendar-page template
scripts/ the render script (HTML -> PNG, updates the client's page)
docs/ what GitHub Pages actually serves publicly. Everything else
in this repo (scripts, templates, this README) stays private.
Each client ends up with docs/<client_slug>/, containing their rendered
previews (previews/*.png), a small JSON manifest of their posts
(posts.json), and their calendar page (index.html).
Create a private GitHub repo (e.g. hush-hush-render) under your
account and push this folder to it.
git remote add origin https://github.com/<your-username>/hush-hush-render.git
git branch -M main
git add .
git commit -m "Initial commit"
git push -u origin main
Repo Settings → Pages → Source: Deploy from a branch → Branch: main, folder: /docs. Save.
Pick a subdomain, e.g. clients.hushhushltd.com.
clients → <your-username>.github.io.docs/CNAME file automatically once the domain is verified (or you can add it yourself, one line, just the domain).DNS can take a little while to propagate. Once it’s live, a client’s page
is at https://clients.hushhushltd.com/<client_slug>/.
n8n needs a token to trigger this repo’s workflow. Settings → Developer settings → Personal access tokens → Fine-grained token, scoped to just this repo, with Contents: read and write and Actions: read and write permissions. Store it as a credential in n8n (HTTP Header Auth, or n8n’s built-in GitHub credential type both work).
Trigger a render — HTTP Request node, POST:
https://api.github.com/repos/<your-username>/hush-hush-render/actions/workflows/render.yml/dispatches
Body:
{
"ref": "main",
"inputs": {
"post_data_json": "{...single JSON string, see field mapping below...}"
}
}
Wait for it to finish — poll
GET /repos/<your-username>/hush-hush-render/actions/runs?event=workflow_dispatch
until the newest run’s status is completed (an n8n Wait node + loop, a
minute apart, works fine, most renders finish in well under a minute).
Write the link back to Notion — once done, the client’s page is simply:
https://clients.hushhushltd.com/<client_slug>/
Write that into the post’s Client Preview property in Notion. No need to
fetch or download anything from GitHub, the image is already published.
post_data_json)A post is one JSON object with a slides array, one entry per photo. A
single photo/video post is just a one-item array; a carousel is the same
shape with more items, each one gets its own rendered PNG, and the client
page groups them into one calendar entry they can swipe through (with
per-slide captions), instead of showing up as separate days.
| Notion property | post_data_json field |
notes |
|---|---|---|
| Page ID | post_id |
unique per post |
| Client (relation) | client_slug |
url-safe, e.g. pelos-deli, stays consistent per client, it’s the folder name + URL path |
| Client (relation) | client_name |
display name, e.g. Pelo's Deli |
| Platform | channel |
|
| Scheduled Date | date_iso |
as Notion gives it, YYYY-MM-DD, drives the calendar grid |
| Scheduled Date | scheduled_date |
human-readable, format however you like |
| Content Type | content_type |
Carousel | Video | Photo |
| Content Pillar | theme |
|
| (today’s date) | prep_date |
|
per slide, inside slides[]: |
||
| Creative Asset (one file URL per slide) | slides[i].photo_url |
re-fetch these in the same n8n run, Notion file URLs expire after about an hour |
| Caption/Copy (split per slide) | slides[i].caption |
this client uses Instagram’s per-slide caption feature, so each slide’s caption is its own string, not one shared caption |
| Clip URL | slides[i].video_link |
only on a video slide |
asset_index/asset_total (“(asset N of X)”) are computed automatically
from each slide’s position, nothing to pass in for that.
npm install
POST_DATA_JSON='{
"post_id": "test-1",
"client_slug": "pelos-deli",
"client_name": "Pelo'\''s Deli",
"channel": "Instagram",
"date_iso": "2026-09-03",
"scheduled_date": "Sept 3, 2026",
"content_type": "Carousel",
"theme": "Fall Cocktail Menu",
"prep_date": "Sept 3, 2026",
"slides": [
{ "photo_url": "https://example.com/sunspill.jpg", "caption": "1/8 — Sun Spill\nOur fall cocktail menu is here..." },
{ "photo_url": "https://example.com/nosaints.jpg", "caption": "2/8 — No Saints\n..." }
]
}' npm run render
open docs/pelos-deli/index.html
Nothing to set up. The first post you render for a new client_slug
creates docs/<client_slug>/ automatically. Send them
https://clients.hushhushltd.com/<client_slug>/, that’s their whole portal.
Client pages are unlisted, not password-protected: robots.txt blocks
indexing and noindex meta tags are on every page, but anyone with the
exact URL can view it. Don’t publish a directory of client links anywhere
public. If you outgrow this later, a free layer like Cloudflare Access can
add real login in front of GitHub Pages without rebuilding anything here.