better cards

This commit is contained in:
2026-02-10 02:34:25 -05:00
parent b63c62a732
commit 03df2b3a6c
24 changed files with 669 additions and 127 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-10

View File

@@ -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).

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-10

View File

@@ -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?

View File

@@ -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.

View File

@@ -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

View File

@@ -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)