refactor: remove unbody service
This commit is contained in:
parent
047f378e4d
commit
6d4f93f71c
2
.env
2
.env
|
@ -1,5 +1,3 @@
|
||||||
UNBODY_API_KEY=
|
|
||||||
UNBODY_PROJECT_ID=
|
|
||||||
SIMPLECAST_ACCESS_TOKEN=
|
SIMPLECAST_ACCESS_TOKEN=
|
||||||
REVALIDATE_WEBHOOK_TOKEN=
|
REVALIDATE_WEBHOOK_TOKEN=
|
||||||
DISCORD_LOGS_WEBHOOK_URL=
|
DISCORD_LOGS_WEBHOOK_URL=
|
||||||
|
|
|
@ -39,3 +39,4 @@ next-env.d.ts
|
||||||
public/rss.xml
|
public/rss.xml
|
||||||
public/atom*.xml
|
public/atom*.xml
|
||||||
public/images/placeholders/*
|
public/images/placeholders/*
|
||||||
|
!public/images/placeholders/.gitkeep
|
||||||
|
|
|
@ -7,10 +7,9 @@ ARG PORT=3000
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
# Credentials
|
# Credentials
|
||||||
ARG UNBODY_PROJECT_ID
|
|
||||||
ARG UNBODY_API_KEY
|
|
||||||
ARG SIMPLECAST_ACCESS_TOKEN
|
ARG SIMPLECAST_ACCESS_TOKEN
|
||||||
ARG REVALIDATE_WEBHOOK_TOKEN
|
ARG REVALIDATE_WEBHOOK_TOKEN
|
||||||
|
ARG STRAPI_API_KEY
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
|
@ -43,7 +43,7 @@ pipeline {
|
||||||
]) {
|
]) {
|
||||||
image = docker.build(
|
image = docker.build(
|
||||||
"${IMAGE_NAME}:${GIT_COMMIT.take(8)}",
|
"${IMAGE_NAME}:${GIT_COMMIT.take(8)}",
|
||||||
["--build-arg='UNBODY_PROJECT_ID=${env.UNBODY_PROJECT_ID}'",
|
["--build-arg='STRAPI_API_KEY=${env.UNBODY_PROJECT_ID}'",
|
||||||
"--build-arg='UNBODY_API_KEY=${env.UNBODY_API_KEY}'",
|
"--build-arg='UNBODY_API_KEY=${env.UNBODY_API_KEY}'",
|
||||||
"--build-arg='SIMPLECAST_ACCESS_TOKEN=${SIMPLECAST_ACCESS_TOKEN}'",
|
"--build-arg='SIMPLECAST_ACCESS_TOKEN=${SIMPLECAST_ACCESS_TOKEN}'",
|
||||||
"--build-arg='REVALIDATE_WEBHOOK_TOKEN=${REVALIDATE_WEBHOOK_TOKEN}'",
|
"--build-arg='REVALIDATE_WEBHOOK_TOKEN=${REVALIDATE_WEBHOOK_TOKEN}'",
|
||||||
|
|
|
@ -12,7 +12,7 @@ The repository for [press.logos.co](https://press.logos.co/) website.
|
||||||
|
|
||||||
- Emotion: CSS-in-JS
|
- Emotion: CSS-in-JS
|
||||||
|
|
||||||
- [Unbody](https://unbody.io/) : CMS
|
- [Strapi](https://strapi.io/) : CMS
|
||||||
|
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
@ -20,8 +20,6 @@ The repository for [press.logos.co](https://press.logos.co/) website.
|
||||||
Please check the environment values in `.env` located in the root directory.
|
Please check the environment values in `.env` located in the root directory.
|
||||||
|
|
||||||
```
|
```
|
||||||
UNBODY_API_KEY=
|
|
||||||
UNBODY_PROJECT_ID=
|
|
||||||
SIMPLECAST_ACCESS_TOKEN=
|
SIMPLECAST_ACCESS_TOKEN=
|
||||||
REVALIDATE_WEBHOOK_TOKEN=
|
REVALIDATE_WEBHOOK_TOKEN=
|
||||||
NEXT_PUBLIC_SITE_URL=https://press.logos.co
|
NEXT_PUBLIC_SITE_URL=https://press.logos.co
|
||||||
|
@ -30,8 +28,6 @@ FATHOM_SITE_ID=
|
||||||
|
|
||||||
This is a template for `.env.local`, which is included in `.gitignore`.
|
This is a template for `.env.local`, which is included in `.gitignore`.
|
||||||
|
|
||||||
You can obtain an Unbody API key and project ID through your [Unbody project](https://app.unbody.io/).
|
|
||||||
|
|
||||||
To find the Simplecast access token, follow these steps on the Simplecast dashboard:
|
To find the Simplecast access token, follow these steps on the Simplecast dashboard:
|
||||||
|
|
||||||
1. Click the gear button in the top-right corner.
|
1. Click the gear button in the top-right corner.
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
type TocItem {
|
|
||||||
level: Int!
|
|
||||||
tag: String!
|
|
||||||
href: String!
|
|
||||||
title: String!
|
|
||||||
blockIndex: Int!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Mention {
|
|
||||||
name: String!
|
|
||||||
emailAddress: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
type Footnote {
|
|
||||||
index: Int!
|
|
||||||
id: String!
|
|
||||||
refId: String!
|
|
||||||
refValue: String!
|
|
||||||
valueHTML: String!
|
|
||||||
valueText: String!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type GoogleDoc {
|
|
||||||
mentionsObj: [Mention]!
|
|
||||||
tocObj: [TocItem]!
|
|
||||||
}
|
|
||||||
|
|
||||||
extend type TextBlock {
|
|
||||||
footnotesObj: [Footnote]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -27,12 +27,6 @@ Page.getLayout = function getLayout(page: React.ReactNode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps<PageProps> = async () => {
|
export const getStaticProps: GetStaticProps<PageProps> = async () => {
|
||||||
// const { data: tags = [] } = await unbodyApi.getTopics(true)
|
|
||||||
// const { data: highlighted } = await unbodyApi.getHighlightedPosts()
|
|
||||||
// const { data: latest } = await unbodyApi.getRecentPosts({
|
|
||||||
// skip: 0,
|
|
||||||
// limit: 15,
|
|
||||||
// })
|
|
||||||
const { data: latest } = await strapiApi.getRecentPosts({
|
const { data: latest } = await strapiApi.getRecentPosts({
|
||||||
highlighted: 'exclude',
|
highlighted: 'exclude',
|
||||||
limit: 15,
|
limit: 15,
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { calcReadingTime } from '../../../utils/string.utils'
|
|
||||||
import { UnbodyResGoogleDocData } from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const ArticleDataType: UnbodyDataTypeConfig<
|
|
||||||
UnbodyResGoogleDocData,
|
|
||||||
LPE.Article.Data
|
|
||||||
> = {
|
|
||||||
key: 'ArticleDocument',
|
|
||||||
objectType: 'GoogleDoc',
|
|
||||||
classes: ['article', 'podcast', 'show', 'episode', 'document'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original, root) =>
|
|
||||||
data.pathString.includes('/Articles/') ||
|
|
||||||
data.pathString.includes('/Podcasts/'),
|
|
||||||
|
|
||||||
transform: async (helpers, data) => {
|
|
||||||
const textBlock = helpers.dataTypes.get({
|
|
||||||
objectType: 'TextBlock',
|
|
||||||
})
|
|
||||||
const imageBlock = helpers.dataTypes.get({
|
|
||||||
objectType: 'ImageBlock',
|
|
||||||
})
|
|
||||||
|
|
||||||
const blocks =
|
|
||||||
await helpers.dataTypes.transformMany<LPE.Article.ContentBlock>(
|
|
||||||
[...textBlock, ...imageBlock],
|
|
||||||
[...(data.blocks || [])]
|
|
||||||
.filter(
|
|
||||||
(block) =>
|
|
||||||
block.__typename === 'ImageBlock' || !!block.html || !!block.text,
|
|
||||||
)
|
|
||||||
.sort((a, b) => a.order - b.order),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
|
|
||||||
const readingTime = calcReadingTime(
|
|
||||||
(data.blocks || [])
|
|
||||||
.filter((block) => block.__typename === 'TextBlock')
|
|
||||||
.map((block) => ('text' in block ? block.text : ''))
|
|
||||||
.join(' '),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data._additional.id,
|
|
||||||
slug: data.slug,
|
|
||||||
tags: data.tags ?? [],
|
|
||||||
title: data.title,
|
|
||||||
subtitle: data.subtitle || '',
|
|
||||||
summary: data.summary || '',
|
|
||||||
authors: data.mentionsObj ?? [],
|
|
||||||
createdAt: data.createdAt || null,
|
|
||||||
modifiedAt: data.modifiedAt || null,
|
|
||||||
content: blocks,
|
|
||||||
coverImage:
|
|
||||||
(blocks.find((block) =>
|
|
||||||
(block.labels || []).includes(
|
|
||||||
LPE.Article.ContentBlockLabels.CoverImage,
|
|
||||||
),
|
|
||||||
) as LPE.Article.ImageBlock) || null,
|
|
||||||
readingTime,
|
|
||||||
toc: data.tocObj ?? [],
|
|
||||||
featured: data.path.includes('featured'),
|
|
||||||
highlighted: data.path.includes('highlighted'),
|
|
||||||
isDraft: data.path.includes('draft'),
|
|
||||||
type: LPE.PostTypes.Article,
|
|
||||||
} as any
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { ArticleBlocksOrders } from '../../../configs/data.configs'
|
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import {
|
|
||||||
UnbodyResGoogleDocData,
|
|
||||||
UnbodyResImageBlockData,
|
|
||||||
} from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const ArticleImageBlockDataType: UnbodyDataTypeConfig<
|
|
||||||
LPE.Article.ImageBlock,
|
|
||||||
LPE.Article.ImageBlock,
|
|
||||||
UnbodyResImageBlockData,
|
|
||||||
UnbodyResGoogleDocData
|
|
||||||
> = {
|
|
||||||
key: 'ArticleImageBlock',
|
|
||||||
objectType: 'ImageBlock',
|
|
||||||
classes: ['article'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original, root) =>
|
|
||||||
data.type === 'image' && (root?.path || []).includes('Articles'),
|
|
||||||
|
|
||||||
transform: (helpers, data, original, root) => {
|
|
||||||
if (!root) return data
|
|
||||||
|
|
||||||
if (data.order === ArticleBlocksOrders.cover)
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
labels: [...data.labels, LPE.Article.ContentBlockLabels.CoverImage],
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { UnbodyHelpers } from '../unbody.helpers'
|
|
||||||
import {
|
|
||||||
UnbodyResSearchGoogleDocData,
|
|
||||||
UnbodyResSearchResultImageBlockData,
|
|
||||||
UnbodyResSearchResultTextBlockData,
|
|
||||||
} from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const ArticleSearchResultItemDataType: UnbodyDataTypeConfig<
|
|
||||||
| UnbodyResSearchGoogleDocData
|
|
||||||
| UnbodyResSearchResultTextBlockData
|
|
||||||
| UnbodyResSearchResultImageBlockData,
|
|
||||||
LPE.Search.ResultItem,
|
|
||||||
| UnbodyResSearchGoogleDocData
|
|
||||||
| UnbodyResSearchResultTextBlockData
|
|
||||||
| UnbodyResSearchResultImageBlockData,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
query: string
|
|
||||||
tags: string[]
|
|
||||||
shows: LPE.Podcast.Show[]
|
|
||||||
}
|
|
||||||
> = {
|
|
||||||
key: 'ArticleSearchResultItem',
|
|
||||||
objectType: 'GoogleDoc',
|
|
||||||
classes: ['article', 'search'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original, root) =>
|
|
||||||
data.__typename === 'GoogleDoc'
|
|
||||||
? !root &&
|
|
||||||
(data.pathString.includes('/Articles/') ||
|
|
||||||
data.pathString.includes('/Podcasts/'))
|
|
||||||
: data.__typename === 'ImageBlock' || data.__typename === 'TextBlock',
|
|
||||||
|
|
||||||
transform: async (helpers, data, original, root, context) => {
|
|
||||||
const { query = '', tags = [] } = context ?? {}
|
|
||||||
|
|
||||||
if (data.__typename === 'GoogleDoc') {
|
|
||||||
const score = UnbodyHelpers.resolveScore(data._additional)
|
|
||||||
|
|
||||||
const transformers = helpers.dataTypes.get({ objectType: 'GoogleDoc' })
|
|
||||||
const transformed = await helpers.dataTypes.transform<LPE.Post.Document>(
|
|
||||||
transformers,
|
|
||||||
data,
|
|
||||||
undefined,
|
|
||||||
{ ...context },
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
score,
|
|
||||||
data: transformed,
|
|
||||||
type: transformed.type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const score =
|
|
||||||
query.length > 0 || tags.length > 0
|
|
||||||
? UnbodyHelpers.resolveScore(data._additional)
|
|
||||||
: 0
|
|
||||||
|
|
||||||
const transformers = helpers.dataTypes.get({ objectType: 'GoogleDoc' })
|
|
||||||
const document = await helpers.dataTypes.transform<LPE.Post.Document>(
|
|
||||||
transformers,
|
|
||||||
'document' in data && data.document?.[0],
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
...context,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const transformed = await helpers.dataTypes.transform<
|
|
||||||
LPE.Post.ContentBlock<any>
|
|
||||||
>(
|
|
||||||
[
|
|
||||||
helpers.dataTypes.getOne({
|
|
||||||
objectType: 'TextBlock',
|
|
||||||
classes: 'article',
|
|
||||||
})!,
|
|
||||||
helpers.dataTypes.getOne({
|
|
||||||
objectType: 'ImageBlock',
|
|
||||||
classes: 'article',
|
|
||||||
})!,
|
|
||||||
],
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
score,
|
|
||||||
data: { ...transformed, document } as any,
|
|
||||||
type: transformed.type,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { similarity } from '../../../utils/string.utils'
|
|
||||||
import { UnbodyResGoogleDocData, UnbodyResTextBlockData } from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const ArticleTextBlockDataType: UnbodyDataTypeConfig<
|
|
||||||
LPE.Article.TextBlock,
|
|
||||||
LPE.Article.TextBlock,
|
|
||||||
UnbodyResTextBlockData,
|
|
||||||
UnbodyResGoogleDocData
|
|
||||||
> = {
|
|
||||||
key: 'ArticleTextBlock',
|
|
||||||
objectType: 'TextBlock',
|
|
||||||
classes: ['article'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original, root) =>
|
|
||||||
data.type === 'text' && (root?.path || []).includes('Articles'),
|
|
||||||
|
|
||||||
transform: (helpers, data, original, root) => {
|
|
||||||
if (!root) return data
|
|
||||||
|
|
||||||
const { text = '', html = '', classNames = [] } = original
|
|
||||||
const { summary, tags = [], mentionsObj: mentions = [] } = root
|
|
||||||
|
|
||||||
const labels: LPE.Article.ContentBlockLabel[] = []
|
|
||||||
|
|
||||||
const isTitle = classNames.includes('title')
|
|
||||||
const isSubtitle = classNames.includes('subtitle')
|
|
||||||
|
|
||||||
const isAuthor =
|
|
||||||
similarity(text, mentions.map((m) => m.name).join('')) > 0.8
|
|
||||||
|
|
||||||
const isSummary = summary === text
|
|
||||||
|
|
||||||
const isTag = similarity(text, tags.map((t) => `#${t}`).join(' ')) > 0.8
|
|
||||||
|
|
||||||
//TODO this is a hack to remove the footnotes from the body
|
|
||||||
// we should find a better way to do this
|
|
||||||
const isFootnotes = html.match(`<a href="#ftnt_ref`)?.length
|
|
||||||
|
|
||||||
isTitle && labels.push(LPE.Article.ContentBlockLabels.Title)
|
|
||||||
isSubtitle && labels.push(LPE.Article.ContentBlockLabels.Subtitle)
|
|
||||||
isAuthor && labels.push(LPE.Article.ContentBlockLabels.Authors)
|
|
||||||
isSummary && labels.push(LPE.Article.ContentBlockLabels.Summary)
|
|
||||||
isTag && labels.push(LPE.Article.ContentBlockLabels.Tags)
|
|
||||||
isFootnotes && labels.push(LPE.Article.ContentBlockLabels.Footnote)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
labels: [...data.labels, ...labels],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import {
|
|
||||||
UnbodyResGoogleDocData,
|
|
||||||
UnbodyResImageBlockData,
|
|
||||||
} from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const ImageBlockDataType: UnbodyDataTypeConfig<
|
|
||||||
UnbodyResImageBlockData,
|
|
||||||
LPE.Article.ImageBlock,
|
|
||||||
UnbodyResImageBlockData,
|
|
||||||
UnbodyResGoogleDocData
|
|
||||||
> = {
|
|
||||||
key: 'ImageBlock',
|
|
||||||
objectType: 'ImageBlock',
|
|
||||||
classes: ['article'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original, root) => data.__typename === 'ImageBlock',
|
|
||||||
|
|
||||||
transform: (helpers, data, original, root) => {
|
|
||||||
return {
|
|
||||||
id: data?._additional?.id || `${data.order}`,
|
|
||||||
type: 'image',
|
|
||||||
alt: data.alt,
|
|
||||||
url: data.url,
|
|
||||||
height: data.height,
|
|
||||||
order: data.order,
|
|
||||||
width: data.width,
|
|
||||||
labels: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,256 +0,0 @@
|
||||||
import parseISO from 'date-fns/parseISO'
|
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { settle } from '../../../utils/promise.utils'
|
|
||||||
import { simplecastApi } from '../../simplecast.service'
|
|
||||||
import { UnbodyResGoogleDocData } from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const PodcastEpisodeDataType: UnbodyDataTypeConfig<
|
|
||||||
LPE.Article.Data,
|
|
||||||
LPE.Podcast.Document,
|
|
||||||
UnbodyResGoogleDocData,
|
|
||||||
any,
|
|
||||||
| {
|
|
||||||
shows: LPE.Podcast.Show[]
|
|
||||||
parseContent?: boolean
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
> = {
|
|
||||||
key: 'PodcastEpisodeDocument',
|
|
||||||
objectType: 'GoogleDoc',
|
|
||||||
classes: ['podcast', 'episode', 'document'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original) =>
|
|
||||||
original
|
|
||||||
? original.pathString.includes('/Podcasts/') &&
|
|
||||||
/^ep\d+-\d{8}-.*/.test(original.slug)
|
|
||||||
: false,
|
|
||||||
|
|
||||||
transform: async (helpers, data, original, root, context) => {
|
|
||||||
const { shows = [] } = context ?? {}
|
|
||||||
|
|
||||||
const show = shows.find((show) => show.slug === original.path[2])
|
|
||||||
|
|
||||||
const name = original.path[original.path.length - 1]
|
|
||||||
const [ep, date] = name.split('-')
|
|
||||||
const slug = original.slug.slice(`${ep}-${date}-`.length)
|
|
||||||
|
|
||||||
const publishedAt = parseISO(date)
|
|
||||||
const episodeNumber = parseInt(ep.slice(2), 10)
|
|
||||||
|
|
||||||
const coverImage = data.content.find(
|
|
||||||
(block) => block.type === 'image' && block.order < 20,
|
|
||||||
) as LPE.Podcast.Metadata['coverImage']
|
|
||||||
|
|
||||||
const channels: LPE.Podcast.Content['channels'] = []
|
|
||||||
const credits: LPE.Podcast.Content['credits'] = []
|
|
||||||
const transcription: LPE.Podcast.Content['transcription'] = []
|
|
||||||
const content: LPE.Podcast.Content['content'] = []
|
|
||||||
|
|
||||||
const allBlocks = data.content.filter((block) => {
|
|
||||||
if (
|
|
||||||
((block.type === 'text' && block.html) || '').match(
|
|
||||||
`<a href="#ftnt_ref`,
|
|
||||||
)?.length
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (context?.parseContent) {
|
|
||||||
const sections = findSections(
|
|
||||||
['Credits', 'Content', 'Timestamps', 'Transcription'],
|
|
||||||
allBlocks,
|
|
||||||
)
|
|
||||||
|
|
||||||
const textBlocks = allBlocks.filter(
|
|
||||||
(block) => block.type === 'text',
|
|
||||||
) as LPE.Post.TextBlock[]
|
|
||||||
|
|
||||||
sections.forEach((section) => {
|
|
||||||
switch (section.name) {
|
|
||||||
case 'Credits': {
|
|
||||||
credits.push(
|
|
||||||
...(section.blocks.filter(
|
|
||||||
(block) => block.type === 'text',
|
|
||||||
) as LPE.Post.TextBlock[]),
|
|
||||||
)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'Content':
|
|
||||||
case 'Timestamps':
|
|
||||||
case 'Transcription': {
|
|
||||||
content.push(...section.blocks)
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'Transcriptions': {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
channels.push(...(await getDistributionChannels(textBlocks)))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
slug,
|
|
||||||
title: data.title,
|
|
||||||
authors: data.authors,
|
|
||||||
description: data.summary,
|
|
||||||
modifiedAt: data.modifiedAt || '',
|
|
||||||
publishedAt: publishedAt.toJSON(),
|
|
||||||
episodeNumber,
|
|
||||||
tags: data.tags,
|
|
||||||
credits,
|
|
||||||
content,
|
|
||||||
transcription,
|
|
||||||
channels,
|
|
||||||
...(show ? { show } : {}),
|
|
||||||
...(show ? { showId: show.id } : {}),
|
|
||||||
...(coverImage ? { coverImage } : {}),
|
|
||||||
highlighted: data.highlighted,
|
|
||||||
isDraft: data.isDraft,
|
|
||||||
type: LPE.PostTypes.Podcast,
|
|
||||||
} as any
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDistributionChannels = async (blocks: LPE.Post.TextBlock[]) => {
|
|
||||||
const channels: LPE.Podcast.Channel[] = []
|
|
||||||
|
|
||||||
{
|
|
||||||
const youtubeUrlRegex =
|
|
||||||
/(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/[^ "]+/gi
|
|
||||||
|
|
||||||
for (const block of blocks) {
|
|
||||||
const match = (block.html || '').match(youtubeUrlRegex)
|
|
||||||
if (match && match.length) {
|
|
||||||
const url = match[0]
|
|
||||||
|
|
||||||
channels.push({
|
|
||||||
name: LPE.Podcast.ChannelNames.Youtube,
|
|
||||||
url,
|
|
||||||
})
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const linkBlocks = blocks.filter((block) =>
|
|
||||||
/^(https):\/\/[^ "]+$/.test((block.text || '').trim()),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const block of linkBlocks) {
|
|
||||||
const { text } = block
|
|
||||||
const url = text
|
|
||||||
|
|
||||||
const name = [
|
|
||||||
[/spotify\.com/i, LPE.Podcast.ChannelNames.Spotify],
|
|
||||||
[/podcasts\.google\.com/i, LPE.Podcast.ChannelNames.GooglePodcasts],
|
|
||||||
[/podcasts\.apple\.com/i, LPE.Podcast.ChannelNames.ApplePodcasts],
|
|
||||||
[/simplecast\.com/i, LPE.Podcast.ChannelNames.Simplecast],
|
|
||||||
].find(
|
|
||||||
([reg, name]) => url.match(reg)?.length,
|
|
||||||
)?.[1] as LPE.Podcast.ChannelName
|
|
||||||
|
|
||||||
if (!name) continue
|
|
||||||
|
|
||||||
switch (name) {
|
|
||||||
case LPE.Podcast.ChannelNames.Simplecast: {
|
|
||||||
if (!simplecastApi.isValidPlayerUrl(url)) {
|
|
||||||
console.error('invalid Simplecast player url!')
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const episodeId = simplecastApi.extractEpisodeIdFromUrl(url)
|
|
||||||
if (!episodeId) {
|
|
||||||
console.error('invalid Simplecast player url!')
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const [res, err] = await settle(() =>
|
|
||||||
simplecastApi.getEpisode({ id: episodeId }),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (err) {
|
|
||||||
console.error('failed to fetch Simplecast episode ', url)
|
|
||||||
console.error(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const data: LPE.Podcast.SimplecastChannelData = {
|
|
||||||
duration: res.duration,
|
|
||||||
audioFileUrl: res.ad_free_audio_file_url ?? res.audio_file?.url,
|
|
||||||
}
|
|
||||||
|
|
||||||
channels.push({
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
channels.push({
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channels
|
|
||||||
}
|
|
||||||
|
|
||||||
const findSections = (
|
|
||||||
names: string[],
|
|
||||||
blocks: LPE.Post.ContentBlock[],
|
|
||||||
): {
|
|
||||||
name: string
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
blocks: LPE.Post.ContentBlock[]
|
|
||||||
}[] => {
|
|
||||||
let sections: {
|
|
||||||
name: string
|
|
||||||
start: number
|
|
||||||
end: number
|
|
||||||
blocks: LPE.Post.ContentBlock[]
|
|
||||||
}[] = names.map((name) => ({ name, start: -1, end: -1, blocks: [] }))
|
|
||||||
|
|
||||||
blocks.forEach((block, index) => {
|
|
||||||
const { type, ...rest } = block
|
|
||||||
if (block.type === 'text') {
|
|
||||||
const sectionIndex = sections.findIndex(
|
|
||||||
({ name }) => block.text.trim() === `[${name}]`,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (sectionIndex === -1) return
|
|
||||||
|
|
||||||
const section = sections[sectionIndex]
|
|
||||||
section.start = index
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sections = [...sections]
|
|
||||||
.sort((a, b) => (a.start < b.start ? -1 : 1))
|
|
||||||
.filter((section) => section.start > -1)
|
|
||||||
|
|
||||||
for (let i = 0; i < sections.length; i++) {
|
|
||||||
const section = sections[i]
|
|
||||||
const nextSection = sections[i + 1]
|
|
||||||
section.end = nextSection ? nextSection.start - 1 : blocks.length - 1
|
|
||||||
|
|
||||||
section.blocks = blocks.slice(section.start + 1, section.end + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sections
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { getPostLink } from '../../../utils/route.utils'
|
|
||||||
import { UnbodyResGoogleDocData } from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const PodcastShowDataType: UnbodyDataTypeConfig<
|
|
||||||
LPE.Article.Data,
|
|
||||||
LPE.Podcast.Show,
|
|
||||||
UnbodyResGoogleDocData,
|
|
||||||
any,
|
|
||||||
| {
|
|
||||||
numberOfEpisodes: number
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
> = {
|
|
||||||
key: 'PodcastShowDocument',
|
|
||||||
objectType: 'GoogleDoc',
|
|
||||||
classes: ['podcast', 'show', 'document'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original) =>
|
|
||||||
original
|
|
||||||
? original.pathString.includes('/Podcasts/') && original.slug === 'index'
|
|
||||||
: false,
|
|
||||||
|
|
||||||
transform: async (helpers, data, original, root, context) => {
|
|
||||||
if (!original) return data as any
|
|
||||||
|
|
||||||
const description = data.content.find(
|
|
||||||
(block) =>
|
|
||||||
block.labels.length === 0 && block.type === 'text' && block.order > 2,
|
|
||||||
)
|
|
||||||
|
|
||||||
const showSlug = original.path[2]
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data.id,
|
|
||||||
slug: showSlug,
|
|
||||||
title: data.title,
|
|
||||||
numberOfEpisodes: context?.numberOfEpisodes || 0,
|
|
||||||
hosts: data.authors,
|
|
||||||
url: getPostLink('podcast', { showSlug }),
|
|
||||||
description: (description?.type === 'text' && description.html) || '',
|
|
||||||
descriptionText: (description?.type === 'text' && description.text) || '',
|
|
||||||
logo: {
|
|
||||||
alt: data.title,
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
url: `/podcasts/${showSlug}-logo.svg`,
|
|
||||||
},
|
|
||||||
episodes: [],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { UnbodyResGoogleDocData } from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const StaticPageDataType: UnbodyDataTypeConfig<
|
|
||||||
UnbodyResGoogleDocData,
|
|
||||||
LPE.StaticPage.Document
|
|
||||||
> = {
|
|
||||||
key: 'StaticPageDocument',
|
|
||||||
objectType: 'GoogleDoc',
|
|
||||||
classes: ['static-page', 'document'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data) =>
|
|
||||||
!!data?.pathString && data.pathString.includes('/Static pages/'),
|
|
||||||
|
|
||||||
transform: async (helpers, data) => {
|
|
||||||
const textBlock = helpers.dataTypes.get({
|
|
||||||
objectType: 'TextBlock',
|
|
||||||
})
|
|
||||||
const imageBlock = helpers.dataTypes.get({
|
|
||||||
objectType: 'ImageBlock',
|
|
||||||
})
|
|
||||||
|
|
||||||
const blocks =
|
|
||||||
await helpers.dataTypes.transformMany<LPE.Article.ContentBlock>(
|
|
||||||
[...textBlock, ...imageBlock],
|
|
||||||
[...(data.blocks || [])].sort((a, b) => a.order - b.order),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data._additional.id,
|
|
||||||
slug: data.slug,
|
|
||||||
title: data.title,
|
|
||||||
subtitle: data.subtitle || '',
|
|
||||||
summary: data.summary || '',
|
|
||||||
createdAt: data.createdAt || null,
|
|
||||||
modifiedAt: data.modifiedAt || null,
|
|
||||||
content: blocks,
|
|
||||||
type: 'static_page',
|
|
||||||
isDraft: data.pathString.includes('/draft/'),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,97 +0,0 @@
|
||||||
import { LPE } from '../../../types/lpe.types'
|
|
||||||
import { setAttributeOnHTML } from '../../../utils/html.utils'
|
|
||||||
import { convertToIframe } from '../../../utils/string.utils'
|
|
||||||
import { UnbodyResGoogleDocData, UnbodyResTextBlockData } from '../unbody.types'
|
|
||||||
import { UnbodyDataTypeConfig } from './types'
|
|
||||||
|
|
||||||
export const TextBlockDataType: UnbodyDataTypeConfig<
|
|
||||||
UnbodyResTextBlockData,
|
|
||||||
LPE.Article.TextBlock,
|
|
||||||
UnbodyResTextBlockData,
|
|
||||||
UnbodyResGoogleDocData
|
|
||||||
> = {
|
|
||||||
key: 'TextBlock',
|
|
||||||
objectType: 'TextBlock',
|
|
||||||
classes: ['article'],
|
|
||||||
|
|
||||||
isMatch: (helpers, data, original, root) => data.__typename === 'TextBlock',
|
|
||||||
|
|
||||||
transform: (helpers, data, original, root) => {
|
|
||||||
let { text = '', html = '' } = data
|
|
||||||
const labels: LPE.Post.ContentBlockLabel[] = []
|
|
||||||
let embed: LPE.Post.TextBlockEmbed | null = null
|
|
||||||
|
|
||||||
if (text.length > 0 || html.length > 0) {
|
|
||||||
const isLink =
|
|
||||||
/^<p[^>]*>(<span[^>]*>[\s]*<\/span>)*<span[^>]*><a[^>]*href="(https):\/\/[^ "]+"[^>]*>.*<\/a><\/span>(<span[^>]*>(\s|(<br(\/)?>))*<\/span>)*<\/p>$/.test(
|
|
||||||
html,
|
|
||||||
)
|
|
||||||
const isIframe = /<iframe[^>]*>(?:<\/iframe>|[^]*?<\/iframe>)/.test(text)
|
|
||||||
|
|
||||||
if (isLink) {
|
|
||||||
labels.push(LPE.Post.ContentBlockLabels.LinkOnly)
|
|
||||||
|
|
||||||
const youtube = html.match(
|
|
||||||
/(https?\:\/\/)?((www\.)?youtube\.com|youtu\.?be)\/[^ "]+/gi,
|
|
||||||
)
|
|
||||||
|
|
||||||
const simplecast = html.match(
|
|
||||||
/(https?\:\/\/)?((player\.)?simplecast\.com)\/[^ "]+/gi,
|
|
||||||
)
|
|
||||||
|
|
||||||
const label = youtube?.[0]
|
|
||||||
? LPE.Post.ContentBlockLabels.YoutubeEmbed
|
|
||||||
: LPE.Post.ContentBlockLabels.SimplecastEmbed
|
|
||||||
const src = youtube?.[0] || simplecast?.[0]
|
|
||||||
if (src && src.length > 0) {
|
|
||||||
labels.push(label)
|
|
||||||
labels.push(LPE.Post.ContentBlockLabels.Embed)
|
|
||||||
|
|
||||||
embed = {
|
|
||||||
src,
|
|
||||||
html: convertToIframe(src),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isIframe) {
|
|
||||||
const src = text.match(/(?<=src=").*?(?=[\?"])/g)?.[0]
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
labels.push(LPE.Post.ContentBlockLabels.Embed)
|
|
||||||
|
|
||||||
embed = {
|
|
||||||
html: text,
|
|
||||||
src,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set target="_blank" on anchor elements
|
|
||||||
{
|
|
||||||
const matches = Array.from(
|
|
||||||
html.matchAll(/<a[^>]*href="http[^>]*"[^>]*>/gi),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
const [anchorHTML] = match
|
|
||||||
const newAnchorHTML = setAttributeOnHTML(anchorHTML, 'target', '_blank')
|
|
||||||
html = html.replace(anchorHTML, newAnchorHTML)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: data?._additional?.id || `${data.order}`,
|
|
||||||
type: 'text',
|
|
||||||
html,
|
|
||||||
text: data.text || '',
|
|
||||||
classNames: data.classNames,
|
|
||||||
footnotes: data.footnotesObj,
|
|
||||||
order: data.order,
|
|
||||||
tagName: data.tagName,
|
|
||||||
labels,
|
|
||||||
...(embed ? { embed } : {}),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
import {
|
|
||||||
UnbodyDataTypeClass,
|
|
||||||
UnbodyDataTypeConfig,
|
|
||||||
UnbodyDataTypeConfigHelpers,
|
|
||||||
UnbodyDataTypeKey,
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
export class UnbodyDataTypes {
|
|
||||||
private dataTypes: UnbodyDataTypeConfig[] = []
|
|
||||||
private helpers: UnbodyDataTypeConfigHelpers
|
|
||||||
|
|
||||||
constructor(dataTypes: UnbodyDataTypeConfig<any>[]) {
|
|
||||||
this.dataTypes = dataTypes
|
|
||||||
|
|
||||||
this.helpers = {
|
|
||||||
dataTypes: this,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getOne = ({
|
|
||||||
key,
|
|
||||||
classes,
|
|
||||||
objectType,
|
|
||||||
}: {
|
|
||||||
key?: UnbodyDataTypeKey
|
|
||||||
classes?: UnbodyDataTypeClass | UnbodyDataTypeClass[]
|
|
||||||
objectType?: UnbodyDataTypeConfig['objectType']
|
|
||||||
}) => {
|
|
||||||
let dataTypes = this.dataTypes
|
|
||||||
|
|
||||||
if (key) {
|
|
||||||
return dataTypes.find((doc) => doc.key === key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.get({ classes, objectType })[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
get = ({
|
|
||||||
classes: _classes,
|
|
||||||
objectType,
|
|
||||||
}: {
|
|
||||||
classes?: UnbodyDataTypeClass | UnbodyDataTypeClass[]
|
|
||||||
objectType?: UnbodyDataTypeConfig['objectType']
|
|
||||||
}) => {
|
|
||||||
let dataTypes = this.dataTypes
|
|
||||||
|
|
||||||
if (objectType)
|
|
||||||
dataTypes = dataTypes.filter(
|
|
||||||
(dataType) => dataType.objectType === objectType,
|
|
||||||
)
|
|
||||||
|
|
||||||
const classes = !_classes
|
|
||||||
? []
|
|
||||||
: Array.isArray(_classes)
|
|
||||||
? _classes
|
|
||||||
: [_classes]
|
|
||||||
if (classes.length > 0)
|
|
||||||
dataTypes = dataTypes.filter((dataType) =>
|
|
||||||
classes.every((cls) => dataType.classes.includes(cls)),
|
|
||||||
)
|
|
||||||
|
|
||||||
return dataTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
transform = async <O = any, T = any>(
|
|
||||||
pipeline: UnbodyDataTypeConfig[],
|
|
||||||
data: T,
|
|
||||||
root?: any,
|
|
||||||
context?: any,
|
|
||||||
): Promise<O> => {
|
|
||||||
let obj = data
|
|
||||||
|
|
||||||
for (const dataType of pipeline) {
|
|
||||||
if (dataType.isMatch(this.helpers, obj, data, root, context)) {
|
|
||||||
obj = await dataType.transform(this.helpers, obj, data, root, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj as O | Promise<O>
|
|
||||||
}
|
|
||||||
|
|
||||||
transformMany = async <O = any, T = any>(
|
|
||||||
pipeline: UnbodyDataTypeConfig[],
|
|
||||||
data: T[],
|
|
||||||
root?: any,
|
|
||||||
context?: any,
|
|
||||||
): Promise<O[]> => {
|
|
||||||
return Promise.all(
|
|
||||||
data.map((d) => this.transform<O, T>(pipeline, d, root, context)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { ArticleDataType } from './ArticleDocument.dataType'
|
|
||||||
import { ArticleImageBlockDataType } from './ArticleImageBlock.dataType'
|
|
||||||
import { ArticleSearchResultItemDataType } from './ArticleSearchResultItem.dataType'
|
|
||||||
import { ArticleTextBlockDataType } from './ArticleTextBlock.dataType'
|
|
||||||
import { ImageBlockDataType } from './ImageBlock.dataType'
|
|
||||||
import { PodcastEpisodeDataType } from './PodcastEpisodeDocument.dataType'
|
|
||||||
import { PodcastShowDataType } from './PodcastShowDocument.dataType'
|
|
||||||
import { StaticPageDataType } from './StaticPageDocument.dataType'
|
|
||||||
import { TextBlockDataType } from './TextBlock.dataType'
|
|
||||||
import { UnbodyDataTypes } from './UnbodyDataTypes'
|
|
||||||
|
|
||||||
export const unbodyDataTypes = new UnbodyDataTypes([
|
|
||||||
ArticleDataType,
|
|
||||||
TextBlockDataType,
|
|
||||||
ImageBlockDataType,
|
|
||||||
ArticleTextBlockDataType,
|
|
||||||
ArticleImageBlockDataType,
|
|
||||||
ArticleSearchResultItemDataType,
|
|
||||||
PodcastShowDataType,
|
|
||||||
PodcastEpisodeDataType,
|
|
||||||
StaticPageDataType,
|
|
||||||
])
|
|
|
@ -1,7 +0,0 @@
|
||||||
export * from './ArticleDocument.dataType'
|
|
||||||
export * from './ArticleImageBlock.dataType'
|
|
||||||
export * from './ArticleTextBlock.dataType'
|
|
||||||
export * from './TextBlock.dataType'
|
|
||||||
export * from './UnbodyDataTypes'
|
|
||||||
export * from './dataTypes'
|
|
||||||
export * from './types'
|
|
|
@ -1,69 +0,0 @@
|
||||||
import {
|
|
||||||
GoogleDoc,
|
|
||||||
ImageBlock,
|
|
||||||
TextBlock,
|
|
||||||
} from '../../../lib/unbody/unbody.generated'
|
|
||||||
import { UnbodyDataTypes } from './UnbodyDataTypes'
|
|
||||||
|
|
||||||
export type UnbodyDataTypeConfig<
|
|
||||||
D = any,
|
|
||||||
T = any,
|
|
||||||
O = any,
|
|
||||||
R = any,
|
|
||||||
C = any,
|
|
||||||
> = {
|
|
||||||
key: UnbodyDataTypeKey
|
|
||||||
classes: UnbodyDataTypeClass[]
|
|
||||||
objectType:
|
|
||||||
| GoogleDoc['__typename']
|
|
||||||
| TextBlock['__typename']
|
|
||||||
| ImageBlock['__typename']
|
|
||||||
|
|
||||||
isMatch: (
|
|
||||||
helpers: UnbodyDataTypeConfigHelpers,
|
|
||||||
object: D,
|
|
||||||
original: O,
|
|
||||||
root: R | undefined,
|
|
||||||
context: C,
|
|
||||||
) => boolean
|
|
||||||
|
|
||||||
transform: (
|
|
||||||
helpers: UnbodyDataTypeConfigHelpers,
|
|
||||||
object: D,
|
|
||||||
original: O,
|
|
||||||
root: R | undefined,
|
|
||||||
context: C,
|
|
||||||
) => T | Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UnbodyDataTypeConfigHelpers = {
|
|
||||||
dataTypes: UnbodyDataTypes
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UnbodyDataTypeKeys = {
|
|
||||||
TextBlock: 'TextBlock',
|
|
||||||
ImageBlock: 'ImageBlock',
|
|
||||||
ArticleDocument: 'ArticleDocument',
|
|
||||||
ArticleTextBlock: 'ArticleTextBlock',
|
|
||||||
ArticleImageBlock: 'ArticleImageBlock',
|
|
||||||
ArticleSearchResultItem: 'ArticleSearchResultItem',
|
|
||||||
PodcastShowDocument: 'PodcastShowDocument',
|
|
||||||
PodcastEpisodeDocument: 'PodcastEpisodeDocument',
|
|
||||||
StaticPageDocument: 'StaticPageDocument',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type UnbodyDataTypeKey =
|
|
||||||
(typeof UnbodyDataTypeKeys)[keyof typeof UnbodyDataTypeKeys]
|
|
||||||
|
|
||||||
export const UnbodyDataTypeClasses = {
|
|
||||||
Article: 'article',
|
|
||||||
Podcast: 'podcast',
|
|
||||||
Show: 'show',
|
|
||||||
Episode: 'episode',
|
|
||||||
Document: 'document',
|
|
||||||
Search: 'search',
|
|
||||||
StaticPage: 'static-page',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
export type UnbodyDataTypeClass =
|
|
||||||
(typeof UnbodyDataTypeClasses)[keyof typeof UnbodyDataTypeClasses]
|
|
|
@ -1 +0,0 @@
|
||||||
export * from './unbody.service'
|
|
|
@ -1,151 +0,0 @@
|
||||||
import { gql } from 'graphql-request'
|
|
||||||
|
|
||||||
export const TEXT_BLOCK_FRAGMENT_COMMON = gql`
|
|
||||||
fragment TextBlockCommon on TextBlock {
|
|
||||||
footnotes
|
|
||||||
footnotesObj @client(always: true) {
|
|
||||||
index
|
|
||||||
id
|
|
||||||
refId
|
|
||||||
refValue
|
|
||||||
valueHTML
|
|
||||||
valueText
|
|
||||||
}
|
|
||||||
html
|
|
||||||
order
|
|
||||||
text
|
|
||||||
tagName
|
|
||||||
classNames
|
|
||||||
__typename
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const IMAGE_BLOCK_FRAGMENT_COMMON = gql`
|
|
||||||
fragment ImageBlockCommon on ImageBlock {
|
|
||||||
url
|
|
||||||
alt
|
|
||||||
order
|
|
||||||
width
|
|
||||||
height
|
|
||||||
__typename
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GOOGLE_DOC_FRAGMENT_COMMON = gql`
|
|
||||||
fragment GoogleDocCommon on GoogleDoc {
|
|
||||||
sourceId
|
|
||||||
title
|
|
||||||
subtitle
|
|
||||||
summary
|
|
||||||
tags
|
|
||||||
createdAt
|
|
||||||
modifiedAt
|
|
||||||
slug
|
|
||||||
path
|
|
||||||
pathString
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GOOGLE_DOC_FRAGMENT_MENTIONS = gql`
|
|
||||||
fragment GoogleDocMentions on GoogleDoc {
|
|
||||||
mentions
|
|
||||||
mentionsObj @client(always: true) {
|
|
||||||
name
|
|
||||||
emailAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GOOGLE_DOC_FRAGMENT_TOC = gql`
|
|
||||||
fragment GoogleDocTOC on GoogleDoc {
|
|
||||||
toc
|
|
||||||
tocObj @client(always: true) {
|
|
||||||
level
|
|
||||||
tag
|
|
||||||
href
|
|
||||||
title
|
|
||||||
blockIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SEARCH_IMAGE_BLOCK_FRAGMENT = gql`
|
|
||||||
fragment SearchImageBlock on ImageBlock {
|
|
||||||
__typename
|
|
||||||
|
|
||||||
...ImageBlockCommon
|
|
||||||
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
score
|
|
||||||
certainty
|
|
||||||
}
|
|
||||||
|
|
||||||
document {
|
|
||||||
... on GoogleDoc {
|
|
||||||
...GoogleDocCommon
|
|
||||||
...GoogleDocMentions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${IMAGE_BLOCK_FRAGMENT_COMMON}
|
|
||||||
${GOOGLE_DOC_FRAGMENT_COMMON}
|
|
||||||
${GOOGLE_DOC_FRAGMENT_MENTIONS}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SEARCH_TEXT_BLOCK_FRAGMENT = gql`
|
|
||||||
fragment SearchTextBlock on TextBlock {
|
|
||||||
...TextBlockCommon
|
|
||||||
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
score
|
|
||||||
certainty
|
|
||||||
}
|
|
||||||
|
|
||||||
document {
|
|
||||||
... on GoogleDoc {
|
|
||||||
...GoogleDocCommon
|
|
||||||
...GoogleDocMentions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${TEXT_BLOCK_FRAGMENT_COMMON}
|
|
||||||
${GOOGLE_DOC_FRAGMENT_COMMON}
|
|
||||||
${GOOGLE_DOC_FRAGMENT_MENTIONS}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SEARCH_GOOGLE_DOC_FRAGMENT = gql`
|
|
||||||
fragment SearchGoogleDoc on GoogleDoc {
|
|
||||||
...GoogleDocCommon
|
|
||||||
...GoogleDocMentions
|
|
||||||
|
|
||||||
blocks {
|
|
||||||
... on ImageBlock {
|
|
||||||
...ImageBlockCommon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
score
|
|
||||||
certainty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
${TEXT_BLOCK_FRAGMENT_COMMON}
|
|
||||||
${IMAGE_BLOCK_FRAGMENT_COMMON}
|
|
||||||
${GOOGLE_DOC_FRAGMENT_COMMON}
|
|
||||||
${GOOGLE_DOC_FRAGMENT_MENTIONS}
|
|
||||||
`
|
|
|
@ -1,111 +0,0 @@
|
||||||
import {
|
|
||||||
GetObjectsGoogleDocWhereInpObj,
|
|
||||||
GetObjectsGoogleDocWhereOperandsInpObj,
|
|
||||||
GoogleDocAdditional,
|
|
||||||
ImageBlockAdditional,
|
|
||||||
TextBlockAdditional,
|
|
||||||
} from '../../lib/unbody/unbody.generated'
|
|
||||||
|
|
||||||
export class UnbodyHelpers {
|
|
||||||
static resolveScore = (
|
|
||||||
_additional: Partial<
|
|
||||||
GoogleDocAdditional | ImageBlockAdditional | TextBlockAdditional
|
|
||||||
>,
|
|
||||||
): number =>
|
|
||||||
_additional?.certainty ||
|
|
||||||
(_additional?.score && parseFloat(_additional.score)) ||
|
|
||||||
0
|
|
||||||
|
|
||||||
static args = {
|
|
||||||
limit: (value: number): any => String(value),
|
|
||||||
skip: (value: number): any => String(value),
|
|
||||||
page: (skip: number, limit: number = 10) => ({
|
|
||||||
limit: this.args.limit(limit),
|
|
||||||
skip: this.args.skip(skip),
|
|
||||||
}),
|
|
||||||
wherePath: (
|
|
||||||
path: Array<string | null | undefined | false>,
|
|
||||||
key: string[] = ['path'],
|
|
||||||
) => {
|
|
||||||
const input = path.filter((p) => p && typeof p === 'string') as string[]
|
|
||||||
const paths: string[] = []
|
|
||||||
const or: string[] = []
|
|
||||||
const exclude: string[] = []
|
|
||||||
|
|
||||||
for (const p of input) {
|
|
||||||
if (p.startsWith('!')) exclude.push(p.slice(1))
|
|
||||||
else if (p.includes('|'))
|
|
||||||
or.push(...p.split('|').filter((s) => s.length > 0))
|
|
||||||
else paths.push(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
operator: 'And',
|
|
||||||
operands: [
|
|
||||||
...paths.map(
|
|
||||||
(p) =>
|
|
||||||
({
|
|
||||||
operator: 'Equal',
|
|
||||||
path: key,
|
|
||||||
valueString: p,
|
|
||||||
} as GetObjectsGoogleDocWhereInpObj),
|
|
||||||
),
|
|
||||||
...exclude.map(
|
|
||||||
(p) =>
|
|
||||||
({
|
|
||||||
operator: 'NotEqual',
|
|
||||||
path: key,
|
|
||||||
valueString: p,
|
|
||||||
} as GetObjectsGoogleDocWhereInpObj),
|
|
||||||
),
|
|
||||||
...(or.length
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
operator: 'Or',
|
|
||||||
operands: or.map(
|
|
||||||
(p) =>
|
|
||||||
({
|
|
||||||
operator: 'Equal',
|
|
||||||
path: key,
|
|
||||||
valueString: p,
|
|
||||||
} as GetObjectsGoogleDocWhereInpObj),
|
|
||||||
),
|
|
||||||
} as GetObjectsGoogleDocWhereInpObj,
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
} as GetObjectsGoogleDocWhereInpObj
|
|
||||||
},
|
|
||||||
wherePublished: (value: boolean, path: string[] = ['pathString']) =>
|
|
||||||
({
|
|
||||||
operator: 'Or',
|
|
||||||
operands: value
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
path,
|
|
||||||
operator: 'Like',
|
|
||||||
valueString: '/Articles/published/*',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
} as GetObjectsGoogleDocWhereOperandsInpObj),
|
|
||||||
whereSlugIs: (slug: string, path = ['slug']) =>
|
|
||||||
({
|
|
||||||
operator: 'Equal',
|
|
||||||
path,
|
|
||||||
valueString: slug,
|
|
||||||
} as GetObjectsGoogleDocWhereOperandsInpObj),
|
|
||||||
whereSlugIsNot: (slug: string, path = ['slug']) =>
|
|
||||||
({
|
|
||||||
operator: 'NotEqual',
|
|
||||||
path: path,
|
|
||||||
valueString: slug,
|
|
||||||
} as GetObjectsGoogleDocWhereOperandsInpObj),
|
|
||||||
whereIdIsNot: (id: string) =>
|
|
||||||
({
|
|
||||||
operator: 'NotEqual',
|
|
||||||
path: ['id'],
|
|
||||||
valueString: id,
|
|
||||||
} as GetObjectsGoogleDocWhereOperandsInpObj),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
import { gql } from '@apollo/client'
|
|
||||||
|
|
||||||
export const COUNT_DOCUMENTS_QUERY = gql`
|
|
||||||
query CountDocuments($filter: AggregateObjectsGoogleDocWhereInpObj) {
|
|
||||||
Aggregate {
|
|
||||||
GoogleDoc(where: $filter) {
|
|
||||||
meta {
|
|
||||||
count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
export const GET_POSTS_QUERY = gql`
|
|
||||||
query GetPosts(
|
|
||||||
$filter: GetObjectsGoogleDocWhereInpObj
|
|
||||||
$sort: [GetObjectsGoogleDocSortInpObj]
|
|
||||||
$searchResult: Boolean = false
|
|
||||||
$nearText: Txt2VecC11yGetObjectsGoogleDocNearTextInpObj
|
|
||||||
$hybrid: GetObjectsGoogleDocHybridInpObj
|
|
||||||
$nearObject: GetObjectsGoogleDocNearObjectInpObj
|
|
||||||
$skip: Int = 0
|
|
||||||
$limit: Int = 10
|
|
||||||
$toc: Boolean = false
|
|
||||||
$mentions: Boolean = false
|
|
||||||
$textBlocks: Boolean = false
|
|
||||||
$imageBlocks: Boolean = false
|
|
||||||
$remoteId: Boolean = false
|
|
||||||
) {
|
|
||||||
Get {
|
|
||||||
GoogleDoc(
|
|
||||||
where: $filter
|
|
||||||
hybrid: $hybrid
|
|
||||||
nearText: $nearText
|
|
||||||
nearObject: $nearObject
|
|
||||||
sort: $sort
|
|
||||||
offset: $skip
|
|
||||||
limit: $limit
|
|
||||||
) {
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
score @include(if: $searchResult)
|
|
||||||
distance @include(if: $searchResult)
|
|
||||||
certainty @include(if: $searchResult)
|
|
||||||
}
|
|
||||||
title
|
|
||||||
subtitle
|
|
||||||
summary
|
|
||||||
slug
|
|
||||||
tags
|
|
||||||
path
|
|
||||||
createdAt
|
|
||||||
modifiedAt
|
|
||||||
pathString
|
|
||||||
remoteId @include(if: $remoteId)
|
|
||||||
mentions @include(if: $mentions)
|
|
||||||
mentionsObj @client(always: true) @include(if: $mentions) {
|
|
||||||
name
|
|
||||||
emailAddress
|
|
||||||
}
|
|
||||||
toc @include(if: $toc)
|
|
||||||
tocObj @client(always: true) @include(if: $toc) {
|
|
||||||
level
|
|
||||||
tag
|
|
||||||
href
|
|
||||||
title
|
|
||||||
blockIndex
|
|
||||||
}
|
|
||||||
blocks {
|
|
||||||
... on ImageBlock @include(if: $imageBlocks) {
|
|
||||||
url
|
|
||||||
alt
|
|
||||||
order
|
|
||||||
width
|
|
||||||
height
|
|
||||||
__typename
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on TextBlock @include(if: $textBlocks) {
|
|
||||||
footnotes
|
|
||||||
footnotesObj @client(always: true) {
|
|
||||||
index
|
|
||||||
id
|
|
||||||
refId
|
|
||||||
refValue
|
|
||||||
valueHTML
|
|
||||||
valueText
|
|
||||||
}
|
|
||||||
html
|
|
||||||
order
|
|
||||||
text
|
|
||||||
tagName
|
|
||||||
classNames
|
|
||||||
__typename
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const GET_ALL_TOPICS_QUERY = gql`
|
|
||||||
query GetAllTopics($filter: AggregateObjectsGoogleDocWhereInpObj) {
|
|
||||||
Aggregate {
|
|
||||||
GoogleDoc(where: $filter, groupBy: "tags") {
|
|
||||||
groupedBy {
|
|
||||||
value
|
|
||||||
}
|
|
||||||
tags {
|
|
||||||
topOccurrences {
|
|
||||||
value
|
|
||||||
occurs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SEARCH_BLOCKS_QUERY = gql`
|
|
||||||
query SearchBlocks(
|
|
||||||
$limit: Int
|
|
||||||
$skip: Int
|
|
||||||
$textNearText: Txt2VecC11yGetObjectsTextBlockNearTextInpObj
|
|
||||||
$imageNearText: Txt2VecC11yGetObjectsImageBlockNearTextInpObj
|
|
||||||
$textFilter: GetObjectsTextBlockWhereInpObj
|
|
||||||
$imageFilter: GetObjectsImageBlockWhereInpObj
|
|
||||||
$textHybrid: GetObjectsTextBlockHybridInpObj
|
|
||||||
$imageHybrid: GetObjectsImageBlockHybridInpObj
|
|
||||||
$text: Boolean = true
|
|
||||||
$image: Boolean = true
|
|
||||||
) {
|
|
||||||
Get {
|
|
||||||
TextBlock(
|
|
||||||
where: $textFilter
|
|
||||||
nearText: $textNearText
|
|
||||||
hybrid: $textHybrid
|
|
||||||
limit: $limit
|
|
||||||
offset: $skip
|
|
||||||
) @include(if: $text) {
|
|
||||||
footnotes
|
|
||||||
footnotesObj @client(always: true) {
|
|
||||||
index
|
|
||||||
id
|
|
||||||
refId
|
|
||||||
refValue
|
|
||||||
valueHTML
|
|
||||||
valueText
|
|
||||||
}
|
|
||||||
html
|
|
||||||
order
|
|
||||||
text
|
|
||||||
tagName
|
|
||||||
classNames
|
|
||||||
__typename
|
|
||||||
|
|
||||||
document {
|
|
||||||
... on GoogleDoc {
|
|
||||||
sourceId
|
|
||||||
title
|
|
||||||
subtitle
|
|
||||||
summary
|
|
||||||
tags
|
|
||||||
createdAt
|
|
||||||
modifiedAt
|
|
||||||
slug
|
|
||||||
path
|
|
||||||
pathString
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
mentions
|
|
||||||
mentionsObj @client(always: true) {
|
|
||||||
name
|
|
||||||
emailAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_additional {
|
|
||||||
certainty
|
|
||||||
score
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageBlock(
|
|
||||||
where: $imageFilter
|
|
||||||
nearText: $imageNearText
|
|
||||||
hybrid: $imageHybrid
|
|
||||||
limit: $limit
|
|
||||||
offset: $skip
|
|
||||||
) @include(if: $image) {
|
|
||||||
url
|
|
||||||
alt
|
|
||||||
order
|
|
||||||
width
|
|
||||||
height
|
|
||||||
__typename
|
|
||||||
|
|
||||||
document {
|
|
||||||
... on GoogleDoc {
|
|
||||||
sourceId
|
|
||||||
title
|
|
||||||
subtitle
|
|
||||||
summary
|
|
||||||
tags
|
|
||||||
createdAt
|
|
||||||
modifiedAt
|
|
||||||
slug
|
|
||||||
path
|
|
||||||
pathString
|
|
||||||
_additional {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
mentions
|
|
||||||
mentionsObj @client(always: true) {
|
|
||||||
name
|
|
||||||
emailAddress
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_additional {
|
|
||||||
certainty
|
|
||||||
score
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,36 +0,0 @@
|
||||||
import {
|
|
||||||
GoogleDocCommonFragment,
|
|
||||||
GoogleDocMentionsFragment,
|
|
||||||
GoogleDocTocFragment,
|
|
||||||
ImageBlockCommonFragment,
|
|
||||||
SearchGoogleDocFragment,
|
|
||||||
SearchImageBlockFragment,
|
|
||||||
SearchTextBlockFragment,
|
|
||||||
TextBlockCommonFragment,
|
|
||||||
} from '../../lib/unbody/unbody.generated'
|
|
||||||
|
|
||||||
export type UnbodyResTextBlockData = TextBlockCommonFragment
|
|
||||||
export type UnbodyResImageBlockData = ImageBlockCommonFragment
|
|
||||||
export type UnbodyResGoogleDocData = GoogleDocCommonFragment &
|
|
||||||
GoogleDocMentionsFragment &
|
|
||||||
GoogleDocTocFragment & {
|
|
||||||
blocks: Array<UnbodyResTextBlockData | UnbodyResImageBlockData>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UnbodyResRelatedPostData = GoogleDocCommonFragment &
|
|
||||||
GoogleDocMentionsFragment
|
|
||||||
|
|
||||||
export type UnbodyResPostData = {
|
|
||||||
data: UnbodyResGoogleDocData
|
|
||||||
relatedArticles: UnbodyResRelatedPostData[]
|
|
||||||
articlesFromSameAuthors: UnbodyResRelatedPostData[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type UnbodyResSearchGoogleDocData = SearchGoogleDocFragment
|
|
||||||
export type UnbodyResSearchResultTextBlockData = SearchTextBlockFragment
|
|
||||||
export type UnbodyResSearchResultImageBlockData = SearchImageBlockFragment
|
|
||||||
|
|
||||||
export type ApiSearchResultItem =
|
|
||||||
| UnbodyResSearchGoogleDocData
|
|
||||||
| UnbodyResSearchResultTextBlockData
|
|
||||||
| UnbodyResSearchResultImageBlockData
|
|
|
@ -31,11 +31,7 @@ export type SearchHookDataPayload = {
|
||||||
export type SearchResults = {
|
export type SearchResults = {
|
||||||
articles: SearchHook<LPE.Article.ContentBlock>
|
articles: SearchHook<LPE.Article.ContentBlock>
|
||||||
blocks: SearchHook<LPE.Article.ContentBlock>
|
blocks: SearchHook<LPE.Article.ContentBlock>
|
||||||
search: (
|
search: (query: string, tags: string[], docType: any) => Promise<void>
|
||||||
query: string,
|
|
||||||
tags: string[],
|
|
||||||
docType: any, // TODO: @refactor UnbodyGraphQl.UnbodyDocumentTypeNames
|
|
||||||
) => Promise<void>
|
|
||||||
reset: (initialData: SearchHookDataPayload) => void
|
reset: (initialData: SearchHookDataPayload) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue