Register
Login
Resources
Docs Blog Datasets Glossary Case Studies Tutorials & Webinars
Product
Data Engine LLMs Platform Enterprise
Pricing Explore
Connect to our Discord channel

page.js 11 KB

You have to be logged in to leave a comment. Sign In
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
  1. const assert = require('assert')
  2. const path = require('path')
  3. const cheerio = require('cheerio')
  4. const patterns = require('./patterns')
  5. const getMapTopicContent = require('./get-map-topic-content')
  6. const getApplicableVersions = require('./get-applicable-versions')
  7. const generateRedirectsForPermalinks = require('./redirects/permalinks')
  8. const getEnglishHeadings = require('./get-english-headings')
  9. const getTocItems = require('./get-toc-items')
  10. const pathUtils = require('./path-utils')
  11. const Permalink = require('./permalink')
  12. const languages = require('./languages')
  13. const renderContent = require('./render-content')
  14. const { renderReact } = require('./react/engine')
  15. const { productMap } = require('./all-products')
  16. const slash = require('slash')
  17. const statsd = require('./statsd')
  18. const readFileContents = require('./read-file-contents')
  19. const getLinkData = require('./get-link-data')
  20. const getDocumentType = require('./get-document-type')
  21. const union = require('lodash/union')
  22. class Page {
  23. static async init (opts) {
  24. opts = await Page.read(opts)
  25. if (!opts) return
  26. return new Page(opts)
  27. }
  28. static async read (opts) {
  29. assert(opts.languageCode, 'languageCode is required')
  30. assert(opts.relativePath, 'relativePath is required')
  31. assert(opts.basePath, 'basePath is required')
  32. const relativePath = slash(opts.relativePath)
  33. const fullPath = slash(path.join(opts.basePath, relativePath))
  34. // Per https://nodejs.org/api/fs.html#fs_fs_exists_path_callback
  35. // its better to read and handle errors than to check access/stats first
  36. try {
  37. const { data, content, errors: frontmatterErrors } = await readFileContents(fullPath, opts.languageCode)
  38. return {
  39. ...opts,
  40. relativePath,
  41. fullPath,
  42. ...data,
  43. markdown: content,
  44. frontmatterErrors
  45. }
  46. } catch (err) {
  47. if (err.code === 'ENOENT') return false
  48. console.error(err)
  49. }
  50. }
  51. constructor (opts) {
  52. Object.assign(this, { ...opts })
  53. if (this.frontmatterErrors.length) {
  54. throw new Error(JSON.stringify(this.frontmatterErrors, null, 2))
  55. }
  56. // Store raw data so we can cache parsed versions
  57. this.rawIntro = this.intro
  58. this.rawTitle = this.title
  59. this.rawShortTitle = this.shortTitle
  60. this.rawProduct = this.product
  61. this.rawPermissions = this.permissions
  62. this.rawLearningTracks = this.learningTracks
  63. this.rawIncludeGuides = this.includeGuides
  64. this.raw_product_video = this.product_video
  65. if (this.introLinks) {
  66. this.introLinks.rawQuickstart = this.introLinks.quickstart
  67. this.introLinks.rawReference = this.introLinks.reference
  68. this.introLinks.rawOverview = this.introLinks.overview
  69. }
  70. // Is this the Homepage or a Product, Category, Topic, or Article?
  71. this.documentType = getDocumentType(this.relativePath)
  72. // Get array of versions that the page is available in for fast lookup
  73. this.applicableVersions = getApplicableVersions(this.versions, this.fullPath)
  74. // a page should only be available in versions that its parent product is available in
  75. const versionsParentProductIsNotAvailableIn = this.applicableVersions
  76. // only the homepage will not have this.parentProduct
  77. .filter(availableVersion => this.parentProduct && !this.parentProduct.versions.includes(availableVersion))
  78. if (versionsParentProductIsNotAvailableIn.length) {
  79. throw new Error(`\`versions\` frontmatter in ${this.fullPath} contains ${versionsParentProductIsNotAvailableIn}, which ${this.parentProduct.id} product is not available in!`)
  80. }
  81. // derive array of Permalink objects
  82. this.permalinks = Permalink.derive(this.languageCode, this.relativePath, this.title, this.versions)
  83. if (this.relativePath.endsWith('index.md')) {
  84. // get an array of linked items in product and category TOCs
  85. this.tocItems = getTocItems(this)
  86. }
  87. // if this is an article and it doesn't have showMiniToc = false, set mini TOC to true
  88. if (!this.relativePath.endsWith('index.md') && !this.mapTopic) {
  89. this.showMiniToc = this.showMiniToc === false
  90. ? this.showMiniToc
  91. : true
  92. }
  93. // Instrument the `_render` method, so externally we call #render
  94. // but it's wrapped in a timer that reports to Datadog
  95. this.render = statsd.asyncTimer(this._render.bind(this), 'page.render')
  96. return this
  97. }
  98. buildRedirects () {
  99. // create backwards-compatible old paths for page permalinks and frontmatter redirects
  100. this.redirects = generateRedirectsForPermalinks(this.permalinks, this.redirect_from)
  101. return this.redirects
  102. }
  103. // Infer the parent product ID from the page's relative file path
  104. get parentProductId () {
  105. // Each page's top-level content directory matches its product ID
  106. const id = this.relativePath.split('/')[0]
  107. // ignore top-level content/index.md
  108. if (id === 'index.md') return null
  109. // make sure the ID is valid
  110. if (process.env.NODE_ENV !== 'test') {
  111. assert(
  112. Object.keys(productMap).includes(id),
  113. `page ${this.fullPath} has an invalid product ID: ${id}`
  114. )
  115. }
  116. return id
  117. }
  118. get parentProduct () {
  119. return productMap[this.parentProductId]
  120. }
  121. async renderTitle (context, opts = {}) {
  122. return this.shortTitle
  123. ? this.renderProp('shortTitle', context, opts)
  124. : this.renderProp('title', context, opts)
  125. }
  126. async _render (context) {
  127. // use English IDs/anchors for translated headings, so links don't break (see #8572)
  128. if (this.languageCode !== 'en') {
  129. const englishHeadings = getEnglishHeadings(this, context)
  130. context.englishHeadings = englishHeadings
  131. }
  132. this.intro = await renderContent(this.rawIntro, context)
  133. this.introPlainText = await renderContent(this.rawIntro, context, { textOnly: true })
  134. this.title = await renderContent(this.rawTitle, context, { textOnly: true, encodeEntities: true })
  135. this.shortTitle = await renderContent(this.shortTitle, context, { textOnly: true, encodeEntities: true })
  136. this.product_video = await renderContent(this.raw_product_video, context, { textOnly: true })
  137. if (this.introLinks) {
  138. this.introLinks.quickstart = await renderContent(this.introLinks.rawQuickstart, context, { textOnly: true })
  139. this.introLinks.reference = await renderContent(this.introLinks.rawReference, context, { textOnly: true })
  140. this.introLinks.overview = await renderContent(this.introLinks.rawOverview, context, { textOnly: true })
  141. }
  142. let markdown = this.mapTopic
  143. // get the map topic child articles from the siteTree
  144. ? getMapTopicContent(this.parentProduct.id, context.siteTree, context.currentLanguage, context.currentVersion, context.currentPath)
  145. : this.markdown
  146. // If the article is interactive parse the React!
  147. if (this.interactive) {
  148. // Search for the react code comments to find the react components
  149. const reactComponents = markdown.match(/<!--react-->(.*?)<!--end-react-->/gs)
  150. // Render each of the react components in the markdown
  151. await Promise.all(reactComponents.map(async (reactComponent) => {
  152. let componentStr = reactComponent
  153. // Remove the React comment indicators
  154. componentStr = componentStr.replace('<!--react-->\n', '').replace('<!--react-->', '')
  155. componentStr = componentStr.replace('\n<!--end-react-->', '').replace('<!--end-react-->', '')
  156. // Get the rendered component
  157. const renderedComponent = await renderReact(componentStr)
  158. // Replace the react component with the rendered markdown
  159. markdown = markdown.replace(reactComponent, renderedComponent)
  160. }))
  161. }
  162. context.relativePath = this.relativePath
  163. const html = await renderContent(markdown, context)
  164. // product frontmatter may contain liquid
  165. if (this.product) {
  166. this.product = await renderContent(this.rawProduct, context)
  167. }
  168. // permissions frontmatter may contain liquid
  169. if (this.permissions) {
  170. this.permissions = await renderContent(this.rawPermissions, context)
  171. }
  172. if (this.learningTracks) {
  173. const learningTracks = []
  174. for await (const rawTrackName of this.rawLearningTracks) {
  175. // Track names in frontmatter may include Liquid conditionals
  176. const renderedTrackName = await renderContent(rawTrackName, context, { textOnly: true, encodeEntities: true })
  177. if (!renderedTrackName) continue
  178. const track = context.site.data['learning-tracks'][context.currentProduct][renderedTrackName]
  179. if (!track) continue
  180. learningTracks.push({
  181. trackName: renderedTrackName,
  182. title: await renderContent(track.title, context, { textOnly: true, encodeEntities: true }),
  183. description: await renderContent(track.description, context, { textOnly: true, encodeEntities: true }),
  184. guides: await getLinkData(track.guides, context)
  185. })
  186. }
  187. this.learningTracks = learningTracks
  188. }
  189. if (this.rawIncludeGuides) {
  190. this.allTopics = []
  191. this.includeGuides = await getLinkData(this.rawIncludeGuides, context)
  192. this.includeGuides.map((guide) => {
  193. const { page } = guide
  194. guide.type = page.type
  195. if (page.topics) {
  196. this.allTopics = union(this.allTopics, page.topics)
  197. guide.topics = page.topics
  198. }
  199. delete guide.page
  200. return guide
  201. })
  202. }
  203. // set a flag so layout knows whether to render a mac/windows/linux switcher element
  204. this.includesPlatformSpecificContent = (
  205. html.includes('extended-markdown mac') ||
  206. html.includes('extended-markdown windows') ||
  207. html.includes('extended-markdown linux')
  208. )
  209. return html
  210. }
  211. // Allow other modules (like custom liquid tags) to make one-off requests
  212. // for a page's rendered properties like `title` and `intro`
  213. async renderProp (propName, context, opts = { unwrap: false }) {
  214. let prop
  215. if (propName === 'title') {
  216. prop = this.rawTitle
  217. } else if (propName === 'shortTitle') {
  218. prop = this.rawShortTitle || this.rawTitle // fall back to title
  219. } else if (propName === 'intro') {
  220. prop = this.rawIntro
  221. } else {
  222. prop = this[propName]
  223. }
  224. const html = await renderContent(prop, context, opts)
  225. if (!opts.unwrap) return html
  226. // The unwrap option removes surrounding tags from a string, preserving any inner HTML
  227. const $ = cheerio.load(html, { xmlMode: true })
  228. return $.root().contents().html()
  229. }
  230. // infer current page's corresponding homepage
  231. // /en/articles/foo -> /en
  232. // /en/enterprise/2.14/user/articles/foo -> /en/enterprise/2.14/user
  233. static getHomepage (requestPath) {
  234. return requestPath.replace(/\/articles.*/, '')
  235. }
  236. // given a page path, return an array of objects containing hrefs
  237. // for that page in all languages
  238. static getLanguageVariants (href) {
  239. const suffix = pathUtils.getPathWithoutLanguage(href)
  240. return Object.values(languages).map(({ name, code, hreflang }) => { // eslint-disable-line
  241. return {
  242. name,
  243. code,
  244. hreflang,
  245. href: `/${code}${suffix}`.replace(patterns.trailingSlash, '$1')
  246. }
  247. })
  248. }
  249. }
  250. module.exports = Page
Tip!

Press p or to see the previous file or, n or to see the next file

Comments

Loading...