@teleporthq/teleport-plugin-next-data-source 0.43.71 → 0.43.75
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/__tests__/_helpers/pagination-widget-fixture.ts +8 -1
- package/__tests__/blog-adjacent-arranged.test.ts +176 -0
- package/__tests__/blog-post-contents.test.ts +212 -0
- package/__tests__/blog-post-page-enrichment.test.ts +28 -5
- package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
- package/__tests__/blog-post-reading-time.test.ts +55 -0
- package/__tests__/browser-row-policy.test.ts +5 -1
- package/__tests__/content-preset-wiring.test.ts +108 -0
- package/__tests__/fixed-list-sort.test.ts +183 -0
- package/__tests__/pagination-filter-groups.test.ts +186 -0
- package/__tests__/plain-list-sort.test.ts +215 -0
- package/__tests__/product-price-sort.ts +47 -19
- package/__tests__/rest-api-key-header.test.ts +18 -0
- package/__tests__/search-url-sync.test.ts +34 -15
- package/__tests__/shared-resource-prefetch.test.ts +65 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +3 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/filter-utils.d.ts +19 -7
- package/dist/cjs/filter-utils.d.ts.map +1 -1
- package/dist/cjs/filter-utils.js +49 -13
- package/dist/cjs/filter-utils.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +137 -118
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +0 -9
- package/dist/cjs/product-price-sort.d.ts.map +1 -1
- package/dist/cjs/product-price-sort.js +1 -13
- package/dist/cjs/product-price-sort.js.map +1 -1
- package/dist/cjs/row-order.d.ts +19 -0
- package/dist/cjs/row-order.d.ts.map +1 -0
- package/dist/cjs/row-order.js +45 -0
- package/dist/cjs/row-order.js.map +1 -0
- package/dist/cjs/sort-utils.d.ts +10 -0
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +22 -1
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-comments.js +1 -1
- package/dist/cjs/transformations/blog-comments.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts +14 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +4 -1
- package/dist/cjs/transformations/blog-post.js.map +1 -1
- package/dist/cjs/transformations/heading-anchors.d.ts +32 -8
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/cjs/transformations/heading-anchors.js +34 -10
- package/dist/cjs/transformations/heading-anchors.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +24 -12
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +59 -11
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +52 -3
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -2
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/filter-utils.d.ts +19 -7
- package/dist/esm/filter-utils.d.ts.map +1 -1
- package/dist/esm/filter-utils.js +46 -12
- package/dist/esm/filter-utils.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +140 -121
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +0 -9
- package/dist/esm/product-price-sort.d.ts.map +1 -1
- package/dist/esm/product-price-sort.js +0 -11
- package/dist/esm/product-price-sort.js.map +1 -1
- package/dist/esm/row-order.d.ts +19 -0
- package/dist/esm/row-order.d.ts.map +1 -0
- package/dist/esm/row-order.js +40 -0
- package/dist/esm/row-order.js.map +1 -0
- package/dist/esm/sort-utils.d.ts +10 -0
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +20 -0
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/esm/transformations/blog-comments.d.ts +1 -1
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
- package/dist/esm/transformations/blog-comments.js +1 -1
- package/dist/esm/transformations/blog-comments.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts +14 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +4 -1
- package/dist/esm/transformations/blog-post.js.map +1 -1
- package/dist/esm/transformations/heading-anchors.d.ts +32 -8
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/esm/transformations/heading-anchors.js +33 -9
- package/dist/esm/transformations/heading-anchors.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +24 -12
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +58 -11
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +52 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/fetchers/postgresql.ts +11 -15
- package/src/fetchers/rest-api.ts +3 -1
- package/src/fetchers/teleport.ts +12 -16
- package/src/fetchers/utils/browser-row-policy.ts +10 -20
- package/src/filter-utils.ts +102 -28
- package/src/pagination-plugin.ts +211 -175
- package/src/product-price-sort.ts +0 -11
- package/src/row-order.ts +54 -0
- package/src/sort-utils.ts +25 -0
- package/src/transformations/blog-adjacent-posts.ts +89 -3
- package/src/transformations/blog-comments.ts +5 -5
- package/src/transformations/blog-post.ts +64 -13
- package/src/transformations/heading-anchors.ts +79 -16
- package/src/transformations/index.ts +80 -13
- package/src/utils.ts +74 -3
|
@@ -17,14 +17,25 @@ const ADJACENT_POST_CANDIDATES = 5
|
|
|
17
17
|
*
|
|
18
18
|
* MUST mirror `findAdjacentBlogPosts` in teleport-gui
|
|
19
19
|
* `packages/renderer/src/utils/blog-adjacent-posts.ts` (the canvas).
|
|
20
|
+
*
|
|
21
|
+
* In Manual Order (`arranged`) a post walks the published posts of its
|
|
22
|
+
* PRIMARY category (the first of `category_ids`) the way the author arranged
|
|
23
|
+
* them: the ones with a place (`sort_order`) by it, then the rest newest
|
|
24
|
+
* first, ties by id — "previous" the one above it, "next" the one below. The
|
|
25
|
+
* category is read off each row in code, since a list cell may be JSON or a
|
|
26
|
+
* comma-separated list (see `array-overlap-sql.ts`). A post without a
|
|
27
|
+
* category, or a table the query cannot read (a column it lacks), walks the
|
|
28
|
+
* newest-first order above. MUST mirror `findArrangedAdjacentPosts` in
|
|
29
|
+
* teleport-gui `apps/gui/app/project-page/features/blog/utils/arranged-order.ts`.
|
|
20
30
|
*/
|
|
21
|
-
export const generateAdjacentPostsCode = (): string => `
|
|
31
|
+
export const generateAdjacentPostsCode = (postsTable: string, arranged = false): string => `
|
|
22
32
|
var BLOG_ADJACENT_CANDIDATES = ${ADJACENT_POST_CANDIDATES}
|
|
33
|
+
var BLOG_ARRANGED_ORDER = ${arranged}
|
|
23
34
|
|
|
24
35
|
function blogAdjacentPostsSql(comparison, direction) {
|
|
25
36
|
return (
|
|
26
|
-
'SELECT p.* FROM
|
|
27
|
-
' (SELECT created_at, id FROM
|
|
37
|
+
'SELECT p.* FROM ${postsTable} p,' +
|
|
38
|
+
' (SELECT created_at, id FROM ${postsTable} WHERE id = $1) cur' +
|
|
28
39
|
" WHERE p.status = 'published' AND (p.created_at, p.id) " + comparison + ' (cur.created_at, cur.id)' +
|
|
29
40
|
' ORDER BY p.created_at ' + direction + ', p.id ' + direction +
|
|
30
41
|
' LIMIT ' + BLOG_ADJACENT_CANDIDATES
|
|
@@ -44,6 +55,69 @@ function firstLinkableBlogPost(result) {
|
|
|
44
55
|
return null
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
function blogPlaceOf(row) {
|
|
59
|
+
var raw = pickFirst(row.sort_order, row.sortOrder)
|
|
60
|
+
if (raw === null || raw === undefined || raw === '') return null
|
|
61
|
+
var value = Number(raw)
|
|
62
|
+
return isFinite(value) ? value : null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The arranged posts by their place, then the rest newest first, ties by id descending.
|
|
66
|
+
function compareBlogPlaces(a, b) {
|
|
67
|
+
var placeA = blogPlaceOf(a)
|
|
68
|
+
var placeB = blogPlaceOf(b)
|
|
69
|
+
if (placeA !== placeB) {
|
|
70
|
+
if (placeA === null) return 1
|
|
71
|
+
if (placeB === null) return -1
|
|
72
|
+
return placeA - placeB
|
|
73
|
+
}
|
|
74
|
+
var byDate =
|
|
75
|
+
normalizeTimestamp(pickFirst(b.created_at, b.createdAt)) -
|
|
76
|
+
normalizeTimestamp(pickFirst(a.created_at, a.createdAt))
|
|
77
|
+
if (byDate !== 0) return byDate
|
|
78
|
+
var idA = String(a.id)
|
|
79
|
+
var idB = String(b.id)
|
|
80
|
+
return idA < idB ? 1 : idA > idB ? -1 : 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// { previous, next } along the post's primary category in Manual Order, or
|
|
84
|
+
// null when the post has none (the caller then walks the newest-first order).
|
|
85
|
+
async function getArrangedAdjacentPosts(client, record) {
|
|
86
|
+
var primary = parseJsonArray(record.category_ids)[0]
|
|
87
|
+
if (!primary) return null
|
|
88
|
+
var postId = String(record.id)
|
|
89
|
+
var listed = await client.query(
|
|
90
|
+
"SELECT id, category_ids, sort_order, created_at FROM ${postsTable} WHERE status = 'published' OR id = $1",
|
|
91
|
+
[postId]
|
|
92
|
+
)
|
|
93
|
+
var chain = (listed && Array.isArray(listed.rows) ? listed.rows : [])
|
|
94
|
+
.filter(function (row) {
|
|
95
|
+
return String(row.id) === postId || parseJsonArray(row.category_ids)[0] === primary
|
|
96
|
+
})
|
|
97
|
+
.sort(compareBlogPlaces)
|
|
98
|
+
var at = -1
|
|
99
|
+
for (var i = 0; i < chain.length; i++) {
|
|
100
|
+
if (String(chain[i].id) === postId) at = i
|
|
101
|
+
}
|
|
102
|
+
if (at < 0) return null
|
|
103
|
+
var above = chain.slice(Math.max(0, at - BLOG_ADJACENT_CANDIDATES), at).reverse()
|
|
104
|
+
var below = chain.slice(at + 1, at + 1 + BLOG_ADJACENT_CANDIDATES)
|
|
105
|
+
var ids = above.concat(below).map(function (row) { return String(row.id) })
|
|
106
|
+
if (ids.length === 0) return { previous: null, next: null }
|
|
107
|
+
var full = await client.query(
|
|
108
|
+
'SELECT p.* FROM ${postsTable} p WHERE p.id::text = ANY($1::text[])',
|
|
109
|
+
[ids]
|
|
110
|
+
)
|
|
111
|
+
var byId = {}
|
|
112
|
+
;(full && Array.isArray(full.rows) ? full.rows : []).forEach(function (row) {
|
|
113
|
+
byId[String(row.id)] = row
|
|
114
|
+
})
|
|
115
|
+
var rowsOf = function (entries) {
|
|
116
|
+
return { rows: entries.map(function (row) { return byId[String(row.id)] }) }
|
|
117
|
+
}
|
|
118
|
+
return { previous: firstLinkableBlogPost(rowsOf(above)), next: firstLinkableBlogPost(rowsOf(below)) }
|
|
119
|
+
}
|
|
120
|
+
|
|
47
121
|
// { [postId]: { previous, next } } — raw rows or null — for a single-record
|
|
48
122
|
// fetch; {} otherwise and on any failure (the navigation then hides).
|
|
49
123
|
async function getAdjacentPostsMap(getClientFn, records) {
|
|
@@ -56,6 +130,18 @@ async function getAdjacentPostsMap(getClientFn, records) {
|
|
|
56
130
|
try {
|
|
57
131
|
client = getClientFn()
|
|
58
132
|
await client.connect()
|
|
133
|
+
var arranged = null
|
|
134
|
+
if (BLOG_ARRANGED_ORDER) {
|
|
135
|
+
try {
|
|
136
|
+
arranged = await getArrangedAdjacentPosts(client, records[0])
|
|
137
|
+
} catch (e) {
|
|
138
|
+
// A table without the columns: the newest-first order below.
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (arranged) {
|
|
142
|
+
map[String(postId)] = arranged
|
|
143
|
+
return map
|
|
144
|
+
}
|
|
59
145
|
var older = await client.query(blogAdjacentPostsSql('<', 'DESC'), [String(postId)])
|
|
60
146
|
var newer = await client.query(blogAdjacentPostsSql('>', 'ASC'), [String(postId)])
|
|
61
147
|
map[String(postId)] = {
|
|
@@ -27,17 +27,17 @@ export const BLOG_COMMENTS_RENDER_LIMIT = 200
|
|
|
27
27
|
* MUST mirror `buildBlogComments` in teleport-gui
|
|
28
28
|
* `packages/renderer/src/utils/blog-comments.ts` (the canvas).
|
|
29
29
|
*/
|
|
30
|
-
export const generateBlogCommentsCode = (): string => `
|
|
30
|
+
export const generateBlogCommentsCode = (commentsTable: string): string => `
|
|
31
31
|
var BLOG_COMMENTS_RENDER_LIMIT = ${BLOG_COMMENTS_RENDER_LIMIT}
|
|
32
32
|
|
|
33
33
|
var BLOG_COMMENTS_SQL =
|
|
34
34
|
'WITH top AS (' +
|
|
35
|
-
' SELECT id FROM
|
|
35
|
+
' SELECT id FROM ${commentsTable}' +
|
|
36
36
|
" WHERE post_id = $1 AND parent_id IS NULL AND status = 'approved'" +
|
|
37
37
|
' ORDER BY created_at DESC, id DESC LIMIT ' + BLOG_COMMENTS_RENDER_LIMIT +
|
|
38
38
|
')' +
|
|
39
39
|
' SELECT c.id, c.parent_id, c.author_name, c.content, c.is_author_reply, c.created_at' +
|
|
40
|
-
' FROM
|
|
40
|
+
' FROM ${commentsTable} c' +
|
|
41
41
|
" WHERE c.post_id = $1 AND c.status = 'approved'" +
|
|
42
42
|
' AND (c.id IN (SELECT id FROM top) OR c.parent_id IN (SELECT id FROM top))' +
|
|
43
43
|
' ORDER BY c.created_at ASC, c.id ASC'
|
|
@@ -45,10 +45,10 @@ var BLOG_COMMENTS_SQL =
|
|
|
45
45
|
// Every approved comment the page could show — a reply counts only while the
|
|
46
46
|
// comment it answers is approved too.
|
|
47
47
|
var BLOG_COMMENTS_COUNT_SQL =
|
|
48
|
-
'SELECT COUNT(*) AS n FROM
|
|
48
|
+
'SELECT COUNT(*) AS n FROM ${commentsTable} c' +
|
|
49
49
|
" WHERE c.post_id = $1 AND c.status = 'approved'" +
|
|
50
50
|
' AND (c.parent_id IS NULL OR EXISTS (' +
|
|
51
|
-
' SELECT 1 FROM
|
|
51
|
+
' SELECT 1 FROM ${commentsTable} p' +
|
|
52
52
|
" WHERE p.id = c.parent_id AND p.status = 'approved'))"
|
|
53
53
|
|
|
54
54
|
function blogAuthorInitials(name) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
2
|
+
import { ContentTables, RichTextContentCodegen } from '@teleporthq/teleport-shared'
|
|
2
3
|
import { generateCategoryTaxonomyCode } from './category-taxonomy'
|
|
3
4
|
import { generateAdjacentPostsCode } from './blog-adjacent-posts'
|
|
4
5
|
import { generateBlogCommentsCode } from './blog-comments'
|
|
@@ -6,10 +7,23 @@ import { generateHeadingAnchorsCode } from './heading-anchors'
|
|
|
6
7
|
|
|
7
8
|
/** What the blog-post transform bakes in at export time beyond the row itself. */
|
|
8
9
|
export interface BlogPostTransformOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The preset's tables (the Blog's by default): the posts table the related
|
|
12
|
+
* and adjacent posts are read from, the comments table a post page counts.
|
|
13
|
+
*/
|
|
14
|
+
tables?: ContentTables.ContentTables
|
|
9
15
|
/** Category taxonomy — lives only in the UIDL, there is no DB table for it. */
|
|
10
16
|
categories?: UIDLEcommerceCategory[]
|
|
11
17
|
/** Content headings get ids and `#` links — see `heading-anchors.ts`. */
|
|
12
18
|
headingAnchors?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* The post pages carry a Contents list: the content's headings get their ids
|
|
21
|
+
* (the `#` links only with `headingAnchors`) and a post page's post its
|
|
22
|
+
* `sections` — see `blogContentsSectionsOf`.
|
|
23
|
+
*/
|
|
24
|
+
contents?: boolean
|
|
25
|
+
/** Manual Order: Previous / Next walk the post's category as arranged — see `blog-adjacent-posts.ts`. */
|
|
26
|
+
arranged?: boolean
|
|
13
27
|
}
|
|
14
28
|
|
|
15
29
|
/**
|
|
@@ -20,15 +34,28 @@ export interface BlogPostTransformOptions {
|
|
|
20
34
|
export const generateBlogPostTransformationCode = (
|
|
21
35
|
options: BlogPostTransformOptions = {}
|
|
22
36
|
): string => {
|
|
37
|
+
const tables = options.tables ?? ContentTables.contentTablesByKey('blog')
|
|
23
38
|
return `
|
|
24
39
|
// ============================================================
|
|
25
40
|
// Blog Post Transformation
|
|
26
41
|
// ============================================================
|
|
27
42
|
${generateCategoryTaxonomyCode('BLOG_CATEGORIES_BY_ID', options.categories)}
|
|
28
|
-
${generateBlogCommentsCode()}
|
|
29
|
-
${generateAdjacentPostsCode()}
|
|
43
|
+
${generateBlogCommentsCode(tables.comments ?? `${tables.posts}_comments`)}
|
|
44
|
+
${generateAdjacentPostsCode(tables.posts, options.arranged === true)}
|
|
30
45
|
${generateHeadingAnchorsCode()}
|
|
46
|
+
${RichTextContentCodegen.generateLegacyRichTextNormalizerCode()}
|
|
31
47
|
var BLOG_HEADING_ANCHORS = ${options.headingAnchors === true}
|
|
48
|
+
var BLOG_CONTENTS = ${options.contents === true}
|
|
49
|
+
var BLOG_PICTURE_STAND_IN = ${tables.pictureStandIn !== false}
|
|
50
|
+
|
|
51
|
+
// A post's reading time from its body, for one that stores none: its words at
|
|
52
|
+
// about 200 a minute, at least one — what the editor's post form computes. A
|
|
53
|
+
// card or post page otherwise printed "min read" with no number before it.
|
|
54
|
+
// MUST mirror readingTimeOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.
|
|
55
|
+
function blogReadingTimeOf(html) {
|
|
56
|
+
var text = String(html || '').replace(/<[^>]*>/g, ' ').replace(/\\s+/g, ' ').trim()
|
|
57
|
+
return text ? Math.max(1, Math.ceil(text.split(' ').length / 200)) : null
|
|
58
|
+
}
|
|
32
59
|
|
|
33
60
|
function buildBlogPost(record, options) {
|
|
34
61
|
if (!record || typeof record !== 'object') return record
|
|
@@ -42,8 +69,21 @@ function buildBlogPost(record, options) {
|
|
|
42
69
|
// i18n-resolved text fields
|
|
43
70
|
var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''
|
|
44
71
|
var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''
|
|
45
|
-
|
|
46
|
-
|
|
72
|
+
// A body stored before the rich-text contract (the studio's flat list lines,
|
|
73
|
+
// the admin panel's no-break spaces) reads as stored HTML from here on.
|
|
74
|
+
var content = normalizeLegacyRichTextHtml(
|
|
75
|
+
resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''
|
|
76
|
+
)
|
|
77
|
+
// Counted before the heading anchors add their marks.
|
|
78
|
+
var bodyReadingTime = blogReadingTimeOf(content)
|
|
79
|
+
// The Contents list links to the headings, so they get their ids for it too
|
|
80
|
+
// — the # links only with the heading anchors on. Its sections ride on a
|
|
81
|
+
// post page's own fetch only (options.details): a listing draws no list, and
|
|
82
|
+
// every card of it would otherwise carry one.
|
|
83
|
+
var contentsHeadings = BLOG_CONTENTS && options.details === true ? [] : null
|
|
84
|
+
if (BLOG_HEADING_ANCHORS || BLOG_CONTENTS) {
|
|
85
|
+
content = addHeadingAnchors(content, { links: BLOG_HEADING_ANCHORS, sections: contentsHeadings })
|
|
86
|
+
}
|
|
47
87
|
var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''
|
|
48
88
|
var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null
|
|
49
89
|
|
|
@@ -77,11 +117,15 @@ function buildBlogPost(record, options) {
|
|
|
77
117
|
rawFeaturedImage === TELEPORT_DEFAULT_FEATURED_IMG ||
|
|
78
118
|
featuredImageUrl === TELEPORT_DEFAULT_FEATURED_IMG
|
|
79
119
|
) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
120
|
+
// No picture of its own: the grey "No image" stand-in, or none at all for a
|
|
121
|
+
// preset whose posts are text (ContentTables.pictureStandIn) — its card and
|
|
122
|
+
// post page show the picture only when there is one.
|
|
123
|
+
featuredImageUrl = BLOG_PICTURE_STAND_IN
|
|
124
|
+
? 'data:image/svg+xml;charset=utf-8,' +
|
|
125
|
+
encodeURIComponent(
|
|
126
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="240" viewBox="0 0 400 240"><rect fill="#e5e7eb" width="400" height="240"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" fill="#9ca3af" font-family="system-ui,sans-serif" font-size="13">No image</text></svg>'
|
|
127
|
+
)
|
|
128
|
+
: null
|
|
85
129
|
}
|
|
86
130
|
|
|
87
131
|
var rawAuthorAvatar = pickFirst(record.author_avatar_url, record.authorAvatarUrl)
|
|
@@ -90,8 +134,11 @@ function buildBlogPost(record, options) {
|
|
|
90
134
|
// Simple pass-through fields
|
|
91
135
|
var authorName = pickFirst(record.author_name, record.authorName)
|
|
92
136
|
var authorInitials = blogAuthorInitials(authorName)
|
|
93
|
-
|
|
137
|
+
// The author's email never leaves the server: a visitor's API read hides it
|
|
138
|
+
// (browser-row-policy.ts), and the page data a post or listing page ships
|
|
139
|
+
// is built here, so it is not part of the post a page sees either.
|
|
94
140
|
var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)
|
|
141
|
+
if (!(readingTimeMinutes > 0)) readingTimeMinutes = bodyReadingTime
|
|
95
142
|
|
|
96
143
|
// Tags - i18n-resolved then parsed as JSON array
|
|
97
144
|
var rawTags = resolveI18nField(record, 'tags', 'tags', currentLang, mainLang)
|
|
@@ -207,15 +254,16 @@ function buildBlogPost(record, options) {
|
|
|
207
254
|
galleryImages: galleryImages,
|
|
208
255
|
allImages: allImages,
|
|
209
256
|
authorName: authorName,
|
|
210
|
-
authorEmail: authorEmail,
|
|
211
257
|
author_name: authorName,
|
|
212
|
-
author_email: authorEmail,
|
|
213
258
|
authorAvatarUrl: authorAvatarUrl,
|
|
214
259
|
authorBio: authorBio,
|
|
215
260
|
authorInitials: authorInitials,
|
|
216
261
|
metaTitle: metaTitle,
|
|
217
262
|
metaDescription: metaDescription,
|
|
218
263
|
readingTimeMinutes: readingTimeMinutes,
|
|
264
|
+
// The post's sections, what its page's Contents list links to — see
|
|
265
|
+
// blogContentsSectionsOf; empty off a post page or under three sections.
|
|
266
|
+
sections: contentsHeadings ? blogContentsSectionsOf(contentsHeadings) : [],
|
|
219
267
|
isFeatured: isFeatured,
|
|
220
268
|
allowComments: allowComments,
|
|
221
269
|
comments: comments,
|
|
@@ -231,6 +279,9 @@ function buildBlogPost(record, options) {
|
|
|
231
279
|
createdAt: createdAt,
|
|
232
280
|
updatedAt: updatedAt,
|
|
233
281
|
created: created,
|
|
282
|
+
// The post's place among its primary category's posts (Manual Order), or
|
|
283
|
+
// null: what the category pages order by.
|
|
284
|
+
sortOrder: safeNumber(pickFirst(record.sort_order, record.sortOrder), null),
|
|
234
285
|
}
|
|
235
286
|
}
|
|
236
287
|
|
|
@@ -261,7 +312,7 @@ async function getRelatedPostsMap(getClientFn, records) {
|
|
|
261
312
|
try {
|
|
262
313
|
client = getClientFn()
|
|
263
314
|
await client.connect()
|
|
264
|
-
var result = await client.query('SELECT * FROM
|
|
315
|
+
var result = await client.query('SELECT * FROM ${tables.posts} WHERE id = ANY($1)', [
|
|
265
316
|
wantedIds,
|
|
266
317
|
])
|
|
267
318
|
if (result && result.rows) {
|
|
@@ -9,22 +9,51 @@
|
|
|
9
9
|
export const BLOG_HEADING_ANCHOR_CLASS = 'tq-heading-anchor'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* gets an `id` (kept when it already has one, otherwise a slug of its text,
|
|
14
|
-
* unique within the post) and a trailing `#` link to itself, so a section can
|
|
15
|
-
* be linked to (`/blog/post#setup`) and the link works on the very first load —
|
|
16
|
-
* the ids are in the server-rendered HTML.
|
|
12
|
+
* The class of the link a post's content heading's own text becomes.
|
|
17
13
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
14
|
+
* ⛔ PAIRED with `BLOG_HEADING_LINK_CLASS` in teleport-gui
|
|
15
|
+
* `features/blog/utils/elements/heading-anchor-styles.ts`, whose stylesheet
|
|
16
|
+
* keeps the heading looking like a heading rather than like a link.
|
|
17
|
+
*/
|
|
18
|
+
export const BLOG_HEADING_LINK_CLASS = 'tq-heading-link'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The fewest sections a post's Contents list is drawn for: a shorter post is
|
|
22
|
+
* taken in at a glance.
|
|
23
|
+
*
|
|
24
|
+
* ⛔ PAIRED with `CONTENTS_MIN_SECTIONS` in teleport-gui
|
|
25
|
+
* `packages/renderer/src/utils/blog-posts.ts`, the canvas's copy.
|
|
26
|
+
*/
|
|
27
|
+
export const CONTENTS_MIN_SECTIONS = 3
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Generates `addHeadingAnchors(html, options)`: every `<h2>`–`<h6>` of a post's
|
|
31
|
+
* content gets an `id` (kept when it already has one, otherwise a slug of its
|
|
32
|
+
* text, unique within the post), its text becomes a link to itself and a `#`
|
|
33
|
+
* follows it, shown on hover, so a section can be linked to
|
|
34
|
+
* (`/blog/post#setup`) by clicking its heading, and the link works on the very
|
|
35
|
+
* first load — the ids are in the server-rendered HTML. A heading that holds a
|
|
36
|
+
* link of its own keeps its text as it is (a link cannot hold another) and
|
|
37
|
+
* links through the `#` alone.
|
|
38
|
+
*
|
|
39
|
+
* `options.links: false` gives the headings their ids without the links —
|
|
40
|
+
* what a post page's Contents list needs when the heading anchors are off.
|
|
41
|
+
* `options.sections`, an array, collects every heading as `{ level, id, text }`
|
|
42
|
+
* in reading order, and `blogContentsSectionsOf` turns those into the list.
|
|
43
|
+
*
|
|
44
|
+
* The heading's text is the link a screen reader announces and the keyboard
|
|
45
|
+
* reaches; the `#` repeats it, so it is hidden from assistive technology and
|
|
46
|
+
* left out of the tab order.
|
|
21
47
|
*
|
|
22
48
|
* Idempotent: a heading that already carries the link is left alone. `h1` is
|
|
23
49
|
* the post title's level and is never touched.
|
|
24
50
|
*/
|
|
25
51
|
export const generateHeadingAnchorsCode = (): string => `
|
|
26
52
|
var BLOG_HEADING_ANCHOR_CLASS = '${BLOG_HEADING_ANCHOR_CLASS}'
|
|
53
|
+
var BLOG_HEADING_LINK_CLASS = '${BLOG_HEADING_LINK_CLASS}'
|
|
27
54
|
var BLOG_HEADING_ID_PATTERN = /\\sid\\s*=\\s*("([^"]*)"|'([^']*)')/i
|
|
55
|
+
var BLOG_HEADING_ANCHOR_LINK_PATTERN = /<a\\s[^>]*class="${BLOG_HEADING_ANCHOR_CLASS}"[^>]*>[\\s\\S]*?<\\/a>/gi
|
|
56
|
+
var BLOG_CONTENTS_MIN_SECTIONS = ${CONTENTS_MIN_SECTIONS}
|
|
28
57
|
|
|
29
58
|
function escapeHeadingAttribute(value) {
|
|
30
59
|
return String(value)
|
|
@@ -52,11 +81,15 @@ function blogHeadingSlug(text, used) {
|
|
|
52
81
|
return slug
|
|
53
82
|
}
|
|
54
83
|
|
|
55
|
-
function addHeadingAnchors(html) {
|
|
84
|
+
function addHeadingAnchors(html, options) {
|
|
85
|
+
options = options || {}
|
|
86
|
+
var withLinks = options.links !== false
|
|
87
|
+
var sections = Array.isArray(options.sections) ? options.sections : null
|
|
56
88
|
if (typeof html !== 'string' || !/<h[2-6][\\s>]/i.test(html)) return html
|
|
57
89
|
// Every id the content already uses is taken, so a generated one never
|
|
58
|
-
// duplicates it.
|
|
59
|
-
|
|
90
|
+
// duplicates it. No prototype: a heading titled "Constructor" is free to be
|
|
91
|
+
// #constructor.
|
|
92
|
+
var used = Object.create(null)
|
|
60
93
|
var existingIds = new RegExp(BLOG_HEADING_ID_PATTERN.source, 'gi')
|
|
61
94
|
var existing
|
|
62
95
|
while ((existing = existingIds.exec(html)) !== null) {
|
|
@@ -64,22 +97,52 @@ function addHeadingAnchors(html) {
|
|
|
64
97
|
}
|
|
65
98
|
|
|
66
99
|
return html.replace(/<h([2-6])(\\s[^>]*)?>([\\s\\S]*?)<\\/h\\1>/gi, function(match, level, rawAttrs, inner) {
|
|
67
|
-
if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) return match
|
|
68
|
-
var text = stripHtmlTags(inner)
|
|
69
|
-
if (!text) return match
|
|
70
100
|
var attrs = rawAttrs || ''
|
|
71
101
|
var idMatch = attrs.match(BLOG_HEADING_ID_PATTERN)
|
|
72
102
|
var id = idMatch ? decodeHtmlEntities(idMatch[2] || idMatch[3] || '') : ''
|
|
103
|
+
if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) {
|
|
104
|
+
if (sections && id) {
|
|
105
|
+
var anchoredText = stripHtmlTags(inner.replace(BLOG_HEADING_ANCHOR_LINK_PATTERN, ''))
|
|
106
|
+
if (anchoredText) sections.push({ level: Number(level), id: id, text: anchoredText })
|
|
107
|
+
}
|
|
108
|
+
return match
|
|
109
|
+
}
|
|
110
|
+
var text = stripHtmlTags(inner)
|
|
111
|
+
if (!text) return match
|
|
73
112
|
if (!id) {
|
|
74
113
|
id = blogHeadingSlug(text, used)
|
|
75
114
|
attrs = ' id="' + escapeHeadingAttribute(id) + '"' + attrs
|
|
76
115
|
}
|
|
116
|
+
if (sections) sections.push({ level: Number(level), id: id, text: text })
|
|
117
|
+
if (!withLinks) return '<h' + level + attrs + '>' + inner + '</h' + level + '>'
|
|
118
|
+
var href = '#' + escapeHeadingAttribute(id)
|
|
119
|
+
var ownText = /<a[\\s>]/i.test(inner)
|
|
120
|
+
? inner
|
|
121
|
+
: '<a class="' + BLOG_HEADING_LINK_CLASS + '" href="' + href + '">' + inner + '</a>'
|
|
77
122
|
return (
|
|
78
|
-
'<h' + level + attrs + '>' +
|
|
79
|
-
'<a class="' + BLOG_HEADING_ANCHOR_CLASS + '" href="
|
|
123
|
+
'<h' + level + attrs + '>' + ownText +
|
|
124
|
+
'<a class="' + BLOG_HEADING_ANCHOR_CLASS + '" href="' + href +
|
|
80
125
|
'" aria-hidden="true" tabindex="-1">#</a>' +
|
|
81
126
|
'</h' + level + '>'
|
|
82
127
|
)
|
|
83
128
|
})
|
|
84
129
|
}
|
|
130
|
+
|
|
131
|
+
// A post page's Contents list: the post's sections, i.e. its headings of the
|
|
132
|
+
// topmost level it uses, each with the address that jumps to it; none for a
|
|
133
|
+
// post of fewer than BLOG_CONTENTS_MIN_SECTIONS sections.
|
|
134
|
+
// MUST mirror contentsSectionsOf in teleport-gui packages/renderer/src/utils/blog-posts.ts.
|
|
135
|
+
function blogContentsSectionsOf(headings) {
|
|
136
|
+
if (!Array.isArray(headings) || headings.length === 0) return []
|
|
137
|
+
var top = 6
|
|
138
|
+
for (var i = 0; i < headings.length; i++) {
|
|
139
|
+
if (headings[i].level < top) top = headings[i].level
|
|
140
|
+
}
|
|
141
|
+
var sections = []
|
|
142
|
+
for (var j = 0; j < headings.length; j++) {
|
|
143
|
+
if (headings[j].level !== top) continue
|
|
144
|
+
sections.push({ id: headings[j].id, title: headings[j].text, href: '#' + headings[j].id })
|
|
145
|
+
}
|
|
146
|
+
return sections.length >= BLOG_CONTENTS_MIN_SECTIONS ? sections : []
|
|
147
|
+
}
|
|
85
148
|
`
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
GeneratorOptions,
|
|
3
|
+
UIDLBlogSettings,
|
|
3
4
|
UIDLAuthentication,
|
|
4
5
|
UIDLEcommerceCategory,
|
|
5
6
|
} from '@teleporthq/teleport-types'
|
|
6
|
-
import { StorefrontTax, TableAccess } from '@teleporthq/teleport-shared'
|
|
7
|
+
import { ContentTables, StorefrontTax, TableAccess } from '@teleporthq/teleport-shared'
|
|
7
8
|
import { generateSharedTransformationCode } from './shared-utils'
|
|
8
9
|
import { generateBlogPostTransformationCode } from './blog-post'
|
|
9
10
|
import { generateCustomPageTransformationCode } from './custom-page'
|
|
@@ -26,7 +27,22 @@ export type { EcommerceProductTransformOptions }
|
|
|
26
27
|
* to serve whichever transform that resolves to — the product fields are ignored
|
|
27
28
|
* for a blog table and vice versa.
|
|
28
29
|
*/
|
|
30
|
+
/** What a content preset bakes into its post transform — see `UIDLBlogSettings`. */
|
|
31
|
+
export interface ContentPresetTransformSettings {
|
|
32
|
+
categories?: UIDLEcommerceCategory[]
|
|
33
|
+
headingAnchors?: boolean
|
|
34
|
+
contents?: boolean
|
|
35
|
+
comments?: boolean
|
|
36
|
+
order?: 'manual'
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
export interface EntityTransformOptions extends EcommerceProductTransformOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The settings of every content preset, by preset key — the post transform
|
|
42
|
+
* of a table reads its own preset's (`contentSettingsFor`). The `blog*`
|
|
43
|
+
* fields below are the Blog's, kept for the callers that name them.
|
|
44
|
+
*/
|
|
45
|
+
contentSettings?: Partial<Record<ContentTables.ContentPresetKey, ContentPresetTransformSettings>>
|
|
30
46
|
/** Blog post-category taxonomy — see `blogSettings.categories`. */
|
|
31
47
|
blogCategories?: UIDLEcommerceCategory[]
|
|
32
48
|
/** Post content headings get ids and `#` links — see `blogSettings.headingAnchors`. */
|
|
@@ -77,10 +93,50 @@ const resolveAllowBackorders = (
|
|
|
77
93
|
* tax-inclusive or untaxed stores. `allowBackorders` is the effective
|
|
78
94
|
* "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
|
|
79
95
|
*/
|
|
96
|
+
const contentPresetTransformSettings = (
|
|
97
|
+
settings: UIDLBlogSettings | undefined
|
|
98
|
+
): ContentPresetTransformSettings => ({
|
|
99
|
+
categories: settings?.categories,
|
|
100
|
+
headingAnchors: settings?.headingAnchors === true,
|
|
101
|
+
...(settings?.contents === true && { contents: true }),
|
|
102
|
+
comments: settings?.comments === true,
|
|
103
|
+
...(settings?.order === 'manual' && { order: 'manual' as const }),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The settings the transform of `tableName` bakes in: its preset's entry in
|
|
108
|
+
* `contentSettings`, or — for the Blog, and for a caller that only set the
|
|
109
|
+
* `blog*` fields — those fields. Empty for a table no preset owns.
|
|
110
|
+
*/
|
|
111
|
+
export const contentSettingsFor = (
|
|
112
|
+
options: Pick<
|
|
113
|
+
EntityTransformOptions,
|
|
114
|
+
'contentSettings' | 'blogCategories' | 'blogHeadingAnchors' | 'blogComments'
|
|
115
|
+
>,
|
|
116
|
+
tableName: string
|
|
117
|
+
): ContentPresetTransformSettings => {
|
|
118
|
+
const role = ContentTables.contentTableRole(stripSchemaQualifier(tableName))
|
|
119
|
+
if (!role) {
|
|
120
|
+
return {}
|
|
121
|
+
}
|
|
122
|
+
const own = options.contentSettings?.[role.key]
|
|
123
|
+
if (own) {
|
|
124
|
+
return own
|
|
125
|
+
}
|
|
126
|
+
if (role.key === 'blog') {
|
|
127
|
+
return {
|
|
128
|
+
categories: options.blogCategories,
|
|
129
|
+
headingAnchors: options.blogHeadingAnchors,
|
|
130
|
+
comments: options.blogComments,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {}
|
|
134
|
+
}
|
|
135
|
+
|
|
80
136
|
export const buildProductTransformOptions = (
|
|
81
137
|
options: Pick<
|
|
82
138
|
GeneratorOptions,
|
|
83
|
-
'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'auth'
|
|
139
|
+
'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'helpCenterSettings' | 'auth'
|
|
84
140
|
> &
|
|
85
141
|
LocalizedProjectOptions & {
|
|
86
142
|
/**
|
|
@@ -96,6 +152,10 @@ export const buildProductTransformOptions = (
|
|
|
96
152
|
}
|
|
97
153
|
): EntityTransformOptions => ({
|
|
98
154
|
categories: options.ecommerceSettings?.categories,
|
|
155
|
+
contentSettings: {
|
|
156
|
+
blog: contentPresetTransformSettings(options.blogSettings),
|
|
157
|
+
help: contentPresetTransformSettings(options.helpCenterSettings),
|
|
158
|
+
},
|
|
99
159
|
blogCategories: options.blogSettings?.categories,
|
|
100
160
|
blogHeadingAnchors: options.blogSettings?.headingAnchors === true,
|
|
101
161
|
blogComments: options.blogSettings?.comments === true,
|
|
@@ -125,9 +185,9 @@ const stripSchemaQualifier = (tableName: string): string => {
|
|
|
125
185
|
* Detects which transformation type to apply based on the table name.
|
|
126
186
|
* Returns null if no transformation is needed.
|
|
127
187
|
*
|
|
128
|
-
* IMPORTANT: Only the real platform-managed tables (`teleport_products`,
|
|
129
|
-
*
|
|
130
|
-
* transforms. A previous loose substring match (`lower.includes('products')`)
|
|
188
|
+
* IMPORTANT: Only the real platform-managed tables (`teleport_products`, the
|
|
189
|
+
* content presets' posts tables) are routed through the e-commerce/blog
|
|
190
|
+
* view-model transforms. A previous loose substring match (`lower.includes('products')`)
|
|
131
191
|
* incorrectly routed CUSTOM tables such as `products`, `store_products` or
|
|
132
192
|
* `wholesale_products` through `buildEcommerceProduct`, which emits a fixed set
|
|
133
193
|
* of platform product fields and silently drops all custom columns. Matching the
|
|
@@ -138,7 +198,7 @@ export const detectTransformationType = (tableName: string): TransformationType
|
|
|
138
198
|
return null
|
|
139
199
|
}
|
|
140
200
|
const bare = stripSchemaQualifier(tableName)
|
|
141
|
-
if (bare
|
|
201
|
+
if (ContentTables.isContentPostsTable(bare)) {
|
|
142
202
|
return 'blog-post'
|
|
143
203
|
}
|
|
144
204
|
if (bare === 'teleport_products') {
|
|
@@ -167,14 +227,20 @@ export const getTransformationCode = (
|
|
|
167
227
|
const shared = generateSharedTransformationCode()
|
|
168
228
|
|
|
169
229
|
switch (type) {
|
|
170
|
-
case 'blog-post':
|
|
230
|
+
case 'blog-post': {
|
|
231
|
+
const role = ContentTables.contentTableRole(stripSchemaQualifier(tableName))
|
|
232
|
+
const settings = contentSettingsFor(options, tableName)
|
|
171
233
|
return (
|
|
172
234
|
shared +
|
|
173
235
|
generateBlogPostTransformationCode({
|
|
174
|
-
|
|
175
|
-
|
|
236
|
+
tables: role ? ContentTables.contentTablesByKey(role.key) : undefined,
|
|
237
|
+
categories: settings.categories,
|
|
238
|
+
headingAnchors: settings.headingAnchors,
|
|
239
|
+
contents: settings.contents === true,
|
|
240
|
+
arranged: settings.order === 'manual',
|
|
176
241
|
})
|
|
177
242
|
)
|
|
243
|
+
}
|
|
178
244
|
case 'ecommerce-product':
|
|
179
245
|
return shared + generateEcommerceProductTransformationCode(options)
|
|
180
246
|
case 'custom-page':
|
|
@@ -300,7 +366,7 @@ export const REVIEWS_PER_PRODUCT = 5
|
|
|
300
366
|
*/
|
|
301
367
|
export const getTransformWrapperCode = (
|
|
302
368
|
tableName: string,
|
|
303
|
-
options: Pick<EntityTransformOptions, 'localization' | 'blogComments'> = {}
|
|
369
|
+
options: Pick<EntityTransformOptions, 'localization' | 'blogComments' | 'contentSettings'> = {}
|
|
304
370
|
): string => {
|
|
305
371
|
const type = detectTransformationType(tableName)
|
|
306
372
|
if (!type) {
|
|
@@ -422,7 +488,8 @@ export const getTransformWrapperCode = (
|
|
|
422
488
|
|
|
423
489
|
// A post page's neighbours and comments, on the same single-record heuristic
|
|
424
490
|
// as the related rail. The comments are only asked for when the blog takes
|
|
425
|
-
// them — see `EntityTransformOptions.blogComments`.
|
|
491
|
+
// them — see `EntityTransformOptions.blogComments`. `details` tells the
|
|
492
|
+
// transform the same thing: what only a post page draws (its Contents list).
|
|
426
493
|
const blogPageEnrichment =
|
|
427
494
|
type === 'blog-post'
|
|
428
495
|
? `
|
|
@@ -434,7 +501,7 @@ export const getTransformWrapperCode = (
|
|
|
434
501
|
} catch (e) {
|
|
435
502
|
// Best-effort; the post navigation stays hidden.
|
|
436
503
|
}${
|
|
437
|
-
options.
|
|
504
|
+
contentSettingsFor(options, tableName).comments === true
|
|
438
505
|
? `
|
|
439
506
|
try {
|
|
440
507
|
commentsByPostId = await getBlogCommentsMap(getClientFn, records)
|
|
@@ -447,7 +514,7 @@ export const getTransformWrapperCode = (
|
|
|
447
514
|
: ''
|
|
448
515
|
const blogPageOption =
|
|
449
516
|
type === 'blog-post'
|
|
450
|
-
? ', adjacentPostsById: adjacentPostsById, commentsByPostId: commentsByPostId'
|
|
517
|
+
? ', adjacentPostsById: adjacentPostsById, commentsByPostId: commentsByPostId, details: Array.isArray(records) && records.length === 1'
|
|
451
518
|
: ''
|
|
452
519
|
|
|
453
520
|
return `${RAW_ROWS_CODE}
|