Initial commit - but way too late.
Some checks failed
ci / site (push) Has been cancelled

This commit is contained in:
2026-02-10 00:22:18 -05:00
commit af112a713c
173 changed files with 27667 additions and 0 deletions

15
site/.env.example Normal file
View File

@@ -0,0 +1,15 @@
# Public site base URL used for canonical URLs (no trailing slash)
PUBLIC_SITE_URL=https://example.com
# Umami (optional). If not set, analytics is disabled.
PUBLIC_UMAMI_SCRIPT_URL=https://analytics.example.com/script.js
PUBLIC_UMAMI_WEBSITE_ID=00000000-0000-0000-0000-000000000000
# Content ingestion configuration (used by scripts)
YOUTUBE_CHANNEL_ID=UCxxxxxxxxxxxxxxxxxxxxxx
YOUTUBE_API_KEY=
PODCAST_RSS_URL=https://example.com/podcast.rss
# Instagram embed-first list (JSON file containing {"postUrls":[...]})
INSTAGRAM_POST_URLS_FILE=content/instagram-posts.json

24
site/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

5
site/.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
node_modules
dist
.astro
package-lock.json

4
site/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
site/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

43
site/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

9
site/astro.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
// @ts-check
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
// https://astro.build/config
export default defineConfig({
site: process.env.PUBLIC_SITE_URL || "http://localhost:4321",
integrations: [sitemap()],
});

1
site/content/cache/.gitkeep vendored Normal file
View File

@@ -0,0 +1 @@

680
site/content/cache/content.json vendored Normal file
View File

@@ -0,0 +1,680 @@
{
"generatedAt": "2026-02-10T04:14:37.396Z",
"items": [
{
"id": "gPGbtfQdaw4",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=gPGbtfQdaw4",
"title": "AI Agents Are Hiring HUMANS Now? RentAHuman.ai Explained",
"publishedAt": "2026-02-08T19:57:08.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/gPGbtfQdaw4/hqdefault.jpg",
"metrics": {
"views": 41
}
},
{
"id": "aesTuu2nS-I",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=aesTuu2nS-I",
"title": "I will not die. Not today!!",
"publishedAt": "2026-02-05T05:53:25.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/aesTuu2nS-I/hqdefault.jpg",
"metrics": {
"views": 147
}
},
{
"id": "9t8cBpZLHUo",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=9t8cBpZLHUo",
"title": "I Cant Believe This Exists: ThePrimeagens Terminal.shop is INSANE",
"publishedAt": "2026-02-05T04:31:18.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/9t8cBpZLHUo/hqdefault.jpg",
"metrics": {
"views": 325
}
},
{
"id": "71S5viSJG20",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=71S5viSJG20",
"title": "Is This Real Life? ✈️ Ultra 4K Flight Over Europes Most Iconic Cities",
"publishedAt": "2026-01-29T13:54:28.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/71S5viSJG20/hqdefault.jpg",
"metrics": {
"views": 49
}
},
{
"id": "SO-tjsB4ZJs",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=SO-tjsB4ZJs",
"title": "16 OSCAR NOMINATIONS?! 🏆 This movie is Next Level.",
"publishedAt": "2026-01-28T06:30:50.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/SO-tjsB4ZJs/hqdefault.jpg",
"metrics": {
"views": 470
}
},
{
"id": "FV30wjF1WQ4",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=FV30wjF1WQ4",
"title": "Can We Survive Winter Storm Fern? ❄️ (Northeast US Live Weather) ✈️ | MSFS 2020 (No Commentary)",
"publishedAt": "2026-01-26T13:12:12.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/FV30wjF1WQ4/hqdefault.jpg",
"metrics": {
"views": 32
}
},
{
"id": "_1-albWBfoc",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=_1-albWBfoc",
"title": "#Psychological #thriller #movie from #Indonesia",
"publishedAt": "2026-01-25T16:55:51.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/_1-albWBfoc/hqdefault.jpg",
"metrics": {
"views": 101
}
},
{
"id": "ts-DWD8F68Q",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=ts-DWD8F68Q",
"title": "Sleepless Skies: Finding Calm Above the Clouds ✈️ | MSFS 2024 (No Commentary)",
"publishedAt": "2026-01-24T18:12:01.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/ts-DWD8F68Q/hqdefault.jpg",
"metrics": {
"views": 35
}
},
{
"id": "nlbkGnznzA8",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=nlbkGnznzA8",
"title": "A #movie that will creep you out! #survival",
"publishedAt": "2026-01-22T02:21:01.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/nlbkGnznzA8/hqdefault.jpg",
"metrics": {
"views": 358
}
},
{
"id": "zR9Ey8DjG5s",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=zR9Ey8DjG5s",
"title": "A Tour of the Worlds Most Iconic Cities 🌍✨✈️ | MSFS 2020",
"publishedAt": "2026-01-20T17:02:14.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/zR9Ey8DjG5s/hqdefault.jpg",
"metrics": {
"views": 24
}
},
{
"id": "oerSPWeIy5k",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=oerSPWeIy5k",
"title": "🎥 Dive into the quirky world of Kumiko",
"publishedAt": "2026-01-20T03:01:02.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/oerSPWeIy5k/hqdefault.jpg",
"metrics": {
"views": 75
}
},
{
"id": "fzw7GUszgdQ",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=fzw7GUszgdQ",
"title": "RUSTY DRIVER RETURNS 🏎️ F1 25 Short Season Practice (Logitech G920)",
"publishedAt": "2026-01-18T17:36:36.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/fzw7GUszgdQ/hqdefault.jpg",
"metrics": {
"views": 54
}
},
{
"id": "dlIADQOfXlQ",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=dlIADQOfXlQ",
"title": "Sleepless Skies: Finding Calm Above the Clouds ✈️ | MSFS 2020 (No Commentary)",
"publishedAt": "2026-01-16T18:00:30.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/dlIADQOfXlQ/hqdefault.jpg",
"metrics": {
"views": 19
}
},
{
"id": "0-AX9KaJUSg",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=0-AX9KaJUSg",
"title": "💸 Your Daily Reset 🧘 Cities: Skylines | Billionaire Paradise Build | Lofi",
"publishedAt": "2026-01-16T01:07:06.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/0-AX9KaJUSg/hqdefault.jpg",
"metrics": {
"views": 12
}
},
{
"id": "xiSka36EF5c",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=xiSka36EF5c",
"title": "Episode 41a (Recap) - US History Podcast CatchUp: From Colonization to the Early Civil Rights Mo...",
"publishedAt": "2026-01-15T16:58:14.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/xiSka36EF5c/hqdefault.jpg",
"metrics": {
"views": 8
}
},
{
"id": "oBXH9VhnZCs",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=oBXH9VhnZCs",
"title": "No Mic, Just Silk: Finding My Way in Silksong | Stress-Busting Stream",
"publishedAt": "2026-01-14T16:17:29.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/oBXH9VhnZCs/hqdefault.jpg",
"metrics": {
"views": 1
}
},
{
"id": "IYxjSZStHJ0",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=IYxjSZStHJ0",
"title": "STRESS BUSTING | Trying out some new games!",
"publishedAt": "2026-01-14T14:22:54.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/IYxjSZStHJ0/hqdefault.jpg",
"metrics": {
"views": 55
}
},
{
"id": "aAufRPAScCE",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=aAufRPAScCE",
"title": "STRESS BUSTING | Trying out some new games!",
"publishedAt": "2026-01-14T01:26:33.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/aAufRPAScCE/hqdefault.jpg",
"metrics": {
"views": 3
}
},
{
"id": "dY2tlGRaUj4",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=dY2tlGRaUj4",
"title": "Lofi Loops & Legends: Flying Iconic US Cities 🇺🇸 | MSFS 2020",
"publishedAt": "2026-01-13T20:42:25.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/dY2tlGRaUj4/hqdefault.jpg",
"metrics": {
"views": 36
}
},
{
"id": "mj5-oTVJ0AU",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=mj5-oTVJ0AU",
"title": "America from Above: Iconic Landmarks & Relaxing Lofi Vibes | MSFS 2020",
"publishedAt": "2026-01-13T16:35:38.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/mj5-oTVJ0AU/hqdefault.jpg",
"metrics": {
"views": 3
}
},
{
"id": "ULtQuR1tsOg",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=ULtQuR1tsOg",
"title": "Lofi Loops & Legends: Flying Iconic US Cities 🇺🇸 | MSFS 2020",
"publishedAt": "2026-01-13T01:56:35.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/ULtQuR1tsOg/hqdefault.jpg",
"metrics": {
"views": 103
}
},
{
"id": "zaQyRCujqk4",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=zaQyRCujqk4",
"title": "No More Red Roads! 🛑 Fixing Yesterdays Traffic Mess in Cities: Skylines",
"publishedAt": "2026-01-12T10:26:54.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/zaQyRCujqk4/hqdefault.jpg",
"metrics": {
"views": 20
}
},
{
"id": "HQWyqb4I0Bo",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=HQWyqb4I0Bo",
"title": "A mind bending movie waiting for you!! #bugonia #emmastone #andromeda",
"publishedAt": "2026-01-11T23:53:54.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/HQWyqb4I0Bo/hqdefault.jpg",
"metrics": {
"views": 1223
}
},
{
"id": "m5OEtszkSyA",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=m5OEtszkSyA",
"title": "Can an Introvert Build the Perfect City? | Cities: Skylines Zen Stream",
"publishedAt": "2026-01-11T22:18:08.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/m5OEtszkSyA/hqdefault.jpg",
"metrics": {
"views": 31
}
},
{
"id": "XsJCIeqFWCY",
"source": "youtube",
"url": "https://www.youtube.com/watch?v=XsJCIeqFWCY",
"title": "🚧 ZERO Traffic Jams in Cities: Skylines? My Impossible Build!",
"publishedAt": "2026-01-11T22:15:03.000Z",
"thumbnailUrl": "https://i.ytimg.com/vi/XsJCIeqFWCY/hqdefault.jpg",
"metrics": {
"views": 5
}
},
{
"id": "2dad79d8-ed54-42a8-91a1-4c12a22e3070",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E43--US-History--Understanding-This-Country--Childrens-Crusade--the-Civil-Rights-Act-of-1964-Turning-Protest-into-Law-e3e3t94",
"title": "E43. US History Understanding This Country | Children's Crusade & the Civil Rights Act of 1964: Turning Protest into Law",
"publishedAt": "2026-01-24T03:17:34.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "90b42ea8-16e9-4374-8043-858c5c04db3f",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E42--US-History--Understanding-This-Country--Civil-Rights-Movement-from-Courtrooms-to-the-Streets-e3doqu8",
"title": "E42. US History Understanding This Country | Civil Rights Movement from Courtrooms to the Streets",
"publishedAt": "2026-01-16T19:59:44.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "9900f1c9-315b-42ba-991f-c31241bd9b55",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Episode-41a-Recap---US-History-Podcast-CatchUp-From-Colonization-to-the-Early-Civil-Rights-Movement-e3dmuli",
"title": "Episode 41a (Recap) - US History Podcast CatchUp: From Colonization to the Early Civil Rights Movement",
"publishedAt": "2026-01-15T16:24:16.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "9002e7f6-8ecf-47e2-9590-ba4adfc5dc6d",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E41--US-History--Understanding-This-Country--Civil-Rights-Beginnings-Brown--Parks--King-e38fi0r",
"title": "E41. US History Understanding This Country | Civil Rights Beginnings: Brown, Parks & King",
"publishedAt": "2025-09-20T03:47:37.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "96e72f96-fb71-4717-9047-120c6e32973a",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E40--US-History--Understanding-This-Country--Prosperity--TV--Rock-n-Roll-e387ogh",
"title": "E40. US History Understanding This Country | Prosperity, TV, Rock n Roll",
"publishedAt": "2025-09-15T02:28:58.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "bb4faa6e-384d-4204-b133-438c5a82aefd",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E39--US-History--Understanding-This-Country--The-Korean-War-Americas-First-Test-of-the-Cold-War-e37t0f5",
"title": "E39. US History Understanding This Country | The Korean War: Americas First Test of the Cold War",
"publishedAt": "2025-09-07T04:58:14.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "2d56a5a0-948d-446f-b05d-f18f4299530f",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E38--US-History--Understanding-This-Country--Victory-to-Cold-War-Tensions-e36tqu6",
"title": "E38. US History Understanding This Country | Victory to Cold War Tensions",
"publishedAt": "2025-08-16T03:52:22.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "14912927-4ea3-4946-948a-dbeeacbd1535",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E37--US-History--Understanding-This-Country--From-D-Day-to-Nagasaki-e36ks7j",
"title": "E37. US History Understanding This Country | From D-Day to Nagasaki",
"publishedAt": "2025-08-09T04:15:45.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "063ab850-90c1-4e42-af44-9a64eaa1bf72",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E36--US-History--Understanding-This-Country--The-World-at-War--Again-How-the-US-stepped-into-WWII-e36bkih",
"title": "E36. US History Understanding This Country | The World at War, Again: How the US stepped into WWII",
"publishedAt": "2025-08-02T03:41:33.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "4d616ef1-46e4-46b5-8fa4-e3e71688d2c0",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E35--US-History--Understanding-This-Country--The-Great-Depression-e361hrl",
"title": "E35. US History Understanding This Country | The Great Depression",
"publishedAt": "2025-07-26T02:32:26.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "2a5efb30-1749-4bcb-b54b-a83878a6015e",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E34--US-History--Understanding-This-Country--The-Roaring-Twenties-e35o9s0",
"title": "E34. US History Understanding This Country | The Roaring Twenties",
"publishedAt": "2025-07-19T03:58:56.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "d41eeed2-96a0-4afe-a8e4-1cf30dec8988",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E33--US-History--Understanding-This-Country--The-Great-War-How-World-War-I-Transformed-America-and-the-World-e35eos4",
"title": "E33. US History Understanding This Country | The Great War: How World War I Transformed America and the World",
"publishedAt": "2025-07-12T04:33:52.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "1fc5bbbf-e649-41b2-9145-85757cbee0a8",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E32--US-History--Understanding-This-Country--American-Muscle-and-Presidents-of-Power-e354i1u",
"title": "E32. US History Understanding This Country | American Muscle and Presidents of Power",
"publishedAt": "2025-07-05T03:30:53.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "9d5a87a4-bdb7-474f-a249-a43ac6d477e6",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E31--US-History--Understanding-This-Country--Expansionism-and-Imperialism-e34r500",
"title": "E31. US History Understanding This Country | Expansionism and Imperialism",
"publishedAt": "2025-06-28T03:30:57.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "6b47cd15-1f10-4175-a8d2-0863777022a7",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E30--US-History--Understanding-This-Country--Progressivism-in-America-e34es36",
"title": "E30. US History Understanding This Country | Progressivism in America",
"publishedAt": "2025-06-19T03:42:14.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "f73008f3-78d6-4644-975f-f5d00d21f404",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E29--US-History--Understanding-This-Country--Immigration-and-New-Cities-e347o88",
"title": "E29. US History Understanding This Country | Immigration and New Cities",
"publishedAt": "2025-06-14T03:58:41.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "c1c4778d-2597-4b8a-86e2-dd2e5adfe526",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E28--US-History--Understanding-This-Country--Second-Industrial-Revolution-and-The-Age-of-Capitalism-e33togi",
"title": "E28. US History Understanding This Country | Second Industrial Revolution and The Age of Capitalism",
"publishedAt": "2025-06-07T03:51:30.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "13359365-5be2-4f7c-ae11-94d412be7561",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E27--US-History--Understanding-This-Country--Clash-of-Cultures-e33josn",
"title": "E27. US History Understanding This Country | Clash of Cultures",
"publishedAt": "2025-05-31T04:14:46.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "f7c449ee-f274-4944-9370-0d0c000a053c",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E26--US-History--Understanding-This-Country--Trains--Bonanzas-and-Cowboys-e339m68",
"title": "E26. US History Understanding This Country | Trains, Bonanzas and Cowboys",
"publishedAt": "2025-05-24T03:30:17.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "b3edb562-64be-4902-a503-f9a93719caf9",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E25--US-History--Understanding-This-Country--Reconstruction-Redefining-Freedom-e32v9a2",
"title": "E25. US History Understanding This Country | Reconstruction: Redefining Freedom",
"publishedAt": "2025-05-17T04:35:58.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "bd1cb52f-29fc-456a-b971-13030def86eb",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E24--US-History--Understanding-This-Country--The-Civil-War-e32l3na",
"title": "E24. US History Understanding This Country | The Civil War",
"publishedAt": "2025-05-10T04:07:23.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "086d2da0-57b4-4410-8637-f076b19bdaf9",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E23--US-History--Understanding-This-Country--Prelude-to-Civil-War-e32al0g",
"title": "E23. US History Understanding This Country | Prelude to Civil War",
"publishedAt": "2025-05-03T03:30:33.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "b22a7a44-8028-4c84-9dd6-31e7a62a6def",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E22--US-History--Understanding-This-Country--The-Age-of-Reform-e3217aj",
"title": "E22. US History Understanding This Country | The Age of Reform",
"publishedAt": "2025-04-26T03:38:18.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "035b8239-d954-42a8-a1a5-d4fce21ff15a",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E21--US-History--Understanding-This-Country--The-Lone-Star-and-the-Borderlands-e31nkn1",
"title": "E21. US History Understanding This Country | The Lone Star and the Borderlands",
"publishedAt": "2025-04-19T03:26:55.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "92f62156-44ed-4757-b46b-7b8d3b0deb00",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E20--US-History--Understanding-This-Country--The-Oregon-Trail-e31eh44",
"title": "E20. US History Understanding This Country | The Oregon Trail",
"publishedAt": "2025-04-12T04:39:58.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "3fef0da4-41a1-4f69-a444-30a846ba6817",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E19--US-History--Understanding-This-Country--The-Wild-West-Journeys-e3139gh",
"title": "E19. US History Understanding This Country | The Wild West Journeys",
"publishedAt": "2025-04-04T23:00:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "564c1115-f9ac-4c18-8e6f-2e805db638e2",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E18--US-History--Understanding-This-Country--From-Corrupt-Bargains-to-the-Trail-of-Tears-e30ql0c",
"title": "E18. US History Understanding This Country | From Corrupt Bargains to the Trail of Tears",
"publishedAt": "2025-03-29T04:10:39.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "3fcbc28c-728f-42cf-83cd-812ba49db367",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E17--US-History--Understanding-This-Country--The-Era-of-Good-Feelings--Nationalism--Industry--Division-in-Early-America-e30fabn",
"title": "E17. US History Understanding This Country | The Era of Good Feelings? Nationalism, Industry & Division in Early America",
"publishedAt": "2025-03-22T01:00:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "dbf4251d-3b67-4e98-be85-d4888aac4357",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E16--US-History--Understanding-This-Country--The-War-of-1812-and-Birth-of-the-Star-Spangled-Banner-e300cll",
"title": "E16. US History Understanding This Country | The War of 1812 and Birth of the Star Spangled Banner",
"publishedAt": "2025-03-15T01:00:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "87f6b19f-b74a-4345-929e-084dec7236b5",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E15--US-History---Understanding-this-Country--Expansion--Power--and-Contradictions-e2vsbq8",
"title": "E15. US History - Understanding this Country | Expansion, Power, and Contradictions",
"publishedAt": "2025-03-08T05:43:34.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "0bb43b9e-c1bc-40be-b642-5b8c0a7977a1",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E14--US-History---Understanding-this-Country--Presidents-setting-precedents-e2vhud8",
"title": "E14. US History - Understanding this Country | Presidents setting precedents",
"publishedAt": "2025-03-01T04:33:33.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "344e00d3-7a6c-4941-81d2-cb3c60f567cb",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E13--US-History---Understanding-this-Country--Inside-the-U-S--Constitution-e2v887v",
"title": "E13. US History - Understanding this Country | Inside the U.S. Constitution",
"publishedAt": "2025-02-23T03:35:43.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "2ffd8004-b419-4260-80de-e3e04518f70d",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E12--US-History---Understanding-this-Country--Finding-the-Balance-e2uubvc",
"title": "E12. US History - Understanding this Country | Finding the Balance",
"publishedAt": "2025-02-16T04:57:29.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "ba1a2bcc-8ba8-40f6-afef-5528a7dae897",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E11--US-History---Understanding-this-Country--Building-a-nation-from-the-scratch-e2uk5j4",
"title": "E11. US History - Understanding this Country | Building a nation from the scratch",
"publishedAt": "2025-02-09T04:42:25.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "0c25ee7a-bc69-4791-995a-07cc9456f980",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E10--US-History---Understanding-this-Country--The-Revolutions-Final-Stand-e2u63b1",
"title": "E10. US History - Understanding this Country | The Revolutions Final Stand",
"publishedAt": "2025-02-01T02:00:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "047a6592-70d3-4ff9-9f2f-f6302c4091f0",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E9--US-History---Understanding-this-Country--From-Declaration-to-Victory-e2u0336",
"title": "E9. US History - Understanding this Country | From Declaration to Victory",
"publishedAt": "2025-01-26T04:01:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "c7484015-9116-4cd2-ae10-95e10b25cfe2",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E8--US-History---Understanding-this-Country--The-Road-to-Independence-e2tl8to",
"title": "E8. US History - Understanding this Country | The Road to Independence",
"publishedAt": "2025-01-18T05:08:28.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "3f3949a2-032d-41ed-bb40-29d5d39ecd63",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E7--US-History---Understanding-this-Country--The-British-Are-Coming-e2ta21g",
"title": "E7. US History - Understanding this Country | The British Are Coming",
"publishedAt": "2025-01-10T04:14:48.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "e415d267-4104-4f40-a6d8-364cd6c36ccb",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E6--US-History---Understanding-this-Country--The-Spark-of-Revolution-e2svhfd",
"title": "E6. US History - Understanding this Country | The Spark of Revolution",
"publishedAt": "2025-01-04T14:12:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "82811636-1652-4d69-8c80-62a3021ecc18",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E5--US-History---Understanding-this-Country--Colonial-America-The-Spark-Before-the-Revolution-e2sqg20",
"title": "E5. US History - Understanding this Country | Colonial America: The Spark Before the Revolution",
"publishedAt": "2024-12-28T03:46:34.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "edae75c4-3a65-4771-8445-ae4a76e9c6c9",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E4--US-History---Understanding-this-Country--Colonial-America-Roots-of-a-New-Nation-e2sdvgp",
"title": "E4. US History - Understanding this Country | Colonial America: Roots of a New Nation",
"publishedAt": "2024-12-20T13:30:00.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "87c0d58b-ac9f-4017-a269-c7f16ff67587",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E3--US-History---Understanding-this-Country--Empires--Exploration--and-the-Birth-of-Colonies-e2s7mt6",
"title": "E3. US History - Understanding this Country | Empires, Exploration, and the Birth of Colonies",
"publishedAt": "2024-12-17T04:34:51.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "d8f3684a-1fab-4ff1-a638-d48564c870a5",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E2--US-History---Understanding-this-Country--1492-and-Beyond-The-Atlantic-World-Unveiled-e2s2s85",
"title": "E2. US History - Understanding this Country | 1492 and Beyond: The Atlantic World Unveiled",
"publishedAt": "2024-12-09T05:11:09.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "7f5c488b-3a7b-4d62-847c-fd5a807577a9",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/E1--US-History---Understanding-this-Country-e2rojk0",
"title": "E1. US History - Understanding this Country",
"publishedAt": "2024-12-02T05:02:25.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg"
},
{
"id": "f25afab7-54b8-4a61-bc0d-143986c7d475",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/One-Person---One-Relation-e1fkfab",
"title": "One Person - One Relation",
"publishedAt": "2022-03-12T19:43:48.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded400/7490178/7490178-1644680237670-d15b3f1acda1b.jpg"
},
{
"id": "95b07a0e-6f13-4822-9d4f-2fa74dd4cff9",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Conclusion-e1dvq6i",
"title": "Dharmaraja.- Conclusion",
"publishedAt": "2022-02-05T19:28:44.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644089311927-91c36cb8383e5.jpg"
},
{
"id": "e72fb42d-1f10-4fe6-842c-811e260a54e7",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-28-e1dvpa3",
"title": "Dharmaraja - Episode 28",
"publishedAt": "2022-02-05T19:02:09.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644087715926-5f708b8948277.jpg"
},
{
"id": "e1c0d4dc-35e7-4cb7-b022-0bebef8c3ed1",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-27-e1dvmg0",
"title": "Dharmaraja - Episode 27",
"publishedAt": "2022-02-05T17:52:55.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644083569325-d4aaaf3e6d72.jpg"
},
{
"id": "677892be-4457-49cb-a2b1-b7a696ea7275",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-26-e1duh8e",
"title": "Dharmaraja - Episode 26",
"publishedAt": "2022-02-04T21:12:06.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644083230812-bbcda04085e26.jpg"
},
{
"id": "8a09387c-bee8-46b9-97f1-828e28dfa09a",
"source": "podcast",
"url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-25-e1dsusg",
"title": "Dharmaraja - Episode 25",
"publishedAt": "2022-02-03T21:23:34.000Z",
"thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1643923406877-eef4729d8dab5.jpg"
}
]
}

View File

@@ -0,0 +1,3 @@
{
"videoIds": []
}

View File

@@ -0,0 +1,3 @@
{
"posts": []
}

6958
site/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
site/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "site",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"fetch-content": "tsx scripts/fetch-content.ts",
"typecheck": "astro check",
"format": "prettier -w .",
"format:check": "prettier -c .",
"test": "vitest run",
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.7.0",
"astro": "^5.17.1",
"rss-parser": "^3.13.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@types/node": "^25.2.2",
"dotenv": "^17.2.4",
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
}

16
site/prettier.config.cjs Normal file
View File

@@ -0,0 +1,16 @@
/** @type {import("prettier").Config} */
module.exports = {
semi: true,
singleQuote: false,
printWidth: 100,
trailingComma: "all",
plugins: ["prettier-plugin-astro"],
overrides: [
{
files: "*.astro",
options: {
parser: "astro",
},
},
],
};

BIN
site/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

9
site/public/favicon.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

5
site/public/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
User-agent: *
Allow: /
Sitemap: /sitemap-index.xml

View File

@@ -0,0 +1,270 @@
:root {
--bg0: #0b1020;
--bg1: #0f1b38;
--fg: #f2f4ff;
--muted: rgba(242, 244, 255, 0.72);
--card: rgba(255, 255, 255, 0.06);
--card2: rgba(255, 255, 255, 0.1);
--stroke: rgba(255, 255, 255, 0.16);
--accent: #ffcd4a;
--accent2: #5ee4ff;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
color: var(--fg);
background:
radial-gradient(1000px 600px at 10% 10%, rgba(94, 228, 255, 0.22), transparent 55%),
radial-gradient(900px 600px at 90% 20%, rgba(255, 205, 74, 0.18), transparent 50%),
radial-gradient(900px 700px at 30% 90%, rgba(140, 88, 255, 0.14), transparent 55%),
linear-gradient(180deg, var(--bg0), var(--bg1));
font-family:
ui-sans-serif,
system-ui,
-apple-system,
Segoe UI,
Roboto,
Ubuntu,
Cantarell,
Noto Sans,
Arial,
"Apple Color Emoji",
"Segoe UI Emoji";
}
a {
color: inherit;
text-decoration: none;
}
.container {
width: min(1100px, calc(100% - 48px));
margin: 0 auto;
padding: 32px 0 72px;
}
.site-header {
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: blur(10px);
background: rgba(10, 14, 28, 0.7);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.brand {
font-weight: 800;
letter-spacing: -0.02em;
font-size: 18px;
}
.nav {
display: flex;
gap: 16px;
font-weight: 600;
color: var(--muted);
}
.nav a:hover {
color: var(--fg);
}
.site-footer {
border-top: 1px solid rgba(255, 255, 255, 0.08);
padding: 20px 24px;
text-align: center;
}
.muted {
color: var(--muted);
}
.hero {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 24px;
align-items: start;
padding: 28px;
border: 1px solid var(--stroke);
background: rgba(255, 255, 255, 0.04);
border-radius: 18px;
}
.hero h1 {
margin: 0 0 10px;
font-size: clamp(34px, 4vw, 52px);
letter-spacing: -0.04em;
line-height: 1.05;
}
.hero p {
margin: 0;
font-size: 16px;
line-height: 1.6;
color: var(--muted);
}
.cta-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 18px;
}
.cta {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 14px;
border-radius: 999px;
border: 1px solid var(--stroke);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
font-weight: 800;
letter-spacing: -0.01em;
}
.cta.primary {
border-color: rgba(255, 205, 74, 0.45);
box-shadow: 0 0 0 3px rgba(255, 205, 74, 0.1);
}
.cta:hover {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06));
}
.section {
margin-top: 28px;
}
.section-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 12px;
}
.section h2 {
margin: 0;
font-size: 20px;
letter-spacing: -0.02em;
}
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.card {
display: grid;
grid-template-columns: 110px 1fr;
gap: 12px;
padding: 12px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.04);
transition:
transform 120ms ease,
background 120ms ease;
}
.card:hover {
transform: translateY(-2px);
background: rgba(255, 255, 255, 0.06);
}
.card-media img {
width: 110px;
height: 70px;
border-radius: 10px;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card-placeholder {
width: 110px;
height: 70px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card-meta {
display: flex;
gap: 10px;
align-items: center;
font-size: 12px;
}
.card-title {
margin: 8px 0 0;
font-size: 14px;
line-height: 1.35;
}
.pill {
font-size: 11px;
font-weight: 800;
padding: 4px 8px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.06);
}
.pill-youtube {
border-color: rgba(255, 74, 74, 0.35);
}
.pill-podcast {
border-color: rgba(94, 228, 255, 0.35);
}
.pill-instagram {
border-color: rgba(255, 205, 74, 0.35);
}
.empty {
padding: 16px;
border-radius: 14px;
border: 1px dashed rgba(255, 255, 255, 0.18);
color: var(--muted);
background: rgba(255, 255, 255, 0.03);
}
.instagram-media {
width: 100% !important;
max-width: 100% !important;
border-radius: 16px !important;
overflow: hidden;
}
@media (max-width: 880px) {
.hero {
grid-template-columns: 1fr;
}
.grid {
grid-template-columns: 1fr;
}
.card {
grid-template-columns: 90px 1fr;
}
.card-media img,
.card-placeholder {
width: 90px;
height: 60px;
}
}

View File

@@ -0,0 +1,101 @@
import "dotenv/config";
import { promises as fs } from "node:fs";
import path from "node:path";
import { getIngestConfigFromEnv } from "../src/lib/config";
import type { ContentCache, ContentItem } from "../src/lib/content/types";
import { readInstagramEmbedPosts } from "../src/lib/ingest/instagram";
import { fetchPodcastRss } from "../src/lib/ingest/podcast";
import { fetchYoutubeViaApi, fetchYoutubeViaRss } from "../src/lib/ingest/youtube";
function log(msg: string) {
// simple, cron-friendly logs
// eslint-disable-next-line no-console
console.log(`[fetch-content] ${msg}`);
}
async function writeAtomic(filePath: string, content: string) {
const tmpPath = `${filePath}.tmp`;
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(tmpPath, content, "utf8");
await fs.rename(tmpPath, filePath);
}
function dedupe(items: ContentItem[]): ContentItem[] {
const seen = new Set<string>();
const out: ContentItem[] = [];
for (const it of items) {
const k = `${it.source}:${it.id}`;
if (seen.has(k)) continue;
seen.add(k);
out.push(it);
}
return out;
}
async function main() {
const cfg = getIngestConfigFromEnv(process.env);
const generatedAt = new Date().toISOString();
const all: ContentItem[] = [];
// YouTube
if (!cfg.youtubeChannelId) {
log("YouTube: skipped (missing YOUTUBE_CHANNEL_ID)");
} else if (cfg.youtubeApiKey) {
try {
const items = await fetchYoutubeViaApi(cfg.youtubeChannelId, cfg.youtubeApiKey, 25);
log(`YouTube: API ok (${items.length} items)`);
all.push(...items);
} catch (e) {
log(`YouTube: API failed (${String(e)}), falling back to RSS`);
const items = await fetchYoutubeViaRss(cfg.youtubeChannelId, 25);
log(`YouTube: RSS ok (${items.length} items)`);
all.push(...items);
}
} else {
const items = await fetchYoutubeViaRss(cfg.youtubeChannelId, 25);
log(`YouTube: RSS ok (${items.length} items)`);
all.push(...items);
}
// Podcast
if (!cfg.podcastRssUrl) {
log("Podcast: skipped (missing PODCAST_RSS_URL)");
} else {
try {
const items = await fetchPodcastRss(cfg.podcastRssUrl, 50);
log(`Podcast: RSS ok (${items.length} items)`);
all.push(...items);
} catch (e) {
log(`Podcast: RSS failed (${String(e)})`);
}
}
// Instagram (embed-first list)
try {
const filePath = path.isAbsolute(cfg.instagramPostUrlsFile)
? cfg.instagramPostUrlsFile
: path.join(process.cwd(), cfg.instagramPostUrlsFile);
const items = await readInstagramEmbedPosts(filePath);
log(`Instagram: embed list ok (${items.length} items)`);
all.push(...items);
} catch (e) {
log(`Instagram: embed list failed (${String(e)})`);
}
const cache: ContentCache = {
generatedAt,
items: dedupe(all),
};
const outPath = path.join(process.cwd(), "content", "cache", "content.json");
await writeAtomic(outPath, JSON.stringify(cache, null, 2));
log(`Wrote cache: ${outPath} (${cache.items.length} total items)`);
}
main().catch((e) => {
log(`fatal: ${String(e)}`);
process.exitCode = 1;
});

View File

@@ -0,0 +1,58 @@
---
import type { ContentItem } from "../lib/content/types";
type Props = {
item: ContentItem;
placement: string;
};
const { item, placement } = Astro.props;
const d = new Date(item.publishedAt);
const dateLabel = Number.isFinite(d.valueOf())
? d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" })
: "";
const targetId = `card.${placement}.${item.source}.${item.id}`;
let domain = "";
try {
domain = new URL(item.url).hostname;
} catch {
domain = "";
}
---
<a
class="card"
href={item.url}
target="_blank"
rel="noopener noreferrer"
data-umami-event="outbound_click"
data-umami-event-target_id={targetId}
data-umami-event-placement={placement}
data-umami-event-target_url={item.url}
data-umami-event-domain={domain || "unknown"}
data-umami-event-source={item.source}
data-umami-event-ui_placement="content_card"
>
<div class="card-media">
{
item.thumbnailUrl ? (
<img src={item.thumbnailUrl} alt="" loading="lazy" />
) : (
<div class="card-placeholder" />
)
}
</div>
<div class="card-body">
<div class="card-meta">
<span class={`pill pill-${item.source}`}>{item.source}</span>
{dateLabel ? <span class="muted">{dateLabel}</span> : null}
{
item.metrics?.views !== undefined ? (
<span class="muted">{item.metrics.views.toLocaleString()} views</span>
) : null
}
</div>
<h3 class="card-title">{item.title}</h3>
</div>
</a>

View File

@@ -0,0 +1,50 @@
---
import { withUtm } from "../lib/url";
type Platform = "youtube" | "instagram" | "podcast";
type Props = {
platform: Platform;
placement: string;
url: string;
label: string;
id?: string;
campaign?: string;
class?: string;
};
const { platform, placement, url, label, id, campaign, class: className } = Astro.props;
const trackedUrl = withUtm(url, {
utm_source: "website",
utm_medium: "cta",
utm_campaign: campaign || "social-acquisition",
utm_content: `${platform}:${placement}`,
});
function slugify(input: string) {
return input
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 40);
}
const fallbackId = `cta.${placement}.${platform}.${slugify(label) || "action"}`;
const targetId = id || fallbackId;
---
<a
class={`cta ${className || ""}`}
href={trackedUrl}
rel="me noopener noreferrer"
target="_blank"
data-umami-event="cta_click"
data-umami-event-target_id={targetId}
data-umami-event-placement={placement}
data-umami-event-target_url={url}
data-umami-event-platform={platform}
data-umami-event-target={url}
>
{label}
</a>

View File

@@ -0,0 +1,33 @@
---
type Props = {
url: string;
placement?: string;
};
const { url, placement } = Astro.props;
const p = placement || "instagram_embed";
const targetId = `ig.${p}.${url}`;
let domain = "";
try {
domain = new URL(url).hostname;
} catch {
domain = "";
}
---
<blockquote class="instagram-media" data-instgrm-permalink={url} data-instgrm-version="14">
<a
href={url}
target="_blank"
rel="noopener noreferrer"
data-umami-event="outbound_click"
data-umami-event-target_id={targetId}
data-umami-event-placement={p}
data-umami-event-target_url={url}
data-umami-event-domain={domain || "unknown"}
data-umami-event-source="instagram"
data-umami-event-ui_placement="instagram_embed"
>
View on Instagram
</a>
</blockquote>

11
site/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/// <reference types="astro/client" />
interface ImportMetaEnv {
readonly PUBLIC_SITE_URL?: string;
readonly PUBLIC_UMAMI_SCRIPT_URL?: string;
readonly PUBLIC_UMAMI_WEBSITE_ID?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -0,0 +1,102 @@
---
import { getPublicConfig } from "../lib/config";
type Props = {
title: string;
description: string;
canonicalPath: string;
ogImageUrl?: string;
};
const { title, description, canonicalPath, ogImageUrl } = Astro.props;
const cfg = getPublicConfig();
const siteUrl = (cfg.siteUrl || "http://localhost:4321").replace(/\/$/, "");
const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath : `/${canonicalPath}`}`;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalUrl} />
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
{ogImageUrl ? <meta property="og:image" content={ogImageUrl} /> : null}
<meta name="twitter:card" content={ogImageUrl ? "summary_large_image" : "summary"} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
{ogImageUrl ? <meta name="twitter:image" content={ogImageUrl} /> : null}
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<link rel="stylesheet" href="/styles/global.css" />
{
cfg.umami ? (
<script async defer data-website-id={cfg.umami.websiteId} src={cfg.umami.scriptUrl} />
) : null
}
</head>
<body>
<header class="site-header">
<a
class="brand"
href="/"
data-umami-event="click"
data-umami-event-target_id="nav.brand"
data-umami-event-placement="nav"
data-umami-event-target_url="/"
>
SanthoshJ
</a>
<nav class="nav">
<a
href="/videos"
data-umami-event="click"
data-umami-event-target_id="nav.videos"
data-umami-event-placement="nav"
data-umami-event-target_url="/videos"
>
Videos
</a>
<a
href="/podcast"
data-umami-event="click"
data-umami-event-target_id="nav.podcast"
data-umami-event-placement="nav"
data-umami-event-target_url="/podcast"
>
Podcast
</a>
<a
href="/about"
data-umami-event="click"
data-umami-event-target_id="nav.about"
data-umami-event-placement="nav"
data-umami-event-target_url="/about"
>
About
</a>
</nav>
</header>
<main class="container">
<slot />
</main>
<footer class="site-footer">
<p class="muted">© {new Date().getFullYear()} SanthoshJ</p>
</footer>
</body>
</html>

35
site/src/lib/config.ts Normal file
View File

@@ -0,0 +1,35 @@
type PublicConfig = {
siteUrl?: string;
umami?: {
scriptUrl: string;
websiteId: string;
};
};
type IngestConfig = {
youtubeChannelId?: string;
youtubeApiKey?: string;
podcastRssUrl?: string;
instagramPostUrlsFile: string;
};
export function getPublicConfig(): PublicConfig {
const siteUrl = import.meta.env.PUBLIC_SITE_URL;
const scriptUrl = import.meta.env.PUBLIC_UMAMI_SCRIPT_URL;
const websiteId = import.meta.env.PUBLIC_UMAMI_WEBSITE_ID;
return {
siteUrl,
umami: scriptUrl && websiteId ? { scriptUrl, websiteId } : undefined,
};
}
// Ingestion scripts run under Node (not inside Astro runtime).
export function getIngestConfigFromEnv(env: NodeJS.ProcessEnv): IngestConfig {
return {
youtubeChannelId: env.YOUTUBE_CHANNEL_ID,
youtubeApiKey: env.YOUTUBE_API_KEY,
podcastRssUrl: env.PODCAST_RSS_URL,
instagramPostUrlsFile: env.INSTAGRAM_POST_URLS_FILE || "content/instagram-posts.json",
};
}

View File

@@ -0,0 +1,26 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import type { ContentCache } from "./types";
const DEFAULT_CACHE: ContentCache = { generatedAt: new Date(0).toISOString(), items: [] };
function getCachePath() {
// Read from the repo-local content cache (populated by scripts/fetch-content.ts).
return path.join(process.cwd(), "content", "cache", "content.json");
}
export async function readContentCache(): Promise<ContentCache> {
const cachePath = getCachePath();
try {
const raw = await fs.readFile(cachePath, "utf8");
const parsed = JSON.parse(raw) as ContentCache;
if (!parsed || !Array.isArray(parsed.items) || typeof parsed.generatedAt !== "string") {
return DEFAULT_CACHE;
}
return parsed;
} catch {
// Cache missing is normal for first run.
return DEFAULT_CACHE;
}
}

View File

@@ -0,0 +1,19 @@
import { promises as fs } from "node:fs";
import path from "node:path";
type FeaturedVideosFile = {
videoIds: string[];
};
export async function readFeaturedVideoIds(): Promise<string[]> {
const p = path.join(process.cwd(), "content", "featured-videos.json");
try {
const raw = await fs.readFile(p, "utf8");
const parsed = JSON.parse(raw) as FeaturedVideosFile;
return Array.isArray(parsed.videoIds)
? parsed.videoIds.filter((x) => typeof x === "string")
: [];
} catch {
return [];
}
}

View File

@@ -0,0 +1,45 @@
import type { ContentCache, ContentItem, ContentSource } from "./types";
export function newestItems(
cache: ContentCache,
limit: number,
sources?: ContentSource[],
): ContentItem[] {
const items = sources ? cache.items.filter((i) => sources.includes(i.source)) : cache.items;
return [...items]
.sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt))
.slice(0, Math.max(0, limit));
}
export function youtubeVideos(cache: ContentCache): ContentItem[] {
return cache.items.filter((i) => i.source === "youtube");
}
export function podcastEpisodes(cache: ContentCache): ContentItem[] {
return cache.items.filter((i) => i.source === "podcast");
}
export function instagramPosts(cache: ContentCache): ContentItem[] {
return cache.items.filter((i) => i.source === "instagram");
}
export function highPerformingYoutubeVideos(
cache: ContentCache,
limit: number,
curatedIds: string[],
): ContentItem[] {
const videos = youtubeVideos(cache);
// If we have a curated list, it wins (keeps the page stable even without metrics).
if (curatedIds.length > 0) {
const byId = new Map(videos.map((v) => [v.id, v]));
return curatedIds
.map((id) => byId.get(id))
.filter(Boolean)
.slice(0, Math.max(0, limit)) as ContentItem[];
}
// Otherwise rank by views where possible.
const ranked = [...videos].sort((a, b) => (b.metrics?.views || 0) - (a.metrics?.views || 0));
return ranked.slice(0, Math.max(0, limit));
}

View File

@@ -0,0 +1,20 @@
export type ContentSource = "youtube" | "instagram" | "podcast";
export type ContentMetrics = {
views?: number;
};
export type ContentItem = {
id: string;
source: ContentSource;
url: string;
title: string;
publishedAt: string; // ISO-8601
thumbnailUrl?: string;
metrics?: ContentMetrics;
};
export type ContentCache = {
generatedAt: string; // ISO-8601
items: ContentItem[];
};

View File

@@ -0,0 +1,32 @@
import { promises as fs } from "node:fs";
import type { ContentItem } from "../content/types";
type InstagramPostsFile = {
posts: Array<{
url: string;
title?: string;
publishedAt?: string;
thumbnailUrl?: string;
}>;
};
export async function readInstagramEmbedPosts(filePath: string): Promise<ContentItem[]> {
const raw = await fs.readFile(filePath, "utf8");
const parsed = JSON.parse(raw) as InstagramPostsFile;
const now = new Date().toISOString();
const posts = Array.isArray(parsed.posts) ? parsed.posts : [];
return posts
.filter((p) => typeof p.url === "string" && p.url.length > 0)
.map((p) => ({
id: p.url,
source: "instagram" as const,
url: p.url,
title: p.title || "Instagram post",
// If the user doesn't provide a publish date, we still generate valid ISO-8601.
// It won't be accurate, but it keeps the system functional until real ingestion is added.
publishedAt: p.publishedAt ? new Date(p.publishedAt).toISOString() : now,
thumbnailUrl: p.thumbnailUrl,
}));
}

View File

@@ -0,0 +1,27 @@
import Parser from "rss-parser";
import type { ContentItem } from "../content/types";
export async function fetchPodcastRss(rssUrl: string, limit = 50): Promise<ContentItem[]> {
const parser = new Parser();
const feed = await parser.parseURL(rssUrl);
return normalizePodcastFeedItems(feed.items || [], limit);
}
export function normalizePodcastFeedItems(items: any[], limit: number): ContentItem[] {
const out = (items || []).slice(0, limit).map((it) => {
const url = it.link || "";
const id = (it.guid || it.id || url).toString();
const publishedAt = (it.isoDate || it.pubDate || new Date(0).toISOString()).toString();
return {
id,
source: "podcast" as const,
url,
title: (it.title || "").toString(),
publishedAt: new Date(publishedAt).toISOString(),
thumbnailUrl: (it.itunes?.image || undefined) as string | undefined,
};
});
return out.filter((x) => x.url && x.title);
}

View File

@@ -0,0 +1,6 @@
import type { ContentItem, ContentSource } from "../content/types";
export type IngestResult = {
source: ContentSource;
items: ContentItem[];
};

View File

@@ -0,0 +1,112 @@
import Parser from "rss-parser";
import type { ContentItem } from "../content/types";
type YoutubeApiVideo = {
id: string;
url: string;
title: string;
publishedAt: string;
thumbnailUrl?: string;
views?: number;
};
export async function fetchYoutubeViaRss(channelId: string, limit = 20): Promise<ContentItem[]> {
const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${encodeURIComponent(channelId)}`;
const parser = new Parser();
const feed = await parser.parseURL(feedUrl);
return normalizeYoutubeRssFeedItems(feed.items || [], limit);
}
async function youtubeApiGetJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`YouTube API request failed: ${res.status} ${res.statusText}`);
return (await res.json()) as T;
}
export function normalizeYoutubeRssFeedItems(items: any[], limit: number): ContentItem[] {
return (items || [])
.slice(0, limit)
.map((it) => {
const url = it.link || "";
const id = (it.id || url).toString();
const publishedAt = (it.isoDate || it.pubDate || new Date(0).toISOString()).toString();
return {
id,
source: "youtube" as const,
url,
title: (it.title || "").toString(),
publishedAt: new Date(publishedAt).toISOString(),
thumbnailUrl: (it.enclosure?.url || undefined) as string | undefined,
};
})
.filter((x) => x.url && x.title);
}
export function normalizeYoutubeApiVideos(
items: Array<{
id: string;
snippet: { title: string; publishedAt: string; thumbnails?: Record<string, { url: string }> };
statistics?: { viewCount?: string };
}>,
): ContentItem[] {
const normalized: YoutubeApiVideo[] = (items || []).map((v) => ({
id: v.id,
url: `https://www.youtube.com/watch?v=${encodeURIComponent(v.id)}`,
title: v.snippet.title,
publishedAt: new Date(v.snippet.publishedAt).toISOString(),
thumbnailUrl: v.snippet.thumbnails?.high?.url || v.snippet.thumbnails?.default?.url,
views: v.statistics?.viewCount ? Number(v.statistics.viewCount) : undefined,
}));
return normalized.map<ContentItem>((v) => ({
id: v.id,
source: "youtube",
url: v.url,
title: v.title,
publishedAt: v.publishedAt,
thumbnailUrl: v.thumbnailUrl,
metrics: v.views !== undefined ? { views: v.views } : undefined,
}));
}
export async function fetchYoutubeViaApi(
channelId: string,
apiKey: string,
limit = 20,
): Promise<ContentItem[]> {
// 1) Get latest video IDs from channel.
const searchUrl =
"https://www.googleapis.com/youtube/v3/search" +
`?part=snippet&channelId=${encodeURIComponent(channelId)}` +
`&maxResults=${encodeURIComponent(String(limit))}` +
`&order=date&type=video&key=${encodeURIComponent(apiKey)}`;
const search = await youtubeApiGetJson<{
items: Array<{
id: { videoId: string };
snippet: { title: string; publishedAt: string; thumbnails?: any };
}>;
}>(searchUrl);
const videoIds = (search.items || []).map((x) => x.id.videoId).filter(Boolean);
if (videoIds.length === 0) return [];
// 2) Fetch statistics.
const videosUrl =
"https://www.googleapis.com/youtube/v3/videos" +
`?part=snippet,statistics&maxResults=${encodeURIComponent(String(videoIds.length))}` +
`&id=${encodeURIComponent(videoIds.join(","))}` +
`&key=${encodeURIComponent(apiKey)}`;
const videos = await youtubeApiGetJson<{
items: Array<{
id: string;
snippet: { title: string; publishedAt: string; thumbnails?: Record<string, { url: string }> };
statistics?: { viewCount?: string };
}>;
}>(videosUrl);
return normalizeYoutubeApiVideos(videos.items || []);
}

5
site/src/lib/links.ts Normal file
View File

@@ -0,0 +1,5 @@
export const LINKS = {
youtubeChannel: "https://www.youtube.com/santhoshj",
instagramProfile: "https://www.instagram.com/santhoshjanan/",
podcast: "https://podcasters.spotify.com/pod/show/irregularmind", // default; override in CTA props if needed
};

10
site/src/lib/url.ts Normal file
View File

@@ -0,0 +1,10 @@
export function withUtm(
url: string,
utm: Partial<Record<"utm_source" | "utm_medium" | "utm_campaign" | "utm_content", string>>,
): string {
const u = new URL(url);
for (const [k, v] of Object.entries(utm)) {
if (v) u.searchParams.set(k, v);
}
return u.toString();
}

View File

@@ -0,0 +1,34 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import CtaLink from "../components/CtaLink.astro";
import { LINKS } from "../lib/links";
---
<BaseLayout
title="About | SanthoshJ"
description="About SanthoshJ and where to follow."
canonicalPath="/about"
>
<section class="section">
<div class="section-header">
<h2>About</h2>
<span class="muted">Tech, streaming, movies, travel</span>
</div>
<div class="empty">
<p style="margin-top: 0;">
This is a lightweight site that aggregates my content so it can be discovered via search and
shared cleanly.
</p>
<div class="cta-row">
<CtaLink platform="youtube" placement="about" url={LINKS.youtubeChannel} label="YouTube" />
<CtaLink
platform="instagram"
placement="about"
url={LINKS.instagramProfile}
label="Instagram"
/>
<CtaLink platform="podcast" placement="about" url={LINKS.podcast} label="Podcast" />
</div>
</div>
</section>
</BaseLayout>

238
site/src/pages/index.astro Normal file
View File

@@ -0,0 +1,238 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import CtaLink from "../components/CtaLink.astro";
import ContentCard from "../components/ContentCard.astro";
import InstagramEmbed from "../components/InstagramEmbed.astro";
import { readContentCache } from "../lib/content/cache";
import {
newestItems,
highPerformingYoutubeVideos,
instagramPosts,
podcastEpisodes,
} from "../lib/content/selectors";
import { readFeaturedVideoIds } from "../lib/content/curation";
import { LINKS } from "../lib/links";
const cache = await readContentCache();
const featuredIds = await readFeaturedVideoIds();
const newest = newestItems(cache, 9);
const highPerf = highPerformingYoutubeVideos(cache, 6, featuredIds);
const ig = instagramPosts(cache).slice(0, 6);
const pods = podcastEpisodes(cache)
.slice(0, 6)
.sort((a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt));
---
<BaseLayout
title="SanthoshJ | Tech, streaming, movies, travel"
description="A fast, SEO-first home for videos, movie posts, and the Irregular Mind podcast."
canonicalPath="/"
>
<section class="hero">
<div>
<h1>Fast content. Clear next actions.</h1>
<p>
I post about technology, game streaming, movies, and travel. This site collects the best of
it and points you to the platform you prefer.
</p>
<div class="cta-row">
<CtaLink
platform="youtube"
placement="hero"
url={LINKS.youtubeChannel}
label="Subscribe on YouTube"
class="primary"
/>
<CtaLink
platform="instagram"
placement="hero"
url={LINKS.instagramProfile}
label="Follow on Instagram"
/>
<CtaLink
platform="podcast"
placement="hero"
url={LINKS.podcast}
label="Listen to the podcast"
/>
</div>
<p class="muted" style="margin-top: 14px;">
Last updated: {new Date(cache.generatedAt).toLocaleString()}
</p>
</div>
<div class="empty">
<strong>Goal:</strong> 10% month-over-month growth in followers and engagement.
<br />
<span class="muted"
>This site is the SEO landing surface that turns search traffic into followers.</span
>
</div>
</section>
<section class="section">
<div class="section-header">
<h2>Newest</h2>
<a
class="muted"
href="/videos"
data-umami-event="click"
data-umami-event-target_id="section_header.newest.browse_all"
data-umami-event-placement="section_header"
data-umami-event-target_url="/videos"
>
Browse all →
</a>
</div>
{
newest.length > 0 ? (
<div class="grid">
{newest.map((item) => (
<ContentCard item={item} placement="home.newest" />
))}
</div>
) : (
<div class="empty">
No content cache yet. Run <code>npm run fetch-content</code> in <code>site/</code>.
</div>
)
}
</section>
<section class="section">
<div class="section-header">
<h2>High-performing videos</h2>
<a
class="muted"
href="/videos"
data-umami-event="click"
data-umami-event-target_id="section_header.high_performing.videos"
data-umami-event-placement="section_header"
data-umami-event-target_url="/videos"
>
Videos →
</a>
</div>
{
highPerf.length > 0 ? (
<div class="grid">
{highPerf.map((item) => (
<ContentCard item={item} placement="home.high_performing" />
))}
</div>
) : (
<div class="empty">
No video stats found yet. Add <code>YOUTUBE_API_KEY</code> or curate{" "}
<code>content/featured-videos.json</code>.
</div>
)
}
</section>
<section class="section">
<div class="section-header">
<h2>Instagram</h2>
<a
class="muted"
href={LINKS.instagramProfile}
target="_blank"
rel="noopener noreferrer"
data-umami-event="outbound_click"
data-umami-event-target_id="section_header.instagram.profile"
data-umami-event-placement="section_header"
data-umami-event-target_url={LINKS.instagramProfile}
data-umami-event-domain="www.instagram.com"
data-umami-event-source="instagram"
data-umami-event-ui_placement="section_header"
>
Profile →
</a>
</div>
{
ig.length > 0 ? (
<div class="grid">
{ig.map((item) => (
<InstagramEmbed url={item.url} placement="home.instagram" />
))}
</div>
) : (
<div class="empty">
Add post URLs to <code>content/instagram-posts.json</code> to show Instagram here.
</div>
)
}
</section>
{
ig.length > 0 ? (
<script
is:inline
define:vars={{}}
set:html={`
(function(){
var s = document.createElement('script');
s.async = true;
s.defer = true;
s.src = 'https://www.instagram.com/embed.js';
s.onload = function(){
try { window.instgrm && window.instgrm.Embeds && window.instgrm.Embeds.process(); } catch(e) {}
};
document.head.appendChild(s);
})();
`}
/>
) : null
}
<section class="section">
<div class="section-header">
<h2>Podcast: Irregular Mind</h2>
<a
class="muted"
href="/podcast"
data-umami-event="click"
data-umami-event-target_id="section_header.podcast.episodes"
data-umami-event-placement="section_header"
data-umami-event-target_url="/podcast"
>
Episodes →
</a>
</div>
{
pods.length > 0 ? (
<div class="grid">
{pods.map((item) => (
<ContentCard item={item} placement="home.podcast" />
))}
</div>
) : (
<div class="empty">
Set <code>PODCAST_RSS_URL</code> and run <code>npm run fetch-content</code> to populate
episodes.
</div>
)
}
</section>
<section class="section">
<div class="section-header">
<h2>Follow</h2>
<span class="muted">Pick your platform</span>
</div>
<div class="cta-row">
<CtaLink
platform="youtube"
placement="footer_cta"
url={LINKS.youtubeChannel}
label="YouTube"
/>
<CtaLink
platform="instagram"
placement="footer_cta"
url={LINKS.instagramProfile}
label="Instagram"
/>
<CtaLink platform="podcast" placement="footer_cta" url={LINKS.podcast} label="Podcast" />
</div>
</section>
</BaseLayout>

View File

@@ -0,0 +1,38 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import ContentCard from "../components/ContentCard.astro";
import { readContentCache } from "../lib/content/cache";
import { podcastEpisodes } from "../lib/content/selectors";
const cache = await readContentCache();
const episodes = podcastEpisodes(cache).sort(
(a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt),
);
---
<BaseLayout
title="Podcast | Irregular Mind"
description="Episodes from the Irregular Mind podcast."
canonicalPath="/podcast"
>
<section class="section">
<div class="section-header">
<h2>Irregular Mind</h2>
<span class="muted">{episodes.length} episodes</span>
</div>
{
episodes.length > 0 ? (
<div class="grid">
{episodes.map((item) => (
<ContentCard item={item} placement="podcast.list" />
))}
</div>
) : (
<div class="empty">
No episodes yet. Set <code>PODCAST_RSS_URL</code> and run{" "}
<code>npm run fetch-content</code>.
</div>
)
}
</section>
</BaseLayout>

View File

@@ -0,0 +1,74 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro";
import { readContentCache } from "../../lib/content/cache";
import { podcastEpisodes } from "../../lib/content/selectors";
export async function getStaticPaths() {
const cache = await readContentCache();
const eps = podcastEpisodes(cache);
return eps.map((e) => ({ params: { id: e.id }, props: { episode: e } }));
}
const { episode } = Astro.props;
const jsonLd = {
"@context": "https://schema.org",
"@type": "PodcastEpisode",
name: episode.title,
datePublished: episode.publishedAt,
url: episode.url,
};
let episodeDomain = "";
try {
episodeDomain = new URL(episode.url).hostname;
} catch {
episodeDomain = "";
}
---
<BaseLayout
title={`${episode.title} | Irregular Mind`}
description={`Listen: ${episode.title}`}
canonicalPath={`/podcast/${encodeURIComponent(episode.id)}`}
ogImageUrl={episode.thumbnailUrl}
>
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<section class="section">
<div class="section-header">
<h2>{episode.title}</h2>
<a
class="muted"
href="/podcast"
data-umami-event="click"
data-umami-event-target_id="podcast_detail.back_to_podcast"
data-umami-event-placement="section_header"
data-umami-event-target_url="/podcast"
>
Back to podcast →
</a>
</div>
<div class="empty">
<p style="margin-top: 0;">
This page exists for SEO and sharing. Listening happens on your platform.
</p>
<p>
<a
class="cta primary"
href={episode.url}
target="_blank"
rel="noopener noreferrer"
data-umami-event="outbound_click"
data-umami-event-target_id={`podcast_detail.open.${episode.id}`}
data-umami-event-placement="podcast_detail"
data-umami-event-target_url={episode.url}
data-umami-event-domain={episodeDomain || "unknown"}
data-umami-event-source="podcast"
data-umami-event-ui_placement="podcast_detail"
>
Open episode
</a>
</p>
</div>
</section>
</BaseLayout>

View File

@@ -0,0 +1,38 @@
---
import BaseLayout from "../layouts/BaseLayout.astro";
import ContentCard from "../components/ContentCard.astro";
import { readContentCache } from "../lib/content/cache";
import { youtubeVideos } from "../lib/content/selectors";
const cache = await readContentCache();
const videos = youtubeVideos(cache).sort(
(a, b) => Date.parse(b.publishedAt) - Date.parse(a.publishedAt),
);
---
<BaseLayout
title="Videos | SanthoshJ"
description="Latest and featured YouTube videos."
canonicalPath="/videos"
>
<section class="section">
<div class="section-header">
<h2>Videos</h2>
<span class="muted">{videos.length} items</span>
</div>
{
videos.length > 0 ? (
<div class="grid">
{videos.map((item) => (
<ContentCard item={item} placement="videos.list" />
))}
</div>
) : (
<div class="empty">
No videos yet. Configure <code>YOUTUBE_CHANNEL_ID</code> and run{" "}
<code>npm run fetch-content</code>.
</div>
)
}
</section>
</BaseLayout>

View File

@@ -0,0 +1,75 @@
---
import BaseLayout from "../../layouts/BaseLayout.astro";
import { readContentCache } from "../../lib/content/cache";
import { youtubeVideos } from "../../lib/content/selectors";
export async function getStaticPaths() {
const cache = await readContentCache();
const videos = youtubeVideos(cache);
return videos.map((v) => ({ params: { id: v.id }, props: { video: v } }));
}
const { video } = Astro.props;
const jsonLd = {
"@context": "https://schema.org",
"@type": "VideoObject",
name: video.title,
uploadDate: video.publishedAt,
thumbnailUrl: video.thumbnailUrl ? [video.thumbnailUrl] : undefined,
url: video.url,
};
let videoDomain = "";
try {
videoDomain = new URL(video.url).hostname;
} catch {
videoDomain = "";
}
---
<BaseLayout
title={`${video.title} | SanthoshJ`}
description={`Watch: ${video.title}`}
canonicalPath={`/videos/${encodeURIComponent(video.id)}`}
ogImageUrl={video.thumbnailUrl}
>
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<section class="section">
<div class="section-header">
<h2>{video.title}</h2>
<a
class="muted"
href="/videos"
data-umami-event="click"
data-umami-event-target_id="video_detail.back_to_videos"
data-umami-event-placement="section_header"
data-umami-event-target_url="/videos"
>
Back to videos →
</a>
</div>
<div class="empty">
<p style="margin-top: 0;">
This page exists for SEO and sharing. The canonical watch page is YouTube.
</p>
<p>
<a
class="cta primary"
href={video.url}
target="_blank"
rel="noopener noreferrer"
data-umami-event="outbound_click"
data-umami-event-target_id={`video_detail.watch.${video.id}`}
data-umami-event-placement="video_detail"
data-umami-event-target_url={video.url}
data-umami-event-domain={videoDomain || "unknown"}
data-umami-event-source="youtube"
data-umami-event-ui_placement="video_detail"
>
Watch on YouTube
</a>
</p>
</div>
</section>
</BaseLayout>

13
site/tests/fixtures/podcast-feed.xml vendored Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Irregular Mind</title>
<item>
<guid>ep-001</guid>
<title>Episode One</title>
<link>https://example.com/podcast/ep-001</link>
<pubDate>Tue, 10 Feb 2026 10:00:00 GMT</pubDate>
</item>
</channel>
</rss>

17
site/tests/fixtures/youtube-feed.xml vendored Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>YouTube channel feed</title>
<entry>
<id>yt:video:abc123</id>
<title>Test Video One</title>
<published>2026-02-09T12:34:56+00:00</published>
<link rel="alternate" href="https://www.youtube.com/watch?v=abc123"/>
</entry>
<entry>
<id>yt:video:def456</id>
<title>Test Video Two</title>
<published>2026-02-08T10:00:00+00:00</published>
<link rel="alternate" href="https://www.youtube.com/watch?v=def456"/>
</entry>
</feed>

13
site/tests/fixtures/youtube-videos.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"items": [
{
"id": "abc123",
"snippet": {
"title": "API Video One",
"publishedAt": "2026-02-09T12:34:56Z",
"thumbnails": { "high": { "url": "https://img.example.com/1.jpg" } }
},
"statistics": { "viewCount": "12345" }
}
]
}

45
site/tests/ingest.test.ts Normal file
View File

@@ -0,0 +1,45 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
import Parser from "rss-parser";
import { normalizePodcastFeedItems } from "../src/lib/ingest/podcast";
import { normalizeYoutubeApiVideos, normalizeYoutubeRssFeedItems } from "../src/lib/ingest/youtube";
const fixturesDir = path.join(process.cwd(), "tests", "fixtures");
describe("ingestion normalization", () => {
it("normalizes YouTube RSS/Atom feed items", async () => {
const xml = await readFile(path.join(fixturesDir, "youtube-feed.xml"), "utf8");
const parser = new Parser();
const feed = await parser.parseString(xml);
const items = normalizeYoutubeRssFeedItems(feed.items || [], 10);
expect(items.length).toBeGreaterThan(0);
expect(items[0].source).toBe("youtube");
expect(items[0].title).toBeTruthy();
expect(items[0].url).toMatch(/youtube\.com/);
expect(items[0].publishedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});
it("normalizes YouTube API videos and captures view count", async () => {
const raw = await readFile(path.join(fixturesDir, "youtube-videos.json"), "utf8");
const json = JSON.parse(raw) as any;
const items = normalizeYoutubeApiVideos(json.items);
expect(items).toHaveLength(1);
expect(items[0].metrics?.views).toBe(12345);
});
it("normalizes podcast RSS items and ensures ISO dates", async () => {
const xml = await readFile(path.join(fixturesDir, "podcast-feed.xml"), "utf8");
const parser = new Parser();
const feed = await parser.parseString(xml);
const items = normalizePodcastFeedItems(feed.items || [], 10);
expect(items).toHaveLength(1);
expect(items[0].source).toBe("podcast");
expect(items[0].publishedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/);
});
});

View File

@@ -0,0 +1,31 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { describe, expect, it } from "vitest";
async function read(rel: string) {
return await readFile(path.join(process.cwd(), rel), "utf8");
}
describe("umami event attributes", () => {
it("instruments nav links using data-umami-event", async () => {
const src = await read("src/layouts/BaseLayout.astro");
expect(src).toContain('data-umami-event="click"');
expect(src).toContain('data-umami-event-target_id="nav.videos"');
expect(src).toContain('data-umami-event-placement="nav"');
});
it("instruments CTAs using data-umami-event and unique target_id", async () => {
const src = await read("src/components/CtaLink.astro");
expect(src).toContain('data-umami-event="cta_click"');
expect(src).toContain("data-umami-event-target_id");
expect(src).toContain("data-umami-event-placement");
});
it("instruments content cards using outbound_click", async () => {
const src = await read("src/components/ContentCard.astro");
expect(src).toContain('data-umami-event="outbound_click"');
expect(src).toContain("data-umami-event-target_id");
expect(src).toContain("data-umami-event-domain");
});
});

5
site/tsconfig.json Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}