diff --git a/src/service.ts b/src/service.ts index 4c31154..1309f7d 100644 --- a/src/service.ts +++ b/src/service.ts @@ -61,10 +61,10 @@ const buildSeoSearchParams = (baseParams: URLSearchParams): URLSearchParams => { "_yoast_wpseo_metadesc,_yoast_wpseo_title,_yoast_wpseo_canonical,_yoast_wpseo_meta-robots-noindex,_yoast_wpseo_meta-robots-nofollow,_yoast_wpseo_opengraph-title,_yoast_wpseo_opengraph-description,_yoast_wpseo_opengraph-image,_yoast_wpseo_twitter-title,_yoast_wpseo_twitter-description,_yoast_wpseo_twitter-image,_rank_math_title,_rank_math_description,_rank_math_canonical_url,_rank_math_robots,_rank_math_facebook_title,_rank_math_facebook_description,_rank_math_facebook_image,_rank_math_twitter_title,_rank_math_twitter_description,_rank_math_twitter_image" ); - // Include additional fields that might contain SEO data + // // Include additional fields that might contain SEO data baseParams.set( "_fields", - "id,date,date_gmt,guid,modified,modified_gmt,slug,status,type,link,title,content,excerpt,author,featured_media,comment_status,ping_status,sticky,template,format,meta,categories,tags,class_list,_links,_embedded" + "id,content.rendered,content.raw,block_data,has_blocks,date,date_gmt,guid,modified,modified_gmt,slug,status,type,link,title,excerpt,author,featured_media,comment_status,ping_status,sticky,template,format,meta,categories,tags,class_list,_links,_embedded" ); return baseParams; @@ -73,7 +73,9 @@ const buildSeoSearchParams = (baseParams: URLSearchParams): URLSearchParams => { export const WordpressServiceLayer = Layer.effect( WordpressService, Effect.gen(function* () { - const WORDPRESS_API_URL = yield* Config.url("WORDPRESS_API_URL"); + const WORDPRESS_API_URL = yield* Config.url("WORDPRESS_API_URL").pipe( + Effect.map((url) => url.toString().replace(/\/$/, "")) + ); const WORDPRESS_USERNAME = yield* Config.string("WORDPRESS_USERNAME"); const WORDPRESS_PASSWORD = yield* Config.redacted("WORDPRESS_PASSWORD"); const WORDPRESS_STATUS = yield* Config.string("WORDPRESS_STATUS"); @@ -348,11 +350,24 @@ export const WordpressServiceLayer = Layer.effect( const url = new URL( `${WORDPRESS_API_URL}/wp-json/wp/v2/posts?${seoParams.toString()}` ); + yield* Effect.logDebug("Requesting post detail", { + url: url.toString(), + }); const response = yield* httpClient.get(url, { headers: { Authorization: `Basic ${Redacted.value(WORDPRESS_API_KEY)}`, }, }); + if (response.status !== 200) { + yield* Effect.logError("Failed to fetch post detail", { + status: response.status, + }); + const text = yield* response.text; + yield* Effect.logError(text); + return yield* Effect.fail( + new WordpressError({ message: "Failed to fetch post detail" }) + ); + } const json = yield* response.json; const posts = WpPostDetail.array().safeParse(json); if (!posts.success) { diff --git a/src/types.ts b/src/types.ts index 6a849ba..4ba34f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -111,9 +111,13 @@ export const WpPostDetail = z.object({ rendered: z.string(), }), content: z.object({ + raw: z.string().optional(), rendered: z.string(), }), + block_data: z.array(z.unknown()).optional(), + has_blocks: z.boolean().optional(), excerpt: z.object({ + raw: z.string().optional(), rendered: z.string(), }), date: z.string(), @@ -203,9 +207,12 @@ export const WpPageDetail = WpPageOverview.extend({ modified: z.string(), modified_gmt: z.string(), content: z.object({ + raw: z.string().optional(), rendered: z.string(), protected: z.boolean(), }), + has_blocks: z.boolean().optional(), + block_data: z.array(z.unknown()).optional(), author: z.number(), featured_media: z.number(), comment_status: z.string(), @@ -249,13 +256,200 @@ export const WpTag = z.object({ seo_meta: WpSeoMeta.optional(), }); -export type WPSeoMeta = z.infer; -export type WPPostOverview = z.infer; -export type WPPostDetail = z.infer; -export type WPPageOverview = z.infer; -export type WPPageDetail = z.infer; -export type WPCategory = z.infer; -export type WPTag = z.infer; +export type WPSeoMeta = { + meta_title?: string; + meta_description?: string; + canonical_url?: string; + robots?: string; + og_title?: string; + og_description?: string; + og_image?: string; + og_image_alt?: string; + og_url?: string; + og_type?: string; + og_site_name?: string; + og_locale?: string; + twitter_card?: string; + twitter_title?: string; + twitter_description?: string; + twitter_image?: string; + twitter_image_alt?: string; + twitter_site?: string; + twitter_creator?: string; + schema_type?: string; + schema_json?: string; + focus_keyword?: string; + readability_score?: number; + seo_score?: number; + breadcrumbs?: Array<{ + text: string; + url: string; + }>; +}; + +export type WPPostOverview = { + id: number; + title: { + rendered: string; + }; + excerpt: { + rendered: string; + }; + date: string; + slug: string; + author?: number; + featured_media?: number; + seo_meta?: WPSeoMeta; + _embedded?: { + author?: Array<{ + id: number; + name: string; + url?: string; + description?: string; + avatar_urls?: Record; + }>; + "wp:featuredmedia"?: Array<{ + id: number; + source_url: string; + alt_text?: string; + media_details?: { + width?: number; + height?: number; + file?: string; + sizes?: Record; + }; + }>; + }; +}; + +export type WPPostDetail = { + id: number; + title: { + rendered: string; + }; + content: { + raw?: string; + rendered: string; + }; + block_data?: unknown[]; + has_blocks?: boolean; + excerpt: { + raw?: string; + rendered: string; + }; + date: string; + modified?: string; + slug: string; + author?: number; + featured_media?: number; + seo_meta?: WPSeoMeta; + reading_time?: number; + word_count?: number; + _embedded?: { + author?: Array<{ + id: number; + name: string; + url?: string; + description?: string; + avatar_urls?: Record; + }>; + "wp:featuredmedia"?: Array<{ + id: number; + source_url: string; + alt_text?: string; + media_details?: { + width?: number; + height?: number; + file?: string; + sizes?: Record; + }; + }>; + }; +}; + +export type WPPageOverview = { + id: number; + date: string; + slug: string; + status: string; + type: "page"; + link: string; + title: { + rendered: string; + }; + excerpt: { + rendered: string; + protected: boolean; + }; + parent: number; + menu_order: number; + categories: number[]; + tags: number[]; + class_list: string[]; + seo_meta?: WPSeoMeta; +}; + +export type WPPageDetail = WPPageOverview & { + date_gmt: string; + guid: { + rendered: string; + }; + modified: string; + modified_gmt: string; + content: { + raw?: string; + rendered: string; + protected: boolean; + }; + has_blocks?: boolean; + block_data?: unknown[]; + author: number; + featured_media: number; + comment_status: string; + ping_status: string; + template: string; + meta: { + footnotes: string; + }; + seo_meta?: WPSeoMeta; + reading_time?: number; + word_count?: number; +}; + +export type WPCategory = { + id: number; + count: number; + description: string; + link: string; + name: string; + slug: string; + taxonomy: "category"; + parent: number; + meta: unknown[]; + seo_meta?: WPSeoMeta; +}; + +export type WPTag = { + id: number; + count: number; + description: string; + link: string; + name: string; + slug: string; + taxonomy: "post_tag"; + meta: unknown[]; + seo_meta?: WPSeoMeta; +}; export const WpStatus = z.enum(["draft", "publish"]); export type WPStatus = z.infer;