BLOG-118 Fix to allow nullable published_time to support unpublished posts #121

Merged
squid merged 2 commits from BLOG-118_fix_error_when_access_unpublished_post into main 2025-08-06 21:36:54 +08:00
4 changed files with 36 additions and 19 deletions

View File

@ -8,7 +8,7 @@ export const PostInfoResponseSchema = z.object({
description: z.string(), description: z.string(),
preview_image_url: z.url(), preview_image_url: z.url(),
labels: z.array(LabelResponseSchema), labels: z.array(LabelResponseSchema),
published_time: z.iso.datetime({ offset: true }) published_time: z.iso.datetime({ offset: true }).nullable()
}); });
export class PostInfoResponseDto { export class PostInfoResponseDto {
@ -17,7 +17,7 @@ export class PostInfoResponseDto {
readonly description: string; readonly description: string;
readonly previewImageUrl: URL; readonly previewImageUrl: URL;
readonly labels: readonly LabelResponseDto[]; readonly labels: readonly LabelResponseDto[];
readonly publishedTime: Date; readonly publishedTime: Date | null;
private constructor(props: { private constructor(props: {
id: number; id: number;
@ -25,7 +25,7 @@ export class PostInfoResponseDto {
description: string; description: string;
previewImageUrl: URL; previewImageUrl: URL;
labels: LabelResponseDto[]; labels: LabelResponseDto[];
publishedTime: Date; publishedTime: Date | null;
}) { }) {
this.id = props.id; this.id = props.id;
this.title = props.title; this.title = props.title;
@ -37,13 +37,19 @@ export class PostInfoResponseDto {
static fromJson(json: unknown): PostInfoResponseDto { static fromJson(json: unknown): PostInfoResponseDto {
const parsedJson = PostInfoResponseSchema.parse(json); 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({ return new PostInfoResponseDto({
id: parsedJson.id, id: parsedJson.id,
title: parsedJson.title, title: parsedJson.title,
description: parsedJson.description, description: parsedJson.description,
previewImageUrl: new URL(parsedJson.preview_image_url), previewImageUrl: new URL(parsedJson.preview_image_url),
labels: parsedJson.labels.map((label) => LabelResponseDto.fromJson(label)), 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 description: string;
readonly previewImageUrl: URL; readonly previewImageUrl: URL;
readonly labels: readonly LabelViewModel[]; readonly labels: readonly LabelViewModel[];
readonly publishedTime: Date; readonly publishedTime: Date | null;
private constructor(props: { private constructor(props: {
id: number; id: number;
@ -18,7 +18,7 @@ export class PostInfoViewModel {
description: string; description: string;
previewImageUrl: URL; previewImageUrl: URL;
labels: readonly LabelViewModel[]; labels: readonly LabelViewModel[];
publishedTime: Date; publishedTime: Date | null;
}) { }) {
this.id = props.id; this.id = props.id;
this.title = props.title; this.title = props.title;
@ -40,18 +40,27 @@ export class PostInfoViewModel {
} }
static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel { static rehydrate(props: DehydratedPostInfoProps): PostInfoViewModel {
let publishedTime: Date | null = null;
if (props.publishedTime !== null) {
publishedTime = new Date(props.publishedTime);
}
return new PostInfoViewModel({ return new PostInfoViewModel({
id: props.id, id: props.id,
title: props.title, title: props.title,
description: props.description, description: props.description,
previewImageUrl: new URL(props.previewImageUrl), previewImageUrl: new URL(props.previewImageUrl),
labels: props.labels.map((label) => LabelViewModel.rehydrate(label)), labels: props.labels.map((label) => LabelViewModel.rehydrate(label)),
publishedTime: new Date(props.publishedTime) publishedTime: publishedTime
}); });
} }
get formattedPublishedTime(): string { get isPublished(): boolean {
return this.publishedTime.toISOString().slice(0, 10); return this.publishedTime !== null;
}
get formattedPublishedTime(): string | null {
return this.publishedTime?.toISOString().slice(0, 10) ?? null;
} }
dehydrate(): DehydratedPostInfoProps { dehydrate(): DehydratedPostInfoProps {
@ -61,7 +70,7 @@ export class PostInfoViewModel {
description: this.description, description: this.description,
previewImageUrl: this.previewImageUrl.href, previewImageUrl: this.previewImageUrl.href,
labels: this.labels.map((label) => label.dehydrate()), 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; description: string;
previewImageUrl: string; previewImageUrl: string;
labels: DehydratedLabelProps[]; labels: DehydratedLabelProps[];
publishedTime: number; publishedTime: number | null;
} }

View File

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

View File

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