2
openspec/changes/blogs-section/.openspec.yaml
Normal file
2
openspec/changes/blogs-section/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-10
|
||||
53
openspec/changes/blogs-section/design.md
Normal file
53
openspec/changes/blogs-section/design.md
Normal file
@@ -0,0 +1,53 @@
|
||||
## Context
|
||||
|
||||
The site is currently a static Astro build served via nginx. Content is populated by a build-time fetch step (`site/scripts/fetch-content.ts`) that writes a repo-local cache file consumed by the Astro pages/components.
|
||||
|
||||
We want to add a new Blog section backed by a WordPress site via the `wp-json` REST APIs, including:
|
||||
- a primary header nav link (`/blog`)
|
||||
- blog listing pages (cards with featured image, title, excerpt)
|
||||
- blog detail pages (full content)
|
||||
- a blog-only secondary navigation based on WordPress categories
|
||||
- support for both WordPress posts and pages
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add `/blog` with a listing of WordPress posts rendered as static HTML at build time.
|
||||
- Add detail pages for WordPress posts and pages, rendered as static HTML at build time.
|
||||
- Add category-based browsing within the Blog section (secondary navigation + category listing pages).
|
||||
- Use environment variables for WordPress configuration (site URL and credentials) and fetch via `wp-json`.
|
||||
- Keep pages indexable and included in sitemap output.
|
||||
|
||||
**Non-Goals:**
|
||||
- Real-time updates without rebuilds (v1 remains build-time fetched).
|
||||
- Implementing “like” storage in WordPress or a database (nice-to-have can be a simple outbound share action later).
|
||||
- Full WordPress theme parity (we render a simplified reading surface).
|
||||
|
||||
## Decisions
|
||||
|
||||
- **Decision: Build-time ingestion into the existing content cache.**
|
||||
- Rationale: Matches the current architecture (cache file + static build), keeps the site fast and crawlable, and avoids introducing a runtime server layer.
|
||||
- Alternative: Client-side fetch from WP directly. Rejected for SEO and performance (would rely on client rendering and adds CORS/auth complexity).
|
||||
|
||||
- **Decision: Prefer WordPress Application Passwords over raw user passwords (if possible).**
|
||||
- Rationale: Application passwords are the standard WP approach for API access and can be revoked without changing the user login password.
|
||||
- Alternative: Basic auth with username/password. Allowed if that’s what your WP setup supports, but we should treat credentials as secrets in `.env`.
|
||||
|
||||
- **Decision: Normalize WordPress content into a small internal schema.**
|
||||
- Rationale: Keeps UI components simple and consistent with existing content rendering patterns (cards + detail pages).
|
||||
- Implementation: Add a `wordpress` source to the cache schema, with fields for `id`, `slug`, `kind` (`post|page`), `title`, `excerpt`, `contentHtml`, `featuredImageUrl`, `publishedAt`, `updatedAt`, `categories`.
|
||||
|
||||
- **Decision: Route structure.**
|
||||
- Rationale: Keep URLs clear and stable.
|
||||
- Proposed:
|
||||
- `/blog` (latest posts)
|
||||
- `/blog/category/<slug>` (posts in category)
|
||||
- `/blog/post/<slug>` (post detail)
|
||||
- `/blog/page/<slug>` (page detail)
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] WP API rate limits / downtime break the build. → Mitigation: Cache last-known-good content.json; on fetch failure, retain existing cache and log errors.
|
||||
- [Risk] WordPress HTML content can contain unexpected markup or scripts. → Mitigation: Render server-side as HTML but sanitize or strip scripts; document allowed HTML subset.
|
||||
- [Risk] Auth method differs per WP hosting. → Mitigation: Support both public endpoints for reading (preferred) and authenticated requests when needed; keep config flexible.
|
||||
|
||||
30
openspec/changes/blogs-section/proposal.md
Normal file
30
openspec/changes/blogs-section/proposal.md
Normal file
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
Add a blog section so the site can publish indexable textual content (in addition to videos/podcast), improving SEO and giving visitors another reason to return and engage.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a new primary navigation link in the header: **Blog** (between **Podcast** and **About**).
|
||||
- Add a blog index route that lists WordPress posts as cards (featured image, title, excerpt/summary).
|
||||
- Add blog detail routes so a user can read the full content of a post.
|
||||
- Add a secondary navigation within the blog section driven by WordPress categories (exact structure negotiable).
|
||||
- Support rendering both WordPress **posts** and **pages** within the blog section.
|
||||
- Add configuration via environment variables for WordPress site URL and credentials, and fetch content via the WordPress `wp-json` REST APIs.
|
||||
- (Optional / later) Like and share feature for blog content.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `wordpress-content-source`: Fetch posts, pages, and categories from a configured WordPress site via `wp-json`, and provide them in a form the site can render (including featured images and excerpts).
|
||||
- `blog-section-surface`: Provide blog routes (index, category views, content detail pages) and a secondary navigation for blog browsing.
|
||||
|
||||
### Modified Capabilities
|
||||
- `seo-content-surface`: Include the blog routes in the indexable surface (e.g., sitemap coverage and crawlable HTML for `/blog` and blog detail pages).
|
||||
|
||||
## Impact
|
||||
|
||||
- Site UI/layout: header navigation update; new blog pages; secondary blog navigation.
|
||||
- Content pipeline: extend the content fetching/caching flow to include WordPress content; update any normalized schemas/types as needed.
|
||||
- Configuration: add WordPress settings to environment/config and ensure they are supported in local dev and Docker.
|
||||
- SEO: ensure blog pages have correct titles, descriptions/excerpts, canonical URLs, and appear in `sitemap.xml`.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Primary navigation entry
|
||||
The site MUST add a header navigation link to the blog index at `/blog` labeled "Blog".
|
||||
|
||||
#### Scenario: Blog link in header
|
||||
- **WHEN** a user views any page
|
||||
- **THEN** the header navigation includes a "Blog" link that navigates to `/blog`
|
||||
|
||||
### 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
|
||||
|
||||
The listing MUST be ordered by publish date descending (newest first).
|
||||
|
||||
#### 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
|
||||
|
||||
### Requirement: Blog post detail
|
||||
The site MUST provide a blog post detail page for each WordPress post that renders:
|
||||
- title
|
||||
- publish date
|
||||
- featured image (when available)
|
||||
- full post content
|
||||
|
||||
#### Scenario: Post detail renders
|
||||
- **WHEN** a user navigates to a blog post detail page
|
||||
- **THEN** the page renders the full post content from the cached WordPress dataset
|
||||
|
||||
### Requirement: WordPress pages support
|
||||
The blog section MUST support WordPress pages by rendering page detail routes that show:
|
||||
- title
|
||||
- featured image (when available)
|
||||
- full page content
|
||||
|
||||
#### Scenario: Page detail renders
|
||||
- **WHEN** a user navigates to a WordPress page detail route
|
||||
- **THEN** the page renders the full page content from the cached WordPress dataset
|
||||
|
||||
### Requirement: Category-based secondary navigation
|
||||
The blog section MUST render a secondary navigation under the header derived from the cached WordPress categories.
|
||||
|
||||
Selecting a category MUST navigate to a category listing page showing only posts in that category.
|
||||
|
||||
#### Scenario: Category nav present
|
||||
- **WHEN** the cached WordPress dataset contains categories
|
||||
- **THEN** the blog section shows a secondary navigation with those categories
|
||||
|
||||
#### Scenario: Category listing filters posts
|
||||
- **WHEN** a user navigates to a category listing page
|
||||
- **THEN** only posts assigned to that category are listed
|
||||
|
||||
### Requirement: Graceful empty states
|
||||
If there are no WordPress posts available, the blog index MUST render a non-broken empty state and MUST still render header/navigation.
|
||||
|
||||
#### Scenario: No posts available
|
||||
- **WHEN** the cached WordPress dataset contains no posts
|
||||
- **THEN** `/blog` renders a helpful empty state
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Sitemap and robots
|
||||
The site MUST provide:
|
||||
- `sitemap.xml` enumerating indexable pages
|
||||
- `robots.txt` that allows indexing of indexable pages
|
||||
|
||||
The sitemap MUST include the blog surface routes:
|
||||
- `/blog`
|
||||
- blog post detail routes
|
||||
- blog page detail routes
|
||||
- blog category listing routes
|
||||
|
||||
#### Scenario: Sitemap is available
|
||||
- **WHEN** a crawler requests `/sitemap.xml`
|
||||
- **THEN** the server returns an XML sitemap listing `/`, `/videos`, `/podcast`, `/about`, and `/blog`
|
||||
|
||||
#### Scenario: Blog URLs appear in sitemap
|
||||
- **WHEN** WordPress content is available in the cache at build time
|
||||
- **THEN** the generated sitemap includes the blog detail URLs for those items
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: WordPress API configuration
|
||||
The system MUST allow configuring a WordPress content source using environment/config values:
|
||||
- WordPress base URL
|
||||
- credentials (username + password or application password) when required by the WordPress instance
|
||||
|
||||
The WordPress base URL MUST be used to construct requests to the WordPress `wp-json` REST APIs.
|
||||
|
||||
#### Scenario: Config provided
|
||||
- **WHEN** WordPress configuration values are provided
|
||||
- **THEN** the system can attempt to fetch WordPress content via `wp-json`
|
||||
|
||||
### Requirement: Fetch posts
|
||||
The system MUST fetch the latest WordPress posts via `wp-json` and map them into an internal representation with:
|
||||
- stable ID
|
||||
- slug
|
||||
- title
|
||||
- excerpt/summary
|
||||
- content HTML
|
||||
- featured image URL when available
|
||||
- publish date/time and last modified date/time
|
||||
- category assignments (IDs and slugs when available)
|
||||
|
||||
#### Scenario: Posts fetched successfully
|
||||
- **WHEN** the WordPress posts endpoint returns a non-empty list
|
||||
- **THEN** the system stores the mapped post items in the content cache for rendering
|
||||
|
||||
### Requirement: Fetch pages
|
||||
The system MUST fetch WordPress pages via `wp-json` and map them into an internal representation with:
|
||||
- stable ID
|
||||
- slug
|
||||
- title
|
||||
- excerpt/summary when available
|
||||
- content HTML
|
||||
- featured image URL when available
|
||||
- publish date/time and last modified date/time
|
||||
|
||||
#### Scenario: Pages fetched successfully
|
||||
- **WHEN** the WordPress pages endpoint returns a non-empty list
|
||||
- **THEN** the system stores the mapped page items in the content cache for rendering
|
||||
|
||||
### Requirement: Fetch categories
|
||||
The system MUST fetch WordPress categories via `wp-json` and store them for rendering a category-based secondary navigation under the blog section.
|
||||
|
||||
#### Scenario: Categories fetched successfully
|
||||
- **WHEN** the WordPress categories endpoint returns a list of categories
|
||||
- **THEN** the system stores categories (ID, slug, name) in the content cache for blog navigation
|
||||
|
||||
### Requirement: Build-time caching
|
||||
WordPress posts, pages, and categories MUST be written into the repo-local content cache used by the site build.
|
||||
|
||||
If the WordPress fetch fails, the system MUST NOT crash the entire build pipeline; it MUST either:
|
||||
- keep the last-known-good cached WordPress content (if present), or
|
||||
- store an empty WordPress dataset and allow the rest of the site to build.
|
||||
|
||||
#### Scenario: WordPress fetch fails
|
||||
- **WHEN** a WordPress API request fails
|
||||
- **THEN** the site build can still complete and the blog surface renders a graceful empty state
|
||||
|
||||
28
openspec/changes/blogs-section/tasks.md
Normal file
28
openspec/changes/blogs-section/tasks.md
Normal file
@@ -0,0 +1,28 @@
|
||||
## 1. WordPress Config And Fetch
|
||||
|
||||
- [x] 1.1 Add WordPress env/config variables (base URL + credentials) and document them in `site/.env.example`
|
||||
- [x] 1.2 Extend `site/scripts/fetch-content.ts` to fetch WordPress posts, pages, and categories via `wp-json` and write to `site/content/cache/content.json`
|
||||
- [x] 1.3 Add a failure mode where WP fetch errors do not crash the whole fetch/build (keep last-known-good or write empty WP dataset)
|
||||
|
||||
## 2. Normalize And Select
|
||||
|
||||
- [x] 2.1 Extend content cache/types to represent WordPress items (post/page) and categories
|
||||
- [x] 2.2 Add selector helpers for WordPress posts/pages/categories (ordered by publish date, filter by category)
|
||||
|
||||
## 3. Blog UI Surface
|
||||
|
||||
- [x] 3.1 Add `/blog` index page that renders WordPress post cards (featured image, title, excerpt)
|
||||
- [x] 3.2 Add post detail routes (e.g., `/blog/post/<slug>`) that render the full post content
|
||||
- [x] 3.3 Add page detail routes (e.g., `/blog/page/<slug>`) that render the full page content
|
||||
- [x] 3.4 Add blog secondary navigation under header based on cached categories, with category listing pages (e.g., `/blog/category/<slug>`)
|
||||
- [x] 3.5 Add header nav link "Blog" between "Podcast" and "About"
|
||||
|
||||
## 4. SEO And Sitemap
|
||||
|
||||
- [x] 4.1 Ensure blog pages include title/description/canonical URL metadata
|
||||
- [x] 4.2 Update sitemap generation to include `/blog` and blog content routes when WP content is present at build time
|
||||
|
||||
## 5. Verification
|
||||
|
||||
- [x] 5.1 Add at least one test to assert the header includes the Blog link
|
||||
- [x] 5.2 Add a build verification that `/blog` is generated and renders an empty state when no WP content is available
|
||||
Reference in New Issue
Block a user