deploy without node
This commit is contained in:
@@ -7,3 +7,7 @@
|
|||||||
**/dist
|
**/dist
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
|
# Local secrets
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
|||||||
68
.github/workflows/publish-image.yml
vendored
Normal file
68
.github/workflows/publish-image.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
name: publish-image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# Rebuild periodically so content sources can be refreshed even without code changes.
|
||||||
|
- cron: "0 9 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: site/package-lock.json
|
||||||
|
|
||||||
|
- name: Install + Fetch Content + Build Site
|
||||||
|
working-directory: site
|
||||||
|
env:
|
||||||
|
YOUTUBE_CHANNEL_ID: ${{ secrets.YOUTUBE_CHANNEL_ID }}
|
||||||
|
YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }}
|
||||||
|
PODCAST_RSS_URL: ${{ secrets.PODCAST_RSS_URL }}
|
||||||
|
WORDPRESS_BASE_URL: ${{ secrets.WORDPRESS_BASE_URL }}
|
||||||
|
WORDPRESS_USERNAME: ${{ secrets.WORDPRESS_USERNAME }}
|
||||||
|
WORDPRESS_APP_PASSWORD: ${{ secrets.WORDPRESS_APP_PASSWORD }}
|
||||||
|
REDIS_URL: ${{ secrets.REDIS_URL }}
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run fetch-content
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/metadata-action@v5
|
||||||
|
id: meta
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest
|
||||||
|
type=sha,format=short,prefix=sha-
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
BUILD_SHA=${{ github.sha }}
|
||||||
|
BUILD_DATE=${{ github.run_started_at }}
|
||||||
|
BUILD_REF=${{ github.server_url }}/${{ github.repository }}
|
||||||
20
Dockerfile
20
Dockerfile
@@ -3,15 +3,31 @@ FROM node:24-alpine AS builder
|
|||||||
WORKDIR /app/site
|
WORKDIR /app/site
|
||||||
|
|
||||||
COPY site/package.json site/package-lock.json ./
|
COPY site/package.json site/package-lock.json ./
|
||||||
RUN npm ci
|
RUN npm ci --no-audit --no-fund
|
||||||
|
|
||||||
COPY site/ ./
|
COPY site/ ./
|
||||||
|
# Content is fetched before build (typically in CI) and committed into the build context at
|
||||||
|
# `site/content/cache/content.json`. If env vars aren't configured, the fetch step gracefully
|
||||||
|
# skips sources and/or uses last-known-good cache.
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
ARG BUILD_SHA=unknown
|
||||||
|
ARG BUILD_DATE=unknown
|
||||||
|
ARG BUILD_REF=unknown
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="fast-website"
|
||||||
|
LABEL org.opencontainers.image.description="Lightweight, SEO-first static site packaged as an nginx image."
|
||||||
|
LABEL org.opencontainers.image.revision=$BUILD_SHA
|
||||||
|
LABEL org.opencontainers.image.created=$BUILD_DATE
|
||||||
|
LABEL org.opencontainers.image.source=$BUILD_REF
|
||||||
|
|
||||||
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY --from=builder /app/site/dist/ /usr/share/nginx/html/
|
COPY --from=builder /app/site/dist/ /usr/share/nginx/html/
|
||||||
|
|
||||||
EXPOSE 80
|
# Operator-friendly version visibility.
|
||||||
|
RUN printf '{\n "sha": "%s",\n "builtAt": "%s",\n "ref": "%s"\n}\n' "$BUILD_SHA" "$BUILD_DATE" "$BUILD_REF" \
|
||||||
|
> /usr/share/nginx/html/build.json
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -86,7 +86,9 @@ Instrumentation checklist:
|
|||||||
|
|
||||||
## Deployment (Linode + Docker)
|
## Deployment (Linode + Docker)
|
||||||
|
|
||||||
Build and run:
|
The production host is intentionally minimal and only needs Docker (no Node.js on the server).
|
||||||
|
|
||||||
|
### Local Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose build
|
docker compose build
|
||||||
@@ -95,24 +97,29 @@ docker compose up -d
|
|||||||
|
|
||||||
The container serves the static output on port `8080` (map or proxy as needed).
|
The container serves the static output on port `8080` (map or proxy as needed).
|
||||||
|
|
||||||
### Refreshing Content (Manual)
|
### Production (Docker-Only Host)
|
||||||
|
|
||||||
Content is fetched at build time into `site/content/cache/content.json`.
|
In production, CI builds and publishes a Docker image (nginx serving the static output). The server updates by pulling that image and restarting the service.
|
||||||
|
|
||||||
On the Linode host:
|
Runbook: `deploy/runbook.md`.
|
||||||
|
|
||||||
|
### Refreshing Content (Manual, Docker-Only)
|
||||||
|
|
||||||
|
Content is fetched at build time into `site/content/cache/content.json` (typically in CI), then packaged into the image.
|
||||||
|
|
||||||
|
On the server host:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/refresh.sh
|
./scripts/refresh.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This:
|
This:
|
||||||
1. Runs `npm run fetch-content` in `site/`
|
1. Pulls the latest published image
|
||||||
2. Rebuilds the Docker image
|
2. Restarts the service (no build on the host)
|
||||||
3. Restarts the container
|
|
||||||
|
|
||||||
### Refreshing Content (Scheduled)
|
### Refreshing Content (Scheduled)
|
||||||
|
|
||||||
Install a daily cron using `deploy/cron.example` as a starting point.
|
Install a daily cron using `deploy/cron.example` as a starting point.
|
||||||
|
|
||||||
Rollback:
|
Rollback:
|
||||||
- Re-run `docker compose up -d` with a previously built image/tag, or restore the last known-good repo state and rerun `scripts/refresh.sh`.
|
- Re-deploy a known-good image tag/digest (see `deploy/runbook.md`).
|
||||||
|
|||||||
6
deploy/docker-compose.prod.yml
Normal file
6
deploy/docker-compose.prod.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: ${WEB_IMAGE:?Set WEB_IMAGE to the published image tag or digest}
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
|
||||||
84
deploy/runbook.md
Normal file
84
deploy/runbook.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
## Deploy Runbook (Docker-Only Host)
|
||||||
|
|
||||||
|
This runbook is for a minimal production host where **Docker is installed** and **Node.js is not**.
|
||||||
|
|
||||||
|
The deployment model is:
|
||||||
|
- CI builds and publishes a Docker image containing the built static site
|
||||||
|
- the server updates by pulling that image and restarting the service
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker + Docker Compose plugin available on the host
|
||||||
|
- Registry access (e.g., logged in to GHCR if the image is private)
|
||||||
|
- A `WEB_IMAGE` value pointing at the image to deploy (tag or digest)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WEB_IMAGE=ghcr.io/<owner>/<repo>:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-Time Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Refresh (Pull + Restart)
|
||||||
|
|
||||||
|
Pull first (safe; does not affect the running container):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml pull
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart the service on the newly pulled image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml up -d --no-build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Deployed Version
|
||||||
|
|
||||||
|
1. Check the container's image reference (tag/digest):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml ps
|
||||||
|
docker inspect --format '{{.Image}} {{.Config.Image}}' <container-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Check build metadata served by the site:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://localhost:8080/build.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback
|
||||||
|
|
||||||
|
Re-deploy a known-good version by pinning a previous tag or digest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WEB_IMAGE=ghcr.io/<owner>/<repo>:<known-good-tag>
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml up -d --no-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended: record the image digest for each release (`docker inspect <image> --format '{{.Id}}'`), and use a digest pin for true immutability.
|
||||||
|
|
||||||
|
### Failure Mode Validation (Pull Failure)
|
||||||
|
|
||||||
|
If `docker compose pull` fails, **do not run** the restart step. The running site will continue serving the existing container.
|
||||||
|
|
||||||
|
To simulate a pull failure safely:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export WEB_IMAGE=ghcr.io/<owner>/<repo>:this-tag-does-not-exist
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml pull
|
||||||
|
```
|
||||||
|
|
||||||
|
The pull should fail, but the current service should still be running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker-compose.prod.yml ps
|
||||||
|
curl -fsS http://localhost:8080/ > /dev/null
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
|
image: ${WEB_IMAGE:-fast-website:local}
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ The site MUST provide a blog index page at `/blog` that lists WordPress posts as
|
|||||||
- excerpt/summary
|
- excerpt/summary
|
||||||
- publish date
|
- publish date
|
||||||
|
|
||||||
The card MUST render a meta row with:
|
The card MUST render a footer bar that includes:
|
||||||
- publish date on the left
|
- 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)
|
- views on the right when available (if views are not provided by the dataset, the card MUST omit views without breaking layout)
|
||||||
|
- a content source label (e.g., `blog`)
|
||||||
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).
|
The listing MUST be ordered by publish date descending (newest first).
|
||||||
|
|
||||||
@@ -31,4 +30,4 @@ Each post card MUST be instrumented with Umami Track Events data attributes and
|
|||||||
|
|
||||||
#### Scenario: Blog post card layout is standardized
|
#### Scenario: Blog post card layout is standardized
|
||||||
- **WHEN** `/blog` renders a blog post card
|
- **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
|
- **THEN** the card shows featured image (when available), title, trimmed excerpt, and a footer bar containing date, optional views, and a source label
|
||||||
@@ -7,10 +7,10 @@ The standard card layout MUST be:
|
|||||||
- featured image displayed prominently at the top (when available)
|
- featured image displayed prominently at the top (when available)
|
||||||
- title
|
- title
|
||||||
- summary/excerpt text, trimmed to a fixed maximum length
|
- summary/excerpt text, trimmed to a fixed maximum length
|
||||||
- meta row showing:
|
- footer bar showing:
|
||||||
- publish date on the left
|
- publish date on the left
|
||||||
- views on the right (when available)
|
- views when available (if omitted, the footer MUST still render cleanly)
|
||||||
- footer row showing the content source (e.g., `youtube`, `podcast`, `blog`)
|
- the content source label (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.
|
If a field is not available (for example, views for some sources), the card MUST still render cleanly with that field omitted.
|
||||||
|
|
||||||
@@ -20,9 +20,8 @@ If a field is not available (for example, views for some sources), the card MUST
|
|||||||
|
|
||||||
#### Scenario: Card renders without views
|
#### Scenario: Card renders without views
|
||||||
- **WHEN** a content item has no views data
|
- **WHEN** a content item has no views data
|
||||||
- **THEN** the card renders the meta row with date and omits views without breaking the layout
|
- **THEN** the card renders the footer bar with date + source and omits views without breaking the layout
|
||||||
|
|
||||||
#### Scenario: Card renders without featured image
|
#### Scenario: Card renders without featured image
|
||||||
- **WHEN** a content item has no featured image
|
- **WHEN** a content item has no featured image
|
||||||
- **THEN** the card renders a placeholder media area and still renders the remaining fields
|
- **THEN** the card renders a placeholder media area and still renders the remaining fields
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## 1. Discovery And Current State
|
||||||
|
|
||||||
|
- [x] 1.1 Identify current deploy target and mechanism (compose/swarm/k8s, image vs files) and document constraints in `README` or `ops/` docs
|
||||||
|
- [x] 1.2 Identify the content source(s) that define "latest content" (e.g., WordPress/API) and how builds currently fetch content
|
||||||
|
- [x] 1.3 Confirm current build output (static assets) and runtime server (e.g., nginx) requirements
|
||||||
|
|
||||||
|
## 2. Build And Publish A Deployable Artifact
|
||||||
|
|
||||||
|
- [x] 2.1 Ensure the repo can produce a deterministic production build inside CI (no host dependencies)
|
||||||
|
- [x] 2.2 Create or update a Dockerfile to build the site and package the built output into a runtime image
|
||||||
|
- [x] 2.3 Add build metadata to the image (tagging convention and/or embedded version file)
|
||||||
|
- [x] 2.4 Configure CI to build and publish the image to a registry accessible by the server
|
||||||
|
|
||||||
|
## 3. Server-Side Docker-Only Refresh Workflow
|
||||||
|
|
||||||
|
- [x] 3.1 Add or update the server Docker Compose/service definition to run the published image
|
||||||
|
- [x] 3.2 Add documented operator commands to refresh to the latest image (pull + restart)
|
||||||
|
- [x] 3.3 Add a verification command/procedure to show the currently deployed version (tag/digest/build metadata)
|
||||||
|
- [x] 3.4 Define rollback procedure to re-deploy a previous known-good tag/digest
|
||||||
|
|
||||||
|
## 4. Validation
|
||||||
|
|
||||||
|
- [x] 4.1 Validate a refresh on a test/staging server: pull latest image, restart, confirm content changes are visible
|
||||||
|
- [x] 4.2 Validate failure mode: simulate pull failure and confirm the existing site remains serving
|
||||||
|
- [x] 4.3 Update docs with a minimal "runbook" for operators (refresh, verify, rollback)
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
## 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)
|
|
||||||
@@ -16,9 +16,15 @@ The site MUST provide a blog index page at `/blog` that lists WordPress posts as
|
|||||||
- featured image (when available)
|
- featured image (when available)
|
||||||
- title
|
- title
|
||||||
- excerpt/summary
|
- excerpt/summary
|
||||||
|
- publish date
|
||||||
|
|
||||||
The listing MUST be ordered by publish date descending (newest first).
|
The listing MUST be ordered by publish date descending (newest first).
|
||||||
|
|
||||||
|
The card MUST render a footer row that includes:
|
||||||
|
- 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)
|
||||||
|
- a content source label (e.g., `blog`)
|
||||||
|
|
||||||
Each post card MUST be instrumented with Umami Track Events data attributes and MUST include at minimum:
|
Each post card MUST be instrumented with Umami Track Events data attributes and MUST include at minimum:
|
||||||
- `data-umami-event`
|
- `data-umami-event`
|
||||||
- `data-umami-event-target_id`
|
- `data-umami-event-target_id`
|
||||||
@@ -33,6 +39,10 @@ Each post card MUST be instrumented with Umami Track Events data attributes and
|
|||||||
- **WHEN** a user clicks a blog post card on `/blog`
|
- **WHEN** a user clicks a blog post card on `/blog`
|
||||||
- **THEN** the click emits an Umami event with `target_id`, `placement`, and `target_url`
|
- **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, and a footer bar containing date, optional views, and a source label
|
||||||
|
|
||||||
### Requirement: Blog post detail
|
### Requirement: Blog post detail
|
||||||
The site MUST provide a blog post detail page for each WordPress post that renders:
|
The site MUST provide a blog post detail page for each WordPress post that renders:
|
||||||
- title
|
- title
|
||||||
@@ -83,4 +93,3 @@ If there are no WordPress posts available, the blog index MUST render a non-brok
|
|||||||
#### Scenario: No posts available
|
#### Scenario: No posts available
|
||||||
- **WHEN** the cached WordPress dataset contains no posts
|
- **WHEN** the cached WordPress dataset contains no posts
|
||||||
- **THEN** `/blog` renders a helpful empty state
|
- **THEN** `/blog` renders a helpful empty state
|
||||||
|
|
||||||
|
|||||||
32
openspec/specs/card-layout-system/spec.md
Normal file
32
openspec/specs/card-layout-system/spec.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define a standardized card layout so content cards across surfaces look consistent.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
- footer row showing:
|
||||||
|
- publish date on the left
|
||||||
|
- views when available (if omitted, the footer MUST still render cleanly)
|
||||||
|
- the content source label (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 footer bar with date + source 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
|
||||||
|
|
||||||
31
openspec/specs/docker-content-refresh/spec.md
Normal file
31
openspec/specs/docker-content-refresh/spec.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Enable operators to refresh the deployed site to the latest content on a Docker-only host (no Node.js installed on the server).
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
@@ -11,6 +11,9 @@ The normalized item MUST include at minimum:
|
|||||||
- `publishedAt` (ISO-8601)
|
- `publishedAt` (ISO-8601)
|
||||||
- `thumbnailUrl` (optional)
|
- `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
|
#### Scenario: Normalizing a YouTube video
|
||||||
- **WHEN** the system ingests a YouTube video item
|
- **WHEN** the system ingests a YouTube video item
|
||||||
- **THEN** it produces a normalized item containing `id`, `source: youtube`, `url`, `title`, and `publishedAt`
|
- **THEN** it produces a normalized item containing `id`, `source: youtube`, `url`, `title`, and `publishedAt`
|
||||||
@@ -19,6 +22,10 @@ The normalized item MUST include at minimum:
|
|||||||
- **WHEN** the system ingests a podcast RSS episode
|
- **WHEN** the system ingests a podcast RSS episode
|
||||||
- **THEN** it produces a normalized item containing `id`, `source: podcast`, `url`, `title`, and `publishedAt`
|
- **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
|
||||||
|
|
||||||
### Requirement: YouTube ingestion with stats when available
|
### Requirement: YouTube ingestion with stats when available
|
||||||
The system MUST support ingesting YouTube videos for channel `youtube.com/santhoshj`.
|
The system MUST support ingesting YouTube videos for channel `youtube.com/santhoshj`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ set -eu
|
|||||||
|
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
echo "[refresh] fetching content"
|
echo "[refresh] pulling latest image"
|
||||||
(cd site && npm ci && npm run fetch-content)
|
docker compose -f deploy/docker-compose.prod.yml pull
|
||||||
|
|
||||||
echo "[refresh] building + restarting container"
|
echo "[refresh] restarting service (no build)"
|
||||||
docker compose build web
|
docker compose -f deploy/docker-compose.prod.yml up -d --no-build
|
||||||
docker compose up -d --force-recreate web
|
|
||||||
|
|
||||||
echo "[refresh] done"
|
echo "[refresh] done"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user