Building a Resilient Data Platform with Write-Ahead Log at Netflix
--- import { MdKeyboardArrowRight } from 'react-icons/md'; import type { Post } from '../../types/contentful'; interface Props { post: Post; wordCount?: number; } const { post, wordCount = 0 } = Astro.props as Props; const { title, slug, featuredImage, categories, publishedDate, author } = post; const featuredImageUrl = featuredImage?.fields?.file?.url ?? ''; const minutesToRead = Math.max(1, Math.ceil(wordCount / 200)); --- <article class="group transform overflow-hidden rounded-xl border-2 border-transparent bg-white shadow-md transition-all hover:scale-105 hover:border-purple-500 hover:shadow-xl" > { featuredImageUrl && ( <div class="relative h-48 overflow-hidden bg-gradient-to-br from-purple-400 to-pink-400"> <img src={featuredImageUrl} alt={title} class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110" /> </div> ) } <div class="p-6"> <h3 class="mb-3 line-clamp-2 text-xl font-black text-gray-900 transition group-hover:text-purple-600" > <a href={/posts/${slug}} class="hover:underline"> {title} </a> </h3> { categories?.length > 0 && ( <div class="mb-3 flex flex-wrap gap-2"> {categories.map((cat) => ( <a href={/categories/${cat.slug}} class="text-md rounded-full bg-purple-100 px-3 py-1 font-medium text-purple-700 transition hover:bg-purple-200" > {cat.title} </a> ))} </div> ) } <div class="flex items-center justify-between border-t border-gray-200 pt-4 text-xs text-gray-500" > <span>{minutesToRead} min read</span> <a href={/posts/${slug}} class="transition-gap mt-4 flex items-center gap-2 font-semibold text-purple-600 group-hover:gap-3" > Read more <MdKeyboardArrowRight /> </a> </div> <div class="mt-3 flex items-center justify-between text-xs text-gray-400"> <span> { new Date(publishedDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }) } </span> {author?.name && <span>• {author.name}</span>} </div> </div> </article> and import { client } from '../lib/contentful'; import type { Post } from '../types/contentful'; export async function getPosts(): Promise<Post[]> { const res = await client.getEntries({ content_type: 'post' }); return res.items.map((item) => item.fields as unknown as Post); } export async function getPostBySlug(slug: string): Promise<Post | null> { const res = await client.getEntries({ content_type: 'post', 'fields.slug': slug, limit: 1, }); return res.items.length ? (res.items[0].fields as unknown as Post) : null; } and export interface Post { title: string; slug: string; featuredImage?: { fields: { file: { url: string }; }; }; author: Author; categories: Category[]; tags: Tag[]; publishedDate: string; body: any; } //only catagory titel not show
ChatGPT said:
Ah! I see exactly what’s happening — your issue is that the category titles are not showing in the post card, even though your code is mapping over categories. Let’s break it down carefully.
1️⃣ Check your Post interface
You have:export interface Post {
title: string;
slug: string;
featuredImage?: {
fields: {
file: { url: string };
};
};
author: Author;
categories: Category[];
tags: Tag[];
publishedDate: string;
body: any;
}And Category probably looks like:export interface Category {
title: string;
slug: string;
}✅ So far, correct.
2️⃣ Check how you fetch posts from Contentful
You do:export async function getPosts(): Promise<Post[]> { const res = await client.getEntries({ content_type: 'post' }); return res.items.map((item) => item.fields as unknown as Post); }Here’s the catch:
item.fields.categoriesdoes not directly give youtitleandslug.In Contentful, a reference field (like
categories) is an array of linked entries. Thefieldsfor each category exist in the linked entry object (item.fields.categories[i].fields).
So if you just cast item.fields to Post, categories might be an array of references, not actual objects with title and slug.
3️⃣ Correct way to map categories
You need to resolve the linked entries when fetching:export async function getPosts(): Promise<Post[]> { const res = await client.getEntries({ content_type: 'post', include: 2 }); // include linked entries return res.items.map((item) => { const fields = item.fields; const categories: Category[] = fields.categories?.map((cat: any) => ({ title: cat.fields.title, slug: cat.fields.slug, })) || []; return { ...fields, categories, } as Post; }); }Notice:
include: 2ensures linked entries are included.We manually map
fields.categoriesto extracttitleandslug.
4️⃣ Now in your component
categories.map((cat) => cat.title) will now work because each cat has title and slug.
✅ Summary
The category titles weren’t showing because:
Contentful returns references for linked entries.
You need to map
.fieldsof each reference to gettitle/slug.Casting blindly (
as Post) won’t magically fetch the linked data.
If you want, I can rewrite your getPostBySlug and getPosts functions fully type-safe with Contentful references resolved so your PostCard works perfectly.Do you want me to do that?
You said:
[ { "title": "Carrot Soup with Spiced Ground Beef and Pine Nuts", "slug": "carrot-soup-with-spiced-ground-beef-and-pine-nuts", "featuredImage": {}, "categories": [ { "metadata": { "tags": [], "concepts": [] }, "sys": { "space": { "sys": { "type": "Link", "linkType": "Space", "id": "3fz2r8ltdock" } }, "id": "mejrhnMHIeG3uj2evo8xH", "type": "Entry", "createdAt": "2025-10-16T16:09:47.293Z", "updatedAt": "2025-10-17T09:44:03.135Z", "environment": { "sys": { "id": "master", "type": "Link", "linkType": "Environment" } }, "publishedVersion": 8, "revision": 2, "contentType": { "sys": { "type": "Link", "linkType": "ContentType", "id": "category" } }, "locale": "en-US" }, "fields": { "title": "food", "slug": "food" } } ],
ChatGPT said:
Ah! Now I see the problem clearly. Your categories array in the JSON is an array of Contentful Entry objects, not plain { title, slug }. Each item looks like this:{
"metadata": {...},
"sys": {...},
"fields": {
"title": "food",
"slug": "food"
}
}So in your PostCard, when you do:categories.map((cat) => (
<a href={`/categories/${cat.slug}`}>{cat.title}</a>
))It doesn’t work, because cat.slug and cat.title don’t exist at the top level — they are under cat.fields.
✅ Fix in your component
You can either map categories before passing to the component, or map inside the JSX:{categories?.length > 0 && ( <div class="mb-3 flex flex-wrap gap-2"> {categories.map((cat) => ( <a href={`/categories/${cat.fields.slug}`} class="text-md rounded-full bg-purple-100 px-3 py-1 font-medium text-purple-700 transition hover:bg-purple-200" > {cat.fields.title} </a> ))} </div> )}
Option 2 — Map when fetching posts
Better approach is to normalize categories when fetching:export async function getPosts(): Promise<Post[]> {
const res = await client.getEntries({ content_type: 'post', include: 2 });
return res.items.map((item) => {
const fields = item.fields;
const categories: Category[] = fields.categories?.map((cat: any) => ({
title: cat.fields.title,
slug: cat.fields.slug,
})) || [];
return {
...fields,
categories,
} as Post;
});
}Now categories in your PostCard will be simple { title, slug } objects, and your JSX will work as-is:{categories.map((cat) => (
<a href={`/categories/${cat.slug}`}>{cat.title}</a>
))}
So the root cause: Contentful returns linked entries, not plain objects. You need to access fields or normalize them before using.If you want, I can rewrite your full PostCard + fetching code so it’s fully type-safe and works perfectly with linked categories. Do you want me to do that?
You said:
I apply option one data is show but: show error on code how it is posible: Property 'fields' does not exist on type 'Category'.ts(233
ChatGPT said:
Ah! That error makes total sense — it’s TypeScript telling you:
“Hey, your
Categorytype only hastitleandslug, but you’re trying to accessfieldswhich doesn’t exist on that type.”
This happens because in your interface, you defined Category like this:export interface Category {
title: string;
slug: string;
}…but the JSON you’re actually getting from Contentful looks like this:{
sys: {...},
fields: {
title: "food",
slug: "food"
}
}So TypeScript thinks cat is of type Category (no fields) but at runtime, cat.fields does exist, hence the mismatch.
How to fix it
Option A — Cast cat as any (quick fix)
{categories?.length > 0 && ( <div class="mb-3 flex flex-wrap gap-2"> {categories.map((cat: any) => ( <a href={`/categories/${cat.fields.slug}`} class="text-md rounded-full bg-purple-100 px-3 py-1 font-medium text-purple-700 transition hover:bg-purple-200" > {cat.fields.title} </a> ))} </div> )}✅ Works immediately, but bypasses type safety.
Option B — Update your type definition
If you want type-safe fix, define Category to match the actual Contentful response:export interface ContentfulCategory {
sys: any;
fields: {
title: string;
slug: string;
};
}Then update Post:export interface Post {
...
categories: ContentfulCategory[];
...
}Now TypeScript knows cat.fields.title and cat.fields.slug exist.
Option C — Normalize categories when fetching (best practice)
Instead of touching your PostCard, normalize when fetching:const categories: Category[] = fields.categories?.map((cat: any) => ({
title: cat.fields.title,
slug: cat.fields.slug,
})) || [];Then categories is now { title, slug }[] and your JSX can remain:{categories.map((cat) => (
<a href={`/categories/${cat.slug}`}>{cat.title}</a>
))}✅ This is cleanest because your UI doesn’t care about Contentful’s internal fields structure, and TypeScript stays happy.
So the error is purely TypeScript vs runtime JSON mismatch.I can rewrite your PostCard + getPosts + getPostBySlug fully type-safe so you never have to deal with fields in the component — do you want me to do that?