fix(cms): ship the initial migration so a container finds its tables

Staging failed on boot with 42P01, relation "payload.users" does not
exist. The schema was empty because no migration existed, and the
adapter cannot create tables itself: db-postgres/connect.js gates push
on NODE_ENV !== 'production', so it is inert in a deployed container
regardless of config.

The generated migration is schema-qualified to "payload" throughout but
does not create that schema — schemaName says where tables go, it does
not create anything. It only worked against the throwaway database used
to generate it because the schema was created there by hand, so every
real environment would have failed on the first statement. CREATE SCHEMA
IF NOT EXISTS is hand-added at the top of up(), which makes it exactly
the kind of edit a regeneration discards silently; a test asserts it is
present and ordered before the first CREATE TABLE.

payload-types.ts is now committed rather than ignored. Ignoring it meant
CI typechecked against looser types than a developer with a generated
copy, which is how a Record<string, unknown> cast passed CI and then
failed locally the moment the file appeared. The post page uses the
generated Post and Media types instead, and narrows heroImage rather
than asserting it, since the field is an id at shallow depth.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017YmbBhr8s7GusjDE12hrZM
This commit is contained in:
TudorandClaude Opus 5 committed 2026-09-02 18:31:38 +01:00
1 parent a31d98b086
commit b0c7777e7a
8 files changed
+2319 -22

No files matched your search

+24 -19
View File
@@ -5,6 +5,7 @@ import { notFound } from 'next/navigation';
import { RichText } from '@payloadcms/richtext-lexical/react';
import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react';
import { getCachedPayload } from '@/lib/payload';
import type { Post, Media } from '@/payload-types';
import { absoluteUrl } from '@/lib/site';
import {
blogPostingJsonLd,
@@ -24,11 +25,15 @@ import styles from './Post.module.css';
*/
export const revalidate = 3600;
interface HeroImage {
url?: string | null;
alt?: string | null;
width?: number | null;
height?: number | null;
/**
* heroImage is `number | Media | null`: an id when the query is shallow, the
* populated document at depth 1. Both pages query at depth 1, but narrowing
* rather than asserting keeps it correct if that ever changes.
*/
function heroOf(post: Post): Media | null {
return typeof post.heroImage === 'object' && post.heroImage !== null
? post.heroImage
: null;
}
/**
@@ -68,12 +73,12 @@ const findPost = cache(async (slug: string) => {
return docs[0] ?? null;
});
function summarise(post: Record<string, unknown>) {
function summarise(post: Post) {
return {
title: String(post.title),
slug: String(post.slug),
excerpt: String(post.excerpt),
publishedAt: String(post.publishedAt),
title: post.title,
slug: post.slug,
excerpt: post.excerpt,
publishedAt: post.publishedAt,
};
}
@@ -84,18 +89,18 @@ export async function generateMetadata(
const post = await findPost(slug);
if (!post) return { title: 'Not found' };
const hero = post.heroImage as HeroImage | null | undefined;
const hero = heroOf(post);
return {
title: String(post.title),
description: String(post.excerpt),
title: post.title,
description: post.excerpt,
alternates: { canonical: absoluteUrl(`/blog/${post.slug}`) },
openGraph: {
type: 'article',
title: String(post.title),
description: String(post.excerpt),
title: post.title,
description: post.excerpt,
url: absoluteUrl(`/blog/${post.slug}`),
publishedTime: String(post.publishedAt),
publishedTime: post.publishedAt,
// A post with a hero image shares that; one without falls through to the
// generated share card at app/opengraph-image.tsx.
...(hero?.url ? { images: [{ url: hero.url }] } : {}),
@@ -110,8 +115,8 @@ export default async function PostPage(
const post = await findPost(slug);
if (!post) notFound();
const summary = summarise(post as Record<string, unknown>);
const hero = post.heroImage as HeroImage | null | undefined;
const summary = summarise(post);
const hero = heroOf(post);
const jsonLd = {
'@context': 'https://schema.org',
@@ -164,7 +169,7 @@ export default async function PostPage(
)}
<div className={styles.prose}>
<RichText data={post.content as never} converters={calloutConverters} />
<RichText data={post.content} converters={calloutConverters} />
</div>
</article>
);