From a653cf82e35b38f1d9679d2f4071fc1b49d5c253 Mon Sep 17 00:00:00 2001 From: Ronan Hevenor Date: Sun, 6 Sep 2026 15:35:27 -0400 Subject: [PATCH 1/5] fix(migrations): backfill five columns missing from migrations/ and unbreak migrate:create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fresh environments built purely from `migrations/` (new local checkouts, CI) crashed on any query touching `layout`, `opinion_page_layout` or `live_articles_updates`, because five columns present in the collection definitions were never added by a migration. They reached existing databases via dev-mode `db.push` before migrations became the source of truth: layout.grid json opinion_page_layout.layout json live_articles_updates.author_id -> users _live_articles_v_version_updates.author_id version shadow payload_locked_documents_rels.opinion_page_layout_id `updates.author` is a genuine mistake in 20260420_000000_add_live_articles, which routed it through `live_articles_rels` under path "updates.author". Payload stores single-target, non-hasMany relationships as a scalar `_id` column on the array's own table instead. Every statement is idempotent (ADD COLUMN IF NOT EXISTS / duplicate_object guards), and production predates the migration system and already has these columns, so this is expected to be a no-op there rather than a schema change. Added to both migration paths per docs/migrations.md. Also removes four files in migrations/ that were never drizzle snapshots. They contain `{id, name, batch}` — the shape of a payload_migrations row — so `payload migrate:create` picked the newest as its previous-state snapshot and died in Zod validation on missing `schemas` / `_meta` / `prevId`, which made it impossible to generate any migration at all. Nothing referenced them: migrations/index.ts does not import them and the deploy script carries its own tracking INSERTs. Finally, sets PAYLOAD_DISABLE_PUSH=1 in the generated .env. Without it a fresh `pnpm dev` boots into Payload's dev-mode db.push, which blocks on an interactive "DATA LOSS WARNING ... (y/N)" prompt (it wants to drop `_layout_v.latest`, which exists because layout has drafts disabled) and stalls every request behind it. Verified with `pnpm db:migrate-test --seed`: both the TS and the production SQL path apply cleanly to a fresh database and the seed completes. Co-Authored-By: Claude Opus 5 --- ...7_200000_add_opinion_type_and_caption.json | 5 -- .../20260320_200000_add_write_in_authors.json | 5 -- ...260321_055436_add_opinion_page_layout.json | 5 -- ..._220000_add_more_to_opinion_type_enum.json | 5 -- .../20260906_000000_fix_schema_drift.ts | 84 +++++++++++++++++++ migrations/index.ts | 6 ++ scripts/generate-env.js | 5 ++ scripts/run_deploy_sql_migrations.sh | 50 ++++++++++- 8 files changed, 144 insertions(+), 21 deletions(-) delete mode 100644 migrations/20260317_200000_add_opinion_type_and_caption.json delete mode 100644 migrations/20260320_200000_add_write_in_authors.json delete mode 100644 migrations/20260321_055436_add_opinion_page_layout.json delete mode 100644 migrations/20260322_220000_add_more_to_opinion_type_enum.json create mode 100644 migrations/20260906_000000_fix_schema_drift.ts diff --git a/migrations/20260317_200000_add_opinion_type_and_caption.json b/migrations/20260317_200000_add_opinion_type_and_caption.json deleted file mode 100644 index 78e41c1..0000000 --- a/migrations/20260317_200000_add_opinion_type_and_caption.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "20260317_200000_add_opinion_type_and_caption", - "name": "20260317_200000_add_opinion_type_and_caption", - "batch": -1 -} diff --git a/migrations/20260320_200000_add_write_in_authors.json b/migrations/20260320_200000_add_write_in_authors.json deleted file mode 100644 index 78e41c1..0000000 --- a/migrations/20260320_200000_add_write_in_authors.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "20260317_200000_add_opinion_type_and_caption", - "name": "20260317_200000_add_opinion_type_and_caption", - "batch": -1 -} diff --git a/migrations/20260321_055436_add_opinion_page_layout.json b/migrations/20260321_055436_add_opinion_page_layout.json deleted file mode 100644 index 9ff7571..0000000 --- a/migrations/20260321_055436_add_opinion_page_layout.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "20260321_055436_add_opinion_page_layout", - "name": "20260321_055436_add_opinion_page_layout", - "batch": -1 -} diff --git a/migrations/20260322_220000_add_more_to_opinion_type_enum.json b/migrations/20260322_220000_add_more_to_opinion_type_enum.json deleted file mode 100644 index a85b78b..0000000 --- a/migrations/20260322_220000_add_more_to_opinion_type_enum.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "20260322_220000_add_more_to_opinion_type_enum", - "name": "20260322_220000_add_more_to_opinion_type_enum", - "batch": -1 -} diff --git a/migrations/20260906_000000_fix_schema_drift.ts b/migrations/20260906_000000_fix_schema_drift.ts new file mode 100644 index 0000000..b166365 --- /dev/null +++ b/migrations/20260906_000000_fix_schema_drift.ts @@ -0,0 +1,84 @@ +import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres' + +/** + * Backfill five columns that exist in the Payload collection definitions but + * were never added by a migration. Each was introduced by a collection change + * that relied on dev-mode `db.push` to sync the schema, so environments built + * purely from `migrations/` (fresh local checkouts, CI) end up missing them and + * every query against the affected table fails with `column ... does not exist`. + * + * layout.grid (`grid`, json) + * opinion_page_layout.layout (`layout`, json) + * live_articles_updates.author_id (`updates.author` -> users) + * _live_articles_v_version_updates.author_id (version shadow of above) + * payload_locked_documents_rels.opinion_page_layout_id + * + * Note on `updates.author`: 20260420_000000_add_live_articles assumed this + * relationship would live in `live_articles_rels` under path "updates.author". + * It does not — Payload stores single-target, non-hasMany relationships as a + * scalar `_id` column on the array's own table, so the column belongs on + * `live_articles_updates` directly. The `live_articles_rels` table is left + * alone; it is harmless and still backs any future hasMany relationships. + * + * Every statement is idempotent. Production was built before migrations were + * introduced here and already has these columns, so this migration is expected + * to be a no-op there rather than a schema change. + */ +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "layout" ADD COLUMN IF NOT EXISTS "grid" jsonb; + ALTER TABLE "opinion_page_layout" ADD COLUMN IF NOT EXISTS "layout" jsonb; + + ALTER TABLE "live_articles_updates" ADD COLUMN IF NOT EXISTS "author_id" integer; + ALTER TABLE "_live_articles_v_version_updates" ADD COLUMN IF NOT EXISTS "author_id" integer; + + DO $$ BEGIN + ALTER TABLE "live_articles_updates" ADD CONSTRAINT "live_articles_updates_author_id_users_id_fk" + FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + EXCEPTION WHEN duplicate_object THEN NULL; END $$; + + DO $$ BEGIN + ALTER TABLE "_live_articles_v_version_updates" ADD CONSTRAINT "_live_articles_v_version_updates_author_id_users_id_fk" + FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE NO ACTION; + EXCEPTION WHEN duplicate_object THEN NULL; END $$; + + CREATE INDEX IF NOT EXISTS "live_articles_updates_author_idx" + ON "live_articles_updates" USING btree ("author_id"); + CREATE INDEX IF NOT EXISTS "_live_articles_v_version_updates_author_idx" + ON "_live_articles_v_version_updates" USING btree ("author_id"); + + ALTER TABLE "payload_locked_documents_rels" + ADD COLUMN IF NOT EXISTS "opinion_page_layout_id" integer; + + DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" + ADD CONSTRAINT "payload_locked_documents_rels_opinion_page_layout_fk" + FOREIGN KEY ("opinion_page_layout_id") REFERENCES "public"."opinion_page_layout"("id") + ON DELETE CASCADE; + EXCEPTION WHEN duplicate_object THEN NULL; END $$; + + CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_opinion_page_layout_id_idx" + ON "payload_locked_documents_rels" ("opinion_page_layout_id"); + `) +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + DROP INDEX IF EXISTS "payload_locked_documents_rels_opinion_page_layout_id_idx"; + ALTER TABLE "payload_locked_documents_rels" + DROP CONSTRAINT IF EXISTS "payload_locked_documents_rels_opinion_page_layout_fk"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "opinion_page_layout_id"; + + DROP INDEX IF EXISTS "_live_articles_v_version_updates_author_idx"; + DROP INDEX IF EXISTS "live_articles_updates_author_idx"; + ALTER TABLE "_live_articles_v_version_updates" + DROP CONSTRAINT IF EXISTS "_live_articles_v_version_updates_author_id_users_id_fk"; + ALTER TABLE "live_articles_updates" + DROP CONSTRAINT IF EXISTS "live_articles_updates_author_id_users_id_fk"; + ALTER TABLE "_live_articles_v_version_updates" DROP COLUMN IF EXISTS "author_id"; + ALTER TABLE "live_articles_updates" DROP COLUMN IF EXISTS "author_id"; + + ALTER TABLE "opinion_page_layout" DROP COLUMN IF EXISTS "layout"; + ALTER TABLE "layout" DROP COLUMN IF EXISTS "grid"; + `) +} diff --git a/migrations/index.ts b/migrations/index.ts index 9da6bef..13ba0ae 100644 --- a/migrations/index.ts +++ b/migrations/index.ts @@ -44,6 +44,7 @@ import * as migration_20260506_010000_add_articles_legacy_id_and_category from ' import * as migration_20260506_020000_add_articles_plain_content from './20260506_020000_add_articles_plain_content'; import * as migration_20260507_000000_add_articles_previous_slug from './20260507_000000_add_articles_previous_slug'; import * as migration_20260507_010000_add_legacy_shortlinks from './20260507_010000_add_legacy_shortlinks'; +import * as migration_20260906_000000_fix_schema_drift from './20260906_000000_fix_schema_drift' export const migrations = [ { @@ -276,4 +277,9 @@ export const migrations = [ down: migration_20260507_010000_add_legacy_shortlinks.down, name: '20260507_010000_add_legacy_shortlinks', }, + { + up: migration_20260906_000000_fix_schema_drift.up, + down: migration_20260906_000000_fix_schema_drift.down, + name: '20260906_000000_fix_schema_drift', + }, ]; diff --git a/scripts/generate-env.js b/scripts/generate-env.js index 61d5f47..659a441 100644 --- a/scripts/generate-env.js +++ b/scripts/generate-env.js @@ -10,6 +10,11 @@ if (!fs.existsSync(envPath)) { const secret = crypto.randomBytes(32).toString('hex'); const content = `PAYLOAD_SECRET=${secret} DATABASE_URL=postgres://polymer:polymer@127.0.0.1:5433/polymer_dev +# Migrations in migrations/ are the source of truth for schema in this repo. +# Payload's dev-mode db.push would otherwise try to reconcile the database +# against the collection definitions on boot and block on an interactive +# "DATA LOSS WARNING ... (y/N)" prompt, hanging every request. +PAYLOAD_DISABLE_PUSH=1 NEXT_PUBLIC_POSTHOG_KEY= # LEGACY_DATABASE_URI=postgres://user:password@host:port/legacy_database_name # NEXT_PUBLIC_SITE_URL=http://localhost:3000 diff --git a/scripts/run_deploy_sql_migrations.sh b/scripts/run_deploy_sql_migrations.sh index 583dcff..4ddfa2a 100755 --- a/scripts/run_deploy_sql_migrations.sh +++ b/scripts/run_deploy_sql_migrations.sh @@ -53,7 +53,8 @@ VALUES ('20260506_010000_add_articles_legacy_id_and_category', 27, NOW(), NOW()), ('20260506_020000_add_articles_plain_content', 27, NOW(), NOW()), ('20260507_000000_add_articles_previous_slug', 28, NOW(), NOW()), - ('20260507_010000_add_legacy_shortlinks', 28, NOW(), NOW()) + ('20260507_010000_add_legacy_shortlinks', 28, NOW(), NOW()), + ('20260906_000000_fix_schema_drift', 29, NOW(), NOW()) ON CONFLICT DO NOTHING; -- 20260317: Add opinion_type and image_caption columns @@ -1394,4 +1395,51 @@ CREATE TABLE IF NOT EXISTS "legacy_shortlinks" ( "hit_count" integer NOT NULL DEFAULT 0, "created_at" timestamp(3) with time zone NOT NULL DEFAULT NOW() ); + +-- 20260906_000000: Backfill five columns that existed in the collection +-- definitions but were never added by a migration (they reached this database +-- via dev-mode `db.push` before migrations were introduced). Fresh environments +-- built purely from `migrations/` were missing them and failed every query +-- against the affected tables. Expected to be a no-op in production. +-- +-- `updates.author` note: 20260420_000000 assumed this relationship would live +-- in `live_articles_rels` under path "updates.author". Payload actually stores +-- single-target, non-hasMany relationships as a scalar `_id` column on +-- the array's own table. +ALTER TABLE "layout" ADD COLUMN IF NOT EXISTS "grid" jsonb; +ALTER TABLE "opinion_page_layout" ADD COLUMN IF NOT EXISTS "layout" jsonb; + +ALTER TABLE "live_articles_updates" ADD COLUMN IF NOT EXISTS "author_id" integer; +ALTER TABLE "_live_articles_v_version_updates" ADD COLUMN IF NOT EXISTS "author_id" integer; + +DO $$ BEGIN + ALTER TABLE "live_articles_updates" ADD CONSTRAINT "live_articles_updates_author_id_users_id_fk" + FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE NO ACTION; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + ALTER TABLE "_live_articles_v_version_updates" ADD CONSTRAINT "_live_articles_v_version_updates_author_id_users_id_fk" + FOREIGN KEY ("author_id") REFERENCES "public"."users"("id") ON DELETE SET NULL ON UPDATE NO ACTION; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +CREATE INDEX IF NOT EXISTS "live_articles_updates_author_idx" + ON "live_articles_updates" USING btree ("author_id"); +CREATE INDEX IF NOT EXISTS "_live_articles_v_version_updates_author_idx" + ON "_live_articles_v_version_updates" USING btree ("author_id"); + +ALTER TABLE "payload_locked_documents_rels" + ADD COLUMN IF NOT EXISTS "opinion_page_layout_id" integer; + +DO $$ BEGIN + ALTER TABLE "payload_locked_documents_rels" + ADD CONSTRAINT "payload_locked_documents_rels_opinion_page_layout_fk" + FOREIGN KEY ("opinion_page_layout_id") REFERENCES "public"."opinion_page_layout"("id") + ON DELETE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_opinion_page_layout_id_idx" + ON "payload_locked_documents_rels" ("opinion_page_layout_id"); SQL From 9e49332dd63826e41fc5141e02c2fe6e0e2b59b9 Mon Sep 17 00:00:00 2001 From: Ronan Hevenor Date: Sun, 6 Sep 2026 15:54:50 -0400 Subject: [PATCH 2/5] build(pnpm): move overrides to pnpm-workspace.yaml and pin pnpm to 10.34.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The twelve `pnpm.overrides` entries are minimum-version floors for known- vulnerable transitive dependencies. pnpm stopped reading `pnpm.overrides` from package.json and now warns that the keys are ignored, so they were silently unenforced on any fresh resolution. Moved them verbatim to `overrides:` in pnpm-workspace.yaml, which is where current pnpm reads them. Verified: `pnpm install` with a full (non-frozen) resolution leaves pnpm-lock.yaml byte-identical to the committed one, and `pnpm install --frozen-lockfile` — what CI and the deploy both run — is unchanged. The resolved dependency graph is therefore provably identical to what production already builds from; this only restores enforcement for future resolutions. `pnpm build` passes. Also adds `packageManager: pnpm@10.34.5` to match the pnpm 10 that CI and the deploy runner use. Without it a contributor on pnpm 11 silently rewrites pnpm-lock.yaml — dropping the entire `overrides:` block from the lockfile (729 insertions / 214 deletions locally) — and merging that would have carried the downgraded transitive versions into a deploy. Pinning keeps every machine resolving exactly what CI resolves. Dropped the `version: 10` input from pnpm/action-setup in ci.yml and android-build.yml. The action compares that input against the packageManager field and hard-fails when they differ ("Multiple versions of pnpm specified ... Remove one of these versions"), so leaving both would have broken CI on the first push. package.json is now the single source of truth, which also makes CI use an exact pnpm rather than floating latest-10.x. deploy.yml does not use action-setup (self-hosted runner) and is untouched. No change to build-script approval: sharp ships prebuilt binaries and loads fine with its postinstall skipped, so the ignored-build-scripts warning needs no action and prod install behavior is untouched. Co-Authored-By: Claude Opus 5 --- .github/workflows/android-build.yml | 5 +++-- .github/workflows/ci.yml | 5 +++-- package.json | 17 +---------------- pnpm-workspace.yaml | 19 +++++++++++++++++++ 4 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index dece6c6..3a73967 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -37,10 +37,11 @@ jobs: # Full history so `git tag -l` can see prior android tags for version bumping. fetch-depth: 0 + # Version comes from the "packageManager" field in package.json. Setting + # it here as well makes action-setup fail with "Multiple versions of pnpm + # specified" whenever the two differ. - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7948f88..3d37031 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,11 @@ jobs: - name: Checkout Code uses: actions/checkout@v4 + # Version comes from the "packageManager" field in package.json. Setting + # it here as well makes action-setup fail with "Multiple versions of pnpm + # specified" whenever the two differ. - name: Setup pnpm uses: pnpm/action-setup@v4 - with: - version: 10 - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/package.json b/package.json index 619cc90..f58f377 100644 --- a/package.json +++ b/package.json @@ -63,20 +63,5 @@ "typescript": "^5", "vitest": "^4.1.5" }, - "pnpm": { - "overrides": { - "undici": "^7.24.0", - "minimatch": "^10.2.4", - "picomatch": "^4.0.4", - "flatted": "^3.4.2", - "immutable": "^4.3.8", - "ajv@^6": "^6.14.0", - "ajv@^8": "^8.18.0", - "dompurify": "^3.3.2", - "file-type": "^21.3.1", - "esbuild": "^0.25.0", - "yaml@^1": "^1.10.3", - "yaml@^2": "^2.8.3" - } - } + "packageManager": "pnpm@10.34.5" } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..b510d29 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,19 @@ +# Home for pnpm settings as of pnpm 10.34 / 11.x. These were previously in +# package.json under "pnpm.overrides", which newer pnpm silently ignores — +# leaving the pinned transitive versions below unenforced on any resolution. +# +# All of these are minimum-version floors for known-vulnerable transitive +# dependencies. Do not drop one without checking why it was pinned. +overrides: + undici: ^7.24.0 + minimatch: ^10.2.4 + picomatch: ^4.0.4 + flatted: ^3.4.2 + immutable: ^4.3.8 + ajv@^6: ^6.14.0 + ajv@^8: ^8.18.0 + dompurify: ^3.3.2 + file-type: ^21.3.1 + esbuild: ^0.25.0 + yaml@^1: ^1.10.3 + yaml@^2: ^2.8.3 From 32eb442f17f51caccfafa5670b136b3230838b6d Mon Sep 17 00:00:00 2001 From: Ronan Hevenor Date: Sun, 6 Sep 2026 21:47:37 -0400 Subject: [PATCH 3/5] chore(types): regenerate payload-types for articles.previousSlug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `previousSlug` was added to the Articles collection in 20260507_000000, but payload-types.ts was never regenerated, so the committed types were missing the field and its select entry. Pure `pnpm generate:types` output — no manual edits, and regenerating again is a no-op. Co-Authored-By: Claude Opus 5 --- payload-types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/payload-types.ts b/payload-types.ts index 6cfb185..94fb23e 100644 --- a/payload-types.ts +++ b/payload-types.ts @@ -418,6 +418,10 @@ export interface Article { * Original category/section name from the source system. Preserved for display and search; does not affect routing. */ legacyCategory?: string | null; + /** + * Old slug retained for 301 redirects after a rename. The middleware redirects requests for this slug to the current one. + */ + previousSlug?: string | null; updatedAt: string; createdAt: string; _status?: ('draft' | 'published') | null; @@ -1033,6 +1037,7 @@ export interface ArticlesSelect { legacySource?: T; legacyArticleId?: T; legacyCategory?: T; + previousSlug?: T; updatedAt?: T; createdAt?: T; _status?: T; From e40c28f9a24274e18bf484091f835b1df8f3bc04 Mon Sep 17 00:00:00 2001 From: Ronan Hevenor Date: Sun, 6 Sep 2026 22:45:57 -0400 Subject: [PATCH 4/5] feat(live): give live articles a real section, short header, and recommendations Live article pages had no site chrome and no way back into the section they belong to. `live_articles.section` looked like it could drive that, but it is free text for the homepage strip ("Labor Department", "Election Night") rather than the site taxonomy. Add a separate `siteSection` select (news/sports/features/opinion) so `section` keeps its strip-label meaning, then bring /live/[slug] in line with StandardLayout: the ArticleScrollBar short header and the "Continue Reading /
" recommendations block. ArticleRecommendations only ever read section/id/opinionType off its prop, so narrow it to an exported RecommendationContext. Article still satisfies it structurally, leaving article pages untouched. The id exclusion is now conditional since a live article has no articles row to exclude. Migration is registered in both paths per docs/migrations.md: a TS migration for CI, and the equivalent SQL plus a batch-30 tracking row in run_deploy_sql_migrations.sh for the production deploy. Existing rows backfill to 'news' via a nullable column DEFAULT plus an explicit UPDATE, so no live row breaks on read before an editor sets the field. Co-Authored-By: Claude Opus 5 --- app/(frontend)/live/[slug]/page.tsx | 19 +++++++- collections/LiveArticles.ts | 21 ++++++++ components/Article/ArticleRecommendations.tsx | 24 ++++++++-- ...6_010000_add_live_articles_site_section.ts | 48 +++++++++++++++++++ migrations/index.ts | 6 +++ payload-types.ts | 5 ++ scripts/run_deploy_sql_migrations.sh | 28 ++++++++++- 7 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 migrations/20260906_010000_add_live_articles_site_section.ts diff --git a/app/(frontend)/live/[slug]/page.tsx b/app/(frontend)/live/[slug]/page.tsx index 96fe832..0773020 100644 --- a/app/(frontend)/live/[slug]/page.tsx +++ b/app/(frontend)/live/[slug]/page.tsx @@ -6,6 +6,9 @@ import { getPayload, type CollectionSlug } from 'payload'; import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'; import config from '@/payload.config'; import Footer from '@/components/Footer'; +import ArticleScrollBar from '@/components/ArticleScrollBar'; +import { ArticleRecommendations } from '@/components/Article/ArticleRecommendations'; +import type { Article } from '@/payload-types'; import { ArticleDivider } from '@/components/Article/ArticleDivider'; import { SerializeLexical, type LexicalNode } from '@/components/Article/RichTextParser'; import { extractTextFromLexical, renderLexicalHeadline } from '@/utils/formatArticle'; @@ -45,6 +48,7 @@ type LiveArticle = { plainTitle: string; slug: string; section: string; + siteSection: Article['section']; hero: { url: string; alt?: string; width?: number; height?: number; caption?: string }; summary?: LiveArticleSummaryItem[]; updates: LiveArticleUpdate[]; @@ -165,8 +169,16 @@ export default async function LiveArticlePage({ params }: Args) { const latest = latestTimestamp(article); const hasSummary = Array.isArray(article.summary) && article.summary.length > 0; + const siteSection = article.siteSection ?? 'news'; + return ( -
+ <> + +
{/* Meta row: LIVE badge + relative updated time */} @@ -260,6 +272,8 @@ export default async function LiveArticlePage({ params }: Args) {
+ +