fix(route/patreon): creator ID extraction and render content from JSON (#22188)
* fix(route/patreon): Fix creator ID extraction * fix(route/patreon): render content from content_json_string * fix(route/patreon): satisfy oxlint and use trueUA for creator fetches Co-authored-by: Cursor <cursoragent@cursor.com> * fix(route/patreon): resolve review comments --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6c52888af2
commit
cae03809c0
|
|
@ -8,6 +8,7 @@ import cache from '@/utils/cache';
|
|||
import ofetch from '@/utils/ofetch';
|
||||
import { parseDate } from '@/utils/parse-date';
|
||||
|
||||
import { renderContentJson } from './render-content-json';
|
||||
import type { CreatorData, MediaRelation, PostData } from './types';
|
||||
|
||||
const renderDescription = ({ attributes, relationships, included }) => {
|
||||
|
|
@ -16,6 +17,7 @@ const renderDescription = ({ attributes, relationships, included }) => {
|
|||
const previewImage = attributes.image?.url ?? attributes.meta_image_url;
|
||||
const audioUrl = relationships.audio?.attributes?.download_url || relationships.audio_preview?.attributes?.download_url;
|
||||
const imageItems = imageOrder.map((mediaIdStr) => included.find((item) => item.id === mediaIdStr)).filter(Boolean);
|
||||
const textContent = renderContentJson(attributes.content_json_string) || renderContentJson(attributes.teaser_text_json_string);
|
||||
|
||||
return renderToString(
|
||||
<>
|
||||
|
|
@ -68,7 +70,7 @@ const renderDescription = ({ attributes, relationships, included }) => {
|
|||
</>
|
||||
)}
|
||||
|
||||
{attributes.content || attributes.teaser_text ? raw(attributes.content || attributes.teaser_text) : null}
|
||||
{textContent ? raw(textContent) : null}
|
||||
|
||||
{relationships.attachments_media?.length
|
||||
? relationships.attachments_media.map((media) => (
|
||||
|
|
@ -121,7 +123,7 @@ async function handler(ctx) {
|
|||
const ogUrl = $('meta[property="og:url"]').attr('content');
|
||||
if (ogUrl?.startsWith(`${baseUrl}/cw/`)) {
|
||||
const ogImage = $('meta[property="og:image"]').attr('content');
|
||||
const creatorId = decodeURIComponent(ogImage || '').match(/card-teaser-image\/creator\/(\d+)\?/)?.[1];
|
||||
const creatorId = decodeURIComponent(ogImage || '').match(/card-teaser-image\/creator\/(\d+)/)?.[1];
|
||||
if (creatorId) {
|
||||
const creator = await ofetch(`${baseUrl}/api/campaigns/${creatorId}`);
|
||||
return {
|
||||
|
|
@ -159,7 +161,7 @@ async function handler(ctx) {
|
|||
'campaign,access_rules,access_rules.tier.null,attachments_media,audio,audio_preview.null,drop,images,media,native_video_insights,poll.choices,poll.current_user_responses.user,poll.current_user_responses.choice,poll.current_user_responses.poll,user,user_defined_tags,ti_checks,video.null,content_unlock_options.product_variant.null',
|
||||
'fields[campaign]': 'currency,show_audio_post_download_links,avatar_photo_url,avatar_photo_image_urls,earnings_visibility,is_nsfw,is_monthly,name,url',
|
||||
'fields[post]':
|
||||
'change_visibility_at,comment_count,commenter_count,content,created_at,current_user_can_comment,current_user_can_delete,current_user_can_report,current_user_can_view,current_user_comment_disallowed_reason,current_user_has_liked,embed,image,insights_last_updated_at,is_paid,like_count,meta_image_url,min_cents_pledged_to_view,monetization_ineligibility_reason,post_file,post_metadata,published_at,patreon_url,post_type,pledge_url,preview_asset_type,thumbnail,thumbnail_url,teaser_text,title,upgrade_url,url,was_posted_by_campaign_owner,has_ti_violation,moderation_status,post_level_suspension_removal_date,pls_one_liners_by_category,video,video_preview,view_count,content_unlock_options,is_new_to_current_user,watch_state',
|
||||
'change_visibility_at,comment_count,commenter_count,content_json_string,created_at,current_user_can_comment,current_user_can_delete,current_user_can_report,current_user_can_view,current_user_comment_disallowed_reason,current_user_has_liked,embed,image,insights_last_updated_at,is_paid,like_count,meta_image_url,min_cents_pledged_to_view,monetization_ineligibility_reason,post_file,post_metadata,published_at,patreon_url,post_type,pledge_url,preview_asset_type,thumbnail,thumbnail_url,teaser_text_json_string,title,upgrade_url,url,was_posted_by_campaign_owner,has_ti_violation,moderation_status,post_level_suspension_removal_date,pls_one_liners_by_category,video,video_preview,view_count,content_unlock_options,is_new_to_current_user,watch_state',
|
||||
'fields[post_tag]': 'tag_type,value',
|
||||
'fields[user]': 'image_url,full_name,url',
|
||||
'fields[access_rule]': 'access_rule_type,amount_cents',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { renderToString } from 'hono/jsx/dom/server';
|
||||
|
||||
interface ContentNode {
|
||||
type: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
content?: ContentNode[];
|
||||
text?: string;
|
||||
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
const TextNode = ({ node }: { node: ContentNode }) => {
|
||||
let content: JSX.Element | string = node.text ?? '';
|
||||
for (const mark of node.marks ?? []) {
|
||||
if (mark.type === 'bold') {
|
||||
content = <strong>{content}</strong>;
|
||||
} else if (mark.type === 'link' && mark.attrs?.href) {
|
||||
content = <a href={String(mark.attrs.href)}>{content}</a>;
|
||||
}
|
||||
}
|
||||
return <>{content}</>;
|
||||
};
|
||||
|
||||
const ContentNode = ({ node }: { node: ContentNode }) => {
|
||||
switch (node.type) {
|
||||
case 'doc':
|
||||
return (
|
||||
<>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
case 'paragraph':
|
||||
return (
|
||||
<p>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
case 'text':
|
||||
return <TextNode node={node} />;
|
||||
case 'hardBreak':
|
||||
return <br />;
|
||||
case 'heading': {
|
||||
const level = Math.min(6, Math.max(1, Number(node.attrs?.level) || 3));
|
||||
const Tag = `h${level}` as keyof JSX.IntrinsicElements;
|
||||
return (
|
||||
<Tag>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
case 'image':
|
||||
return node.attrs?.src ? <img src={String(node.attrs.src)} alt={String(node.attrs.alt ?? '')} /> : null;
|
||||
case 'bulletList':
|
||||
return (
|
||||
<ul>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
case 'orderedList':
|
||||
return (
|
||||
<ol>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
case 'listItem':
|
||||
return (
|
||||
<li>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</li>
|
||||
);
|
||||
case 'horizontalRule':
|
||||
return <hr />;
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
{node.content?.map((child, index) => (
|
||||
<ContentNode key={index} node={child} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const renderContentJson = (jsonString?: string | null): string => {
|
||||
if (!jsonString) {
|
||||
return '';
|
||||
}
|
||||
const doc = JSON.parse(jsonString) as ContentNode;
|
||||
return renderToString(<ContentNode node={doc} />);
|
||||
};
|
||||
|
|
@ -63,14 +63,14 @@ interface Attributes {
|
|||
post_type: string;
|
||||
preview_asset_type: string | null;
|
||||
published_at: string;
|
||||
teaser_text: string | null;
|
||||
teaser_text_json_string?: string | null;
|
||||
title: string;
|
||||
upgrade_url: string;
|
||||
url: string;
|
||||
video_preview: VideoPreview | null;
|
||||
was_posted_by_campaign_owner: boolean;
|
||||
thumbnail?: Thumbnail;
|
||||
content?: string;
|
||||
content_json_string?: string | null;
|
||||
embed?: null;
|
||||
post_file?: PostFile;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue