@decocms/apps-blog 8.0.0 → 8.1.0-next.0
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/README.md +21 -0
- package/package.json +14 -5
- package/src/README.md +69 -0
- package/src/__tests__/blogRecords.test.ts +270 -0
- package/src/__tests__/categoryTree.test.ts +209 -0
- package/src/__tests__/date.test.ts +134 -0
- package/src/__tests__/handlePosts.test.ts +256 -230
- package/src/__tests__/loaders.test.ts +198 -154
- package/src/__tests__/mod.test.ts +6 -6
- package/src/__tests__/publicationStatus.test.ts +176 -0
- package/src/__tests__/records.test.ts +68 -68
- package/src/__tests__/seoSections.test.ts +245 -0
- package/src/actions/submitRating.ts +36 -0
- package/src/actions/submitReview.ts +95 -0
- package/src/actions/submitView.ts +28 -0
- package/src/client.ts +70 -0
- package/src/core/blogRecords.ts +94 -0
- package/src/core/categoryTree.ts +210 -0
- package/src/core/handlePosts.ts +160 -64
- package/src/core/records.ts +147 -12
- package/src/index.ts +48 -14
- package/src/loaderMap.ts +28 -24
- package/src/loaders/BlogPostItem.ts +15 -5
- package/src/loaders/BlogPostPage.ts +61 -24
- package/src/loaders/BlogRelatedPosts.ts +45 -45
- package/src/loaders/BlogpostList.ts +93 -0
- package/src/loaders/BlogpostListing.ts +151 -85
- package/src/loaders/GetCategories.ts +51 -28
- package/src/loaders/extensions/BlogpostList/ratings.ts +36 -0
- package/src/loaders/extensions/BlogpostList/reviews.ts +36 -0
- package/src/loaders/extensions/BlogpostListing/ratings.ts +38 -0
- package/src/loaders/extensions/BlogpostListing/reviews.ts +38 -0
- package/src/loaders/extensions/BlogpostPage/ratings.ts +24 -0
- package/src/loaders/extensions/BlogpostPage/reviews.ts +31 -0
- package/src/loaders/extensions/types.ts +15 -0
- package/src/manifest.gen.ts +82 -14
- package/src/mod.ts +73 -20
- package/src/registry.ts +5 -5
- package/src/sections/Seo/SeoBlogPost.tsx +77 -0
- package/src/sections/Seo/SeoBlogPostListing.tsx +80 -0
- package/src/sections/Template.tsx +106 -0
- package/src/sections/blocks/BlockImage.tsx +42 -0
- package/src/sections/blocks/Callout.tsx +64 -0
- package/src/sections/blocks/CardGroup.tsx +60 -0
- package/src/sections/blocks/Checklist.tsx +58 -0
- package/src/sections/blocks/Code.tsx +29 -0
- package/src/sections/blocks/Comparison.tsx +77 -0
- package/src/sections/blocks/Cta.tsx +22 -0
- package/src/sections/blocks/Divider.tsx +3 -0
- package/src/sections/blocks/Heading.tsx +28 -0
- package/src/sections/blocks/List.tsx +39 -0
- package/src/sections/blocks/Paragraph.tsx +15 -0
- package/src/sections/blocks/Quote.tsx +32 -0
- package/src/sections/blocks/Stat.tsx +23 -0
- package/src/sections/blocks/StatGroup.tsx +38 -0
- package/src/sections/blocks/Steps.tsx +64 -0
- package/src/sections/blocks/Table.tsx +80 -0
- package/src/sections/blocks/Video.tsx +39 -0
- package/src/static/css.ts +178 -0
- package/src/types.ts +333 -77
- package/src/utils/blocksToSections.ts +175 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/date.ts +95 -0
- package/src/utils/hardSanitize.ts +106 -0
- package/src/utils/jsonLD.ts +118 -0
- package/src/utils/sanitizeHtml.ts +37 -0
|
@@ -1,101 +1,167 @@
|
|
|
1
|
+
import { logger, serializeError } from "@decocms/blocks/sdk/logger";
|
|
2
|
+
import {
|
|
3
|
+
ancestorsOf,
|
|
4
|
+
descendantSlugs,
|
|
5
|
+
indexCategories,
|
|
6
|
+
withCategoryPath,
|
|
7
|
+
} from "../core/categoryTree";
|
|
1
8
|
import handlePosts, { slicePosts } from "../core/handlePosts";
|
|
2
9
|
import { getRecordsByPath } from "../core/records";
|
|
3
|
-
import type { BlogPost, BlogPostListingPage, PageInfo, SortBy } from "../types";
|
|
10
|
+
import type { BlogPost, BlogPostListingPage, Category, PageInfo, SortBy } from "../types";
|
|
4
11
|
|
|
5
12
|
const COLLECTION_PATH = "collections/blog/posts";
|
|
6
13
|
const ACCESSOR = "post";
|
|
14
|
+
const CATEGORIES_PATH = "collections/blog/categories";
|
|
15
|
+
const CATEGORY_ACCESSOR = "category";
|
|
7
16
|
|
|
8
17
|
export interface Props {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
/**
|
|
19
|
+
* @title Category Slug
|
|
20
|
+
* @description Filter by a category slug. May be a full path
|
|
21
|
+
* ("parent/child"); posts of every subcategory below the last segment are
|
|
22
|
+
* included.
|
|
23
|
+
*/
|
|
24
|
+
slug?: string;
|
|
25
|
+
/**
|
|
26
|
+
* @title Items per page
|
|
27
|
+
* @description Number of posts per page to display.
|
|
28
|
+
*/
|
|
29
|
+
count?: number;
|
|
30
|
+
/**
|
|
31
|
+
* @title Page query parameter
|
|
32
|
+
* @description The current page number. Defaults to 1.
|
|
33
|
+
*/
|
|
34
|
+
page?: number;
|
|
35
|
+
/**
|
|
36
|
+
* @title Page sorting parameter
|
|
37
|
+
* @description The sorting option. Default is "date_desc"
|
|
38
|
+
*/
|
|
39
|
+
sortBy?: SortBy;
|
|
40
|
+
/**
|
|
41
|
+
* @description Overrides the query term at url
|
|
42
|
+
*/
|
|
43
|
+
query?: string;
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
const report = (scope: string, e: unknown) => {
|
|
47
|
+
const error = serializeError(e);
|
|
48
|
+
logger.error(error.message, { error, scope });
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Categories with a usable name and slug, alphabetically. */
|
|
52
|
+
const loadCategories = (): Category[] =>
|
|
53
|
+
getRecordsByPath<Category>(CATEGORIES_PATH, CATEGORY_ACCESSOR)
|
|
54
|
+
.filter(
|
|
55
|
+
(c) =>
|
|
56
|
+
typeof c?.name === "string" &&
|
|
57
|
+
c.name.length > 0 &&
|
|
58
|
+
typeof c?.slug === "string" &&
|
|
59
|
+
c.slug.length > 0,
|
|
60
|
+
)
|
|
61
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
62
|
+
|
|
35
63
|
/**
|
|
36
|
-
* @title
|
|
37
|
-
* @description Retrieves a paginated
|
|
64
|
+
* @title BlogPostListing
|
|
65
|
+
* @description Retrieves a paginated listing page of blog posts.
|
|
38
66
|
*/
|
|
39
|
-
export default function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
): BlogPostListingPage | null {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
export default async function BlogPostListing(
|
|
68
|
+
props: Props & { __pageUrl?: string },
|
|
69
|
+
req?: Request,
|
|
70
|
+
): Promise<BlogPostListingPage | null> {
|
|
71
|
+
const { page, count, slug, sortBy, query } = props;
|
|
72
|
+
const rawUrl = req?.url ?? props.__pageUrl ?? "http://localhost/";
|
|
73
|
+
const url = new URL(rawUrl);
|
|
74
|
+
const params = url.searchParams;
|
|
75
|
+
const postsPerPage = Number(count ?? params.get("count") ?? 12);
|
|
76
|
+
const pageNumber = Number(page ?? params.get("page") ?? 1);
|
|
77
|
+
const pageSort = sortBy ?? (params.get("sortBy") as SortBy) ?? "date_desc";
|
|
78
|
+
const term = query ?? params.get("q") ?? undefined;
|
|
79
|
+
|
|
80
|
+
const posts = getRecordsByPath<BlogPost>(COLLECTION_PATH, ACCESSOR);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
let categories: Category[] | null = null;
|
|
84
|
+
try {
|
|
85
|
+
categories = loadCategories();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
report("blog/BlogpostListing/categories", e);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The slug prop carries the whole category path ("pai/filho") when the
|
|
91
|
+
// site routes the listing as a catch-all; only the leaf identifies the
|
|
92
|
+
// category, the segments before it are its ancestors.
|
|
93
|
+
const requestedSegments = (slug ?? "").split("/").filter(Boolean);
|
|
94
|
+
const leafSlug = requestedSegments[requestedSegments.length - 1];
|
|
95
|
+
|
|
96
|
+
const index = indexCategories(categories);
|
|
97
|
+
const chain = leafSlug ? ancestorsOf(leafSlug, index) : null;
|
|
98
|
+
|
|
99
|
+
// A parent lists its own posts plus every descendant's.
|
|
100
|
+
const categorySlugs = leafSlug ? descendantSlugs(leafSlug, categories) : undefined;
|
|
101
|
+
|
|
102
|
+
const handledPosts = await handlePosts(posts, pageSort, categorySlugs, undefined, term);
|
|
103
|
+
|
|
104
|
+
if (!handledPosts) return null;
|
|
105
|
+
|
|
106
|
+
const slicedPosts = slicePosts(handledPosts, pageNumber, postsPerPage);
|
|
107
|
+
if (slicedPosts.length === 0) return null;
|
|
108
|
+
|
|
109
|
+
let category: Category | null = null;
|
|
110
|
+
if (leafSlug) {
|
|
111
|
+
// Prefer the standalone category record (it carries description and
|
|
112
|
+
// sections); fall back to the denormalized copy embedded in a post,
|
|
113
|
+
// which is all a site that never created category records will have.
|
|
114
|
+
category =
|
|
115
|
+
index.get(leafSlug) ??
|
|
116
|
+
slicedPosts[0]?.categories?.find((c) => c?.slug === leafSlug) ??
|
|
117
|
+
null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Only a chain that came out of the records is trustworthy enough to name
|
|
121
|
+
// a canonical URL; a missing or broken one keeps the flat behaviour.
|
|
122
|
+
const canonical = chain ? withCategoryPath(url, chain, { requested: requestedSegments }) : null;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
posts: slicedPosts,
|
|
126
|
+
category,
|
|
127
|
+
categories,
|
|
128
|
+
categoryPath: chain ?? (category ? [category] : null),
|
|
129
|
+
pageInfo: toPageInfo(handledPosts, postsPerPage, pageNumber, params),
|
|
130
|
+
seo: {
|
|
131
|
+
title: category?.name ?? "",
|
|
132
|
+
description: category?.description,
|
|
133
|
+
// Reached through a stale or wrong path, the page still renders and
|
|
134
|
+
// points at the one canonical URL instead of 404ing.
|
|
135
|
+
canonical: canonical ?? new URL(url.pathname, url.origin).href,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
} catch (e) {
|
|
139
|
+
report("blog/BlogpostListing", e);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
76
142
|
}
|
|
77
143
|
|
|
78
144
|
function toPageInfo(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
145
|
+
posts: BlogPost[],
|
|
146
|
+
postsPerPage: number,
|
|
147
|
+
pageNumber: number,
|
|
148
|
+
params: URLSearchParams,
|
|
83
149
|
): PageInfo {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
150
|
+
const totalPosts = posts.length;
|
|
151
|
+
const totalPages = Math.ceil(totalPosts / postsPerPage);
|
|
152
|
+
const hasNextPage = totalPages > pageNumber;
|
|
153
|
+
const hasPrevPage = pageNumber > 1;
|
|
154
|
+
const nextPage = new URLSearchParams(params);
|
|
155
|
+
const previousPage = new URLSearchParams(params);
|
|
156
|
+
|
|
157
|
+
if (hasNextPage) nextPage.set("page", (pageNumber + 1).toString());
|
|
158
|
+
if (hasPrevPage) previousPage.set("page", (pageNumber - 1).toString());
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
nextPage: hasNextPage ? `?${nextPage}` : undefined,
|
|
162
|
+
previousPage: hasPrevPage ? `?${previousPage}` : undefined,
|
|
163
|
+
currentPage: pageNumber,
|
|
164
|
+
records: totalPosts,
|
|
165
|
+
recordPerPage: postsPerPage,
|
|
166
|
+
};
|
|
101
167
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { childrenOf } from "../core/categoryTree";
|
|
1
2
|
import { getRecordsByPath } from "../core/records";
|
|
2
3
|
import type { Category } from "../types";
|
|
3
4
|
|
|
@@ -5,21 +6,26 @@ const COLLECTION_PATH = "collections/blog/categories";
|
|
|
5
6
|
const ACCESSOR = "category";
|
|
6
7
|
|
|
7
8
|
export interface Props {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @title Category Slug
|
|
11
|
+
* @description Get the category data from a specific slug.
|
|
12
|
+
*/
|
|
13
|
+
slug?: string;
|
|
14
|
+
/**
|
|
15
|
+
* @title Parent category slug
|
|
16
|
+
* @description Return only the direct children of this category.
|
|
17
|
+
*/
|
|
18
|
+
parentSlug?: string;
|
|
19
|
+
/**
|
|
20
|
+
* @title Items count
|
|
21
|
+
* @description Number of categories to return
|
|
22
|
+
*/
|
|
23
|
+
count?: number;
|
|
24
|
+
/**
|
|
25
|
+
* @title Sort
|
|
26
|
+
* @description The sorting option. Default is "title_desc"
|
|
27
|
+
*/
|
|
28
|
+
sortBy?: "title_asc" | "title_desc";
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
/**
|
|
@@ -27,22 +33,39 @@ export interface Props {
|
|
|
27
33
|
* @description Retrieves a list of blog categories.
|
|
28
34
|
*/
|
|
29
35
|
export default function GetCategories({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
count,
|
|
37
|
+
slug,
|
|
38
|
+
parentSlug,
|
|
39
|
+
sortBy = "title_desc",
|
|
33
40
|
}: Props): Category[] | null {
|
|
34
|
-
|
|
41
|
+
const categories = getRecordsByPath<Category>(COLLECTION_PATH, ACCESSOR);
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
if (!categories?.length) return null;
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
// Records come straight from the CMS, so name/slug are strings only by
|
|
46
|
+
// convention. A record missing either has no route and no label, so it would
|
|
47
|
+
// render as a blank chip linking nowhere — and would throw in the
|
|
48
|
+
// `localeCompare` sort below.
|
|
49
|
+
const validCategories = categories.filter(
|
|
50
|
+
(c) =>
|
|
51
|
+
typeof c?.name === "string" &&
|
|
52
|
+
c.name.length > 0 &&
|
|
53
|
+
typeof c?.slug === "string" &&
|
|
54
|
+
c.slug.length > 0,
|
|
55
|
+
);
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
57
|
+
if (slug) {
|
|
58
|
+
return validCategories.filter((c) => c.slug === slug);
|
|
59
|
+
}
|
|
46
60
|
|
|
47
|
-
|
|
61
|
+
if (!validCategories.length) return null;
|
|
62
|
+
|
|
63
|
+
const scoped = parentSlug ? childrenOf(parentSlug, validCategories) : validCategories;
|
|
64
|
+
|
|
65
|
+
const sortedCategories = scoped.sort((a, b) => {
|
|
66
|
+
const comparison = a.name.localeCompare(b.name);
|
|
67
|
+
return sortBy.endsWith("_desc") ? comparison : -comparison;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return count ? sortedCategories.slice(0, count) : sortedCategories;
|
|
48
71
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getRatings } from "../../../core/records";
|
|
2
|
+
import type { BlogPost, Ignore } from "../../../types";
|
|
3
|
+
import type { ExtensionOf } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
/**
|
|
7
|
+
* @description Ignore ratings in the aggregateRating calc
|
|
8
|
+
*/
|
|
9
|
+
ignoreRatings?: Ignore;
|
|
10
|
+
/**
|
|
11
|
+
* @description Return only aggregate rating object
|
|
12
|
+
*/
|
|
13
|
+
onlyAggregate?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @title ExtensionOf BlogPost list: Ratings
|
|
18
|
+
* @description It can harm performance. Use wisely
|
|
19
|
+
*/
|
|
20
|
+
export default function ratingsExt({
|
|
21
|
+
ignoreRatings,
|
|
22
|
+
onlyAggregate,
|
|
23
|
+
}: Props): ExtensionOf<BlogPost[] | null> {
|
|
24
|
+
return async (posts: BlogPost[] | null) => {
|
|
25
|
+
if (!posts || posts.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Promise.all(
|
|
30
|
+
posts.map(async (post) => {
|
|
31
|
+
const ratings = await getRatings({ post, ignoreRatings, onlyAggregate });
|
|
32
|
+
return { ...post, ...ratings };
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getReviews } from "../../../core/records";
|
|
2
|
+
import type { BlogPost, Ignore } from "../../../types";
|
|
3
|
+
import type { ExtensionOf } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
/**
|
|
7
|
+
* @description Ignore specific reviews
|
|
8
|
+
*/
|
|
9
|
+
ignoreReviews?: Ignore;
|
|
10
|
+
/**
|
|
11
|
+
* @description Order By
|
|
12
|
+
*/
|
|
13
|
+
orderBy?: "date_asc" | "date_desc";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @title ExtensionOf BlogPost list: Reviews
|
|
18
|
+
* @description It can harm performance. Use wisely
|
|
19
|
+
*/
|
|
20
|
+
export default function reviewsExt({
|
|
21
|
+
ignoreReviews,
|
|
22
|
+
orderBy,
|
|
23
|
+
}: Props): ExtensionOf<BlogPost[] | null> {
|
|
24
|
+
return async (posts: BlogPost[] | null) => {
|
|
25
|
+
if (!posts || posts.length === 0) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Promise.all(
|
|
30
|
+
posts.map(async (post) => {
|
|
31
|
+
const reviews = await getReviews({ post, ignoreReviews, orderBy });
|
|
32
|
+
return { ...post, ...reviews };
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getRatings } from "../../../core/records";
|
|
2
|
+
import type { BlogPostListingPage, Ignore } from "../../../types";
|
|
3
|
+
import type { ExtensionOf } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
/**
|
|
7
|
+
* @description Ignore ratings in the aggregateRating calc
|
|
8
|
+
*/
|
|
9
|
+
ignoreRatings?: Ignore;
|
|
10
|
+
/**
|
|
11
|
+
* @description Return only aggregate rating object
|
|
12
|
+
*/
|
|
13
|
+
onlyAggregate?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @title ExtensionOf BlogPostListing: Ratings
|
|
18
|
+
* @description It can harm performance. Use wisely
|
|
19
|
+
*/
|
|
20
|
+
export default function ratingsExt({
|
|
21
|
+
ignoreRatings,
|
|
22
|
+
onlyAggregate,
|
|
23
|
+
}: Props): ExtensionOf<BlogPostListingPage | null> {
|
|
24
|
+
return async (blogpostListingPage: BlogPostListingPage | null) => {
|
|
25
|
+
if (!blogpostListingPage) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const posts = await Promise.all(
|
|
30
|
+
blogpostListingPage.posts.map(async (post) => {
|
|
31
|
+
const ratings = await getRatings({ post, onlyAggregate, ignoreRatings });
|
|
32
|
+
return { ...post, ...ratings };
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return { ...blogpostListingPage, posts };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getReviews } from "../../../core/records";
|
|
2
|
+
import type { BlogPostListingPage, Ignore } from "../../../types";
|
|
3
|
+
import type { ExtensionOf } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
/**
|
|
7
|
+
* @description Ignore specific reviews
|
|
8
|
+
*/
|
|
9
|
+
ignoreReviews?: Ignore;
|
|
10
|
+
/**
|
|
11
|
+
* @description Order By
|
|
12
|
+
*/
|
|
13
|
+
orderBy?: "date_asc" | "date_desc";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @title ExtensionOf BlogPostListing: Reviews
|
|
18
|
+
* @description It can harm performance. Use wisely
|
|
19
|
+
*/
|
|
20
|
+
export default function reviewsExt({
|
|
21
|
+
ignoreReviews,
|
|
22
|
+
orderBy,
|
|
23
|
+
}: Props): ExtensionOf<BlogPostListingPage | null> {
|
|
24
|
+
return async (blogpostListingPage: BlogPostListingPage | null) => {
|
|
25
|
+
if (!blogpostListingPage) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const posts = await Promise.all(
|
|
30
|
+
blogpostListingPage.posts.map(async (post) => {
|
|
31
|
+
const reviews = await getReviews({ post, ignoreReviews, orderBy });
|
|
32
|
+
return { ...post, ...reviews };
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return { ...blogpostListingPage, posts };
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getRatings } from "../../../core/records";
|
|
2
|
+
import type { BlogPostPage, Ignore } from "../../../types";
|
|
3
|
+
import type { ExtensionOf } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
/**
|
|
7
|
+
* @description Ignore ratings in the aggregateRating calc
|
|
8
|
+
*/
|
|
9
|
+
ignoreRatings?: Ignore;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @title ExtensionOf BlogPostPage: Ratings
|
|
14
|
+
* @description It can harm performance. Use wisely
|
|
15
|
+
*/
|
|
16
|
+
export default function ratingsExt({ ignoreRatings }: Props): ExtensionOf<BlogPostPage | null> {
|
|
17
|
+
return async (blogpostPage: BlogPostPage | null) => {
|
|
18
|
+
if (!blogpostPage) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const post = await getRatings({ post: blogpostPage.post, ignoreRatings });
|
|
22
|
+
return { ...blogpostPage, post };
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getReviews } from "../../../core/records";
|
|
2
|
+
import type { BlogPostPage, Ignore } from "../../../types";
|
|
3
|
+
import type { ExtensionOf } from "../types";
|
|
4
|
+
|
|
5
|
+
export interface Props {
|
|
6
|
+
/**
|
|
7
|
+
* @description Ignore specific reviews
|
|
8
|
+
*/
|
|
9
|
+
ignoreReviews?: Ignore;
|
|
10
|
+
/**
|
|
11
|
+
* @description Order By
|
|
12
|
+
*/
|
|
13
|
+
orderBy?: "date_asc" | "date_desc";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @title ExtensionOf BlogPostPage: Reviews
|
|
18
|
+
* @description It can harm performance. Use wisely
|
|
19
|
+
*/
|
|
20
|
+
export default function reviewsExt({
|
|
21
|
+
ignoreReviews,
|
|
22
|
+
orderBy,
|
|
23
|
+
}: Props): ExtensionOf<BlogPostPage | null> {
|
|
24
|
+
return async (blogpostPage: BlogPostPage | null) => {
|
|
25
|
+
if (!blogpostPage) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const post = await getReviews({ post: blogpostPage.post, ignoreReviews, orderBy });
|
|
29
|
+
return { ...blogpostPage, post };
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An enrichment step applied to a loader's already-resolved output.
|
|
3
|
+
*
|
|
4
|
+
* In `deco-cx/apps` this type comes from `website/loaders/extension.ts`, which
|
|
5
|
+
* also ships the generic `extension` loader that composes a list of these over
|
|
6
|
+
* one data source. That loader has not been ported into
|
|
7
|
+
* `@decocms/apps-website`, so the type is declared here rather than imported —
|
|
8
|
+
* the *composition* plumbing is a website-app concern and porting it into
|
|
9
|
+
* apps-blog would put it in the wrong package (see this package's README for
|
|
10
|
+
* the tracked gap).
|
|
11
|
+
*
|
|
12
|
+
* Each extension below is therefore usable directly: call it to obtain the
|
|
13
|
+
* enrichment function, then apply that to the loader result.
|
|
14
|
+
*/
|
|
15
|
+
export type ExtensionOf<T> = (data: T) => Promise<T>;
|