@opinly/next 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- package/.turbo/turbo-lint.log +0 -14
- package/CHANGELOG.md +0 -21
- package/src/components/author-page/_helpers/json-ld.ts +0 -0
- package/src/components/author-page/index.tsx +0 -62
- package/src/components/authors-page/index.tsx +0 -90
- package/src/components/blog-post/_components/content.tsx +0 -141
- package/src/components/blog-post/_components/faq.tsx +0 -18
- package/src/components/blog-post/_components/share.tsx +0 -48
- package/src/components/blog-post/_components/table-of-contents.tsx +0 -63
- package/src/components/blog-post/_helpers/calc-reading-time.ts +0 -0
- package/src/components/blog-post/_helpers/extract-headings.ts +0 -25
- package/src/components/blog-post/_helpers/json-ld.ts +0 -176
- package/src/components/blog-post/_helpers/reading-time.ts +0 -28
- package/src/components/blog-post/_types/blog-post.ts +0 -6
- package/src/components/blog-post/_types/class-names.ts +0 -10
- package/src/components/blog-post/index.tsx +0 -126
- package/src/components/bread-crumbs.tsx +0 -48
- package/src/components/folder-page/index.tsx +0 -50
- package/src/components/home-page.tsx +0 -65
- package/src/components/not-found.tsx +0 -36
- package/src/components/opinly-blog.tsx +0 -37
- package/src/components/post-preview.tsx +0 -73
- package/src/index.ts +0 -2
- package/src/sdk-config.ts +0 -37
- package/src/utils/dates.ts +0 -6
- package/src/utils/generate-metadata.ts +0 -54
- package/src/utils/images.ts +0 -7
- package/src/utils/merge.ts +0 -6
- package/sst-env.d.ts +0 -9
- package/tsconfig.json +0 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opinly/next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"devDependencies": {
|
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
"react-syntax-highlighter": "^15.6.1",
|
|
26
26
|
"schema-dts": "^1.1.5",
|
|
27
27
|
"tailwind-merge": "^1.14.0",
|
|
28
|
-
"@opinly/backend": "0.1.
|
|
28
|
+
"@opinly/backend": "0.1.3"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=16"
|
|
32
32
|
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist/**"
|
|
35
|
+
],
|
|
33
36
|
"publishConfig": {
|
|
34
37
|
"access": "public"
|
|
35
38
|
},
|
package/.turbo/turbo-lint.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
> @opinly/next@ lint /Users/kieronmckenna/repos/opinly/packages/sdks/next
|
|
4
|
-
> eslint src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Oops! Something went wrong! :(
|
|
8
|
-
|
|
9
|
-
ESLint: 8.29.0
|
|
10
|
-
|
|
11
|
-
No files matching the pattern "src/" were found.
|
|
12
|
-
Please check for typing mistakes in the pattern.
|
|
13
|
-
|
|
14
|
-
[41m[30m ELIFECYCLE [39m[49m [31mCommand failed with exit code 2.[39m
|
package/CHANGELOG.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# @opinly/next
|
|
2
|
-
|
|
3
|
-
## 0.1.2
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- more tsconfig changes
|
|
8
|
-
- Updated dependencies
|
|
9
|
-
- @opinly/backend@0.1.2
|
|
10
|
-
|
|
11
|
-
## 0.1.1
|
|
12
|
-
|
|
13
|
-
### Patch Changes
|
|
14
|
-
|
|
15
|
-
- updating tsconfigs
|
|
16
|
-
|
|
17
|
-
## 0.1.0
|
|
18
|
-
|
|
19
|
-
### Minor Changes
|
|
20
|
-
|
|
21
|
-
- broke out sdks from internal use
|
|
File without changes
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { Opinly } from "@opinly/backend";
|
|
2
|
-
import Image from "next/image";
|
|
3
|
-
import { opinlyConfig } from "../../sdk-config";
|
|
4
|
-
import { replaceImageNameSpace } from "../../utils/images";
|
|
5
|
-
import { PostPreview } from "../post-preview";
|
|
6
|
-
import { BreadCrumbs } from "../bread-crumbs";
|
|
7
|
-
|
|
8
|
-
export const AuthorPage = ({
|
|
9
|
-
author,
|
|
10
|
-
}: {
|
|
11
|
-
author: Extract<
|
|
12
|
-
Awaited<ReturnType<Opinly["content"]["blog"]>>,
|
|
13
|
-
{ type: "author" }
|
|
14
|
-
>["data"];
|
|
15
|
-
}) => {
|
|
16
|
-
return (
|
|
17
|
-
<div className="max-w-5xl mx-auto px-4 py-10">
|
|
18
|
-
<BreadCrumbs
|
|
19
|
-
breadcrumbs={[
|
|
20
|
-
{
|
|
21
|
-
name: "Authors",
|
|
22
|
-
url: `${opinlyConfig.blogPrefix}/authors`,
|
|
23
|
-
},
|
|
24
|
-
]}
|
|
25
|
-
/>
|
|
26
|
-
{/* Author Header */}
|
|
27
|
-
<div className="flex items-center gap-6 mb-10">
|
|
28
|
-
{author.image.fileKey && (
|
|
29
|
-
<Image
|
|
30
|
-
src={`${opinlyConfig.imagesPrefix}/${replaceImageNameSpace(author.image.fileKey)}`}
|
|
31
|
-
alt={author.image.alt || author.name}
|
|
32
|
-
width={150}
|
|
33
|
-
height={150}
|
|
34
|
-
className="rounded-full border"
|
|
35
|
-
/>
|
|
36
|
-
)}
|
|
37
|
-
<div>
|
|
38
|
-
<h1 className="text-3xl font-bold mb-1">{author.name}</h1>
|
|
39
|
-
{author.bio && (
|
|
40
|
-
<p className="text-muted-foreground text-base max-w-2xl">
|
|
41
|
-
{author.bio}
|
|
42
|
-
</p>
|
|
43
|
-
)}
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
|
|
47
|
-
{/* Posts Grid */}
|
|
48
|
-
<h2 className="text-xl font-semibold mb-6">Posts by {author.name}</h2>
|
|
49
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
50
|
-
{author.posts && author.posts.length > 0 ? (
|
|
51
|
-
author.posts.map(
|
|
52
|
-
(post) => post && <PostPreview key={post.slug} post={post} />
|
|
53
|
-
)
|
|
54
|
-
) : (
|
|
55
|
-
<div className="text-muted-foreground col-span-full">
|
|
56
|
-
No posts yet.
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
};
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import type { Opinly } from "@opinly/backend";
|
|
2
|
-
import Image from "next/image";
|
|
3
|
-
import { opinlyConfig } from "../../sdk-config";
|
|
4
|
-
import { replaceImageNameSpace } from "../../utils/images";
|
|
5
|
-
import { PostPreview } from "../post-preview";
|
|
6
|
-
import { BreadCrumbs } from "../bread-crumbs";
|
|
7
|
-
import Link from "next/link";
|
|
8
|
-
|
|
9
|
-
export const AuthorsPage = ({
|
|
10
|
-
authors,
|
|
11
|
-
}: {
|
|
12
|
-
authors: Extract<
|
|
13
|
-
Awaited<ReturnType<Opinly["content"]["blog"]>>,
|
|
14
|
-
{ type: "authors" }
|
|
15
|
-
>["data"];
|
|
16
|
-
}) => {
|
|
17
|
-
return (
|
|
18
|
-
<div className="max-w-5xl mx-auto px-4 py-10">
|
|
19
|
-
<BreadCrumbs
|
|
20
|
-
breadcrumbs={[
|
|
21
|
-
{
|
|
22
|
-
name: "Authors",
|
|
23
|
-
},
|
|
24
|
-
]}
|
|
25
|
-
/>
|
|
26
|
-
<h1 className="text-3xl font-bold mb-10">Authors</h1>
|
|
27
|
-
<div className="space-y-16">
|
|
28
|
-
{authors.map((author, idx) => {
|
|
29
|
-
const authorSlug = `${opinlyConfig.blogPrefix}/authors/${author.slug}`;
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div
|
|
33
|
-
key={author.name + idx}
|
|
34
|
-
className="rounded-lg p-6 flex flex-col gap-6"
|
|
35
|
-
>
|
|
36
|
-
<div className="flex items-center gap-6 mb-4">
|
|
37
|
-
{author.image?.fileKey && (
|
|
38
|
-
<Link href={authorSlug}>
|
|
39
|
-
<Image
|
|
40
|
-
src={`${opinlyConfig.imagesPrefix}/${replaceImageNameSpace(author.image.fileKey)}`}
|
|
41
|
-
alt={author.image.alt || author.name}
|
|
42
|
-
width={100}
|
|
43
|
-
height={100}
|
|
44
|
-
className="rounded-full border"
|
|
45
|
-
/>
|
|
46
|
-
</Link>
|
|
47
|
-
)}
|
|
48
|
-
<div>
|
|
49
|
-
<Link href={authorSlug}>
|
|
50
|
-
<h2 className="text-2xl font-semibold mb-1">
|
|
51
|
-
{author.name}
|
|
52
|
-
</h2>
|
|
53
|
-
</Link>
|
|
54
|
-
|
|
55
|
-
{author.bio && (
|
|
56
|
-
<p className="text-muted-foreground text-base max-w-2xl">
|
|
57
|
-
{author.bio}
|
|
58
|
-
</p>
|
|
59
|
-
)}
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
<div>
|
|
63
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-4">
|
|
64
|
-
{author.posts && author.posts.length > 0 ? (
|
|
65
|
-
author.posts.map(
|
|
66
|
-
(post) =>
|
|
67
|
-
post && <PostPreview key={post.slug} post={post} />
|
|
68
|
-
)
|
|
69
|
-
) : (
|
|
70
|
-
<div className="text-muted-foreground col-span-full">
|
|
71
|
-
No posts yet.
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
</div>
|
|
75
|
-
<div className="flex justify-end">
|
|
76
|
-
<Link
|
|
77
|
-
href={`${opinlyConfig.blogPrefix}/authors/${author.slug}`}
|
|
78
|
-
className="text-bold"
|
|
79
|
-
>
|
|
80
|
-
View more posts from {author.name}
|
|
81
|
-
</Link>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
);
|
|
86
|
-
})}
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import type { JSONContent } from "@tiptap/core";
|
|
2
|
-
import { BlogComponentClassNames } from "../_types/class-names";
|
|
3
|
-
import { cn } from "../../../utils/merge";
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
import { JSX } from "react";
|
|
6
|
-
|
|
7
|
-
function renderTiptapContent(
|
|
8
|
-
content: JSONContent,
|
|
9
|
-
classNames: BlogComponentClassNames
|
|
10
|
-
): React.ReactNode[] {
|
|
11
|
-
if (!content?.content) return [];
|
|
12
|
-
|
|
13
|
-
return content.content
|
|
14
|
-
.map((node, index) => {
|
|
15
|
-
switch (node.type) {
|
|
16
|
-
case "paragraph":
|
|
17
|
-
return (
|
|
18
|
-
<p key={index} className={classNames.paragraph || "mb-4"}>
|
|
19
|
-
{renderChildren(node.content, classNames)}
|
|
20
|
-
</p>
|
|
21
|
-
);
|
|
22
|
-
case "heading": {
|
|
23
|
-
const text = node.content?.[0]?.text ?? "";
|
|
24
|
-
const slug = text
|
|
25
|
-
.toLowerCase()
|
|
26
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
27
|
-
.replace(/^-+|-+$/g, "");
|
|
28
|
-
const Tag = `h${
|
|
29
|
-
node.attrs?.level || 1
|
|
30
|
-
}` as keyof JSX.IntrinsicElements;
|
|
31
|
-
return (
|
|
32
|
-
<Tag
|
|
33
|
-
key={index}
|
|
34
|
-
id={slug}
|
|
35
|
-
className={
|
|
36
|
-
classNames.heading || "mt-8 mb-4 font-bold text-gray-800"
|
|
37
|
-
}
|
|
38
|
-
>
|
|
39
|
-
{renderChildren(node.content, classNames)}
|
|
40
|
-
</Tag>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
case "image":
|
|
44
|
-
return (
|
|
45
|
-
<div
|
|
46
|
-
key={index}
|
|
47
|
-
className={cn(
|
|
48
|
-
"relative w-full max-w-[800px] my-6",
|
|
49
|
-
classNames.image
|
|
50
|
-
)}
|
|
51
|
-
style={{ aspectRatio: "2 / 1" }}
|
|
52
|
-
>
|
|
53
|
-
{/* <Image
|
|
54
|
-
src={replaceImageHostname(
|
|
55
|
-
node.attrs?.src || "",
|
|
56
|
-
opinlyConfig.imagesPrefix
|
|
57
|
-
)}
|
|
58
|
-
alt={node.attrs?.alt || ""}
|
|
59
|
-
fill
|
|
60
|
-
className="object-contain rounded-lg"
|
|
61
|
-
sizes="(max-width: 800px) 100vw, 800px"
|
|
62
|
-
/> */}
|
|
63
|
-
</div>
|
|
64
|
-
);
|
|
65
|
-
case "bulletList":
|
|
66
|
-
return (
|
|
67
|
-
<ul
|
|
68
|
-
key={index}
|
|
69
|
-
className={classNames.bulletList || "list-disc pl-6 my-4"}
|
|
70
|
-
>
|
|
71
|
-
{renderChildren(node.content, classNames)}
|
|
72
|
-
</ul>
|
|
73
|
-
);
|
|
74
|
-
case "orderedList":
|
|
75
|
-
return (
|
|
76
|
-
<ol
|
|
77
|
-
key={index}
|
|
78
|
-
className={classNames.orderedList || "list-decimal pl-6 my-4"}
|
|
79
|
-
>
|
|
80
|
-
{renderChildren(node.content, classNames)}
|
|
81
|
-
</ol>
|
|
82
|
-
);
|
|
83
|
-
case "listItem":
|
|
84
|
-
return (
|
|
85
|
-
<li key={index} className={classNames.listItem || "my-1"}>
|
|
86
|
-
{renderChildren(node.content, classNames)}
|
|
87
|
-
</li>
|
|
88
|
-
);
|
|
89
|
-
default:
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
.filter(Boolean);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export const Content = ({
|
|
97
|
-
content,
|
|
98
|
-
classNames,
|
|
99
|
-
}: {
|
|
100
|
-
content: JSONContent;
|
|
101
|
-
classNames: BlogComponentClassNames;
|
|
102
|
-
}) => {
|
|
103
|
-
return (
|
|
104
|
-
<div className="prose min-w-full">
|
|
105
|
-
{renderTiptapContent(content, classNames)}
|
|
106
|
-
</div>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
function renderChildren(
|
|
111
|
-
nodes: JSONContent["content"] | undefined,
|
|
112
|
-
classNames: BlogComponentClassNames
|
|
113
|
-
): JSX.Element[] {
|
|
114
|
-
if (!nodes) return [];
|
|
115
|
-
|
|
116
|
-
return nodes
|
|
117
|
-
.map((child, idx) => {
|
|
118
|
-
if (child.type === "text") {
|
|
119
|
-
let text: React.ReactNode = child.text;
|
|
120
|
-
if (child.marks) {
|
|
121
|
-
child.marks.forEach((mark) => {
|
|
122
|
-
if (mark.type === "bold") text = <strong key={idx}>{text}</strong>;
|
|
123
|
-
if (mark.type === "italic") text = <em key={idx}>{text}</em>;
|
|
124
|
-
if (mark.type === "link")
|
|
125
|
-
text = (
|
|
126
|
-
<Link
|
|
127
|
-
key={idx}
|
|
128
|
-
href={mark.attrs?.href}
|
|
129
|
-
className={classNames.link || "text-blue-600 underline"}
|
|
130
|
-
>
|
|
131
|
-
{text}
|
|
132
|
-
</Link>
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
return <span key={idx}>{text}</span>;
|
|
137
|
-
}
|
|
138
|
-
return null;
|
|
139
|
-
})
|
|
140
|
-
.filter(Boolean) as JSX.Element[];
|
|
141
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
2
|
-
|
|
3
|
-
export const FAQS = ({ faqs }: { faqs: BlogPostRes["faqs"] }) => {
|
|
4
|
-
if (!faqs) return null;
|
|
5
|
-
return (
|
|
6
|
-
<div>
|
|
7
|
-
<h2 className="text-lg font-semibold mb-2">FAQS</h2>
|
|
8
|
-
<ul>
|
|
9
|
-
{faqs.map((faq) => (
|
|
10
|
-
<li key={faq.question}>
|
|
11
|
-
<h3 className="text-lg font-semibold mb-2">{faq.question}</h3>
|
|
12
|
-
<p className="text-sm text-gray-700">{faq.answer}</p>
|
|
13
|
-
</li>
|
|
14
|
-
))}
|
|
15
|
-
</ul>
|
|
16
|
-
</div>
|
|
17
|
-
);
|
|
18
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { Facebook, Linkedin, Mail, Twitter } from "lucide-react";
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
import { opinlyConfig } from "../../../sdk-config";
|
|
4
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
5
|
-
|
|
6
|
-
export const Share = ({ post }: { post: BlogPostRes }) => {
|
|
7
|
-
const postUrl = encodeURIComponent(`${opinlyConfig.blogUrl}/${post.slug}`);
|
|
8
|
-
return (
|
|
9
|
-
<div className="flex gap-2 mb-6">
|
|
10
|
-
<Link
|
|
11
|
-
href={`https://linkedin.com/sharing/share-offsite?url=${postUrl}`}
|
|
12
|
-
target="_blank"
|
|
13
|
-
rel="noopener noreferrer"
|
|
14
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
15
|
-
>
|
|
16
|
-
<span className="sr-only">Share on LinkedIn</span>
|
|
17
|
-
<Linkedin className="w-4 h-4" />
|
|
18
|
-
</Link>
|
|
19
|
-
<Link
|
|
20
|
-
href={`https://twitter.com/intent/tweet?url=${postUrl}&text=${encodeURIComponent(
|
|
21
|
-
`Check out this article by ${post.author?.name}`
|
|
22
|
-
)}`}
|
|
23
|
-
target="_blank"
|
|
24
|
-
rel="noopener noreferrer"
|
|
25
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
26
|
-
>
|
|
27
|
-
<span className="sr-only">Share on Twitter</span>
|
|
28
|
-
<Twitter />
|
|
29
|
-
</Link>
|
|
30
|
-
<Link
|
|
31
|
-
href={`https://facebook.com/sharer/sharer.php?u=${postUrl}`}
|
|
32
|
-
target="_blank"
|
|
33
|
-
rel="noopener noreferrer"
|
|
34
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
35
|
-
>
|
|
36
|
-
<span className="sr-only">Share on Facebook</span>
|
|
37
|
-
<Facebook />
|
|
38
|
-
</Link>
|
|
39
|
-
<Link
|
|
40
|
-
href={`mailto:?body=${encodeURIComponent(`${post.title}\n${postUrl}`)}`}
|
|
41
|
-
className="border p-1 rounded flex items-center justify-center w-8 h-8"
|
|
42
|
-
>
|
|
43
|
-
<span className="sr-only">Share via Email</span>
|
|
44
|
-
<Mail className="w-4 h-4" />
|
|
45
|
-
</Link>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { JSONContent } from "@tiptap/core";
|
|
2
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
3
|
-
import { cn } from "../../../utils/merge";
|
|
4
|
-
import Image from "next/image";
|
|
5
|
-
import { opinlyConfig } from "../../../sdk-config";
|
|
6
|
-
import { replaceImageNameSpace } from "../../../utils/images";
|
|
7
|
-
import { extractHeadings } from "../_helpers/extract-headings";
|
|
8
|
-
import { Share } from "./share";
|
|
9
|
-
import Link from "next/link";
|
|
10
|
-
|
|
11
|
-
type TableOfContentsProps = {
|
|
12
|
-
content: JSONContent;
|
|
13
|
-
author: BlogPostRes["author"] | null;
|
|
14
|
-
post: BlogPostRes;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const TableOfContents = ({
|
|
18
|
-
content,
|
|
19
|
-
author,
|
|
20
|
-
post,
|
|
21
|
-
}: TableOfContentsProps) => {
|
|
22
|
-
return (
|
|
23
|
-
<aside className={cn("w-full lg:w-1/3 lg:sticky lg:top-20 lg:self-start")}>
|
|
24
|
-
{author && (
|
|
25
|
-
<div className="w-full mb-6 border p-4 rounded-md text-sm">
|
|
26
|
-
<div className="flex items-center gap-3 mb-3">
|
|
27
|
-
{author.fileKey && (
|
|
28
|
-
<Image
|
|
29
|
-
src={`${opinlyConfig.imagesPrefix}/${replaceImageNameSpace(author.fileKey)}`}
|
|
30
|
-
alt={author.name}
|
|
31
|
-
width={48}
|
|
32
|
-
height={48}
|
|
33
|
-
className="rounded-full"
|
|
34
|
-
/>
|
|
35
|
-
)}
|
|
36
|
-
<div>
|
|
37
|
-
<Link
|
|
38
|
-
href={`${opinlyConfig.blogPrefix}/authors/${author.slug}`}
|
|
39
|
-
className="text-xs uppercase text-muted-foreground font-medium"
|
|
40
|
-
>
|
|
41
|
-
{author.name}
|
|
42
|
-
</Link>
|
|
43
|
-
<div className="text-muted-foreground text-sm">{author.bio}</div>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
)}
|
|
48
|
-
<Share post={post} />
|
|
49
|
-
<div className="border-l-4 border-opinlyAccent pl-4 mb-4">
|
|
50
|
-
<h2 className="text-lg font-semibold mb-2">TABLE OF CONTENTS</h2>
|
|
51
|
-
<ul className="text-sm text-gray-700 space-y-1">
|
|
52
|
-
{(extractHeadings(content) || []).map((heading) => (
|
|
53
|
-
<li key={heading.slug}>
|
|
54
|
-
<a href={`#${heading.slug}`} className="hover:underline">
|
|
55
|
-
{heading.text}
|
|
56
|
-
</a>
|
|
57
|
-
</li>
|
|
58
|
-
))}
|
|
59
|
-
</ul>
|
|
60
|
-
</div>
|
|
61
|
-
</aside>
|
|
62
|
-
);
|
|
63
|
-
};
|
|
File without changes
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { JSONContent } from "@tiptap/core";
|
|
2
|
-
|
|
3
|
-
export function extractHeadings(
|
|
4
|
-
content: JSONContent
|
|
5
|
-
): { text: string; slug: string }[] {
|
|
6
|
-
const headings: { text: string; slug: string }[] = [];
|
|
7
|
-
if (!content?.content) return headings;
|
|
8
|
-
|
|
9
|
-
const walk = (nodes: any[]) => {
|
|
10
|
-
for (const node of nodes) {
|
|
11
|
-
if (node.type === "heading" && node.content?.[0]?.text) {
|
|
12
|
-
const text = node.content[0].text;
|
|
13
|
-
const slug = text
|
|
14
|
-
.toLowerCase()
|
|
15
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
16
|
-
.replace(/^-+|-+$/g, "");
|
|
17
|
-
headings.push({ text, slug });
|
|
18
|
-
}
|
|
19
|
-
if (node.content) walk(node.content);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
walk(content.content);
|
|
24
|
-
return headings;
|
|
25
|
-
}
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BlogPosting,
|
|
3
|
-
BreadcrumbList,
|
|
4
|
-
FAQPage,
|
|
5
|
-
WithContext,
|
|
6
|
-
ListItem,
|
|
7
|
-
} from "schema-dts";
|
|
8
|
-
import { BlogPostRes } from "../_types/blog-post";
|
|
9
|
-
import { opinlyConfig } from "../../../sdk-config";
|
|
10
|
-
import { JSONContent } from "@tiptap/core";
|
|
11
|
-
import { replaceImageNameSpace } from "../../../utils/images";
|
|
12
|
-
|
|
13
|
-
// Add this function somewhere in your BlogPost component file
|
|
14
|
-
export function generateArticleBodyHtml(content: JSONContent): string {
|
|
15
|
-
if (!content?.content) return "";
|
|
16
|
-
|
|
17
|
-
let htmlString = "";
|
|
18
|
-
|
|
19
|
-
content.content.forEach((node) => {
|
|
20
|
-
switch (node.type) {
|
|
21
|
-
case "paragraph":
|
|
22
|
-
htmlString += `<p>${generateChildrenHtml(node.content)}</p>`;
|
|
23
|
-
break;
|
|
24
|
-
case "heading":
|
|
25
|
-
const headingLevel = node.attrs?.level || 1;
|
|
26
|
-
htmlString += `<h${headingLevel}>${generateChildrenHtml(node.content)}</h${headingLevel}>`;
|
|
27
|
-
break;
|
|
28
|
-
case "bulletList":
|
|
29
|
-
htmlString += `<ul>${generateChildrenHtml(node.content)}</ul>`;
|
|
30
|
-
break;
|
|
31
|
-
case "orderedList":
|
|
32
|
-
htmlString += `<ol>${generateChildrenHtml(node.content)}</ol>`;
|
|
33
|
-
break;
|
|
34
|
-
case "listItem":
|
|
35
|
-
htmlString += `<li>${generateChildrenHtml(node.content)}</li>`;
|
|
36
|
-
break;
|
|
37
|
-
// Add other node types if they contain text content relevant for articleBody
|
|
38
|
-
// For images, you might just skip them or include an <img> tag if semantic
|
|
39
|
-
// For example:
|
|
40
|
-
// case "image":
|
|
41
|
-
// if (node.attrs?.src) {
|
|
42
|
-
// htmlString += `<img src="${replaceImageHostname(node.attrs.src, opinlyConfig.imagesPrefix)}" alt="${node.attrs?.alt || ''}" />`;
|
|
43
|
-
// }
|
|
44
|
-
// break;
|
|
45
|
-
default:
|
|
46
|
-
// Handle other block types or just ignore them for articleBody
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return htmlString;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function generateChildrenHtml(
|
|
55
|
-
nodes: JSONContent["content"] | undefined
|
|
56
|
-
): string {
|
|
57
|
-
if (!nodes) return "";
|
|
58
|
-
|
|
59
|
-
let childrenHtml = "";
|
|
60
|
-
nodes.forEach((child) => {
|
|
61
|
-
if (child.type === "text") {
|
|
62
|
-
let text = child.text || "";
|
|
63
|
-
if (child.marks) {
|
|
64
|
-
child.marks.forEach((mark) => {
|
|
65
|
-
if (mark.type === "bold") text = `<strong>${text}</strong>`;
|
|
66
|
-
if (mark.type === "italic") text = `<em>${text}</em>`;
|
|
67
|
-
if (mark.type === "link" && mark.attrs?.href) {
|
|
68
|
-
text = `<a href="${mark.attrs.href}">${text}</a>`;
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
childrenHtml += text;
|
|
73
|
-
}
|
|
74
|
-
// If you have nested content within child nodes, you might need a recursive call here
|
|
75
|
-
// For example, if a list item can contain paragraphs
|
|
76
|
-
// if (child.content) {
|
|
77
|
-
// childrenHtml += generateChildrenHtml(child.content);
|
|
78
|
-
// }
|
|
79
|
-
});
|
|
80
|
-
return childrenHtml;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const genFaqJSONLD = (
|
|
84
|
-
faqs: NonNullable<BlogPostRes["faqs"]>
|
|
85
|
-
): WithContext<FAQPage> => {
|
|
86
|
-
return {
|
|
87
|
-
"@context": "https://schema.org",
|
|
88
|
-
"@type": "FAQPage",
|
|
89
|
-
mainEntity: faqs.map((faq) => ({
|
|
90
|
-
"@type": "Question",
|
|
91
|
-
name: faq.question,
|
|
92
|
-
acceptedAnswer: {
|
|
93
|
-
"@type": "Answer",
|
|
94
|
-
text: faq.answer,
|
|
95
|
-
},
|
|
96
|
-
})),
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export const genBlogPostingJSONLD = (
|
|
101
|
-
post: BlogPostRes
|
|
102
|
-
): WithContext<BlogPosting> => {
|
|
103
|
-
return {
|
|
104
|
-
"@context": "https://schema.org",
|
|
105
|
-
"@type": "BlogPosting",
|
|
106
|
-
headline: post.title,
|
|
107
|
-
description: post.description || undefined,
|
|
108
|
-
url: `${opinlyConfig.blogUrl}/${post.folder ? `${post.folder.slug + "/"}${post.slug}` : post.slug}`,
|
|
109
|
-
datePublished: post.firstPublishedAt || undefined,
|
|
110
|
-
dateModified: post.modifiedAt || undefined,
|
|
111
|
-
image: post.titleFile?.fileKey
|
|
112
|
-
? {
|
|
113
|
-
"@type": "ImageObject",
|
|
114
|
-
url: `${opinlyConfig.imagesUrl}/${replaceImageNameSpace(post.titleFile.fileKey)}`,
|
|
115
|
-
}
|
|
116
|
-
: undefined,
|
|
117
|
-
author: post.author
|
|
118
|
-
? {
|
|
119
|
-
"@type": "Person",
|
|
120
|
-
name: post.author.name,
|
|
121
|
-
url: `${opinlyConfig.blogUrl}/author/${post.author.slug}`,
|
|
122
|
-
description: post.author.bio || undefined,
|
|
123
|
-
}
|
|
124
|
-
: undefined,
|
|
125
|
-
isAccessibleForFree: true,
|
|
126
|
-
articleBody: generateArticleBodyHtml(post.content as JSONContent),
|
|
127
|
-
};
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
export const genBreadcrumbJSONLD = (
|
|
131
|
-
post: BlogPostRes
|
|
132
|
-
): WithContext<BreadcrumbList> => {
|
|
133
|
-
const itemListElement = [
|
|
134
|
-
{
|
|
135
|
-
"@type": "ListItem",
|
|
136
|
-
position: 1,
|
|
137
|
-
name: "Company Name",
|
|
138
|
-
item: opinlyConfig.siteUrl,
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
"@type": "ListItem",
|
|
142
|
-
position: 2,
|
|
143
|
-
name: post.folder?.name || "Blog",
|
|
144
|
-
item: `${opinlyConfig.blogUrl}/blog`,
|
|
145
|
-
},
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
if (post.folder) {
|
|
149
|
-
itemListElement.push({
|
|
150
|
-
"@type": "ListItem",
|
|
151
|
-
position: 3,
|
|
152
|
-
name: post.folder.name,
|
|
153
|
-
item: `${opinlyConfig.blogUrl}/blog/${post.folder.slug}`,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
itemListElement.push({
|
|
157
|
-
"@type": "ListItem",
|
|
158
|
-
position: 4,
|
|
159
|
-
name: post.title,
|
|
160
|
-
item: `${opinlyConfig.blogUrl}/blog/${post.folder.slug}/${post.slug}`,
|
|
161
|
-
});
|
|
162
|
-
} else {
|
|
163
|
-
itemListElement.push({
|
|
164
|
-
"@type": "ListItem",
|
|
165
|
-
position: 3,
|
|
166
|
-
name: post.title,
|
|
167
|
-
item: `${opinlyConfig.blogUrl}/blog/${post.slug}`,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
"@context": "https://schema.org",
|
|
173
|
-
"@type": "BreadcrumbList",
|
|
174
|
-
itemListElement: itemListElement as ListItem[],
|
|
175
|
-
};
|
|
176
|
-
};
|