> ## Content Index
> Fetch the complete content index at: https://www.liamdarmody.ai/llms.txt
> Use this file to discover other available public pages before exploring further.

# Liam's AI Experiment 004: How to Build a Page That Updates Itself
- URL: https://www.liamdarmody.ai/ai-experiment-004/
- Published: 2026-09-21T15:58:05.000Z
- Updated: 2026-09-21T15:58:05.000Z
- Description: I built a page that collects all 21 of my YouTube Shorts as a thumbnail grid, then set up a monthly check that rebuilds the grid when new Shorts appear. The page maintains itself.
- Author: Liam Darmody
- Tags: AI Lab

I recently decided to [start a YouTube channel](https://www.youtube.com/@LIAM.DARMODY?ref=liamdarmody.ai) (at 43-years-old!) because through my work at [Brandi AI](https://mybrandi.ai/?ref=liamdarmody.ai), I've seen firsthand how authoritative YouTube is within AI search. Next to LinkedIn, [YouTube sits at the very top of most top cited lists](https://searchengineland.com/youtube-ai-search-citations-data-462830?ref=liamdarmody.ai). 

![](https://storage.ghost.io/c/4c/2e/4c2ec722-2bd5-4a69-ad93-121c6c1c3e73/content/images/2026/09/Screenshot-2026-09-21-at-11.13.07---AM.png)

## The Problem I'm Trying to Solve

I post YouTube Shorts and they vanish into the feed. Twenty-three of them now, no single place to point people. So this week's experiment: a page on my site that collects every Short as a thumbnail grid, and then keeps itself updated without me.

The page is live at liamdarmody.ai/shorts. Every Short, one grid, each tile linking straight to YouTube.

## How it works

I used Meta's new Muse AI Assistant to help me configure this by making a very simple request: 

> *Is there a way to add my YouTube shorts as a feed on my site?*

Of course there is, Shamus (my muse) said. 

It then generated a small Python script to ask YouTube for everything on my channel's Shorts tab. No API key, just a command-line tool called yt-dlp reading the public page. The script renders the grid as a single HTML snippet: thumbnail, title, link, repeat. Thumbnails come from YouTube's image server, so there's nothing to host.

That snippet gets pasted into an HTML card on the Ghost page. Once a week, on Monday morning, the script runs again. If it finds new Shorts, the grid rebuilds and the page gets refreshed. If nothing's new, nothing happens.

## If at First You Don't Succeed, Try Try Again

The first automated run failed and wiped the page clean. The fetcher couldn't reach YouTube through a network proxy, returned zero Shorts, and the script happily wrote an empty grid over the good one. The fix was a three-line guard: 

> "if the fetch comes back empty but we had data before, abort and keep the last good grid. Never let a failed check destroy a working page."

The last piece works too: the refresh now pushes straight to the live page with zero involvement from me. The scheduled check finds the new Shorts, rebuilds the grid, and a logged-in browser session swaps the HTML card on the Ghost page and verifies the card count. Most website pages start rotting the day you publish them. This one maintains itself.

![](https://storage.ghost.io/c/4c/2e/4c2ec722-2bd5-4a69-ad93-121c6c1c3e73/content/images/2026/09/Screenshot-2026-09-21-at-11.46.20---AM.png)

## Build your own

Want one for your channel? Here's the whole recipe.

**You need:** Python 3, yt-dlp (`pip install yt-dlp`), and a site that accepts raw HTML. One note: the weekly run needs a computer that's awake when the schedule fires. A laptop that sleeps through Monday at 9 AM will skip the run.

**Step 1: the script.** Save this as `build_shorts_grid.py`, replacing `@yourhandle` with your channel handle:

#!/usr/bin/env python3  
"""Fetch a channel's Shorts and render an HTML grid. No API key needed."""  
import html, json, subprocess, sys  
from pathlib import Path  
  
HANDLE = "@yourhandle" # <-- change this  
OUT = Path("shorts-grid.html")  
STATE = Path("shorts-seen.json")  
  
def fetch\_shorts():  
 out = subprocess.run(  
 \["yt-dlp", "--flat-playlist", "--print", "%(id)s\\t%(title)s",  
 f"https://www.youtube.com/{HANDLE}/shorts"\],  
 capture\_output=True, text=True, timeout=180)  
 shorts = \[\]  
 for line in out.stdout.splitlines():  
 if "\\t" not in line:  
 continue  
 vid, title = line.strip().split("\\t", 1)  
 title = title.strip()  
 if title in ("\[Private video\]", "\[Deleted video\]"):  
 continue  
 shorts.append({"id": vid, "title": title})  
 return shorts  
  
def render(shorts):  
 cards = \[\]  
 for s in shorts:  
 title = html.escape(s\["title"\])  
 cards.append(  
 f'<a class="ytg-short" href="https://www.youtube.com/shorts/{s\["id"\]}" target="\_blank" rel="noopener">'  
 f'<span class="ytg-thumb"><img src="https://i.ytimg.com/vi/{s\["id"\]}/hqdefault.jpg" alt="{title}" loading="lazy"></span>'  
 f'<span class="ytg-cap">{title}</span></a>')  
 return f"""<!-- YouTube Shorts grid: {len(shorts)} shorts -->  
<style>  
.ytg-grid {{ display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:18px; }}  
.ytg-short {{ display:block; text-decoration:none; color:inherit; }}  
.ytg-thumb {{ display:block; aspect-ratio:9/16; overflow:hidden; border-radius:12px; background:#111; }}  
.ytg-thumb img {{ width:100%; height:100%; object-fit:cover; display:block; }}  
.ytg-cap {{ display:block; margin-top:8px; font-size:14px; font-weight:600; line-height:1.35; }}  
</style>  
<div class="ytg-grid">  
{chr(10).join(cards)}  
</div>"""  
  
def main():  
 prev = json.loads(STATE.read\_text())\["shorts"\] if STATE.exists() else \[\]  
 shorts = fetch\_shorts()  
 print(f"Found {len(shorts)} shorts")  
 if not shorts and prev:  
 sys.exit("Fetch failed; keeping the last good grid.")  
 new = \[s for s in shorts if s\["id"\] not in {p\["id"\] for p in prev}\]  
 if new:  
 print(f"{len(new)} new since last run:")  
 \[print(" +", s\["title"\]\[:60\]) for s in new\]  
 OUT.write\_text(render(shorts))  
 STATE.write\_text(json.dumps({"shorts": shorts}, indent=1))  
  
if \_\_name\_\_ == "\_\_main\_\_":  
 main()

**Step 2: put it on your site.** Run `python3 build_shorts_grid.py`, open `shorts-grid.html`, and paste the whole thing into a Ghost HTML card (or any raw-HTML block). Publish.

**Step 3: schedule it.** Add a cron job to regenerate the file weekly:

0 9 \* \* 1 cd \~/shorts && python3 build\_shorts\_grid.py

Every Monday at 9 AM it rebuilds. The script prints how many Shorts are new, so you know whether the page needs a fresh paste. The whole refresh takes about 60 seconds of your time.

**Going fully hands-off (optional):** create a custom integration in Ghost (Settings, Integrations) to get an Admin API key, then extend the script to push the new HTML into your page's HTML card through the API. That's the one fiddly part, since you have to locate the card inside the page's content JSON.

**Worth borrowing:** the pattern isn't about Shorts. Anything that accumulates, podcast episodes, press mentions, kind words, can live on a page like this. Build it once, schedule the check, and stop remembering to update it.

My goal heading into the AI Era is to ensure that anything I publish on rented land also gets published on my owned domain at liamdarmody.ai. Setting up systems like this to fetch from other platforms I post on makes that a lot easier to do. 

And don't let they python scripts above intimidate you... I didn't know how to do ANY of this kind of stuff and all I did to learn was talk to AI about what I was trying to do and the tools I was using (Shorts + Ghost.org site) and let AI guide me the rest of the way. 

If I can do it, you can, too! 

Hi5 & Have a great week, friends!  
LD  
🌶️

---

Want to try out Muse? Follow this link [https://muse.ai/join](https://muse.ai/join?ref=liamdarmody.ai) and redeem my code (7RQG82) in Settings within 48 hours of joining and we'll both get 1 billion Muse tokens.

Want to see how AI sees your brand? Get your complimentary [Brandi AI Visibility Audit](https://mybrandi.ai/free-ai-visibility-audit-for-your-website/?ref=liamdarmody.ai) today!