BLOG-118 Fix to allow nullable published_time to support unpublished posts (#121)
All checks were successful
Frontend CI / build (push) Successful in 1m19s

### Description

This PR updates the application to handle posts that may not have a publication date (e.g., drafts) by making the `published_time` field optional across the entire post feature stack.

This ensures that draft posts can be processed and rendered without causing errors, and prevents search engine metadata from being generated for content that is not yet published.

#### Key Changes:

* **DTO & Schema (`postInfoResponseDto.ts`):**
    * The Zod schema for `PostInfoResponseSchema` has been updated to mark `published_time` as `.nullable()`.
    * The `PostInfoResponseDto` class now correctly handles a `null` value from the API, mapping it to `Date | null`.

* **Domain Entity (`postInfo.ts`):**
    * The core `PostInfo` entity's `publishedTime` property is now typed as `Date | null` to reflect the business logic that a post may be unpublished.

* **View Model (`postInfoViewModel.ts`):**
    * Updated `publishedTime` to be `Date | null`.
    * Added a new `isPublished` boolean getter for convenient conditional logic in the UI.
    * The `formattedPublishedTime` getter now returns `string | null`.
    * Dehydration and rehydration logic (`dehydrate`/`rehydrate`) has been updated to correctly handle the nullable `publishedTime`.

* **UI Component (`PostContentPage.svelte`):**
    * The component now uses the new `isPublished` flag to conditionally render the `<StructuredData>` component for SEO. This ensures that structured data is only included for posts that have been officially published.

### Package Changes

_No response_

### Screenshots

_No response_

### Reference

Resolves #118

### Checklist

- [x] A milestone is set
- [x] The related issuse has been linked to this branch

Reviewed-on: #121
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
SquidSpirit 2025-08-06 21:36:54 +08:00 committed by squid
parent 71bae3d8ca
commit 08c5262df6
4 changed files with 36 additions and 19 deletions

View File

@ -8,7 +8,7 @@ export const PostInfoResponseSchema = z.object({
description: z.string(),
preview_image_url: z.url(),
labels: z.array(LabelResponseSchema),
published_time: z.iso.datetime({ offset: true })
published_time: z.iso.datetime({ offset: true }).nullable()
});
export class PostInfoResponseDto {
@ -17,7 +17,7 @@ export class PostInfoResponseDto {
readonly description: string;
readonly previewImageUrl: URL;
readonly labels: readonly LabelResponseDto[];
readonly publishedTime: Date;
readonly publishedTime: Date | null;
private constructor(props: {
id: number;
@ -25,7 +25,7 @@ export class PostInfoResponseDto {
description: string;
previewImageUrl: URL;
labels: LabelResponseDto[];
publishedTime: Date;
publishedTime: Date | null;
}) {
this.id = props.id;
this.title = props.title;
@ -37,13 +37,19 @@ export class PostInfoResponseDto {
static fromJson(json: unknown): PostInfoResponseDto {
const parsedJson = PostInfoResponseSchema.parse(json);
let published_time: Date | null = null;
if (parsedJson.published_time !== null) {
published_time = new Date(parsedJson.published_time);
}
return new PostInfoResponseDto({
id: parsedJson.id,
title: parsedJson.title,
description: parsedJson.description,
previewImageUrl: new URL(parsedJson.preview_image_url),
labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)),
publishedTime: new Date(parsedJson.published_time)
publishedTime: published_time
});
}

View File

@ -10,7 +10,7 @@ export class PostInfoViewModel {
readonly description: string;
readonly previewImageUrl: URL;
readonly labels: readonly LabelViewModel[];
readonly publishedTime: Date;
readonly publishedTime: Date | null;
private constructor(props: {
id: number;
@ -18,7 +18,7 @@ export class PostInfoViewModel {
description: string;
previewImageUrl: URL;
labels: readonly LabelViewModel[];
publishedTime: Date;
publishedTime: Date | null;
}) {
this.id = props.id;
this.title = props.title;
@ -40,18 +40,27 @@ export class PostInfoViewModel {
}
static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel {
let publishedTime: Date | null = null;
if (props.publishedTime !== null) {
publishedTime = new Date(props.publishedTime);
}
return new PostInfoViewModel({
id: props.id,
title: props.title,
description: props.description,
previewImageUrl: new URL(props.previewImageUrl),
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
publishedTime: new Date(props.publishedTime)
publishedTime: publishedTime
});
}
get formattedPublishedTime(): string {
return this.publishedTime.toISOString().slice(0, 10);
get isPublished(): boolean {
return this.publishedTime !== null;
}
get formattedPublishedTime(): string | null {
return this.publishedTime?.toISOString().slice(0, 10) ?? null;
}
dehydrate(): DehydratedPostInfoProps {
@ -61,7 +70,7 @@ export class PostInfoViewModel {
description: this.description,
previewImageUrl: this.previewImageUrl.href,
labels: this.labels.map((label) => label.dehydrate()),
publishedTime: this.publishedTime.getTime()
publishedTime: this.publishedTime?.getTime() ?? null
};
}
}
@ -72,5 +81,5 @@ export interface DehydratedPostInfoProps {
description: string;
previewImageUrl: string;
labels: DehydratedLabelProps[];
publishedTime: number;
publishedTime: number | null;
}

View File

@ -6,7 +6,7 @@ export class PostInfo {
readonly description: string;
readonly previewImageUrl: URL;
readonly labels: readonly Label[];
readonly publishedTime: Date;
readonly publishedTime: Date | null;
constructor(props: {
id: number;
@ -14,7 +14,7 @@ export class PostInfo {
description: string;
previewImageUrl: URL;
labels: readonly Label[];
publishedTime: Date;
publishedTime: Date | null;
}) {
this.id = props.id;
this.title = props.title;

View File

@ -22,12 +22,14 @@
<title>{generateTitle(state.data?.info.title)}</title>
{#if state.data}
<meta name="description" content={state.data.info.description} />
<StructuredData
headline={state.data.info.title}
description={state.data.info.description}
datePublished={state.data.info.publishedTime}
image={state.data.info.previewImageUrl}
/>
{#if state.data.info.isPublished}
<StructuredData
headline={state.data.info.title}
description={state.data.info.description}
datePublished={state.data.info.publishedTime!}
image={state.data.info.previewImageUrl}
/>
{/if}
{/if}
</svelte:head>
<article class="container prose pb-10 prose-gray">