6
6
Response ,
7
7
RouteParams ,
8
8
} from '@blinkk/root' ;
9
- import { RootCMSClient , translationsForLocale } from '@blinkk/root-cms/ client' ;
9
+ import { BatchRequest , RootCMSClient } from './ client.js ' ;
10
10
11
11
export type CMSRequest = Request & {
12
12
cmsClient : RootCMSClient ;
@@ -37,6 +37,16 @@ export interface CMSRouteOptions {
37
37
*/
38
38
slug ?: string ;
39
39
40
+ /**
41
+ * Hook that allows callers to modify the Root CMS `BatchRequest` object. If a
42
+ * new `BatchRequest` is returned, it will replace the default one created by
43
+ * `cmsRoute()`.
44
+ */
45
+ preRequestHook ?: (
46
+ batchRequest : BatchRequest ,
47
+ context : CMSRouteContext
48
+ ) => void | BatchRequest ;
49
+
40
50
/**
41
51
* Callback function that returns a map of Promises that contain fetched data.
42
52
* Once the promise is resolved, the values are injected into page's props
@@ -55,11 +65,6 @@ export interface CMSRouteOptions {
55
65
*/
56
66
setResponseHeaders ?: ( req : Request , res : Response ) => void ;
57
67
58
- /**
59
- * Translations configuration.
60
- */
61
- translations ?: ( context : CMSRouteContext ) => { tags ?: string [ ] } ;
62
-
63
68
/**
64
69
* Sets Cache-Control header to `private`.
65
70
*/
@@ -72,13 +77,18 @@ export interface CMSRouteOptions {
72
77
}
73
78
74
79
export interface CMSDoc {
80
+ id : string ;
81
+ slug : string ;
75
82
sys ?: {
76
83
locales ?: string [ ] ;
77
84
} ;
78
85
}
79
86
87
+ /**
88
+ * Utility for generating SSR and SSG handlers in a Root route file.
89
+ */
80
90
export function cmsRoute ( options : CMSRouteOptions ) {
81
- let cmsClient : RootCMSClient = null ;
91
+ let cmsClient : RootCMSClient | null = null ;
82
92
83
93
function getSlug ( params : RouteParams ) {
84
94
if ( options . slug ) {
@@ -98,27 +108,73 @@ export function cmsRoute(options: CMSRouteOptions) {
98
108
return resolvePromisesMap ( promisesMap ) ;
99
109
}
100
110
101
- async function generateProps ( routeContext : CMSRouteContext , locale : string ) {
111
+ async function generateProps (
112
+ routeContext : CMSRouteContext ,
113
+ preferredLocale : string | ( ( doc : CMSDoc ) => string )
114
+ ) {
102
115
const { slug, mode} = routeContext ;
103
- const translationsTags = [ 'common' , `${ options . collection } /${ slug } ` ] ;
104
- if ( options . translations ) {
105
- const tags = options . translations ( routeContext ) ?. tags || [ ] ;
106
- translationsTags . push ( ...tags ) ;
116
+
117
+ const primaryDocId = `${ options . collection } /${ slug } ` ;
118
+ let batchRequest = routeContext . cmsClient . createBatchRequest ( {
119
+ mode,
120
+ translate : true ,
121
+ } ) ;
122
+ batchRequest . addDoc ( primaryDocId ) ;
123
+
124
+ // Call the pre-request hook to allow users to modify the batch request.
125
+ if ( options . preRequestHook ) {
126
+ const overridedBatchRequest = options . preRequestHook (
127
+ batchRequest ,
128
+ routeContext
129
+ ) ;
130
+ if ( overridedBatchRequest ) {
131
+ batchRequest = overridedBatchRequest ;
132
+ }
107
133
}
108
134
109
- const [ doc , translationsMap , data ] = await Promise . all ( [
110
- cmsClient . getDoc < CMSDoc > ( options . collection , slug , {
111
- mode,
112
- } ) ,
113
- cmsClient . loadTranslations ( { tags : translationsTags } ) ,
135
+ // Fetch the Root CMS BatchRequest in parallel with any other data the
136
+ // caller needs to fetch to render the route.
137
+ const [ batchRes , data ] = await Promise . all ( [
138
+ batchRequest . fetch ( ) ,
114
139
fetchData ( routeContext ) ,
115
140
] ) ;
141
+ const doc = batchRes . docs [ primaryDocId ] ;
116
142
if ( ! doc ) {
117
143
return { notFound : true } ;
118
144
}
119
145
120
- const translations = translationsForLocale ( translationsMap , locale ) ;
146
+ // Determine the preferred locale to render.
147
+ let locale : string ;
148
+ if ( typeof preferredLocale === 'string' ) {
149
+ locale = preferredLocale ;
150
+ } else {
151
+ locale = preferredLocale ( doc ) ;
152
+ }
153
+ const docLocales = doc . sys ?. locales || [ 'en' ] ;
154
+ if ( ! locale || ! docLocales . includes ( locale ) ) {
155
+ return { notFound : true } ;
156
+ }
157
+
158
+ // From the preferred locale, generate a translations map.
159
+ const i18nFallbacks =
160
+ routeContext . cmsClient . rootConfig . i18n ?. fallbacks || { } ;
161
+ const translationFallbackLocales = i18nFallbacks [ locale ] || [ locale ] ;
162
+ const translations = batchRes . getTranslations ( translationFallbackLocales ) ;
163
+
121
164
let props : any = { ...data , locale, mode, slug, doc} ;
165
+
166
+ // For SSR handlers, inject the user's country of origin to props.
167
+ if ( routeContext . req ) {
168
+ const country =
169
+ getFirstQueryParam ( routeContext . req , 'gl' ) ||
170
+ routeContext . req . get ( 'x-country-code' ) ||
171
+ routeContext . req . get ( 'x-appengine-country' ) ||
172
+ null ;
173
+ props . country = country ;
174
+ }
175
+
176
+ // Call the pre-render hook which allows a caller to modify props before
177
+ // it is passed to the route component.
122
178
if ( options . preRenderHook ) {
123
179
props = await options . preRenderHook ( props , routeContext ) ;
124
180
}
@@ -127,8 +183,8 @@ export function cmsRoute(options: CMSRouteOptions) {
127
183
}
128
184
129
185
// SSG handlers are disabled by default. Pass `{enableSSG: true}` to enable.
130
- let getStaticPaths : GetStaticPaths = null ;
131
- let getStaticProps : GetStaticProps = null ;
186
+ let getStaticPaths : GetStaticPaths | null = null ;
187
+ let getStaticProps : GetStaticProps | null = null ;
132
188
133
189
if ( options . enableSSG ) {
134
190
getStaticPaths = async ( ctx ) => {
@@ -138,13 +194,12 @@ export function cmsRoute(options: CMSRouteOptions) {
138
194
if ( ! cmsClient ) {
139
195
cmsClient = new RootCMSClient ( ctx . rootConfig ) ;
140
196
}
141
- // TODO(stevenle): Add support for mode.
142
197
const mode = 'published' ;
143
- const res = await cmsClient . listDocs ( options . collection , { mode} ) ;
144
- const ssgPaths = [ ] ;
145
- res . docs . forEach ( ( doc : { slug : string } ) => {
198
+ const res = await cmsClient . listDocs < CMSDoc > ( options . collection , { mode} ) ;
199
+ const ssgPaths : Array < { params : Record < string , string > } > = [ ] ;
200
+ res . docs . forEach ( ( doc ) => {
146
201
const params : Record < string , string > = { } ;
147
- params [ options . slugParam ] = doc . slug ;
202
+ params [ options . slugParam ! ] = doc . slug ;
148
203
ssgPaths . push ( { params} ) ;
149
204
} ) ;
150
205
return { paths : ssgPaths } ;
@@ -155,10 +210,8 @@ export function cmsRoute(options: CMSRouteOptions) {
155
210
cmsClient = new RootCMSClient ( ctx . rootConfig ) ;
156
211
}
157
212
const slug = getSlug ( ctx . params ) ;
158
- // TODO(stevenle): Add support for mode.
159
213
const mode = 'published' ;
160
- const routeContext : CMSRouteContext = { req : null , slug, mode, cmsClient} ;
161
-
214
+ const routeContext : CMSRouteContext = { slug, mode, cmsClient} ;
162
215
return generateProps ( routeContext , ctx . params . $locale ) ;
163
216
} ;
164
217
}
@@ -169,9 +222,9 @@ export function cmsRoute(options: CMSRouteOptions) {
169
222
getStaticProps : getStaticProps ,
170
223
171
224
// SSR handler.
172
- handle : async ( req , res ) => {
225
+ handle : async ( req : CMSRequest , res : Response ) => {
173
226
if ( ! cmsClient ) {
174
- cmsClient = new RootCMSClient ( req . rootConfig ) ;
227
+ cmsClient = new RootCMSClient ( req . rootConfig ! ) ;
175
228
}
176
229
req . cmsClient = cmsClient ;
177
230
const ctx = req . handlerContext as HandlerContext ;
@@ -180,57 +233,28 @@ export function cmsRoute(options: CMSRouteOptions) {
180
233
res . setHeader ( 'cache-control' , 'private' ) ;
181
234
return ctx . render404 ( ) ;
182
235
}
183
- const primaryDocId = `${ options . collection } /${ slug } ` ;
184
236
const mode = String ( req . query . preview ) === 'true' ? 'draft' : 'published' ;
185
237
const routeContext : CMSRouteContext = { req, slug, mode, cmsClient} ;
186
238
187
- const batchRequest = cmsClient . createBatchRequest ( {
188
- mode ,
189
- translate : true ,
190
- } ) ;
191
- batchRequest . addDoc ( primaryDocId ) ;
192
-
193
- const [ batchRes , data ] = await Promise . all ( [
194
- batchRequest . fetch ( ) ,
195
- fetchData ( routeContext ) ,
196
- ] ) ;
197
- const doc = batchRes . docs [ primaryDocId ] ;
239
+ function getLocale ( doc : CMSDoc ) {
240
+ const docLocales = doc . sys ?. locales || [ 'en' ] ;
241
+ let locale = ctx . route . locale ;
242
+ if ( ctx . route . isDefaultLocale ) {
243
+ locale = ctx . getPreferredLocale ( docLocales ) ;
244
+ if ( docLocales . length > 0 && ! docLocales . includes ( locale ) ) {
245
+ locale = docLocales [ 0 ] ;
246
+ }
247
+ }
248
+ return locale ;
249
+ }
198
250
199
- if ( ! doc ) {
251
+ const resData = await generateProps ( routeContext , getLocale ) ;
252
+ if ( resData . notFound ) {
200
253
res . setHeader ( 'cache-control' , 'private' ) ;
201
254
return ctx . render404 ( ) ;
202
255
}
203
256
204
- const sys = doc . sys ;
205
- const docLocales = sys . locales || [ 'en' ] ;
206
- let locale = ctx . route . locale ;
207
- if ( ctx . route . isDefaultLocale ) {
208
- locale = ctx . getPreferredLocale ( docLocales ) ;
209
- if ( docLocales . length > 0 && ! docLocales . includes ( locale ) ) {
210
- locale = docLocales [ 0 ] ;
211
- }
212
- }
213
- const country =
214
- getFirstQueryParam ( req , 'gl' ) ||
215
- req . get ( 'x-country-code' ) ||
216
- req . get ( 'x-appengine-country' ) ||
217
- null ;
218
-
219
- const i18nFallbacks = req . rootConfig . i18n ?. fallbacks || { } ;
220
- const translationFallbackLocales = i18nFallbacks [ locale ] || [ locale ] ;
221
- const translations = batchRes . getTranslations ( translationFallbackLocales ) ;
222
- let props : any = {
223
- ...data ,
224
- req,
225
- locale,
226
- mode,
227
- slug,
228
- doc,
229
- country,
230
- } ;
231
- if ( options . preRenderHook ) {
232
- props = await options . preRenderHook ( props , routeContext ) ;
233
- }
257
+ const { props, locale, translations} = resData ;
234
258
235
259
if ( props . $redirect ) {
236
260
const redirectCode = props . $redirectCode || 302 ;
0 commit comments