BLOG-118 Fix to allow nullable published_time to support unpublished posts (#121)
All checks were successful
Frontend CI / build (push) Successful in 1m19s
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:
parent
71bae3d8ca
commit
08c5262df6
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user