From 84247d4c431fdb79bc2e23755a566d43de798db5 Mon Sep 17 00:00:00 2001 From: Rafael Tavares <26308880+Rafatcb@users.noreply.github.com> Date: Sun, 25 Aug 2024 11:14:51 -0300 Subject: [PATCH] feat(rich results): add JSON-LD structured data --- models/json-ld.js | 99 +++++ pages/[username]/[slug]/index.public.js | 2 + .../classificados/[page]/index.public.js | 19 +- .../comentarios/[page]/index.public.js | 32 +- .../conteudos/[page]/index.public.js | 31 +- pages/[username]/index.public.js | 11 +- pages/contato/index.public.js | 6 +- pages/faq/index.public.js | 6 +- pages/index.public.js | 3 +- pages/interface/components/Head/index.js | 14 +- pages/museu/index.public.js | 5 + pages/pagina/[page]/index.public.js | 16 +- pages/publicar/index.public.js | 6 +- .../classificados/[page]/index.public.js | 14 + .../comentarios/[page]/index.public.js | 14 + pages/recentes/pagina/[page]/index.public.js | 11 + pages/recentes/todos/[page]/index.public.js | 14 + pages/status/index.public.js | 6 +- pages/termos-de-uso/index.public.js | 6 +- tests/unit/models/json-ld.test.js | 385 ++++++++++++++++++ 20 files changed, 677 insertions(+), 23 deletions(-) create mode 100644 models/json-ld.js create mode 100644 tests/unit/models/json-ld.test.js diff --git a/models/json-ld.js b/models/json-ld.js new file mode 100644 index 000000000..07884324e --- /dev/null +++ b/models/json-ld.js @@ -0,0 +1,99 @@ +import webserver from 'infra/webserver'; +import removeMarkdown from 'models/remove-markdown'; + +function getBreadcrumb(items) { + return { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: items.map((item, index) => ({ + '@type': 'ListItem', + position: index + 1, + name: item.name, + item: item.url, + })), + }; +} + +function getOrganization() { + return { + '@context': 'https://schema.org', + '@type': 'Organization', + url: webserver.host, + logo: `${webserver.host}/brand/rounded-light-filled.svg`, + name: 'TabNews', + description: + 'O TabNews é um site focado na comunidade da área de tecnologia, destinado a debates e troca de conhecimentos por meio de publicações e comentários criados pelos próprios usuários.', + email: 'contato@tabnews.com.br', + foundingDate: '2022-05-06T15:20:01.158Z', + }; +} + +function getPosting(content) { + return { + '@context': 'https://schema.org', + '@type': 'DiscussionForumPosting', + headline: content.title, + sharedContent: content.source_url ? { '@type': 'WebPage', url: content.source_url } : undefined, + ...getCommonPostingAndComment(content), + }; +} + +function getCommonPostingAndComment(content) { + return { + identifier: content.id, + author: { + '@type': 'Person', + name: content.owner_username, + identifier: content.owner_id, + url: `${webserver.host}/${content.owner_username}`, + }, + url: `${webserver.host}/${content.owner_username}/${content.slug}`, + datePublished: content.published_at, + dateModified: content.updated_at, + text: removeMarkdown(content.body), + interactionStatistic: [ + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/LikeAction', + userInteractionCount: content.tabcoins_credit, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/DislikeAction', + userInteractionCount: Math.abs(content.tabcoins_debit), + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/CommentAction', + userInteractionCount: content.children_deep_count, + }, + ], + comment: + content.children?.map((child) => ({ + '@type': 'Comment', + ...getCommonPostingAndComment(child), + })) ?? [], + }; +} + +function getProfile(user) { + return { + '@context': 'https://schema.org', + '@type': 'ProfilePage', + dateCreated: user.created_at, + dateModified: user.updated_at, + mainEntity: { + '@type': 'Person', + name: user.username, + identifier: user.id, + description: user.description ? removeMarkdown(user.description) : undefined, + }, + }; +} + +export default Object.freeze({ + getBreadcrumb, + getOrganization, + getPosting, + getProfile, +}); diff --git a/pages/[username]/[slug]/index.public.js b/pages/[username]/[slug]/index.public.js index fe654f71a..9d0946296 100644 --- a/pages/[username]/[slug]/index.public.js +++ b/pages/[username]/[slug]/index.public.js @@ -8,6 +8,7 @@ import webserver from 'infra/webserver.js'; import ad from 'models/advertisement'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import removeMarkdown from 'models/remove-markdown.js'; import user from 'models/user.js'; import { useCollapse } from 'pages/interface'; @@ -378,6 +379,7 @@ export const getStaticProps = getStaticPropsRevalidate(async (context) => { canonical: secureContentFound.parent_id ? undefined : `${webserver.host}/${secureContentFound.owner_username}/${secureContentFound.slug}`, + jsonLd: jsonLd.getPosting(secureContentFound), }; let secureRootContentFound = null; diff --git a/pages/[username]/classificados/[page]/index.public.js b/pages/[username]/classificados/[page]/index.public.js index 9aa741d6b..83375e323 100644 --- a/pages/[username]/classificados/[page]/index.public.js +++ b/pages/[username]/classificados/[page]/index.public.js @@ -4,8 +4,10 @@ import { getStaticPropsRevalidate } from 'next-swr'; import { ContentList, DefaultLayout, UserHeader } from '@/TabNewsUI'; import { FaUser } from '@/TabNewsUI/icons'; import { NotFoundError } from 'errors'; +import webserver from 'infra/webserver'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import user from 'models/user.js'; import validator from 'models/validator.js'; import { useUser } from 'pages/interface'; @@ -14,9 +16,24 @@ export default function RootContent({ contentListFound, pagination, username }) const { push } = useRouter(); const { user, isLoading } = useUser(); const isAuthenticatedUser = user && user.username === username; + const breadcrumbItems = [ + { name: username, url: `${webserver.host}/${username}` }, + { name: 'Classificados', url: `${webserver.host}/${username}/classificados/1` }, + ]; + + if (pagination.currentPage > 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/${username}/classificados/${pagination.currentPage}`, + }); + } return ( - + 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/${userFound.username}/comentarios/${pagination.currentPage}`, + }); + } return ( - - + + @@ -108,7 +126,7 @@ export const getStaticProps = getStaticPropsRevalidate(async (context) => { props: { contentListFound: secureContentListFound, pagination: results.pagination, - username: secureUserFound.username, + userFound: secureUserFound, }, revalidate: 10, diff --git a/pages/[username]/conteudos/[page]/index.public.js b/pages/[username]/conteudos/[page]/index.public.js index f84a50166..876d9159c 100644 --- a/pages/[username]/conteudos/[page]/index.public.js +++ b/pages/[username]/conteudos/[page]/index.public.js @@ -4,29 +4,47 @@ import { getStaticPropsRevalidate } from 'next-swr'; import { ContentList, DefaultLayout, UserHeader } from '@/TabNewsUI'; import { FaUser } from '@/TabNewsUI/icons'; import { NotFoundError } from 'errors'; +import webserver from 'infra/webserver'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import user from 'models/user.js'; import validator from 'models/validator.js'; import { useUser } from 'pages/interface'; -export default function RootContent({ contentListFound, pagination, username }) { +export default function RootContent({ contentListFound, pagination, userFound }) { const { push } = useRouter(); const { user, isLoading } = useUser(); - const isAuthenticatedUser = user && user.username === username; + const isAuthenticatedUser = user && user.username === userFound.username; + + const breadcrumbItems = [ + { name: userFound.username, url: `${webserver.host}/${userFound.username}` }, + { name: 'Publicações', url: `${webserver.host}/${userFound.username}/conteudos/1` }, + ]; + + if (pagination.currentPage > 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/${userFound.username}/conteudos/${pagination.currentPage}`, + }); + } return ( - - + + { contentListFound: secureContentListFound, pagination: results.pagination, username: secureUserFound.username, + userFound: secureUserFound, }, revalidate: 10, diff --git a/pages/[username]/index.public.js b/pages/[username]/index.public.js index d7dd41c97..a45fd763c 100644 --- a/pages/[username]/index.public.js +++ b/pages/[username]/index.public.js @@ -27,8 +27,10 @@ import { } from '@/TabNewsUI'; import { CircleSlashIcon, GearIcon, KebabHorizontalIcon } from '@/TabNewsUI/icons'; import { NotFoundError } from 'errors'; +import webserver from 'infra/webserver'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import user from 'models/user.js'; import validator from 'models/validator.js'; import { createErrorMessage, useUser } from 'pages/interface'; @@ -47,7 +49,14 @@ export default function Page({ userFound: userFoundFallback }) { } return ( - + ); diff --git a/pages/contato/index.public.js b/pages/contato/index.public.js index a602f1730..f5598f3e7 100644 --- a/pages/contato/index.public.js +++ b/pages/contato/index.public.js @@ -1,6 +1,10 @@ import { Box, DefaultLayout, Heading, Viewer } from '@/TabNewsUI'; +import webserver from 'infra/webserver'; +import jsonLd from 'models/json-ld'; export default function Page() { + const breadcrumbItems = [{ name: 'Contato', url: `${webserver.host}/contato` }]; + const body = `Leia com atenção qual a melhor forma de entrar em contato: ## Anúncios, Publicidade e Patrocínio @@ -26,7 +30,7 @@ export default function Page() { `; return ( - + Contato diff --git a/pages/faq/index.public.js b/pages/faq/index.public.js index 061d424f2..37f6e2f4b 100644 --- a/pages/faq/index.public.js +++ b/pages/faq/index.public.js @@ -1,6 +1,10 @@ import { Box, DefaultLayout, Heading, Viewer } from '@/TabNewsUI'; +import webserver from 'infra/webserver'; +import jsonLd from 'models/json-ld'; export default function Page() { + const breadcrumbItems = [{ name: 'FAQ', url: `${webserver.host}/faq` }]; + const faqContent = [ { id: 'tabnews', @@ -152,7 +156,7 @@ Após o fechamento da falha, o TabNews se compromete em criar um Postmortem púb const content = `${tableOfContents}\n\n${faqMarkdown}`; return ( - + FAQ - Perguntas Frequentes diff --git a/pages/index.public.js b/pages/index.public.js index 2e203f115..52622602f 100644 --- a/pages/index.public.js +++ b/pages/index.public.js @@ -5,13 +5,14 @@ import { FaTree } from '@/TabNewsUI/icons'; import ad from 'models/advertisement'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import user from 'models/user.js'; import validator from 'models/validator.js'; export default function Home({ adFound, contentListFound, pagination }) { return ( <> - + } + {Array.isArray(jsonLd) + ? jsonLd.map((data) => ( + + )) + : jsonLd && } + {children} ); diff --git a/pages/museu/index.public.js b/pages/museu/index.public.js index 36f6388ea..63f2b78f4 100644 --- a/pages/museu/index.public.js +++ b/pages/museu/index.public.js @@ -1,11 +1,16 @@ import { Box, DefaultLayout, PrimerLink, Text } from '@/TabNewsUI'; +import webserver from 'infra/webserver'; +import jsonLd from 'models/json-ld'; export default function Page() { + const breadcrumbItems = [{ name: 'Museu', url: `${webserver.host}/museu` }]; + return ( Museu TabNews diff --git a/pages/pagina/[page]/index.public.js b/pages/pagina/[page]/index.public.js index 41d885dcf..c5bd1e0b3 100644 --- a/pages/pagina/[page]/index.public.js +++ b/pages/pagina/[page]/index.public.js @@ -5,13 +5,27 @@ import webserver from 'infra/webserver'; import ad from 'models/advertisement'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import user from 'models/user.js'; import validator from 'models/validator.js'; export default function Home({ adFound, contentListFound, pagination }) { + let breadcrumbItems; + + if (pagination.currentPage > 1) { + breadcrumbItems = [ + { name: 'Relevantes', url: webserver.host }, + { name: `Página ${pagination.currentPage}`, url: `${webserver.host}/pagina/${pagination.currentPage}` }, + ]; + } + return ( <> - + diff --git a/pages/publicar/index.public.js b/pages/publicar/index.public.js index 99cdb6222..c14f0afc9 100644 --- a/pages/publicar/index.public.js +++ b/pages/publicar/index.public.js @@ -3,6 +3,8 @@ import { useEffect } from 'react'; import useSWR from 'swr'; import { Box, Content, DefaultLayout, Flash, Heading, Link } from '@/TabNewsUI'; +import webserver from 'infra/webserver'; +import jsonLd from 'models/json-ld'; import { useUser } from 'pages/interface'; export default function Post() { @@ -21,8 +23,10 @@ export default function Post() { } }, [user, router, isLoading]); + const breadcrumbItems = [{ name: 'Publicar', url: `${webserver.host}/publicar` }]; + return ( - + {contents?.length === 0 && ( diff --git a/pages/recentes/classificados/[page]/index.public.js b/pages/recentes/classificados/[page]/index.public.js index a9a82ca03..e15e00c0a 100644 --- a/pages/recentes/classificados/[page]/index.public.js +++ b/pages/recentes/classificados/[page]/index.public.js @@ -4,15 +4,29 @@ import { ContentList, DefaultLayout, RecentTabNav } from '@/TabNewsUI'; import webserver from 'infra/webserver'; import authorization from 'models/authorization.js'; import content from 'models/content.js'; +import jsonLd from 'models/json-ld'; import user from 'models/user.js'; import validator from 'models/validator.js'; export default function ContentsPage({ contentListFound, pagination }) { + const breadcrumbItems = [ + { name: 'Recentes', url: `${webserver.host}/recentes/pagina/1` }, + { name: 'Classificados', url: `${webserver.host}/recentes/classificados/1` }, + ]; + + if (pagination.currentPage > 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/recentes/classificados/${pagination.currentPage}`, + }); + } + return ( 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/recentes/comentarios/${pagination.currentPage}`, + }); + } + return ( 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/recentes/pagina/${pagination.currentPage}`, + }); + } + return ( <> 1) { + breadcrumbItems.push({ + name: `Página ${pagination.currentPage}`, + url: `${webserver.host}/recentes/todos/${pagination.currentPage}`, + }); + } + return ( + Estatísticas e Status do Site diff --git a/pages/termos-de-uso/index.public.js b/pages/termos-de-uso/index.public.js index de6baf671..ed496d641 100644 --- a/pages/termos-de-uso/index.public.js +++ b/pages/termos-de-uso/index.public.js @@ -1,6 +1,10 @@ import { Box, DefaultLayout, Heading, Viewer } from '@/TabNewsUI'; +import webserver from 'infra/webserver'; +import jsonLd from 'models/json-ld'; export default function Page() { + const breadcrumbItems = [{ name: 'Termos de Uso', url: `${webserver.host}/termos-de-uso` }]; + const body = `Ao utilizar o TabNews você está de acordo com os seguintes termos: ## TabNews @@ -43,7 +47,7 @@ export default function Page() { `; return ( - + Termos de Uso diff --git a/tests/unit/models/json-ld.test.js b/tests/unit/models/json-ld.test.js new file mode 100644 index 000000000..3f137cb99 --- /dev/null +++ b/tests/unit/models/json-ld.test.js @@ -0,0 +1,385 @@ +import webserver from 'infra/webserver'; +import jsonLd from 'models/json-ld'; + +describe('json-ld model', () => { + describe('getBreadcrumb', () => { + test('complete breadcrumb', () => { + const breadcrumbItems = [ + { name: 'Recentes', url: 'https://tabnews.com.br/recentes/pagina/1' }, + { name: 'Todos', url: 'https://tabnews.com.br/recentes/todos/1' }, + { name: 'Página 7', url: 'https://tabnews.com.br/recentes/todos/7' }, + ]; + + expect(jsonLd.getBreadcrumb(breadcrumbItems)).toStrictEqual({ + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Recentes', item: 'https://tabnews.com.br/recentes/pagina/1' }, + { '@type': 'ListItem', position: 2, name: 'Todos', item: 'https://tabnews.com.br/recentes/todos/1' }, + { '@type': 'ListItem', position: 3, name: 'Página 7', item: 'https://tabnews.com.br/recentes/todos/7' }, + ], + }); + }); + }); + + describe('getOrganization', () => { + test('organization with a logo for a white background', () => { + expect(jsonLd.getOrganization()).toStrictEqual({ + '@context': 'https://schema.org', + '@type': 'Organization', + url: webserver.host, + logo: `${webserver.host}/brand/rounded-light-filled.svg`, + name: 'TabNews', + description: + 'O TabNews é um site focado na comunidade da área de tecnologia, destinado a debates e troca de conhecimentos por meio de publicações e comentários criados pelos próprios usuários.', + email: 'contato@tabnews.com.br', + foundingDate: '2022-05-06T15:20:01.158Z', + }); + }); + }); + + describe('getPosting', () => { + test('root content without children', () => { + const content = { + id: '2a0afc7a-0778-493e-ab5f-2517e303d746', + owner_id: '78c5e17c-14a7-4a32-a932-578477145961', + parent_id: null, + slug: 'content-slug', + title: 'Content title', + body: '# My body with _markdown_\n**Formatting** :)', + status: 'published', + type: 'content', + source_url: 'https://www.tabnews.com.br/recentes/pagina/1', + created_at: new Date('2024-05-12T04:12:32.831Z'), + updated_at: new Date('2024-08-10T20:11:38.290Z'), + published_at: new Date('2024-05-13T04:33:12.421Z'), + deleted_at: null, + owner_username: 'ownerName', + tabcoins: 8, + tabcoins_credit: 10, + tabcoins_debit: -2, + children_deep_count: 0, + }; + + expect(jsonLd.getPosting(content)).toStrictEqual({ + '@context': 'https://schema.org', + '@type': 'DiscussionForumPosting', + headline: 'Content title', + sharedContent: { + '@type': 'WebPage', + url: 'https://www.tabnews.com.br/recentes/pagina/1', + }, + identifier: content.id, + author: { + '@type': 'Person', + name: 'ownerName', + identifier: content.owner_id, + url: `${webserver.host}/ownerName`, + }, + url: `${webserver.host}/ownerName/content-slug`, + datePublished: content.published_at, + dateModified: content.updated_at, + text: 'My body with markdown Formatting :)', + interactionStatistic: [ + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/LikeAction', + userInteractionCount: 10, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/DislikeAction', + userInteractionCount: 2, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/CommentAction', + userInteractionCount: 0, + }, + ], + comment: [], + }); + }); + + test('root content with children', () => { + const content = { + id: '3025c3a5-caca-4299-9ed1-769ffe4e37da', + owner_id: '25553d73-b7dd-47aa-b894-c8b5acc7de83', + parent_id: null, + slug: 'my-content', + title: 'My new content', + body: 'Root content body', + status: 'published', + type: 'content', + source_url: null, + created_at: new Date('2024-07-18T13:22:11.387Z'), + updated_at: new Date('2024-07-20T10:14:49.894Z'), + published_at: new Date('2024-07-19T19:25:31.993Z'), + deleted_at: null, + owner_username: 'rootContentUser', + tabcoins: 3, + tabcoins_credit: 3, + tabcoins_debit: -1, + children_deep_count: 4, + children: [ + { + id: '4d8ae77a-3e0a-4631-8d61-278ba4245392', + owner_id: '991c8775-7bc3-4472-99a6-13ca45817e6b', + parent_id: '3025c3a5-caca-4299-9ed1-769ffe4e37da', + slug: 'first-child-content', + title: null, + body: 'First child content', + status: 'published', + type: 'content', + source_url: null, + created_at: new Date('2024-07-19T20:11:03.100Z'), + updated_at: new Date('2024-07-19T20:11:37.530Z'), + published_at: new Date('2024-07-19T20:11:05.200Z'), + deleted_at: null, + owner_username: 'userChild1', + tabcoins: 3, + tabcoins_credit: 2, + tabcoins_debit: 0, + children_deep_count: 1, + children: [ + { + id: '76c09a77-240f-41a5-abf9-ae6bdb20945d', + owner_id: '12cef543-fa6f-48bb-90d4-fae0b1d65684', + parent_id: '4d8ae77a-3e0a-4631-8d61-278ba4245392', + slug: 'first-child-child', + title: null, + body: "First child's child", + status: 'published', + type: 'content', + source_url: null, + created_at: new Date('2024-01-30T15:12:33.121Z'), + updated_at: new Date('2024-02-03T12:02:10.424Z'), + published_at: new Date('2024-02-02T15:13:22.595Z'), + deleted_at: null, + owner_username: 'userChild11', + tabcoins: 0, + tabcoins_credit: 0, + tabcoins_debit: 0, + children_deep_count: 0, + children: [], + }, + ], + }, + { + id: '88c7e29f-e1d1-440e-9c84-b96c68e4e035', + owner_id: '2b35ca93-2424-45a3-8cfd-7195e6c8e281', + parent_id: '3025c3a5-caca-4299-9ed1-769ffe4e37da', + slug: 'second-child', + title: null, + body: 'Second child', + status: 'published', + type: 'content', + source_url: null, + created_at: new Date('2024-01-01T10:15:18.921Z'), + updated_at: new Date('2024-02-25T15:32:49.894Z'), + published_at: new Date('2024-01-10T19:13:31.993Z'), + deleted_at: null, + owner_username: 'userChild2', + tabcoins: -1, + tabcoins_credit: 1, + tabcoins_debit: -2, + children_deep_count: 1, + children: [], + }, + ], + }; + + expect(jsonLd.getPosting(content)).toStrictEqual({ + '@context': 'https://schema.org', + '@type': 'DiscussionForumPosting', + headline: 'My new content', + sharedContent: undefined, + identifier: content.id, + author: { + '@type': 'Person', + name: 'rootContentUser', + identifier: content.owner_id, + url: `${webserver.host}/rootContentUser`, + }, + url: `${webserver.host}/rootContentUser/my-content`, + datePublished: content.published_at, + dateModified: content.updated_at, + text: 'Root content body', + interactionStatistic: [ + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/LikeAction', + userInteractionCount: 3, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/DislikeAction', + userInteractionCount: 1, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/CommentAction', + userInteractionCount: 4, + }, + ], + comment: [ + { + '@type': 'Comment', + identifier: content.children[0].id, + author: { + '@type': 'Person', + name: 'userChild1', + identifier: content.children[0].owner_id, + url: `${webserver.host}/userChild1`, + }, + url: `${webserver.host}/userChild1/first-child-content`, + datePublished: content.children[0].published_at, + dateModified: content.children[0].updated_at, + text: 'First child content', + interactionStatistic: [ + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/LikeAction', + userInteractionCount: 2, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/DislikeAction', + userInteractionCount: 0, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/CommentAction', + userInteractionCount: 1, + }, + ], + comment: [ + { + '@type': 'Comment', + identifier: content.children[0].children[0].id, + author: { + '@type': 'Person', + name: 'userChild11', + identifier: content.children[0].children[0].owner_id, + url: `${webserver.host}/userChild11`, + }, + url: `${webserver.host}/userChild11/first-child-child`, + datePublished: content.children[0].children[0].published_at, + dateModified: content.children[0].children[0].updated_at, + text: "First child's child", + interactionStatistic: [ + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/LikeAction', + userInteractionCount: 0, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/DislikeAction', + userInteractionCount: 0, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/CommentAction', + userInteractionCount: 0, + }, + ], + comment: [], + }, + ], + }, + { + '@type': 'Comment', + identifier: content.children[1].id, + author: { + '@type': 'Person', + name: 'userChild2', + identifier: content.children[1].owner_id, + url: `${webserver.host}/userChild2`, + }, + url: `${webserver.host}/userChild2/second-child`, + datePublished: content.children[1].published_at, + dateModified: content.children[1].updated_at, + text: 'Second child', + interactionStatistic: [ + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/LikeAction', + userInteractionCount: 1, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/DislikeAction', + userInteractionCount: 2, + }, + { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/CommentAction', + userInteractionCount: 1, + }, + ], + comment: [], + }, + ], + }); + }); + }); + + describe('getProfile', () => { + test('profile with basic data only', () => { + const user = { + id: '90323057-ac67-4d2e-b071-a4bbdf6b8ddd', + username: 'someone', + description: '', + email: 'someone@example.com', + notifications: true, + features: ['create:session', 'read:session', 'create:content'], + tabcoins: 0, + tabcash: 0, + created_at: new Date('2023-05-11T13:14:25.000Z'), + updated_at: new Date('2024-08-24T22:16:32.000Z'), + }; + + expect(jsonLd.getProfile(user)).toStrictEqual({ + '@context': 'https://schema.org', + '@type': 'ProfilePage', + dateCreated: new Date('2023-05-11T13:14:25.000Z'), + dateModified: new Date('2024-08-24T22:16:32.000Z'), + mainEntity: { + '@type': 'Person', + name: 'someone', + identifier: user.id, + description: undefined, + }, + }); + }); + + test('profile with description', () => { + const user = { + id: 'acf5c5c5-12db-4442-b3b2-e4df1ffd4a59', + username: 'newUser', + description: 'My _description_ **with** Markdown! ![image](http://example.com/image.png)\n\nNew line', + email: 'new@example.com', + notifications: false, + features: [], + tabcoins: 3, + tabcash: 7, + created_at: new Date('2024-08-15T15:32:11.370Z'), + updated_at: new Date('2024-08-24T23:10:44.555Z'), + }; + + expect(jsonLd.getProfile(user)).toStrictEqual({ + '@context': 'https://schema.org', + '@type': 'ProfilePage', + dateCreated: new Date('2024-08-15T15:32:11.370Z'), + dateModified: new Date('2024-08-24T23:10:44.555Z'), + mainEntity: { + '@type': 'Person', + name: 'newUser', + identifier: user.id, + description: 'My description with Markdown! image New line', + }, + }); + }); + }); +});