From 03df2b3a6c29557c6226134f64a294f643127ce5 Mon Sep 17 00:00:00 2001 From: Santhosh Janardhanan Date: Tue, 10 Feb 2026 02:34:25 -0500 Subject: [PATCH] better cards --- openspec/changes/card-layout/.openspec.yaml | 2 + openspec/changes/card-layout/design.md | 62 +++++++++ openspec/changes/card-layout/proposal.md | 29 ++++ .../specs/blog-section-surface/spec.md | 34 +++++ .../specs/card-layout-system/spec.md | 28 ++++ .../specs/social-content-aggregation/spec.md | 27 ++++ openspec/changes/card-layout/tasks.md | 20 +++ .../deploy-without-node/.openspec.yaml | 2 + .../changes/deploy-without-node/design.md | 60 +++++++++ .../changes/deploy-without-node/proposal.md | 25 ++++ .../specs/docker-content-refresh/spec.md | 26 ++++ openspec/changes/deploy-without-node/tasks.md | 25 ++++ site/content/cache/content.json | 74 ++++++++++- site/public/styles/global.css | 124 +++++++++--------- site/src/components/BlogPostCard.astro | 43 +++--- site/src/components/ContentCard.astro | 57 +++----- site/src/components/StandardCard.astro | 63 +++++++++ site/src/lib/content/types.ts | 1 + site/src/lib/ingest/podcast.ts | 24 ++++ site/src/lib/ingest/youtube.ts | 29 +++- site/tests/blog-umami-attributes.test.ts | 7 +- site/tests/card-layout.test.ts | 29 ++++ .../content-title-type-attributes.test.ts | 3 +- site/tests/umami-attributes.test.ts | 2 +- 24 files changed, 669 insertions(+), 127 deletions(-) create mode 100644 openspec/changes/card-layout/.openspec.yaml create mode 100644 openspec/changes/card-layout/design.md create mode 100644 openspec/changes/card-layout/proposal.md create mode 100644 openspec/changes/card-layout/specs/blog-section-surface/spec.md create mode 100644 openspec/changes/card-layout/specs/card-layout-system/spec.md create mode 100644 openspec/changes/card-layout/specs/social-content-aggregation/spec.md create mode 100644 openspec/changes/card-layout/tasks.md create mode 100644 openspec/changes/deploy-without-node/.openspec.yaml create mode 100644 openspec/changes/deploy-without-node/design.md create mode 100644 openspec/changes/deploy-without-node/proposal.md create mode 100644 openspec/changes/deploy-without-node/specs/docker-content-refresh/spec.md create mode 100644 openspec/changes/deploy-without-node/tasks.md create mode 100644 site/src/components/StandardCard.astro create mode 100644 site/tests/card-layout.test.ts diff --git a/openspec/changes/card-layout/.openspec.yaml b/openspec/changes/card-layout/.openspec.yaml new file mode 100644 index 0000000..70eb9e0 --- /dev/null +++ b/openspec/changes/card-layout/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-10 diff --git a/openspec/changes/card-layout/design.md b/openspec/changes/card-layout/design.md new file mode 100644 index 0000000..718684d --- /dev/null +++ b/openspec/changes/card-layout/design.md @@ -0,0 +1,62 @@ +## Context + +The site renders multiple card-like UI elements today: +- videos/podcast listings use `site/src/components/ContentCard.astro` +- blog listings use `site/src/components/BlogPostCard.astro` + +These cards have different layouts and metadata placement. This change standardizes the card information architecture so all cards feel consistent. + +The site is statically generated (Astro). Card layout consistency should be enforced primarily through shared components and shared CSS rather than copy/paste per page. + +## Goals / Non-Goals + +**Goals:** + +- Define and implement a single, consistent card structure: + - media (image/placeholder) at top + - title + - trimmed summary/excerpt + - meta row: date (left) + views (right, if available) + - footer: source label (youtube/podcast/blog/etc.) +- Apply to all existing card surfaces: + - `/videos` listing cards + - `/podcast` listing cards + - `/blog` post cards (and category listings) +- Keep the layout resilient when fields are missing (no views, no image, no summary). + +**Non-Goals:** + +- Redesigning non-card list links (e.g., simple navigation links) into cards unless needed for consistency. +- Changing Umami tracking taxonomy (attributes stay intact). +- Large typographic or theme redesign beyond card structure/spacing. + +## Decisions + +- **Decision: Implement a shared Card component used by existing card components.** + - Rationale: Centralizes markup and ensures layout stays consistent across surfaces. + - Approach: + - Create a new component (e.g., `Card.astro`) with props for: + - `href`, `title`, `summary`, `imageUrl`, `dateLabel`, `viewsLabel`, `sourceLabel` + - optional tracking attributes pass-through (keep existing `data-umami-*` behavior) + - Update `ContentCard.astro` and `BlogPostCard.astro` to render the shared Card component. + +- **Decision: Add an optional `summary` field to normalized items.** + - Rationale: Enables the standard card layout to show trimmed summaries for videos/podcast, similar to blog excerpts. + - Approach: + - Extend the normalized content schema/types with `summary?: string`. + - Populate it during ingestion where available (YouTube description snippet; podcast episode summary/description). + +- **Decision: Views are optional and shown only when available.** + - Rationale: Not all sources provide views; the layout should be consistent without forcing synthetic values. + +## Risks / Trade-offs + +- [Risk] Ingestion sources may provide very long summaries. + - Mitigation: Standardize trimming logic in the card component (single truncation helper). + +- [Risk] CSS regressions across multiple pages. + - Mitigation: Add tests that assert key card structure/classes exist; verify build outputs for `/videos`, `/podcast`, `/blog`. + +- [Risk] Blog post cards and content cards have different link targets (internal vs outbound). + - Mitigation: Shared Card component should be able to render both internal links and external links (target/rel configurable). + diff --git a/openspec/changes/card-layout/proposal.md b/openspec/changes/card-layout/proposal.md new file mode 100644 index 0000000..d4e3910 --- /dev/null +++ b/openspec/changes/card-layout/proposal.md @@ -0,0 +1,29 @@ +## Why + +The site currently renders multiple card variants (videos/podcast cards, blog post cards, etc.) with inconsistent structure and metadata placement, which makes the UI feel uneven. A standardized card layout will create a consistent UX across the website. + +## What Changes + +- Standardize the UI structure for all content cards across the site: + - featured image displayed prominently on top (when available) + - title + - summary/excerpt, trimmed + - meta row with date (left) and views (right) when available (`space-between`) + - footer row showing the content source (YouTube/podcast/blog/etc.) +- Update existing card renderers/components to use the standardized structure and styling. +- Where a content source does not provide one of the fields (for example, views for blog posts), the layout MUST still render cleanly with the missing field omitted. + +## Capabilities + +### New Capabilities +- `card-layout-system`: Define the standard card information architecture (image/title/summary/meta/footer) and rules for optional fields so all surfaces render consistently. + +### Modified Capabilities +- `social-content-aggregation`: Extend normalized content items to include an optional `summary`/`excerpt` field where available (e.g., YouTube description snippet, podcast episode summary) so non-blog cards can display a trimmed summary. +- `blog-section-surface`: Standardize blog listing cards to include the meta row (publish date and optional views) and footer source label, consistent with the global card layout system. + +## Impact + +- Affected code: shared card/link components (e.g., `site/src/components/ContentCard.astro`, `site/src/components/BlogPostCard.astro`) and pages that render listings (`/`, `/videos`, `/podcast`, `/blog`). +- Data model: normalized cached items may gain an optional summary field; ingestion code may need to populate it for YouTube/podcast. +- Styling: global CSS updates to ensure consistent spacing/typography and footer/meta layout. diff --git a/openspec/changes/card-layout/specs/blog-section-surface/spec.md b/openspec/changes/card-layout/specs/blog-section-surface/spec.md new file mode 100644 index 0000000..d177602 --- /dev/null +++ b/openspec/changes/card-layout/specs/blog-section-surface/spec.md @@ -0,0 +1,34 @@ +## MODIFIED Requirements + +### Requirement: Blog index listing (posts) +The site MUST provide a blog index page at `/blog` that lists WordPress posts as cards containing: +- featured image (when available) +- title +- excerpt/summary +- publish date + +The card MUST render a meta row with: +- publish date on the left +- views on the right when available (if views are not provided by the dataset, the card MUST omit views without breaking layout) + +The card MUST render a footer row showing the content source label (e.g., `blog`). + +The listing MUST be ordered by publish date descending (newest first). + +Each post card MUST be instrumented with Umami Track Events data attributes and MUST include at minimum: +- `data-umami-event` +- `data-umami-event-target_id` +- `data-umami-event-placement` +- `data-umami-event-target_url` + +#### Scenario: Blog index lists posts +- **WHEN** the cached WordPress dataset contains posts +- **THEN** `/blog` renders a list of post cards ordered by publish date descending + +#### Scenario: Blog post card click is tracked +- **WHEN** a user clicks a blog post card on `/blog` +- **THEN** the click emits an Umami event with `target_id`, `placement`, and `target_url` + +#### Scenario: Blog post card layout is standardized +- **WHEN** `/blog` renders a blog post card +- **THEN** the card shows featured image (when available), title, trimmed excerpt, meta row (date + optional views), and a footer source label diff --git a/openspec/changes/card-layout/specs/card-layout-system/spec.md b/openspec/changes/card-layout/specs/card-layout-system/spec.md new file mode 100644 index 0000000..415dbce --- /dev/null +++ b/openspec/changes/card-layout/specs/card-layout-system/spec.md @@ -0,0 +1,28 @@ +## ADDED Requirements + +### Requirement: Standard card information architecture +All content cards rendered by the site MUST use a standardized layout so cards across different surfaces look consistent. + +The standard card layout MUST be: +- featured image displayed prominently at the top (when available) +- title +- summary/excerpt text, trimmed to a fixed maximum length +- meta row showing: + - publish date on the left + - views on the right (when available) +- footer row showing the content source (e.g., `youtube`, `podcast`, `blog`) + +If a field is not available (for example, views for some sources), the card MUST still render cleanly with that field omitted. + +#### Scenario: Card renders with all fields +- **WHEN** a content item has an image, title, summary, publish date, views, and source +- **THEN** the card renders those fields in the standard card layout order + +#### Scenario: Card renders without views +- **WHEN** a content item has no views data +- **THEN** the card renders the meta row with date and omits views without breaking the layout + +#### Scenario: Card renders without featured image +- **WHEN** a content item has no featured image +- **THEN** the card renders a placeholder media area and still renders the remaining fields + diff --git a/openspec/changes/card-layout/specs/social-content-aggregation/spec.md b/openspec/changes/card-layout/specs/social-content-aggregation/spec.md new file mode 100644 index 0000000..cabe8a5 --- /dev/null +++ b/openspec/changes/card-layout/specs/social-content-aggregation/spec.md @@ -0,0 +1,27 @@ +## MODIFIED Requirements + +### Requirement: Normalized content items +The system MUST normalize all ingested items (YouTube videos, Instagram posts, podcast episodes) into a single internal schema so the website can render them consistently. + +The normalized item MUST include at minimum: +- `id` (stable within its source) +- `source` (`youtube`, `instagram`, or `podcast`) +- `url` +- `title` +- `publishedAt` (ISO-8601) +- `thumbnailUrl` (optional) + +The system MUST support an optional summary field on normalized items when available from the source: +- `summary` (optional, short human-readable excerpt suitable for cards) + +#### Scenario: Normalizing a YouTube video +- **WHEN** the system ingests a YouTube video item +- **THEN** it produces a normalized item containing `id`, `source: youtube`, `url`, `title`, and `publishedAt` + +#### Scenario: Normalizing a podcast episode +- **WHEN** the system ingests a podcast RSS episode +- **THEN** it produces a normalized item containing `id`, `source: podcast`, `url`, `title`, and `publishedAt` + +#### Scenario: Summary available +- **WHEN** an ingested item provides summary/description content +- **THEN** the normalized item includes a `summary` suitable for rendering in cards diff --git a/openspec/changes/card-layout/tasks.md b/openspec/changes/card-layout/tasks.md new file mode 100644 index 0000000..39fce5b --- /dev/null +++ b/openspec/changes/card-layout/tasks.md @@ -0,0 +1,20 @@ +## 1. Card Component + Styles + +- [x] 1.1 Create a shared card component implementing the standard card layout (media, title, summary, meta row, footer) +- [x] 1.2 Add/adjust shared CSS so the card meta row uses `space-between` and the footer consistently shows the source label + +## 2. Data Model Support + +- [x] 2.1 Extend normalized `ContentItem` to support an optional `summary` field and ensure it is persisted in the content cache +- [x] 2.2 Populate `summary` for YouTube and podcast items during ingestion (safe trimming / fallback when missing) + +## 3. Apply Across Site + +- [x] 3.1 Update `ContentCard` surfaces (`/`, `/videos`, `/podcast`) to use the shared card layout and include date/views/source in the standard positions +- [x] 3.2 Update blog post cards (`/blog`, category listings) to use the shared card layout (including publish date and `blog` source footer) +- [x] 3.3 Ensure cards render cleanly when optional fields are missing (no image, no views, no summary) + +## 4. Verify + +- [x] 4.1 Add/update tests to assert standardized card structure/classes across `ContentCard` and blog post cards +- [x] 4.2 Build the site and verify `/videos`, `/podcast`, and `/blog` render cards matching the standard layout diff --git a/openspec/changes/deploy-without-node/.openspec.yaml b/openspec/changes/deploy-without-node/.openspec.yaml new file mode 100644 index 0000000..70eb9e0 --- /dev/null +++ b/openspec/changes/deploy-without-node/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-10 diff --git a/openspec/changes/deploy-without-node/design.md b/openspec/changes/deploy-without-node/design.md new file mode 100644 index 0000000..7f53099 --- /dev/null +++ b/openspec/changes/deploy-without-node/design.md @@ -0,0 +1,60 @@ +## Context + +- Production server environment is intentionally minimal: Docker is available, but Node.js is not installed on the host. +- The site needs a repeatable way to get to the latest built content on that server. + +## Goals / Non-Goals + +**Goals:** +- Update the deployed site to the latest content using Docker-only operations on the server. +- Keep the server host clean (no Node.js installation required). +- Make the refresh procedure repeatable and verifiable. + +**Non-Goals:** +- Building site artifacts directly on the server host outside containers. +- Introducing a new CMS/content authoring workflow. +- Solving content freshness triggers end-to-end (webhooks, scheduling) beyond what is needed to support a Docker-based refresh. + +## Decisions + +1. Build in CI, deploy as a Docker image +Why: keeps host clean and makes deploy deterministic. +Alternatives considered: +- Install Node.js on the host: rejected (violates clean server requirement). +- Build on the host inside a one-off container writing to a bind mount/volume: possible, but adds operational complexity and makes server resources part of the build pipeline. + +2. Refresh by pulling a published image and restarting the service +Why: the server only needs Docker + registry access. +Alternatives considered: +- File-based sync (rsync/scp) of static assets: can work, but requires separate artifact distribution and is easier to drift. +- Automated image updating (e.g., watchtower): may be useful later, but start with an explicit, documented operator command. + +3. Version visibility via image metadata +Why: operators need to confirm what is running. +Approach: +- Publish images with an immutable identifier (digest) and a human-friendly tag. +- Expose build metadata through standard Docker inspection and/or a small endpoint/static file in the image. + +## Risks / Trade-offs + +- [Risk] Content can be stale if the CI build does not run when content changes + Mitigation: add a scheduled build and/or content-change trigger in CI (future enhancement if not already present). + +- [Risk] Registry auth/secrets management on the server + Mitigation: use least-privilege registry credentials and Docker-native secret handling where available. + +- [Risk] Short downtime during restart + Mitigation: use `docker compose up -d` to minimize downtime; consider health checks and rolling strategies if/when multiple replicas are used. + +## Migration Plan + +- Add or update the Docker image build to produce a deployable image containing the built site output. +- Update server deployment configuration (compose/service) to run the published image. +- Document the operator refresh command(s): pull latest image, restart service, verify deployed version. +- Rollback strategy: re-deploy the previously known-good image tag/digest. + +## Open Questions + +- What is the authoritative "latest content" source (e.g., WordPress, filesystem, git) and what is the trigger to rebuild/publish a new image? +- Where should operator commands live (Makefile, `ops/` scripts, README section)? +- What is the current deployment target (single host compose, swarm, k8s) and should this change be scoped to one? \ No newline at end of file diff --git a/openspec/changes/deploy-without-node/proposal.md b/openspec/changes/deploy-without-node/proposal.md new file mode 100644 index 0000000..8c1038f --- /dev/null +++ b/openspec/changes/deploy-without-node/proposal.md @@ -0,0 +1,25 @@ +## Why + +The production server only provides Docker and does not have Node.js installed. We need a way to refresh the site to the latest content on that server without installing Node.js on the host. + +## What Changes + +- Add a Docker-first mechanism to update the deployed site to the latest content without requiring host-installed build tooling (no Node.js on the server). +- Standardize the deploy/update flow so the server updates are performed via Docker (e.g., pulling a new artifact/image and restarting, or running a containerized refresh job). +- Document and automate the update command(s) so content refresh is repeatable and low-risk. + +## Capabilities + +### New Capabilities +- `docker-content-refresh`: The deployed site can be updated to the latest content on a Docker-only server (no host Node.js), using a containerized workflow. + +### Modified Capabilities + +None. + +## Impact + +- Deployment/runtime: Docker compose/service definitions, update procedure, and operational docs. +- CI/CD: build/publish pipeline may need to produce and publish deployable artifacts suitable for Docker-only servers. +- Secrets/credentials: any content source credentials needed for refresh/build must be handled via Docker-friendly secret injection. +- Observability/ops: add or adjust logging/health checks around the refresh/update step to make failures visible. \ No newline at end of file diff --git a/openspec/changes/deploy-without-node/specs/docker-content-refresh/spec.md b/openspec/changes/deploy-without-node/specs/docker-content-refresh/spec.md new file mode 100644 index 0000000..ee85dc9 --- /dev/null +++ b/openspec/changes/deploy-without-node/specs/docker-content-refresh/spec.md @@ -0,0 +1,26 @@ +## ADDED Requirements + +### Requirement: Host update does not require Node.js +The system MUST provide an operator workflow to update the deployed site to the latest content without installing Node.js on the server host. Any build or content-fetch steps MUST run in containers and/or CI, not via host-installed Node.js. + +#### Scenario: Operator updates without host Node.js +- **WHEN** the server host has Docker available but does not have Node.js installed +- **THEN** the operator can complete the update procedure using Docker commands only + +### Requirement: Image-based content refresh is supported +The system MUST support refreshing the deployed site to the latest content by pulling a newly built deployable artifact (for example, a Docker image) and restarting the running service. + +#### Scenario: Successful refresh to latest image +- **WHEN** the operator runs the documented refresh command +- **THEN** the server pulls the latest published image and restarts the service using that image + +#### Scenario: Refresh failure does not break running site +- **WHEN** the operator runs the documented refresh command and the pull fails +- **THEN** the site remains running on the previously deployed image + +### Requirement: Refresh is repeatable and auditable +The system MUST document the refresh procedure and provide a way to verify which version is deployed (for example, image tag/digest or build metadata). + +#### Scenario: Operator verifies deployed version +- **WHEN** the operator runs the documented verification command +- **THEN** the system reports the currently deployed version identifier \ No newline at end of file diff --git a/openspec/changes/deploy-without-node/tasks.md b/openspec/changes/deploy-without-node/tasks.md new file mode 100644 index 0000000..2dede6a --- /dev/null +++ b/openspec/changes/deploy-without-node/tasks.md @@ -0,0 +1,25 @@ +## 1. Discovery And Current State + +- [ ] 1.1 Identify current deploy target and mechanism (compose/swarm/k8s, image vs files) and document constraints in `README` or `ops/` docs +- [ ] 1.2 Identify the content source(s) that define "latest content" (e.g., WordPress/API) and how builds currently fetch content +- [ ] 1.3 Confirm current build output (static assets) and runtime server (e.g., nginx) requirements + +## 2. Build And Publish A Deployable Artifact + +- [ ] 2.1 Ensure the repo can produce a deterministic production build inside CI (no host dependencies) +- [ ] 2.2 Create or update a Dockerfile to build the site and package the built output into a runtime image +- [ ] 2.3 Add build metadata to the image (tagging convention and/or embedded version file) +- [ ] 2.4 Configure CI to build and publish the image to a registry accessible by the server + +## 3. Server-Side Docker-Only Refresh Workflow + +- [ ] 3.1 Add or update the server Docker Compose/service definition to run the published image +- [ ] 3.2 Add documented operator commands to refresh to the latest image (pull + restart) +- [ ] 3.3 Add a verification command/procedure to show the currently deployed version (tag/digest/build metadata) +- [ ] 3.4 Define rollback procedure to re-deploy a previous known-good tag/digest + +## 4. Validation + +- [ ] 4.1 Validate a refresh on a test/staging server: pull latest image, restart, confirm content changes are visible +- [ ] 4.2 Validate failure mode: simulate pull failure and confirm the existing site remains serving +- [ ] 4.3 Update docs with a minimal "runbook" for operators (refresh, verify, rollback) \ No newline at end of file diff --git a/site/content/cache/content.json b/site/content/cache/content.json index 507025e..741469a 100644 --- a/site/content/cache/content.json +++ b/site/content/cache/content.json @@ -1,11 +1,12 @@ { - "generatedAt": "2026-02-10T06:16:26.031Z", + "generatedAt": "2026-02-10T07:10:57.264Z", "items": [ { "id": "gPGbtfQdaw4", "source": "youtube", "url": "https://www.youtube.com/watch?v=gPGbtfQdaw4", "title": "AI Agents Are Hiring HUMANS Now? RentAHuman.ai Explained", + "summary": "RentAHuman.ai is breaking the internet - a platform where AI agents can actually hire humans for physical-world tasks. In this video, I break down everything you need to know about this controversial new marketplace. 🤖 What is RentAHuman.…", "publishedAt": "2026-02-08T19:57:08.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/gPGbtfQdaw4/hqdefault.jpg", "metrics": { @@ -17,6 +18,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=aesTuu2nS-I", "title": "I will not die. Not today!!", + "summary": "I will not die. Not today! That's what this movie tells us. Sisu (Finnish) #movie #finnish #worldwar #gold", "publishedAt": "2026-02-05T05:53:25.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/aesTuu2nS-I/hqdefault.jpg", "metrics": { @@ -28,10 +30,11 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=9t8cBpZLHUo", "title": "I Can’t Believe This Exists: ThePrimeagen’s Terminal.shop is INSANE", + "summary": "I was absolutely flabbergasted when I saw terminal.shop for the first time. It’s a terminal-based online shopping experience by @ThePrimeTimeagen that proves we might have reached peak developer culture. In this video, we’re diving into th…", "publishedAt": "2026-02-05T04:31:18.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/9t8cBpZLHUo/hqdefault.jpg", "metrics": { - "views": 325 + "views": 328 } }, { @@ -39,6 +42,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=71S5viSJG20", "title": "Is This Real Life? ✈️ Ultra 4K Flight Over Europe’s Most Iconic Cities", + "summary": "Today we are exploring the stunningly detailed landscapes of Europe in Microsoft Flight Simulator 2024. From the historic streets of London to the romantic skyline of Paris, we’re pushing the new graphics engine to its absolute limits in 4…", "publishedAt": "2026-01-29T13:54:28.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/71S5viSJG20/hqdefault.jpg", "metrics": { @@ -50,6 +54,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=SO-tjsB4ZJs", "title": "16 OSCAR NOMINATIONS?! 🏆 This movie is Next Level.", + "summary": "I expected a good movie. I got a legendary one. Sinners just broke the record with 16 Academy Award nominations and honestly? It deserves more. This film is playing in a completely different league. 🎬🔥 #Sinners #Oscars #AcademyAwards #Mo…", "publishedAt": "2026-01-28T06:30:50.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/SO-tjsB4ZJs/hqdefault.jpg", "metrics": { @@ -61,6 +66,7 @@ "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)", + "summary": "Can a virtual plane handle a real-world monster storm? Today we are taking to the skies in Microsoft Flight Simulator 2024 to fly directly into the heart of Winter Storm Fern. With over 10,000 flights grounded in the real world, we’re the…", "publishedAt": "2026-01-26T13:12:12.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/FV30wjF1WQ4/hqdefault.jpg", "metrics": { @@ -72,6 +78,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=_1-albWBfoc", "title": "#Psychological #thriller #movie from #Indonesia", + "summary": "I never knew Indonesia had this good a movie industry! My bad! This one movie made me fan of Indonesian movies. Dendam Malam Kelam!!", "publishedAt": "2026-01-25T16:55:51.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/_1-albWBfoc/hqdefault.jpg", "metrics": { @@ -83,6 +90,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=ts-DWD8F68Q", "title": "Sleepless Skies: Finding Calm Above the Clouds ✈️ | MSFS 2024 (No Commentary)", + "summary": "For the night owls and the restless minds. 🌌✨ It’s one of those nights where sleep won’t come, so I’m taking to the skies to find some stillness. Join me for a midnight tour of the world’s most iconic locations in Microsoft Flight Simulat…", "publishedAt": "2026-01-24T18:12:01.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/ts-DWD8F68Q/hqdefault.jpg", "metrics": { @@ -94,6 +102,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=nlbkGnznzA8", "title": "A #movie that will creep you out! #survival", + "summary": "Are you in a mood for a survival-thriller movie? I've a good suggestion. Deeply disturbing one though. Later learned that it was based on a real incident. #welcomehome #movie #indian #thriller", "publishedAt": "2026-01-22T02:21:01.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/nlbkGnznzA8/hqdefault.jpg", "metrics": { @@ -105,6 +114,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=zR9Ey8DjG5s", "title": "A Tour of the World’s Most Iconic Cities 🌍✨✈️ | MSFS 2020", + "summary": "For the night owls and the restless minds. 🌌✨ It’s one of those nights where sleep won’t come, so I’m taking to the skies to find some stillness. Join me for a midnight tour of the world’s most iconic locations in Microsoft Flight Simulat…", "publishedAt": "2026-01-20T17:02:14.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/zR9Ey8DjG5s/hqdefault.jpg", "metrics": { @@ -116,6 +126,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=oerSPWeIy5k", "title": "🎥 Dive into the quirky world of Kumiko", + "summary": "🎥 Dive into the quirky world of “Kumiko: The Treasure Hunter” – a hidden gem inspired by Fargo’s wild tales! This offbeat adventure follows a Tokyo office worker on a surreal quest for buried treasure in America’s frozen Midwest. Perfect…", "publishedAt": "2026-01-20T03:01:02.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/oerSPWeIy5k/hqdefault.jpg", "metrics": { @@ -127,6 +138,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=fzw7GUszgdQ", "title": "RUSTY DRIVER RETURNS 🏎️ F1 25 Short Season Practice (Logitech G920)", + "summary": "Back in the cockpit! 🏁 Tonight, we are shaking off the rust in F1 25. It’s been a long time since I hit the track, so I'm jumping into a short season career mode to practice and find the limit again. I'm playing on the Xbox Series S using…", "publishedAt": "2026-01-18T17:36:36.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/fzw7GUszgdQ/hqdefault.jpg", "metrics": { @@ -138,6 +150,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=dlIADQOfXlQ", "title": "Sleepless Skies: Finding Calm Above the Clouds ✈️ | MSFS 2020 (No Commentary)", + "summary": "For the night owls and the restless minds. 🌌✨ It’s one of those nights where sleep won’t come, so I’m taking to the skies to find some stillness. Join me for a midnight tour of the world’s most iconic locations in Microsoft Flight Simulat…", "publishedAt": "2026-01-16T18:00:30.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/dlIADQOfXlQ/hqdefault.jpg", "metrics": { @@ -149,6 +162,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=0-AX9KaJUSg", "title": "💸 Your Daily Reset 🧘 Cities: Skylines | Billionaire Paradise Build | Lofi", + "summary": "Tired of the high-stakes pressure cooker? Me too. Today is about the \"Slow Game.\" We’re trading the finish line for a skyline, building a tranquil billionaire's paradise where the only goal is beauty and peace. The Vibe: - Zero-stress envi…", "publishedAt": "2026-01-16T01:07:06.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/0-AX9KaJUSg/hqdefault.jpg", "metrics": { @@ -160,6 +174,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=xiSka36EF5c", "title": "Episode 41a (Recap) - US History Podcast Catch‑Up: From Colonization to the Early Civil Rights Mo...", + "summary": "I’m back - and I owe you an apology. I went AWOL after September for personal reasons, but US History - Understanding This Country by Irregular Mind is back in the groove. In this recap episode, I quickly bring you up to speed on everythin…", "publishedAt": "2026-01-15T16:58:14.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/xiSka36EF5c/hqdefault.jpg", "metrics": { @@ -171,6 +186,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=oBXH9VhnZCs", "title": "No Mic, Just Silk: Finding My Way in Silksong | Stress-Busting Stream", + "summary": "Stepping into the unknown to find some peace. 🕯️✨ Today, I’m putting down the flight controls and picking up the needle. I’ve heard so much about Hollow Knight: Silksong, and as someone looking to bust some stress, I’ve decided to dive in…", "publishedAt": "2026-01-14T16:17:29.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/oBXH9VhnZCs/hqdefault.jpg", "metrics": { @@ -215,6 +231,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=mj5-oTVJ0AU", "title": "America from Above: Iconic Landmarks & Relaxing Lofi Vibes | MSFS 2020", + "summary": "From the Statue of Liberty to the Golden Gate. 🏙️✨ Today, we’re crossing the ocean to navigate the most recognizable skylines in the world. Experience the towering skyscrapers of New York City, the historic monuments of Washington D.C., a…", "publishedAt": "2026-01-13T16:35:38.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/mj5-oTVJ0AU/hqdefault.jpg", "metrics": { @@ -237,6 +254,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=zaQyRCujqk4", "title": "No More Red Roads! 🛑 Fixing Yesterday’s Traffic Mess in Cities: Skylines", + "summary": "Is your city's traffic turning red? 🛑 In this quick highlight from my latest stream, I take on one of the worst traffic bottlenecks in my city and fix it using professional road hierarchy and some clever lane management. If you've ever st…", "publishedAt": "2026-01-12T10:26:54.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/zaQyRCujqk4/hqdefault.jpg", "metrics": { @@ -259,6 +277,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=m5OEtszkSyA", "title": "Can an Introvert Build the Perfect City? | Cities: Skylines Zen Stream", + "summary": "Welcome to a quiet corner of the internet. Today, we are building, zoning, and managing traffic in Cities: Skylines. As an introverted creator, I find peace in the details of city planning. You won't find loud commentary or high-energy sho…", "publishedAt": "2026-01-11T22:18:08.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/m5OEtszkSyA/hqdefault.jpg", "metrics": { @@ -270,6 +289,7 @@ "source": "youtube", "url": "https://www.youtube.com/watch?v=XsJCIeqFWCY", "title": "🚧 ZERO Traffic Jams in Cities: Skylines? My Impossible Build!", + "summary": "Is your city's traffic turning red? 🛑 In this quick highlight from my latest stream, I take on one of the worst traffic bottlenecks in my city and fix it using professional road hierarchy and some clever lane management. If you've ever st…", "publishedAt": "2026-01-11T22:15:03.000Z", "thumbnailUrl": "https://i.ytimg.com/vi/XsJCIeqFWCY/hqdefault.jpg", "metrics": { @@ -281,6 +301,7 @@ "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", + "summary": "Dive into the pivotal summer of 1963 in this episode of US History - Understanding This Country, hosted by Santhosh Janardhanan. Explore the Birmingham Campaign's bold Project C, where brave children faced fire hoses and police dogs in the…", "publishedAt": "2026-01-24T03:17:34.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -289,6 +310,7 @@ "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", + "summary": "Episode 42 is where the Civil Rights Movement shifts from courtrooms to the streets. After legal wins like Brown v. Board of Education, activists and students push for real change in everyday life-at lunch counters, bus stations, and on in…", "publishedAt": "2026-01-16T19:59:44.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -297,6 +319,7 @@ "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 Catch‑Up: From Colonization to the Early Civil Rights Movement", + "summary": "I’m back - and I owe you an apology. I went AWOL after September for personal reasons, but US History - Understanding This Country by Irregular Mind is back in the groove. In this recap episode, I quickly bring you up to speed on everythin…", "publishedAt": "2026-01-15T16:24:16.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -305,6 +328,7 @@ "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", + "summary": "Explore the early US civil rights movement: Brown v. Board of Education, Montgomery Bus Boycott, Little Rock Nine, Rosa Parks, Martin Luther King Jr., school integration, nonviolence, and resistance in the 1950s. Legal cases, social justic…", "publishedAt": "2025-09-20T03:47:37.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -313,6 +337,7 @@ "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", + "summary": "Step into the 1950s: GI Bill-fueled growth, Levittown suburbs, TV in every living room, and rock ’n’ roll teens—alongside redlining, poverty, and early sparks of Civil Rights. Booming, but brittle. Listen now.", "publishedAt": "2025-09-15T02:28:58.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -321,6 +346,7 @@ "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: America’s First Test of the Cold War", + "summary": "From the North Korean invasion to MacArthur’s Inchon landing, Chinese intervention, and Truman’s clash with his general — discover how the Korean War became the blueprint for Cold War conflicts and earned the name “The Forgotten War.”", "publishedAt": "2025-09-07T04:58:14.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -329,6 +355,7 @@ "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", + "summary": "In 1945, victory brought hope and change. From the GI Bill and baby boom to the UN, Marshall Plan, Berlin Airlift, and Truman’s Fair Deal — discover how America emerged as a global leader while stepping into the Cold War.", "publishedAt": "2025-08-16T03:52:22.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -337,6 +364,7 @@ "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", + "summary": "Coincidentally, on the anniversary of the bombing of Nagasaki, we retrace America’s path to victory in WWII — from D-Day’s stormed beaches and the Battle of the Bulge, to the Pacific war, the A-Bomb, and the dawn of a new world order.", "publishedAt": "2025-08-09T04:15:45.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -345,6 +373,7 @@ "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", + "summary": "Before Normandy and D-Day, there was fear, fascism, and a world unraveling. In this episode, we trace the rise of totalitarian regimes in Europe and Asia, the blitzkrieg that stunned the West, and the slow but steady shift in American sent…", "publishedAt": "2025-08-02T03:41:33.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -353,6 +382,7 @@ "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", + "summary": "When the Roaring Twenties crashed into economic ruin, America found itself spiraling into the Great Depression. In this episode, we explore the causes, the fallout, and the ambitious response—from Hoover’s failure to FDR’s New Deal. Escapi…", "publishedAt": "2025-07-26T02:32:26.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -361,6 +391,7 @@ "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", + "summary": "Explore the dazzling highs and hidden lows of 1920s America - from jazz clubs and cultural revolutions to rising nativism, fundamentalism, and an economy teetering on collapse. This episode dives deep into how the decade shaped modern Amer…", "publishedAt": "2025-07-19T03:58:56.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -369,6 +400,7 @@ "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", + "summary": "Explore America’s entry into World War I, from trench warfare and propaganda to Wilson’s Fourteen Points and the Treaty of Versailles. Discover how the Great War reshaped the U.S. and set the stage for the Roaring Twenties.", "publishedAt": "2025-07-12T04:33:52.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -377,6 +409,7 @@ "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", + "summary": "In this episode, we follow America’s bold stride into global influence and domestic reform through the eyes of Theodore Roosevelt, William Howard Taft, and Woodrow Wilson. From the Panama Canal to trust-busting, and from Dollar Diplomacy t…", "publishedAt": "2025-07-05T03:30:53.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -385,6 +418,7 @@ "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", + "summary": "Explore how the United States expanded its reach beyond its borders through imperial ambition, war, and diplomacy. From Hawaii to the Philippines, this episode traces America’s rise as a world power.", "publishedAt": "2025-06-28T03:30:57.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -393,6 +427,7 @@ "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", + "summary": "Explore the transformative Progressive Era in U.S. history — from trust-busting and muckraking journalism to child labor laws, women’s suffrage, and civil rights movements. This episode dives into how reformers, activists, and everyday cit…", "publishedAt": "2025-06-19T03:42:14.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -401,6 +436,7 @@ "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", + "summary": "In this episode, we explore the explosive rise of American cities during the late 1800s and early 1900s. From Ellis Island to ethnic neighborhoods, from nativist backlash to reform movements, discover how waves of new immigrants shaped the…", "publishedAt": "2025-06-14T03:58:41.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -409,6 +445,7 @@ "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", + "summary": "In this episode, we explore the Second Industrial Revolution — a time of booming invention, corporate empires, and factory-floor struggles. From Edison’s lightbulb to Ford’s assembly line, and from the rise of the corporation to the birth…", "publishedAt": "2025-06-07T03:51:30.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -417,6 +454,7 @@ "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", + "summary": "The American frontier was not an empty land — it was home. In this powerful episode, we uncover the story of the Lakota and other Plains tribes who resisted removal, reservations, and forced assimilation. From Custer’s Last Stand to the bo…", "publishedAt": "2025-05-31T04:14:46.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -425,6 +463,7 @@ "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", + "summary": "From steam engines to cattle drives, and from homesteads to populist rallies — this episode explores how the American West was won, worked, and mythologized. Discover how the Transcontinental Railroad changed everything, how farmers organi…", "publishedAt": "2025-05-24T03:30:17.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -433,6 +472,7 @@ "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", + "summary": "After the Civil War, the United States faced its most difficult question yet: how do you rebuild a country that just tried to destroy itself? In this episode, we explore the highs and heartbreaks of Reconstruction — from the promise of fre…", "publishedAt": "2025-05-17T04:35:58.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -441,6 +481,7 @@ "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", + "summary": "Dive deep into the American Civil War — a conflict that shattered the nation, ended slavery, and changed the course of U.S. history. This episode traces the path from secession to surrender, explores key battles like Gettysburg and Antieta…", "publishedAt": "2025-05-10T04:07:23.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -449,6 +490,7 @@ "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", + "summary": "In this gripping episode of US History – Understanding This Country, we trace the volatile road to the American Civil War—from the Wilmot Proviso to Lincoln’s First Inaugural Address. Learn how heated debates over slavery, landmark legisla…", "publishedAt": "2025-05-03T03:30:33.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -457,6 +499,7 @@ "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", + "summary": "In this episode of US History – Understanding This Country, explore the powerful movements that reshaped America in the mid-1800s. From immigration waves and city life challenges to the Second Great Awakening, the Temperance Movement, pris…", "publishedAt": "2025-04-26T03:38:18.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -465,6 +508,7 @@ "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", + "summary": "In this episode of US History – Understanding This Country, we explore how Texas went from Mexican territory to independent republic—and how that sparked the Mexican-American War. Learn about the Alamo, the Battle of San Jacinto, the Treat…", "publishedAt": "2025-04-19T03:26:55.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -473,6 +517,7 @@ "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", + "summary": "In this immersive episode of US History – Understanding This Country, we journey along the iconic Oregon Trail — the 2,000-mile route that carried America’s pioneer spirit westward. From river crossings and food shortages to the resilience…", "publishedAt": "2025-04-12T04:39:58.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -481,6 +526,7 @@ "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", + "summary": "In this gripping episode of US History – Understanding This Country, we journey deep into the legendary Oregon Trail — the 2,000-mile path that turned ordinary families into pioneers and a young nation into a continental power. Discover wh…", "publishedAt": "2025-04-04T23:00:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -489,6 +535,7 @@ "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", + "summary": "In this powerful episode of US History – Understanding This Country, host Santhosh Janardhanan unpacks the pivotal elections of 1824, 1828, and 1832—revealing how political rivalries, populist movements, and bitter controversies shaped the…", "publishedAt": "2025-03-29T04:10:39.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -497,6 +544,7 @@ "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", + "summary": "Was the “Era of Good Feelings” truly a time of peace and progress—or a calm before the storm? In Episode 17 of US History – Understanding This Country, we explore how the post-War of 1812 boom brought nationalism, innovation, and expansion…", "publishedAt": "2025-03-22T01:00:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -505,6 +553,7 @@ "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", + "summary": "The War of 1812 shaped America’s destiny—Tecumseh’s confederation, the Battle of Tippecanoe, the burning of Washington D.C., the Star-Spangled Banner, and Andrew Jackson’s victory at New Orleans. Discover how this war redefined U.S. indepe…", "publishedAt": "2025-03-15T01:00:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -513,6 +562,7 @@ "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", + "summary": "Thomas Jefferson’s presidency reshaped America—doubling its size with the Louisiana Purchase, battling the Supreme Court, and enforcing the disastrous Embargo Act. Explore his legacy in this episode!", "publishedAt": "2025-03-08T05:43:34.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -521,6 +571,7 @@ "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", + "summary": "Dive into the transformative era of the first presidents as they laid the groundwork for the United States by setting precedents in early America. This episode traces George Washington’s historic election, the formation of his cabinet, and…", "publishedAt": "2025-03-01T04:33:33.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -529,6 +580,7 @@ "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", + "summary": "The U.S. Constitution was written—but could it survive the fight for approval? In this episode, we dive into the ratification debates, the clash between Federalists and Anti-Federalists, and the political deals that saved the Constitution.…", "publishedAt": "2025-02-23T03:35:43.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -537,6 +589,7 @@ "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", + "summary": "The U.S. Constitution wasn’t built overnight—it was shaped by intense debates and hard-fought compromises. In this episode, we dive into The Great Compromise, The Three-Fifths Compromise, and how these decisions created the foundation for…", "publishedAt": "2025-02-16T04:57:29.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -545,6 +598,7 @@ "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", + "summary": "How did the U.S. go from revolution to a functioning government? Of course, it was not a cake walk. In this episode, we break down the period of Article of Confederation - the first government in the U.S, creation of the Constitution, the…", "publishedAt": "2025-02-09T04:42:25.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -553,6 +607,7 @@ "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 Revolution’s Final Stand", + "summary": "The American Revolution wasn’t just won on battlefields—it was won through resilience, strategy, and unexpected allies. In this episode, we cover the final battles, the global impact of the war, and the unsung heroes who made independence…", "publishedAt": "2025-02-01T02:00:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -561,6 +616,7 @@ "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", + "summary": "Explore the gritty journey from the Declaration of Independence to the early milestones of the American Revolution. In this episode, we cover the Battle of Long Island, Washington’s daring Delaware crossing, the turning point at Saratoga,…", "publishedAt": "2025-01-26T04:01:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -569,6 +625,7 @@ "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", + "summary": "Join us in Episode 8 of Irregular Mind as we explore the pivotal moments that shaped America's fight for freedom. From the Second Continental Congress to the bold stand at Bunker Hill, the Olive Branch Petition, and Washington’s victory in…", "publishedAt": "2025-01-18T05:08:28.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -577,6 +634,7 @@ "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", + "summary": "Dive into the electrifying moments that sparked the American Revolution in this episode of Irregular Mind. From the bold actions of the Sons of Liberty and Patrick Henry’s fiery “Give me liberty or give me death” speech to the first shots…", "publishedAt": "2025-01-10T04:14:48.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -585,6 +643,7 @@ "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", + "summary": "In this episode of Irregular Mind, we trace the growing tensions that lit the fuse of the American Revolution. Explore the cries of “No Taxation Without Representation”, the defiance of the Stamp Act, and the fiery rhetoric of Patrick Henr…", "publishedAt": "2025-01-04T14:12:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -593,6 +652,7 @@ "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", + "summary": "Dive into the pivotal moments that set the stage for the American Revolution in this episode of Irregular Mind. Explore the impact of the English Bill of Rights, the Great Awakening, and the Intolerable Acts as tensions between Britain and…", "publishedAt": "2024-12-28T03:46:34.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -601,6 +661,7 @@ "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", + "summary": "Explore the birth of Colonial America in this episode of Irregular Mind. Discover the rise of Virginia, the establishment of the Thirteen Colonies, and how regional differences shaped the early American identity. From Jamestown’s struggles…", "publishedAt": "2024-12-20T13:30:00.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -609,6 +670,7 @@ "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", + "summary": "Discover the dramatic shifts in power as the Spanish Armada falls, the rise of tobacco in Virginia, and the relentless search for the Northwest Passage. Explore the early European settlements, trade networks, and cultural exchanges that sh…", "publishedAt": "2024-12-17T04:34:51.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -617,6 +679,7 @@ "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", + "summary": "Uncover the pivotal moments that reshaped history in Episode Two of Irregular Mind's U.S. history series, \"Understanding This Country.\" From Columbus' daring voyage and the race for riches in 15th-century Europe to the clash of cultures, t…", "publishedAt": "2024-12-09T05:11:09.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -625,6 +688,7 @@ "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", + "summary": "I came to the United States chasing opportunities, but I quickly realized that this land is more than just skyscrapers and Hollywood. The history of this place is layered, complicated, and, honestly, sometimes stranger than fiction. And th…", "publishedAt": "2024-12-02T05:02:25.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded/7490178/7490178-1644680234566-cf5628ab210f1.jpg" }, @@ -633,6 +697,7 @@ "source": "podcast", "url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/One-Person---One-Relation-e1fkfab", "title": "One Person - One Relation", + "summary": "A common issue faced by most of us - relationships. Just a wakeup call on it.", "publishedAt": "2022-03-12T19:43:48.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded400/7490178/7490178-1644680237670-d15b3f1acda1b.jpg" }, @@ -641,6 +706,7 @@ "source": "podcast", "url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Conclusion-e1dvq6i", "title": "Dharmaraja.- Conclusion", + "summary": "Conclusion and Aftermath of Yogi's plot.", "publishedAt": "2022-02-05T19:28:44.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644089311927-91c36cb8383e5.jpg" }, @@ -649,6 +715,7 @@ "source": "podcast", "url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-28-e1dvpa3", "title": "Dharmaraja - Episode 28", + "summary": "The final showdown. Major players face off. And Yogi does the undoable.", "publishedAt": "2022-02-05T19:02:09.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644087715926-5f708b8948277.jpg" }, @@ -657,6 +724,7 @@ "source": "podcast", "url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-27-e1dvmg0", "title": "Dharmaraja - Episode 27", + "summary": "Yogi is shocked with a surprise news. Relationship between brothers crack. Can Yogi see what kind of mess he is into?", "publishedAt": "2022-02-05T17:52:55.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644083569325-d4aaaf3e6d72.jpg" }, @@ -665,6 +733,7 @@ "source": "podcast", "url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-26-e1duh8e", "title": "Dharmaraja - Episode 26", + "summary": "Helplessness grasps Chandrakkaran. Thripurasundarikunjamma sees visions...", "publishedAt": "2022-02-04T21:12:06.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1644083230812-bbcda04085e26.jpg" }, @@ -673,6 +742,7 @@ "source": "podcast", "url": "https://podcasters.spotify.com/pod/show/the-irregular-mind/episodes/Dharmaraja---Episode-25-e1dsusg", "title": "Dharmaraja - Episode 25", + "summary": "Conflict comes to day light. Kesavan Kunju escapes. Padathalavan is all out beast mode after knowing what happened to Kuppassar.", "publishedAt": "2022-02-03T21:23:34.000Z", "thumbnailUrl": "https://d3t3ozftmdmh3i.cloudfront.net/production/podcast_uploaded_episode/7490178/7490178-1643923406877-eef4729d8dab5.jpg" } diff --git a/site/public/styles/global.css b/site/public/styles/global.css index e51a636..ea8e00d 100644 --- a/site/public/styles/global.css +++ b/site/public/styles/global.css @@ -114,46 +114,7 @@ a { gap: 14px; } -.blog-card { - border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.1); - background: rgba(255, 255, 255, 0.04); - overflow: hidden; - transition: - transform 120ms ease, - background 120ms ease; -} - -.blog-card:hover { - transform: translateY(-2px); - background: rgba(255, 255, 255, 0.06); -} - -.blog-card img { - width: 100%; - height: 180px; - object-fit: cover; - display: block; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); -} - -.blog-card-body { - padding: 12px 12px 14px; -} - -.blog-card-title { - margin: 0 0 8px; - font-size: 15px; - line-height: 1.25; - letter-spacing: -0.01em; -} - -.blog-card-excerpt { - margin: 0; - color: var(--muted); - font-size: 13px; - line-height: 1.5; -} +/* blog cards are now rendered via the shared `.card` component styles */ .prose { line-height: 1.75; @@ -256,53 +217,88 @@ a { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; + align-items: stretch; } .card { - display: grid; - grid-template-columns: 110px 1fr; - gap: 12px; - padding: 12px; + display: flex; + flex-direction: column; + height: 100%; border-radius: 16px; border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.04); + overflow: hidden; transition: transform 120ms ease, background 120ms ease; } +.card-media { + flex: 0 0 auto; +} + .card:hover { transform: translateY(-2px); background: rgba(255, 255, 255, 0.06); } .card-media img { - width: 110px; - height: 70px; - border-radius: 10px; + width: 100%; + height: 180px; object-fit: cover; - border: 1px solid rgba(255, 255, 255, 0.1); + display: block; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); } .card-placeholder { - width: 110px; - height: 70px; - border-radius: 10px; + width: 100%; + height: 180px; background: rgba(255, 255, 255, 0.06); - border: 1px solid rgba(255, 255, 255, 0.1); + border-bottom: 1px solid rgba(255, 255, 255, 0.08); } -.card-meta { +.card-body { display: flex; - gap: 10px; - align-items: center; - font-size: 12px; + flex: 1; + flex-direction: column; + padding: 0; +} + +.card-content { + flex: 1; + padding: 12px 12px 12px; + background: linear-gradient(180deg, rgba(15, 27, 56, 0.75), rgba(11, 16, 32, 0.32)); } .card-title { - margin: 8px 0 0; - font-size: 14px; - line-height: 1.35; + margin: 0 0 8px; + font-size: 15px; + line-height: 1.25; + letter-spacing: -0.01em; +} + +.card-summary { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.5; +} + +.card-footer { + margin-top: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-top: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(11, 16, 32, 0.45); + font-size: 12px; +} + +.card-footer .card-views { + flex: 1; + text-align: center; } .pill { @@ -326,6 +322,10 @@ a { border-color: rgba(255, 205, 74, 0.35); } +.pill-blog { + border-color: rgba(140, 88, 255, 0.35); +} + .empty { padding: 16px; border-radius: 14px; @@ -351,12 +351,8 @@ a { .blog-grid { grid-template-columns: 1fr; } - .card { - grid-template-columns: 90px 1fr; - } .card-media img, .card-placeholder { - width: 90px; - height: 60px; + height: 200px; } } diff --git a/site/src/components/BlogPostCard.astro b/site/src/components/BlogPostCard.astro index 2f416ef..4be6b08 100644 --- a/site/src/components/BlogPostCard.astro +++ b/site/src/components/BlogPostCard.astro @@ -1,5 +1,6 @@ --- import type { WordpressPost } from "../lib/content/types"; +import StandardCard from "./StandardCard.astro"; type Props = { post: WordpressPost; @@ -10,26 +11,34 @@ type Props = { const { post, placement, targetId } = Astro.props; function truncate(s: string, n: number) { - if (!s) return ""; - const t = s.trim(); + const t = (s || "").trim(); + if (!t) return ""; if (t.length <= n) return t; return `${t.slice(0, Math.max(0, n - 1)).trimEnd()}…`; } + +const d = new Date(post.publishedAt); +const dateLabel = Number.isFinite(d.valueOf()) + ? d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }) + : ""; --- - - {post.featuredImageUrl ? : null} -
-

{post.title}

-

{truncate(post.excerpt || "", 180)}

-
-
+ title={post.title} + summary={post.excerpt} + imageUrl={post.featuredImageUrl} + dateLabel={dateLabel} + viewsLabel={undefined} + sourceLabel="blog" + isExternal={false} + linkAttrs={{ + "data-umami-event": "click", + "data-umami-event-target_id": targetId, + "data-umami-event-placement": placement, + "data-umami-event-target_url": `/blog/post/${post.slug}`, + "data-umami-event-title": truncate(post.title, 160), + "data-umami-event-type": "blog_post", + }} +/> + diff --git a/site/src/components/ContentCard.astro b/site/src/components/ContentCard.astro index fe9913d..99b9d26 100644 --- a/site/src/components/ContentCard.astro +++ b/site/src/components/ContentCard.astro @@ -1,5 +1,6 @@ --- import type { ContentItem } from "../lib/content/types"; +import StandardCard from "./StandardCard.astro"; type Props = { item: ContentItem; @@ -36,40 +37,24 @@ const umamiType = const umamiTitle = umamiType ? truncate(item.title, 160) : undefined; --- - -
- { - item.thumbnailUrl ? ( - - ) : ( -
- ) - } -
-
-
- {item.source} - {dateLabel ? {dateLabel} : null} - { - item.metrics?.views !== undefined ? ( - {item.metrics.views.toLocaleString()} views - ) : null - } -
-

{item.title}

-
-
+ title={item.title} + summary={item.summary} + imageUrl={item.thumbnailUrl} + dateLabel={dateLabel} + viewsLabel={item.metrics?.views !== undefined ? `${item.metrics.views.toLocaleString()} views` : undefined} + sourceLabel={item.source} + isExternal={true} + linkAttrs={{ + "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-title": umamiTitle, + "data-umami-event-type": umamiType, + "data-umami-event-domain": domain || "unknown", + "data-umami-event-source": item.source, + "data-umami-event-ui_placement": "content_card", + }} +/> diff --git a/site/src/components/StandardCard.astro b/site/src/components/StandardCard.astro new file mode 100644 index 0000000..61b581a --- /dev/null +++ b/site/src/components/StandardCard.astro @@ -0,0 +1,63 @@ +--- +type Props = { + href: string; + title: string; + summary?: string; + imageUrl?: string; + dateLabel?: string; + viewsLabel?: string; + sourceLabel: string; + isExternal?: boolean; + linkAttrs?: Record; +}; + +const { + href, + title, + summary, + imageUrl, + dateLabel, + viewsLabel, + sourceLabel, + isExternal, + linkAttrs, +} = Astro.props; + +function truncate(s: string, n: number) { + const t = (s || "").trim(); + if (!t) return ""; + if (t.length <= n) return t; + // ASCII ellipsis to avoid encoding issues in generated HTML. + return `${t.slice(0, Math.max(0, n - 3)).trimEnd()}...`; +} + +const summaryText = truncate(summary || "", 180); + +--- + + +
+ {imageUrl ? :
} +
+ +
+
+

{title}

+ {summaryText ?

{summaryText}

: null} +
+ + +
+
diff --git a/site/src/lib/content/types.ts b/site/src/lib/content/types.ts index 6c615a7..fe972ba 100644 --- a/site/src/lib/content/types.ts +++ b/site/src/lib/content/types.ts @@ -9,6 +9,7 @@ export type ContentItem = { source: ContentSource; url: string; title: string; + summary?: string; publishedAt: string; // ISO-8601 thumbnailUrl?: string; metrics?: ContentMetrics; diff --git a/site/src/lib/ingest/podcast.ts b/site/src/lib/ingest/podcast.ts index f4f56e1..7c62182 100644 --- a/site/src/lib/ingest/podcast.ts +++ b/site/src/lib/ingest/podcast.ts @@ -8,16 +8,40 @@ export async function fetchPodcastRss(rssUrl: string, limit = 50): Promise]+>/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function truncate(s: string, n: number) { + const t = stripHtml(s); + if (!t) return ""; + if (t.length <= n) return t; + return `${t.slice(0, Math.max(0, n - 1)).trimEnd()}…`; +} + 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(); + const summary = truncate( + (it.contentSnippet || + it.summary || + it.content || + it["content:encoded"] || + it.itunes?.subtitle || + "").toString(), + 240, + ); return { id, source: "podcast" as const, url, title: (it.title || "").toString(), + summary: summary || undefined, publishedAt: new Date(publishedAt).toISOString(), thumbnailUrl: (it.itunes?.image || undefined) as string | undefined, }; diff --git a/site/src/lib/ingest/youtube.ts b/site/src/lib/ingest/youtube.ts index ff2ed69..14792cf 100644 --- a/site/src/lib/ingest/youtube.ts +++ b/site/src/lib/ingest/youtube.ts @@ -6,11 +6,26 @@ type YoutubeApiVideo = { id: string; url: string; title: string; + summary?: string; publishedAt: string; thumbnailUrl?: string; views?: number; }; +function stripHtml(s: string) { + return (s || "") + .replace(/<[^>]+>/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function truncate(s: string, n: number) { + const t = stripHtml(s); + if (!t) return ""; + if (t.length <= n) return t; + return `${t.slice(0, Math.max(0, n - 1)).trimEnd()}…`; +} + export async function fetchYoutubeViaRss(channelId: string, limit = 20): Promise { const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${encodeURIComponent(channelId)}`; const parser = new Parser(); @@ -32,11 +47,16 @@ export function normalizeYoutubeRssFeedItems(items: any[], limit: number): Conte const url = it.link || ""; const id = (it.id || url).toString(); const publishedAt = (it.isoDate || it.pubDate || new Date(0).toISOString()).toString(); + const summary = truncate( + (it.contentSnippet || it.summary || it.content || it["content:encoded"] || "").toString(), + 240, + ); return { id, source: "youtube" as const, url, title: (it.title || "").toString(), + summary: summary || undefined, publishedAt: new Date(publishedAt).toISOString(), thumbnailUrl: (it.enclosure?.url || undefined) as string | undefined, }; @@ -47,7 +67,12 @@ export function normalizeYoutubeRssFeedItems(items: any[], limit: number): Conte export function normalizeYoutubeApiVideos( items: Array<{ id: string; - snippet: { title: string; publishedAt: string; thumbnails?: Record }; + snippet: { + title: string; + description?: string; + publishedAt: string; + thumbnails?: Record; + }; statistics?: { viewCount?: string }; }>, ): ContentItem[] { @@ -55,6 +80,7 @@ export function normalizeYoutubeApiVideos( id: v.id, url: `https://www.youtube.com/watch?v=${encodeURIComponent(v.id)}`, title: v.snippet.title, + summary: v.snippet.description ? truncate(v.snippet.description, 240) : undefined, 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, @@ -65,6 +91,7 @@ export function normalizeYoutubeApiVideos( source: "youtube", url: v.url, title: v.title, + summary: v.summary, publishedAt: v.publishedAt, thumbnailUrl: v.thumbnailUrl, metrics: v.views !== undefined ? { views: v.views } : undefined, diff --git a/site/tests/blog-umami-attributes.test.ts b/site/tests/blog-umami-attributes.test.ts index 5ed0d63..30ec4cf 100644 --- a/site/tests/blog-umami-attributes.test.ts +++ b/site/tests/blog-umami-attributes.test.ts @@ -22,9 +22,9 @@ describe("blog umami event attributes", () => { it("instruments blog post cards with deterministic target_id and placement", async () => { const src = await read("src/components/BlogPostCard.astro"); - expect(src).toContain('data-umami-event="click"'); - expect(src).toContain("data-umami-event-target_id={targetId}"); - expect(src).toContain("data-umami-event-placement={placement}"); + expect(src).toContain('"data-umami-event": "click"'); + expect(src).toContain('"data-umami-event-target_id": targetId'); + expect(src).toContain('"data-umami-event-placement": placement'); expect(src).toContain("data-umami-event-target_url"); }); @@ -61,4 +61,3 @@ describe("blog umami event attributes", () => { expect(categorySrc).toContain("targetId={`blog.category.${activeCategory.slug}.card.post.${p.slug}`}"); }); }); - diff --git a/site/tests/card-layout.test.ts b/site/tests/card-layout.test.ts new file mode 100644 index 0000000..e2ea30f --- /dev/null +++ b/site/tests/card-layout.test.ts @@ -0,0 +1,29 @@ +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("card layout", () => { + it("uses a shared StandardCard component for content and blog cards", async () => { + const contentCard = await read("src/components/ContentCard.astro"); + expect(contentCard).toContain('import StandardCard from "./StandardCard.astro";'); + expect(contentCard).toContain(" { + const css = await read("public/styles/global.css"); + + expect(css).toContain(".card-content"); + expect(css).toContain("justify-content: space-between"); + expect(css).toContain(".card-footer"); + expect(css).toContain(".card-summary"); + }); +}); diff --git a/site/tests/content-title-type-attributes.test.ts b/site/tests/content-title-type-attributes.test.ts index 834c1ab..23e92d3 100644 --- a/site/tests/content-title-type-attributes.test.ts +++ b/site/tests/content-title-type-attributes.test.ts @@ -31,7 +31,7 @@ describe("content link umami title/type attributes", () => { it("adds title/type on blog post cards and pages links", async () => { const cardSrc = await read("src/components/BlogPostCard.astro"); - expect(cardSrc).toContain('data-umami-event-type="blog_post"'); + expect(cardSrc).toContain('"data-umami-event-type": "blog_post"'); expect(cardSrc).toContain("data-umami-event-title"); const blogIndexSrc = await read("src/pages/blog/index.astro"); @@ -43,4 +43,3 @@ describe("content link umami title/type attributes", () => { expect(blogPagesSrc).toContain("data-umami-event-title={p.title}"); }); }); - diff --git a/site/tests/umami-attributes.test.ts b/site/tests/umami-attributes.test.ts index 3dd3962..6fcb96b 100644 --- a/site/tests/umami-attributes.test.ts +++ b/site/tests/umami-attributes.test.ts @@ -24,7 +24,7 @@ describe("umami event attributes", () => { 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": "outbound_click"'); expect(src).toContain("data-umami-event-target_id"); expect(src).toContain("data-umami-event-domain"); });