@teleporthq/teleport-plugin-next-data-source 0.43.67 → 0.43.68
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__/array-overlap-sql.test.ts +53 -0
- package/__tests__/blog-post-page-enrichment.test.ts +214 -0
- package/__tests__/browser-row-policy.test.ts +503 -0
- package/__tests__/data-source-cache-wrapping.test.ts +18 -2
- package/__tests__/ecommerce-product-options.test.ts +206 -0
- package/__tests__/ecommerce-product-pages.test.ts +82 -0
- package/__tests__/ecommerce-product-pre-options.golden.json +505 -0
- package/__tests__/ecommerce-product-storefront-contract.test.ts +293 -0
- package/__tests__/ecommerce-product-subscriptions.test.ts +121 -0
- package/__tests__/fetchers.test.ts +2 -2
- package/__tests__/items-path.test.ts +150 -0
- package/__tests__/product-options-storefront.fixture.json +2510 -0
- package/__tests__/product-page-url-fixtures.json +25 -0
- package/__tests__/product-price-sort.ts +4 -2
- package/__tests__/raw-rows-mode.test.ts +111 -0
- package/__tests__/rest-api-response-cache.test.ts +434 -0
- package/__tests__/table-read-route-protected-tables.test.ts +493 -0
- package/dist/cjs/array-overlap-sql.d.ts +21 -0
- package/dist/cjs/array-overlap-sql.d.ts.map +1 -0
- package/dist/cjs/array-overlap-sql.js +25 -0
- package/dist/cjs/array-overlap-sql.js.map +1 -0
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +10 -4
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
- package/dist/cjs/fetchers/javascript.js +1 -1
- package/dist/cjs/fetchers/javascript.js.map +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts +1 -1
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +8 -4
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts +8 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +42 -4
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +7 -2
- 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 +8 -5
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +43 -0
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/browser-row-policy.js +66 -0
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.d.ts +8 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.js +12 -0
- package/dist/cjs/fetchers/utils/request-sql-guards.js.map +1 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts +40 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.js +64 -0
- package/dist/cjs/fetchers/utils/rest-api-response-cache.js.map +1 -0
- package/dist/cjs/fetchers/utils/table-access-guard.d.ts +23 -0
- package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -0
- package/dist/cjs/fetchers/utils/table-access-guard.js +34 -0
- package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -0
- package/dist/cjs/items-path.d.ts +10 -0
- package/dist/cjs/items-path.d.ts.map +1 -0
- package/dist/cjs/items-path.js +86 -0
- package/dist/cjs/items-path.js.map +1 -0
- package/dist/cjs/localized-columns.d.ts.map +1 -1
- package/dist/cjs/localized-columns.js +13 -0
- package/dist/cjs/localized-columns.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +17 -5
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +16 -0
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -0
- package/dist/cjs/transformations/blog-adjacent-posts.js +25 -0
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -0
- package/dist/cjs/transformations/blog-comments.d.ts +30 -0
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -0
- package/dist/cjs/transformations/blog-comments.js +34 -0
- package/dist/cjs/transformations/blog-comments.js.map +1 -0
- package/dist/cjs/transformations/blog-post.d.ts +2 -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/ecommerce-product.d.ts +9 -1
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +28 -1
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/heading-anchors.d.ts +25 -0
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -0
- package/dist/cjs/transformations/heading-anchors.js +29 -0
- package/dist/cjs/transformations/heading-anchors.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +47 -4
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +56 -18
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
- package/dist/cjs/transformations/shared-utils.js +1 -1
- package/dist/cjs/transformations/shared-utils.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 +1 -1
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/array-overlap-sql.d.ts +21 -0
- package/dist/esm/array-overlap-sql.d.ts.map +1 -0
- package/dist/esm/array-overlap-sql.js +21 -0
- package/dist/esm/array-overlap-sql.js.map +1 -0
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +11 -5
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/javascript.d.ts.map +1 -1
- package/dist/esm/fetchers/javascript.js +1 -1
- package/dist/esm/fetchers/javascript.js.map +1 -1
- package/dist/esm/fetchers/postgresql.d.ts +1 -1
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +8 -4
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts +8 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +42 -4
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +7 -2
- 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 +8 -5
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +43 -0
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/browser-row-policy.js +60 -0
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -0
- package/dist/esm/fetchers/utils/request-sql-guards.d.ts +8 -0
- package/dist/esm/fetchers/utils/request-sql-guards.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/request-sql-guards.js +8 -0
- package/dist/esm/fetchers/utils/request-sql-guards.js.map +1 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts +40 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.js +59 -0
- package/dist/esm/fetchers/utils/rest-api-response-cache.js.map +1 -0
- package/dist/esm/fetchers/utils/table-access-guard.d.ts +23 -0
- package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -0
- package/dist/esm/fetchers/utils/table-access-guard.js +29 -0
- package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -0
- package/dist/esm/items-path.d.ts +10 -0
- package/dist/esm/items-path.d.ts.map +1 -0
- package/dist/esm/items-path.js +58 -0
- package/dist/esm/items-path.js.map +1 -0
- package/dist/esm/localized-columns.d.ts.map +1 -1
- package/dist/esm/localized-columns.js +13 -0
- package/dist/esm/localized-columns.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +17 -5
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +16 -0
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -0
- package/dist/esm/transformations/blog-adjacent-posts.js +21 -0
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -0
- package/dist/esm/transformations/blog-comments.d.ts +30 -0
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -0
- package/dist/esm/transformations/blog-comments.js +30 -0
- package/dist/esm/transformations/blog-comments.js.map +1 -0
- package/dist/esm/transformations/blog-post.d.ts +2 -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/ecommerce-product.d.ts +9 -1
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +29 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/heading-anchors.d.ts +25 -0
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -0
- package/dist/esm/transformations/heading-anchors.js +25 -0
- package/dist/esm/transformations/heading-anchors.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +47 -4
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +56 -18
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
- package/dist/esm/transformations/shared-utils.js +1 -1
- package/dist/esm/transformations/shared-utils.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 +1 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/array-overlap-sql.ts +41 -0
- package/src/data-source-fetchers.ts +41 -5
- package/src/fetchers/javascript.ts +9 -1
- package/src/fetchers/postgresql.ts +37 -10
- package/src/fetchers/raw-query.ts +60 -5
- package/src/fetchers/rest-api.ts +27 -5
- package/src/fetchers/teleport.ts +37 -13
- package/src/fetchers/utils/browser-row-policy.ts +212 -0
- package/src/fetchers/utils/request-sql-guards.ts +25 -0
- package/src/fetchers/utils/rest-api-response-cache.ts +188 -0
- package/src/fetchers/utils/table-access-guard.ts +39 -0
- package/src/items-path.ts +74 -0
- package/src/localized-columns.ts +13 -0
- package/src/pagination-plugin.ts +29 -7
- package/src/transformations/blog-adjacent-posts.ts +74 -0
- package/src/transformations/blog-comments.ts +132 -0
- package/src/transformations/blog-post.ts +38 -5
- package/src/transformations/ecommerce-product.ts +568 -5
- package/src/transformations/heading-anchors.ts +85 -0
- package/src/transformations/index.ts +165 -22
- package/src/transformations/shared-utils.ts +6 -0
- package/src/utils.ts +26 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Where a paginated/searchable list's rows sit inside the payload its data
|
|
5
|
+
* source returns, read off the repeater's `source` expression.
|
|
6
|
+
*
|
|
7
|
+
* A REST API or JavaScript source may answer `{ templates: [...], total }`, and
|
|
8
|
+
* the editor binds the list to `All_Templates_data_data?.templates || []`. The
|
|
9
|
+
* generated fetcher only searches, filters, sorts and slices a payload that IS
|
|
10
|
+
* an array, so without this every row came back on every page (the canvas
|
|
11
|
+
* slices on the client and looked right) and the count measured the wrapper as
|
|
12
|
+
* 0 rows, disabling "Next".
|
|
13
|
+
*
|
|
14
|
+
* Returns undefined when the list is bound to the payload itself, or when the
|
|
15
|
+
* source is anything other than a plain property chain off the data source's
|
|
16
|
+
* render prop — an expression this cannot read is left exactly as before.
|
|
17
|
+
*/
|
|
18
|
+
const SEGMENT_RE =
|
|
19
|
+
/^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)')\s*\])/
|
|
20
|
+
|
|
21
|
+
const PATH_AWARE_SOURCE_TYPES = new Set(['rest-api', 'javascript'])
|
|
22
|
+
|
|
23
|
+
export const extractItemsPath = (
|
|
24
|
+
source: unknown,
|
|
25
|
+
dataSourceIdentifier: string,
|
|
26
|
+
dataSourceType: string
|
|
27
|
+
): string[] | undefined => {
|
|
28
|
+
if (typeof source !== 'string' || !dataSourceIdentifier) {
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
if (!PATH_AWARE_SOURCE_TYPES.has(dataSourceType)) {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let rest = source
|
|
36
|
+
.trim()
|
|
37
|
+
.replace(/\s*\|\|\s*\[\s*\]$/, '')
|
|
38
|
+
.trim()
|
|
39
|
+
if (!rest.startsWith(dataSourceIdentifier)) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
rest = rest.slice(dataSourceIdentifier.length)
|
|
43
|
+
|
|
44
|
+
const path: string[] = []
|
|
45
|
+
while (rest.length > 0) {
|
|
46
|
+
const match = rest.match(SEGMENT_RE)
|
|
47
|
+
if (!match) {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
path.push(match[1] ?? match[2] ?? match[3] ?? match[4])
|
|
51
|
+
rest = rest.slice(match[0].length)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return path.length > 0 ? path : undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Push `<key>: '["templates"]'` onto a params object. Fetch calls send it as
|
|
59
|
+
* `itemsPath` (process the inner array, keep the wrapper the page reads back
|
|
60
|
+
* through); count calls send it as `collectionPath`, so the count handler
|
|
61
|
+
* measures the inner array instead of the wrapper.
|
|
62
|
+
*/
|
|
63
|
+
export const appendItemsPathParam = (
|
|
64
|
+
paramsProps: types.ObjectProperty[],
|
|
65
|
+
itemsPath: string[] | undefined,
|
|
66
|
+
key: 'itemsPath' | 'collectionPath'
|
|
67
|
+
): void => {
|
|
68
|
+
if (!itemsPath || itemsPath.length === 0) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
paramsProps.push(
|
|
72
|
+
types.objectProperty(types.identifier(key), types.stringLiteral(JSON.stringify(itemsPath)))
|
|
73
|
+
)
|
|
74
|
+
}
|
package/src/localized-columns.ts
CHANGED
|
@@ -118,6 +118,19 @@ export const generateLocalizedColumnsHelper = (families: LocalizedColumnMap): st
|
|
|
118
118
|
" return '(' + clauses.join(' OR ') + ')'",
|
|
119
119
|
'}',
|
|
120
120
|
'',
|
|
121
|
+
'// A translatable list (tags) and its per-language copies, for a filter that',
|
|
122
|
+
'// matches a value in ANY language (a localized tag link resolves under every',
|
|
123
|
+
'// locale, like a localized slug). Null for a column with no copies.',
|
|
124
|
+
'function localizedColumnList(field) {',
|
|
125
|
+
' var copies = LOCALIZED_COLUMNS[field]',
|
|
126
|
+
' if (!copies) return null',
|
|
127
|
+
' var columns = [quoteLocalizedColumn(field)]',
|
|
128
|
+
' for (var locale in copies) {',
|
|
129
|
+
' if (Object.prototype.hasOwnProperty.call(copies, locale)) columns.push(quoteLocalizedColumn(copies[locale]))',
|
|
130
|
+
' }',
|
|
131
|
+
' return columns',
|
|
132
|
+
'}',
|
|
133
|
+
'',
|
|
121
134
|
"// ORDER BY a translatable column in the request's language, falling back to",
|
|
122
135
|
'// the base column for a row with no translation — the same resolution the',
|
|
123
136
|
'// row transform applies to what is displayed. Null when the field or the',
|
package/src/pagination-plugin.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
|
|
|
13
13
|
import { buildProductTransformOptions, type EntityTransformOptions } from './transformations'
|
|
14
14
|
import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
|
|
15
15
|
import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './filter-utils'
|
|
16
|
+
import { appendItemsPathParam, extractItemsPath } from './items-path'
|
|
16
17
|
import {
|
|
17
18
|
applyLoadingStateToDataProvider,
|
|
18
19
|
buildLoadingStateDeclarations,
|
|
@@ -255,6 +256,9 @@ interface DataSourceUsage {
|
|
|
255
256
|
// `URLSearchParamSync` builders. Page 1 is written as the ABSENCE of the key.
|
|
256
257
|
// Cleared when `infiniteScroll` is set.
|
|
257
258
|
pageUrlParamKey?: string
|
|
259
|
+
// Path to the rows inside a wrapped REST/JavaScript payload, when the list is
|
|
260
|
+
// bound to one (`data?.templates` -> `['templates']`). See `items-path.ts`.
|
|
261
|
+
itemsPath?: string[]
|
|
258
262
|
// Computed category
|
|
259
263
|
category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
|
|
260
264
|
}
|
|
@@ -656,6 +660,11 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
|
|
|
656
660
|
infiniteScroll,
|
|
657
661
|
infiniteScrollLoadMore: infiniteScroll && !!content.infiniteScrollLoadMore,
|
|
658
662
|
pageUrlParamKey,
|
|
663
|
+
itemsPath: extractItemsPath(
|
|
664
|
+
content.source,
|
|
665
|
+
parentDataSource.identifier,
|
|
666
|
+
parentDataSource.resourceDef.dataSourceType || ''
|
|
667
|
+
),
|
|
659
668
|
category: 'plain',
|
|
660
669
|
}
|
|
661
670
|
|
|
@@ -1410,6 +1419,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1410
1419
|
}
|
|
1411
1420
|
// Add filters to count fetch params if present
|
|
1412
1421
|
appendFiltersParam(urlParams, usage.filters, buildFilterDestinationExpression)
|
|
1422
|
+
appendItemsPathParam(urlParams, usage.itemsPath, 'collectionPath')
|
|
1413
1423
|
|
|
1414
1424
|
const countEffectDeps: types.Expression[] = [
|
|
1415
1425
|
types.memberExpression(
|
|
@@ -1526,6 +1536,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
|
|
|
1526
1536
|
)
|
|
1527
1537
|
}
|
|
1528
1538
|
appendFiltersParam(countUrlParams, usage.filters, buildFilterDestinationExpression)
|
|
1539
|
+
appendItemsPathParam(countUrlParams, usage.itemsPath, 'collectionPath')
|
|
1529
1540
|
|
|
1530
1541
|
// Default to mount-only; refresh when ANY filter destination
|
|
1531
1542
|
// changes — state-bound (e.g. `selectedCategory`) and URL-driven —
|
|
@@ -2364,6 +2375,7 @@ function updateDataProviderForPaginatedSearch(
|
|
|
2364
2375
|
|
|
2365
2376
|
// Add filters if present
|
|
2366
2377
|
appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
|
|
2378
|
+
appendItemsPathParam(paramsProps, usage.itemsPath, 'itemsPath')
|
|
2367
2379
|
|
|
2368
2380
|
// Build useMemo dependencies including filter state IDs and dynamic sort state IDs
|
|
2369
2381
|
const memoDeps: types.Expression[] = [types.identifier(vars.combinedStateVar)]
|
|
@@ -2523,6 +2535,7 @@ function updateDataProviderForPaginationOnly(
|
|
|
2523
2535
|
|
|
2524
2536
|
// Add filters if present
|
|
2525
2537
|
appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
|
|
2538
|
+
appendItemsPathParam(paramsProps, usage.itemsPath, 'itemsPath')
|
|
2526
2539
|
|
|
2527
2540
|
// Build useMemo dependencies including filter state IDs and dynamic sort state IDs
|
|
2528
2541
|
const memoDeps: types.Expression[] = [types.identifier(vars.pageStateVar)]
|
|
@@ -2643,6 +2656,7 @@ function updateDataProviderForSearchOnly(
|
|
|
2643
2656
|
|
|
2644
2657
|
// Add filters if present
|
|
2645
2658
|
appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
|
|
2659
|
+
appendItemsPathParam(paramsProps, usage.itemsPath, 'itemsPath')
|
|
2646
2660
|
|
|
2647
2661
|
// Build useMemo dependencies including filter state IDs and dynamic sort state IDs
|
|
2648
2662
|
const memoDeps: types.Expression[] = [types.identifier(vars.debouncedSearchQueryVar)]
|
|
@@ -3668,6 +3682,8 @@ function updateGetStaticProps(
|
|
|
3668
3682
|
)
|
|
3669
3683
|
}
|
|
3670
3684
|
|
|
3685
|
+
appendItemsPathParam(fetchParams, usage.itemsPath, 'itemsPath')
|
|
3686
|
+
|
|
3671
3687
|
// The prefetch is per locale — getStaticProps runs once per language.
|
|
3672
3688
|
if (localized) {
|
|
3673
3689
|
fetchParams.push(buildServerLocaleParam())
|
|
@@ -3749,8 +3765,10 @@ function updateGetStaticProps(
|
|
|
3749
3765
|
const staticFiltersForCount = (usage.filters || []).filter(
|
|
3750
3766
|
(f: any) => !ASTUtils.isUIDLDynamicReference(f.destination)
|
|
3751
3767
|
)
|
|
3752
|
-
|
|
3753
|
-
|
|
3768
|
+
// A count scoped to an inner array is a different number from the
|
|
3769
|
+
// whole-payload one, so it gets its own variable just like a filtered one.
|
|
3770
|
+
const hasScopedCount = staticFiltersForCount.length > 0 || !!usage.itemsPath
|
|
3771
|
+
const countVarName = hasScopedCount
|
|
3754
3772
|
? `${usage.dataSourceIdentifier}_ds_${usage.index}_count`
|
|
3755
3773
|
: `${usage.dataSourceIdentifier}_count`
|
|
3756
3774
|
|
|
@@ -3822,7 +3840,7 @@ function updateGetStaticProps(
|
|
|
3822
3840
|
// Group usages by their filters (stringify for comparison)
|
|
3823
3841
|
const usagesByFilters = new Map<string, DataSourceUsage[]>()
|
|
3824
3842
|
for (const usage of usagesForDataSource) {
|
|
3825
|
-
const filtersKey = JSON.stringify(usage.filters || [])
|
|
3843
|
+
const filtersKey = JSON.stringify([usage.filters || [], usage.itemsPath || []])
|
|
3826
3844
|
const existing = usagesByFilters.get(filtersKey) || []
|
|
3827
3845
|
existing.push(usage)
|
|
3828
3846
|
usagesByFilters.set(filtersKey, existing)
|
|
@@ -3837,10 +3855,12 @@ function updateGetStaticProps(
|
|
|
3837
3855
|
)
|
|
3838
3856
|
const hasStaticFilters = staticFiltersForCount.length > 0
|
|
3839
3857
|
|
|
3840
|
-
// Create unique count variable name based on filters
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3858
|
+
// Create unique count variable name based on filters (and inner-array
|
|
3859
|
+
// scope — must match the naming at the maxPages site above)
|
|
3860
|
+
const countVarName =
|
|
3861
|
+
hasStaticFilters || firstUsage.itemsPath
|
|
3862
|
+
? `${firstUsage.dataSourceIdentifier}_ds_${firstUsage.index}_count`
|
|
3863
|
+
: `${firstUsage.dataSourceIdentifier}_count`
|
|
3844
3864
|
|
|
3845
3865
|
// Check if this count was already processed
|
|
3846
3866
|
const countKey = `${key}:${filtersKey}`
|
|
@@ -3892,6 +3912,8 @@ function updateGetStaticProps(
|
|
|
3892
3912
|
)
|
|
3893
3913
|
}
|
|
3894
3914
|
|
|
3915
|
+
appendItemsPathParam(countParams, firstUsage.itemsPath, 'collectionPath')
|
|
3916
|
+
|
|
3895
3917
|
fetchesArray.elements.push(
|
|
3896
3918
|
types.callExpression(
|
|
3897
3919
|
types.memberExpression(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many posts each direction is read, so a neighbour that only REDIRECTS
|
|
3
|
+
* elsewhere can be stepped over without a second query.
|
|
4
|
+
*/
|
|
5
|
+
const ADJACENT_POST_CANDIDATES = 5
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generates the code that finds a post's neighbours for its "previous / next"
|
|
9
|
+
* navigation: the published posts right before and after it in the order the
|
|
10
|
+
* blog listing shows them (`created_at`, then `id`, newest first). "Previous"
|
|
11
|
+
* is the older one.
|
|
12
|
+
*
|
|
13
|
+
* Only a post DETAILS fetch (a single record) asks. A neighbour whose row
|
|
14
|
+
* carries a redirect is skipped — linking to it would only bounce the reader
|
|
15
|
+
* somewhere else — which is read off the row in code rather than in SQL, so a
|
|
16
|
+
* table provisioned before the SEO columns still works.
|
|
17
|
+
*
|
|
18
|
+
* MUST mirror `findAdjacentBlogPosts` in teleport-gui
|
|
19
|
+
* `packages/renderer/src/utils/blog-adjacent-posts.ts` (the canvas).
|
|
20
|
+
*/
|
|
21
|
+
export const generateAdjacentPostsCode = (): string => `
|
|
22
|
+
var BLOG_ADJACENT_CANDIDATES = ${ADJACENT_POST_CANDIDATES}
|
|
23
|
+
|
|
24
|
+
function blogAdjacentPostsSql(comparison, direction) {
|
|
25
|
+
return (
|
|
26
|
+
'SELECT p.* FROM teleport_blog_posts p,' +
|
|
27
|
+
' (SELECT created_at, id FROM teleport_blog_posts WHERE id = $1) cur' +
|
|
28
|
+
" WHERE p.status = 'published' AND (p.created_at, p.id) " + comparison + ' (cur.created_at, cur.id)' +
|
|
29
|
+
' ORDER BY p.created_at ' + direction + ', p.id ' + direction +
|
|
30
|
+
' LIMIT ' + BLOG_ADJACENT_CANDIDATES
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function firstLinkableBlogPost(result) {
|
|
35
|
+
var rows = result && Array.isArray(result.rows) ? result.rows : []
|
|
36
|
+
for (var i = 0; i < rows.length; i++) {
|
|
37
|
+
var row = rows[i]
|
|
38
|
+
if (!row) continue
|
|
39
|
+
var redirect = pickFirst(row.redirect_url, row.redirectUrl)
|
|
40
|
+
if (typeof redirect === 'string' && redirect.trim() !== '') continue
|
|
41
|
+
// Through JSON, like the rows the fetcher itself hands the transform.
|
|
42
|
+
return JSON.parse(JSON.stringify(row))
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// { [postId]: { previous, next } } — raw rows or null — for a single-record
|
|
48
|
+
// fetch; {} otherwise and on any failure (the navigation then hides).
|
|
49
|
+
async function getAdjacentPostsMap(getClientFn, records) {
|
|
50
|
+
var map = {}
|
|
51
|
+
if (!Array.isArray(records) || records.length !== 1) return map
|
|
52
|
+
var postId = records[0] && records[0].id
|
|
53
|
+
if (postId === null || postId === undefined) return map
|
|
54
|
+
|
|
55
|
+
var client
|
|
56
|
+
try {
|
|
57
|
+
client = getClientFn()
|
|
58
|
+
await client.connect()
|
|
59
|
+
var older = await client.query(blogAdjacentPostsSql('<', 'DESC'), [String(postId)])
|
|
60
|
+
var newer = await client.query(blogAdjacentPostsSql('>', 'ASC'), [String(postId)])
|
|
61
|
+
map[String(postId)] = {
|
|
62
|
+
previous: firstLinkableBlogPost(older),
|
|
63
|
+
next: firstLinkableBlogPost(newer),
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// Leaves the navigation hidden.
|
|
67
|
+
} finally {
|
|
68
|
+
if (client) {
|
|
69
|
+
try { await client.end() } catch (e) { /* ignore */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return map
|
|
73
|
+
}
|
|
74
|
+
`
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How many TOP-LEVEL comments a post page carries: the newest ones, shown
|
|
3
|
+
* oldest-first, each with all of its replies. The count in the section's
|
|
4
|
+
* heading still counts every approved comment.
|
|
5
|
+
*
|
|
6
|
+
* ⛔ PAIRED with `BLOG_COMMENTS_RENDER_LIMIT` in teleport-gui
|
|
7
|
+
* `features/blog/constants/blog-comments.ts` (the canvas mirror).
|
|
8
|
+
*/
|
|
9
|
+
export const BLOG_COMMENTS_RENDER_LIMIT = 200
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates the code that loads and shapes a post's approved comments.
|
|
13
|
+
*
|
|
14
|
+
* Read server-side, with the post: only a post DETAILS fetch (a single record)
|
|
15
|
+
* asks, so a listing never pays for it, and the comments arrive in the page's
|
|
16
|
+
* HTML — rendered by SSR/ISR, carried along a client-side navigation to another
|
|
17
|
+
* post, no request of their own.
|
|
18
|
+
*
|
|
19
|
+
* Public columns only. `teleport_blog_comments` holds the commenters' email
|
|
20
|
+
* addresses and the comments still waiting for approval, which is why it is a
|
|
21
|
+
* customer-record table no browser may read (see teleport-shared's
|
|
22
|
+
* `table-access.ts`); nothing here selects either.
|
|
23
|
+
*
|
|
24
|
+
* Replies are one level deep: a reply whose parent is not among the rows (not
|
|
25
|
+
* approved, deleted) is dropped rather than promoted to the top level.
|
|
26
|
+
*
|
|
27
|
+
* MUST mirror `buildBlogComments` in teleport-gui
|
|
28
|
+
* `packages/renderer/src/utils/blog-comments.ts` (the canvas).
|
|
29
|
+
*/
|
|
30
|
+
export const generateBlogCommentsCode = (): string => `
|
|
31
|
+
var BLOG_COMMENTS_RENDER_LIMIT = ${BLOG_COMMENTS_RENDER_LIMIT}
|
|
32
|
+
|
|
33
|
+
var BLOG_COMMENTS_SQL =
|
|
34
|
+
'WITH top AS (' +
|
|
35
|
+
' SELECT id FROM teleport_blog_comments' +
|
|
36
|
+
" WHERE post_id = $1 AND parent_id IS NULL AND status = 'approved'" +
|
|
37
|
+
' ORDER BY created_at DESC, id DESC LIMIT ' + BLOG_COMMENTS_RENDER_LIMIT +
|
|
38
|
+
')' +
|
|
39
|
+
' SELECT c.id, c.parent_id, c.author_name, c.content, c.is_author_reply, c.created_at' +
|
|
40
|
+
' FROM teleport_blog_comments c' +
|
|
41
|
+
" WHERE c.post_id = $1 AND c.status = 'approved'" +
|
|
42
|
+
' AND (c.id IN (SELECT id FROM top) OR c.parent_id IN (SELECT id FROM top))' +
|
|
43
|
+
' ORDER BY c.created_at ASC, c.id ASC'
|
|
44
|
+
|
|
45
|
+
// Every approved comment the page could show — a reply counts only while the
|
|
46
|
+
// comment it answers is approved too.
|
|
47
|
+
var BLOG_COMMENTS_COUNT_SQL =
|
|
48
|
+
'SELECT COUNT(*) AS n FROM teleport_blog_comments c' +
|
|
49
|
+
" WHERE c.post_id = $1 AND c.status = 'approved'" +
|
|
50
|
+
' AND (c.parent_id IS NULL OR EXISTS (' +
|
|
51
|
+
' SELECT 1 FROM teleport_blog_comments p' +
|
|
52
|
+
" WHERE p.id = c.parent_id AND p.status = 'approved'))"
|
|
53
|
+
|
|
54
|
+
function blogAuthorInitials(name) {
|
|
55
|
+
if (typeof name !== 'string') return ''
|
|
56
|
+
var words = name.trim().split(' ').filter(function(word) { return word !== '' })
|
|
57
|
+
if (words.length === 0) return ''
|
|
58
|
+
// Code points, not UTF-16 units: an initial may be an emoji or a letter
|
|
59
|
+
// outside the BMP, and half of a surrogate pair renders as a box.
|
|
60
|
+
var first = Array.from(words[0])[0] || ''
|
|
61
|
+
var last = words.length > 1 ? Array.from(words[words.length - 1])[0] || '' : ''
|
|
62
|
+
return (first + last).toUpperCase()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildBlogComment(row) {
|
|
66
|
+
var name = typeof row.author_name === 'string' ? row.author_name.trim() : ''
|
|
67
|
+
var authorName = name || 'Reader'
|
|
68
|
+
return {
|
|
69
|
+
id: String(row.id),
|
|
70
|
+
authorName: authorName,
|
|
71
|
+
authorInitials: blogAuthorInitials(authorName),
|
|
72
|
+
content: typeof row.content === 'string' ? row.content : '',
|
|
73
|
+
createdAt: normalizeTimestamp(row.created_at),
|
|
74
|
+
isAuthorReply: coerceBoolean(row.is_author_reply, false),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isTopLevelComment(row) {
|
|
79
|
+
return row.parent_id === null || row.parent_id === undefined || row.parent_id === ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildBlogComments(rows) {
|
|
83
|
+
var comments = []
|
|
84
|
+
if (!Array.isArray(rows)) return comments
|
|
85
|
+
var byId = {}
|
|
86
|
+
for (var i = 0; i < rows.length; i++) {
|
|
87
|
+
var row = rows[i]
|
|
88
|
+
if (!row || row.id == null || !isTopLevelComment(row)) continue
|
|
89
|
+
var comment = buildBlogComment(row)
|
|
90
|
+
comment.replies = []
|
|
91
|
+
byId[comment.id] = comment
|
|
92
|
+
comments.push(comment)
|
|
93
|
+
}
|
|
94
|
+
for (var j = 0; j < rows.length; j++) {
|
|
95
|
+
var reply = rows[j]
|
|
96
|
+
if (!reply || reply.id == null || isTopLevelComment(reply)) continue
|
|
97
|
+
var parent = byId[String(reply.parent_id)]
|
|
98
|
+
if (parent) parent.replies.push(buildBlogComment(reply))
|
|
99
|
+
}
|
|
100
|
+
return comments
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// { [postId]: { rows, count } } for a single-record (details) fetch; {} for
|
|
104
|
+
// anything else, and on any failure — a blog whose comments table does not
|
|
105
|
+
// exist yet renders its posts without comments rather than failing the page.
|
|
106
|
+
async function getBlogCommentsMap(getClientFn, records) {
|
|
107
|
+
var map = {}
|
|
108
|
+
if (!Array.isArray(records) || records.length !== 1) return map
|
|
109
|
+
var postId = records[0] && records[0].id
|
|
110
|
+
if (postId === null || postId === undefined) return map
|
|
111
|
+
|
|
112
|
+
var client
|
|
113
|
+
try {
|
|
114
|
+
client = getClientFn()
|
|
115
|
+
await client.connect()
|
|
116
|
+
var listed = await client.query(BLOG_COMMENTS_SQL, [String(postId)])
|
|
117
|
+
var counted = await client.query(BLOG_COMMENTS_COUNT_SQL, [String(postId)])
|
|
118
|
+
// Through JSON, like every other row the fetcher hands the transform: the
|
|
119
|
+
// driver returns timestamps as Date objects.
|
|
120
|
+
var rows = listed && Array.isArray(listed.rows) ? JSON.parse(JSON.stringify(listed.rows)) : []
|
|
121
|
+
var count = counted && counted.rows && counted.rows[0] ? Number(counted.rows[0].n) : NaN
|
|
122
|
+
map[String(postId)] = { rows: rows, count: isNaN(count) ? rows.length : count }
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Leaves the post without comments.
|
|
125
|
+
} finally {
|
|
126
|
+
if (client) {
|
|
127
|
+
try { await client.end() } catch (e) { /* ignore */ }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return map
|
|
131
|
+
}
|
|
132
|
+
`
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
|
|
2
2
|
import { generateCategoryTaxonomyCode } from './category-taxonomy'
|
|
3
|
+
import { generateAdjacentPostsCode } from './blog-adjacent-posts'
|
|
4
|
+
import { generateBlogCommentsCode } from './blog-comments'
|
|
5
|
+
import { generateHeadingAnchorsCode } from './heading-anchors'
|
|
3
6
|
|
|
4
7
|
/** What the blog-post transform bakes in at export time beyond the row itself. */
|
|
5
8
|
export interface BlogPostTransformOptions {
|
|
6
9
|
/** Category taxonomy — lives only in the UIDL, there is no DB table for it. */
|
|
7
10
|
categories?: UIDLEcommerceCategory[]
|
|
11
|
+
/** Content headings get ids and `#` links — see `heading-anchors.ts`. */
|
|
12
|
+
headingAnchors?: boolean
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
/**
|
|
@@ -20,6 +25,10 @@ export const generateBlogPostTransformationCode = (
|
|
|
20
25
|
// Blog Post Transformation
|
|
21
26
|
// ============================================================
|
|
22
27
|
${generateCategoryTaxonomyCode('BLOG_CATEGORIES_BY_ID', options.categories)}
|
|
28
|
+
${generateBlogCommentsCode()}
|
|
29
|
+
${generateAdjacentPostsCode()}
|
|
30
|
+
${generateHeadingAnchorsCode()}
|
|
31
|
+
var BLOG_HEADING_ANCHORS = ${options.headingAnchors === true}
|
|
23
32
|
|
|
24
33
|
function buildBlogPost(record, options) {
|
|
25
34
|
if (!record || typeof record !== 'object') return record
|
|
@@ -34,6 +43,7 @@ function buildBlogPost(record, options) {
|
|
|
34
43
|
var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''
|
|
35
44
|
var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''
|
|
36
45
|
var content = resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''
|
|
46
|
+
if (BLOG_HEADING_ANCHORS) content = addHeadingAnchors(content)
|
|
37
47
|
var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''
|
|
38
48
|
var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null
|
|
39
49
|
|
|
@@ -53,6 +63,7 @@ function buildBlogPost(record, options) {
|
|
|
53
63
|
var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null
|
|
54
64
|
var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null
|
|
55
65
|
var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null
|
|
66
|
+
var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null
|
|
56
67
|
|
|
57
68
|
// Status
|
|
58
69
|
var status = record.status || 'draft'
|
|
@@ -78,6 +89,7 @@ function buildBlogPost(record, options) {
|
|
|
78
89
|
|
|
79
90
|
// Simple pass-through fields
|
|
80
91
|
var authorName = pickFirst(record.author_name, record.authorName)
|
|
92
|
+
var authorInitials = blogAuthorInitials(authorName)
|
|
81
93
|
var authorEmail = pickFirst(record.author_email, record.authorEmail)
|
|
82
94
|
var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)
|
|
83
95
|
|
|
@@ -132,12 +144,14 @@ function buildBlogPost(record, options) {
|
|
|
132
144
|
var relatedPostIds = parseRelatedIds(record.related_post_ids)
|
|
133
145
|
var relatedPostsById = options.relatedPostsById
|
|
134
146
|
var relatedPosts = []
|
|
147
|
+
// The nested builds of the related and adjacent posts get NO maps: each is a
|
|
148
|
+
// card, not a page, and two posts that point at each other would recurse.
|
|
149
|
+
var relatedNestedOptions = {
|
|
150
|
+
assetMap: assetMap,
|
|
151
|
+
currentLanguage: currentLang,
|
|
152
|
+
mainLanguage: mainLang,
|
|
153
|
+
}
|
|
135
154
|
if (relatedPostsById && relatedPostIds.length > 0) {
|
|
136
|
-
var relatedNestedOptions = {
|
|
137
|
-
assetMap: assetMap,
|
|
138
|
-
currentLanguage: currentLang,
|
|
139
|
-
mainLanguage: mainLang,
|
|
140
|
-
}
|
|
141
155
|
for (var rp = 0; rp < relatedPostIds.length; rp++) {
|
|
142
156
|
var relatedId = relatedPostIds[rp]
|
|
143
157
|
if (id != null && relatedId === String(id)) continue
|
|
@@ -150,6 +164,19 @@ function buildBlogPost(record, options) {
|
|
|
150
164
|
}
|
|
151
165
|
}
|
|
152
166
|
|
|
167
|
+
// The previous (older) and next (newer) published post, as 0-or-1-item
|
|
168
|
+
// lists: the post navigation maps over them like the related rail does, so an
|
|
169
|
+
// end of the blog simply renders no card on that side.
|
|
170
|
+
var adjacent = options.adjacentPostsById && id != null ? options.adjacentPostsById[String(id)] : null
|
|
171
|
+
var previousPosts = adjacent && adjacent.previous ? [buildBlogPost(adjacent.previous, relatedNestedOptions)] : []
|
|
172
|
+
var nextPosts = adjacent && adjacent.next ? [buildBlogPost(adjacent.next, relatedNestedOptions)] : []
|
|
173
|
+
|
|
174
|
+
// The approved comments — the newest top-level ones, each with its replies —
|
|
175
|
+
// present only on a details fetch; see blog-comments.ts.
|
|
176
|
+
var commentsEntry = options.commentsByPostId && id != null ? options.commentsByPostId[String(id)] : null
|
|
177
|
+
var comments = commentsEntry ? buildBlogComments(commentsEntry.rows) : []
|
|
178
|
+
var commentsCount = commentsEntry ? commentsEntry.count : 0
|
|
179
|
+
|
|
153
180
|
// Timestamps
|
|
154
181
|
var rawPublishedAt = pickFirst(record.published_at, record.publishedAt)
|
|
155
182
|
var publishedAt = rawPublishedAt != null ? normalizeTimestamp(rawPublishedAt) : null
|
|
@@ -184,11 +211,17 @@ function buildBlogPost(record, options) {
|
|
|
184
211
|
author_name: authorName,
|
|
185
212
|
author_email: authorEmail,
|
|
186
213
|
authorAvatarUrl: authorAvatarUrl,
|
|
214
|
+
authorBio: authorBio,
|
|
215
|
+
authorInitials: authorInitials,
|
|
187
216
|
metaTitle: metaTitle,
|
|
188
217
|
metaDescription: metaDescription,
|
|
189
218
|
readingTimeMinutes: readingTimeMinutes,
|
|
190
219
|
isFeatured: isFeatured,
|
|
191
220
|
allowComments: allowComments,
|
|
221
|
+
comments: comments,
|
|
222
|
+
commentsCount: commentsCount,
|
|
223
|
+
previousPosts: previousPosts,
|
|
224
|
+
nextPosts: nextPosts,
|
|
192
225
|
noIndex: noIndex,
|
|
193
226
|
canonicalUrl: canonicalUrl,
|
|
194
227
|
redirectUrl: redirectUrl,
|