Skip to content

Commit 479c451

Browse files
committed
feat: json-ld
1 parent a0dfb35 commit 479c451

File tree

7 files changed

+238
-73
lines changed

7 files changed

+238
-73
lines changed

packages/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"react": "18.2.0",
5656
"react-dom": "18.2.0",
5757
"react-error-boundary": "4.0.11",
58+
"schema-dts": "1.1.5",
5859
"super-tiny-icons": "0.5.0",
5960
"superjson": "1.13.1",
6061
"tailwindcss-animate": "1.0.6",

packages/ui/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ export * from './text-area';
2828
export * from './text-field';
2929
export * from './toast';
3030
export * from './toggle';
31+
export * from './json-ld';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './json-ld';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Head from 'next/head';
2+
import * as React from 'react';
3+
import type { Thing, WithContext } from 'schema-dts';
4+
5+
// Export specific schema types if needed, re-exporting from schema-dts
6+
export type { Comment, DiscussionForumPosting } from 'schema-dts';
7+
8+
export type JSONLDProps<T extends Thing> = {
9+
data: WithContext<T>;
10+
};
11+
12+
/**
13+
* Adds structured data for SEO using JSON-LD
14+
* Generic type T extends Thing from schema-dts for strong typing
15+
*/
16+
export function JSONLD<T extends Thing>({ data }: JSONLDProps<T>): JSX.Element {
17+
return (
18+
<Head>
19+
<script
20+
type="application/ld+json"
21+
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
22+
/>
23+
</Head>
24+
);
25+
}

packages/ui/src/pages/widget/page-url.tsx

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
import { trpc } from '@chirpy-dev/trpc/src/client';
2-
import { CommonWidgetProps } from '@chirpy-dev/types';
1+
import { trpc, type RouterOutputs } from '@chirpy-dev/trpc/src/client';
2+
import { CommonWidgetProps, type RTEValue } from '@chirpy-dev/types';
3+
import { getTextFromRteValue } from '@chirpy-dev/utils';
34
import * as React from 'react';
5+
import type {
6+
Comment,
7+
DiscussionForumPosting,
8+
InteractionCounter,
9+
LikeAction,
10+
Person,
11+
WithContext,
12+
} from 'schema-dts';
413

514
import { CommentForest, PoweredBy, WidgetLayout } from '../../blocks';
615
import { Text } from '../../components';
16+
import { JSONLD } from '../../components/json-ld/json-ld';
717
import { CommentContextProvider } from '../../contexts';
818
import { useRefetchInterval } from './use-refetch-interval';
919

@@ -15,6 +25,45 @@ export type PageCommentProps = CommonWidgetProps & {
1525
};
1626
};
1727

28+
type ForestComment = NonNullable<RouterOutputs['comment']['forest'][number]>;
29+
30+
/**
31+
* Creates a JSON-LD comment structure recursively for a comment and its replies
32+
*/
33+
function createCommentJsonLd(comment: ForestComment, pageUrl: string): Comment {
34+
const author: Person = {
35+
'@type': 'Person',
36+
name: comment.user.name || '',
37+
image: comment.user.image || undefined,
38+
};
39+
40+
const commentJsonLd: Comment = {
41+
'@type': 'Comment',
42+
text: getTextFromRteValue(comment.content as RTEValue),
43+
dateCreated: comment.createdAt.toISOString(),
44+
author: author,
45+
url: `${pageUrl}#comment-${comment.id}`,
46+
};
47+
48+
// Add likes information if available
49+
if (comment.likes && comment.likes.length > 0) {
50+
commentJsonLd.interactionStatistic = {
51+
'@type': 'InteractionCounter',
52+
interactionType: 'https://schema.org/LikeAction' as unknown as LikeAction,
53+
userInteractionCount: comment.likes.length,
54+
} as InteractionCounter;
55+
}
56+
57+
// Recursively process replies
58+
if (comment.replies && comment.replies.length > 0) {
59+
commentJsonLd.comment = comment.replies.map((reply: ForestComment) =>
60+
createCommentJsonLd(reply, pageUrl),
61+
) as Comment[];
62+
}
63+
64+
return commentJsonLd;
65+
}
66+
1867
/**
1968
* Comment tree widget for a page
2069
* @param props
@@ -39,8 +88,19 @@ export function CommentWidgetPage(props: PageCommentProps): JSX.Element {
3988
);
4089
}
4190

91+
// Create JSON-LD structured data for the comments
92+
const jsonLdData: WithContext<DiscussionForumPosting> = {
93+
'@context': 'https://schema.org',
94+
'@type': 'DiscussionForumPosting',
95+
url: props.page.url,
96+
comment: comments.map((comment: ForestComment) =>
97+
createCommentJsonLd(comment, props.page.url),
98+
) as Comment[],
99+
};
100+
42101
return (
43102
<WidgetLayout widgetTheme={props.theme} title="Comment">
103+
<JSONLD<DiscussionForumPosting> data={jsonLdData} />
44104
<CommentContextProvider projectId={props.projectId} page={props.page}>
45105
<div className="pt-1">
46106
{/* @ts-ignore */}

packages/ui/src/pages/widget/timeline.tsx

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
1-
import { trpc } from '@chirpy-dev/trpc/src/client';
2-
import { CommonWidgetProps } from '@chirpy-dev/types';
1+
import { trpc, type RouterOutputs } from '@chirpy-dev/trpc/src/client';
2+
import { CommonWidgetProps, type RTEValue } from '@chirpy-dev/types';
3+
import { getTextFromRteValue } from '@chirpy-dev/utils';
34
import * as React from 'react';
5+
import type {
6+
Comment,
7+
InteractionCounter,
8+
LikeAction,
9+
Person,
10+
} from 'schema-dts';
411

512
import {
6-
CommentTimeline,
13+
CommentTimeline as CommentTimelineComponent,
714
PoweredBy,
815
UserMenu,
916
WidgetLayout,
1017
} from '../../blocks';
1118
import { Heading, IconArrowLeft, IconButton, Link } from '../../components';
19+
import { JSONLD } from '../../components/json-ld/json-ld';
1220
import { CommentContextProvider } from '../../contexts';
1321
import { useRefetchInterval } from './use-refetch-interval';
1422

@@ -21,6 +29,56 @@ export type CommentTimelineWidgetProps = CommonWidgetProps & {
2129
};
2230
};
2331

32+
type TimelineComment = NonNullable<RouterOutputs['comment']['timeline']>;
33+
/**
34+
* Creates a JSON-LD comment structure recursively for a comment and its replies
35+
*/
36+
function createCommentJsonLd(
37+
comment: TimelineComment,
38+
pageUrl: string,
39+
): Comment | null {
40+
const author: Person = {
41+
'@type': 'Person',
42+
name: comment.user.name || '',
43+
image: comment.user.image || undefined,
44+
};
45+
46+
const commentJsonLd: Comment = {
47+
'@type': 'Comment',
48+
text: getTextFromRteValue(comment.content as RTEValue),
49+
dateCreated: comment.createdAt.toISOString(),
50+
author: author,
51+
url: `${pageUrl}#comment-${comment.id}`,
52+
mainEntityOfPage: pageUrl,
53+
};
54+
55+
// Add likes information if available
56+
if (comment.likes && comment.likes.length > 0) {
57+
commentJsonLd.interactionStatistic = {
58+
'@type': 'InteractionCounter',
59+
interactionType: 'https://schema.org/LikeAction' as unknown as LikeAction,
60+
userInteractionCount: comment.likes.length,
61+
} as InteractionCounter;
62+
}
63+
64+
// Add information about parent comment if it exists
65+
if (comment.parentId) {
66+
commentJsonLd.parentItem = {
67+
'@type': 'Comment',
68+
url: `${pageUrl}#comment-${comment.parentId}`,
69+
} as Comment;
70+
}
71+
72+
// Recursively process replies (if they exist in the timeline data)
73+
if (comment.replies && comment.replies.length > 0) {
74+
commentJsonLd.comment = comment.replies
75+
.map((reply: any) => createCommentJsonLd(reply, pageUrl))
76+
.filter(Boolean) as Comment[];
77+
}
78+
79+
return commentJsonLd;
80+
}
81+
2482
export function CommentTimelineWidget(
2583
props: CommentTimelineWidgetProps,
2684
): JSX.Element {
@@ -34,8 +92,18 @@ export function CommentTimelineWidget(
3492
},
3593
);
3694

95+
// Generate JSON-LD data for the comment timeline
96+
const jsonLdData = comment
97+
? createCommentJsonLd(comment, props.page.url)
98+
: null;
99+
37100
return (
38101
<WidgetLayout widgetTheme={props.theme} title="Comment timeline">
102+
{jsonLdData && (
103+
<JSONLD<Comment>
104+
data={{ '@context': 'https://schema.org', ...jsonLdData }}
105+
/>
106+
)}
39107
<CommentContextProvider projectId={props.projectId} page={props.page}>
40108
<div className="mb-4 flex flex-row items-center justify-between">
41109
{/* Can't use history.back() here in case user open this page individual */}
@@ -54,7 +122,9 @@ export function CommentTimelineWidget(
54122
<UserMenu variant="Widget" />
55123
</div>
56124

57-
{comment?.id && <CommentTimeline key={comment.id} comment={comment} />}
125+
{comment?.id && (
126+
<CommentTimelineComponent key={comment.id} comment={comment} />
127+
)}
58128
{props.plan === 'HOBBY' && <PoweredBy />}
59129
</CommentContextProvider>
60130
</WidgetLayout>

0 commit comments

Comments
 (0)