@salesforcedevs/docs-components 0.0.0 → 0.0.1-superscript

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.
Files changed (83) hide show
  1. package/lwc.config.json +12 -2
  2. package/package.json +29 -27
  3. package/src/modules/doc/{amfReference/utils.ts → amfModelParser/amfModelParser.ts} +10 -5
  4. package/src/modules/doc/amfReference/amfReference.css +11 -3
  5. package/src/modules/doc/amfReference/amfReference.html +35 -26
  6. package/src/modules/doc/amfReference/amfReference.ts +239 -95
  7. package/src/modules/doc/amfReference/types.ts +3 -11
  8. package/src/modules/doc/amfTopic/amfTopic.css +20 -0
  9. package/src/modules/doc/amfTopic/amfTopic.ts +59 -18
  10. package/src/modules/doc/amfTopic/types.ts +15 -13
  11. package/src/modules/doc/amfTopic/utils.ts +12 -6
  12. package/src/modules/doc/breadcrumbItem/breadcrumbItem.ts +17 -10
  13. package/src/modules/doc/breadcrumbs/breadcrumbs.html +6 -9
  14. package/src/modules/doc/breadcrumbs/breadcrumbs.ts +30 -34
  15. package/src/modules/doc/componentPlayground/componentPlayground.css +30 -0
  16. package/src/modules/doc/componentPlayground/componentPlayground.html +20 -0
  17. package/src/modules/doc/componentPlayground/componentPlayground.ts +97 -0
  18. package/src/modules/doc/content/content.css +70 -76
  19. package/src/modules/doc/content/content.html +1 -0
  20. package/src/modules/doc/content/content.ts +24 -46
  21. package/src/modules/doc/contentCallout/contentCallout.css +11 -7
  22. package/src/modules/doc/contentCallout/contentCallout.html +11 -4
  23. package/src/modules/doc/contentCallout/contentCallout.ts +8 -1
  24. package/src/modules/doc/contentLayout/contentLayout.css +1 -98
  25. package/src/modules/doc/contentLayout/contentLayout.html +46 -31
  26. package/src/modules/doc/contentLayout/contentLayout.ts +337 -89
  27. package/src/modules/doc/contentMedia/contentMedia.css +1 -1
  28. package/src/modules/doc/doDont/doDont.css +47 -0
  29. package/src/modules/doc/doDont/doDont.html +27 -0
  30. package/src/modules/doc/doDont/doDont.ts +17 -0
  31. package/src/modules/doc/header/header.css +65 -36
  32. package/src/modules/doc/header/header.html +44 -138
  33. package/src/modules/doc/header/header.ts +62 -74
  34. package/src/modules/doc/heading/heading.css +16 -37
  35. package/src/modules/doc/heading/heading.html +4 -4
  36. package/src/modules/doc/heading/heading.ts +12 -10
  37. package/src/modules/doc/headingAnchor/headingAnchor.css +2 -2
  38. package/src/modules/doc/headingAnchor/headingAnchor.ts +2 -2
  39. package/src/modules/doc/headingContent/headingContent.css +1 -1
  40. package/src/modules/doc/headingContent/headingContent.html +2 -2
  41. package/src/modules/doc/headingContent/headingContent.ts +2 -2
  42. package/src/modules/doc/lwcContentLayout/lwcContentLayout.css +9 -0
  43. package/src/modules/doc/lwcContentLayout/lwcContentLayout.html +64 -0
  44. package/src/modules/doc/lwcContentLayout/lwcContentLayout.ts +269 -0
  45. package/src/modules/doc/overview/overview.css +40 -0
  46. package/src/modules/doc/overview/overview.html +34 -0
  47. package/src/modules/doc/overview/overview.ts +12 -0
  48. package/src/modules/doc/phase/phase.css +11 -3
  49. package/src/modules/doc/phase/phase.html +12 -2
  50. package/src/modules/doc/phase/phase.ts +44 -8
  51. package/src/modules/doc/redocReference/redocReference.css +7 -0
  52. package/src/modules/doc/redocReference/redocReference.html +13 -0
  53. package/src/modules/doc/redocReference/redocReference.ts +425 -0
  54. package/src/modules/doc/specificationContent/specificationContent.css +36 -0
  55. package/src/modules/doc/specificationContent/specificationContent.html +177 -0
  56. package/src/modules/doc/specificationContent/specificationContent.ts +166 -0
  57. package/src/modules/doc/sprigSurvey/sprigSurvey.html +20 -0
  58. package/src/modules/doc/sprigSurvey/sprigSurvey.scoped.css +16 -0
  59. package/src/modules/doc/sprigSurvey/sprigSurvey.ts +16 -0
  60. package/src/modules/doc/superscriptSubscript/superscriptSubscript.html +8 -0
  61. package/src/modules/doc/superscriptSubscript/superscriptSubscript.ts +16 -0
  62. package/src/modules/doc/toc/toc.html +1 -3
  63. package/src/modules/doc/toc/toc.ts +1 -1
  64. package/src/modules/doc/toolbar/toolbar.ts +6 -6
  65. package/src/modules/doc/versionPicker/versionPicker.css +64 -0
  66. package/src/modules/doc/versionPicker/versionPicker.html +40 -0
  67. package/src/modules/doc/versionPicker/versionPicker.ts +65 -0
  68. package/src/modules/doc/xmlContent/types.ts +9 -3
  69. package/src/modules/doc/xmlContent/utils.ts +3 -1
  70. package/src/modules/doc/xmlContent/xmlContent.css +16 -4
  71. package/src/modules/doc/xmlContent/xmlContent.html +36 -21
  72. package/src/modules/doc/xmlContent/xmlContent.ts +295 -120
  73. package/src/modules/docHelpers/amfStyle/amfStyle.css +6 -8
  74. package/src/modules/docHelpers/contentLayoutStyle/contentLayoutStyle.css +162 -0
  75. package/src/modules/docHelpers/imgStyle/imgStyle.css +59 -0
  76. package/src/modules/docHelpers/status/status.css +1 -1
  77. package/src/modules/docUtils/{SearchSyncer/SearchSyncer.ts → searchSyncer/searchSyncer.ts} +1 -0
  78. package/src/modules/docUtils/utils/__mocks__/coveo.analytics.ts +16 -0
  79. package/src/modules/docUtils/utils/coveo.analytics.d.ts +10 -0
  80. package/src/modules/docUtils/utils/utils.ts +32 -0
  81. package/LICENSE +0 -12
  82. package/src/modules/docBaseElements/lightningElementWithState/lightningElementWithState.ts +0 -93
  83. package/src/modules/docHelpers/phaseContentLayout/phaseContentLayout.css +0 -39
@@ -1,36 +1,47 @@
1
+ /* eslint-disable @lwc/lwc/no-document-query */
1
2
  import { api, track } from "lwc";
2
- import { normalizeBoolean } from "dxUtils/normalizers";
3
3
  import { FetchContent } from "./utils";
4
4
  import {
5
- CoveoAdvancedQueryXMLConfig,
6
5
  DocLanguage,
7
6
  DocVersion,
8
7
  TreeNode,
9
8
  Header,
9
+ SiderbarFooter,
10
10
  HistoryState,
11
11
  PageReference,
12
- TocMap
12
+ TocMap,
13
+ ContentData
13
14
  } from "./types";
14
- import { SearchSyncer } from "docUtils/SearchSyncer";
15
- import { LightningElementWithState } from "docBaseElements/lightningElementWithState";
16
- import { Breadcrumb, Language } from "typings/custom";
15
+ import { SearchSyncer } from "docUtils/searchSyncer";
16
+ import { LightningElementWithState } from "dxBaseElements/lightningElementWithState";
17
+ import { oldVersionDocInfo } from "docUtils/utils";
18
+ import { Breadcrumb, DocPhaseInfo, Language } from "typings/custom";
19
+ import { track as trackGTM } from "dxUtils/analytics";
20
+ import DOMPurify from "dompurify";
17
21
 
18
22
  // TODO: Imitating from actual implementation as doc-content use it like this. We should refactor it later.
19
- const handleContentError = (error): void => console.log(error);
23
+ const handleContentError = (error: any): void => console.log(error);
20
24
 
21
25
  const PIXEL_PER_CHARACTER_MAP: { [key: string]: number } = {
22
26
  default: 7.7,
23
27
  "ja-jp": 12.5
24
28
  };
25
29
 
30
+ const defaultSidebarFooter: SiderbarFooter = {
31
+ bailHref: "",
32
+ bailLabel: "",
33
+ languages: [],
34
+ language: ""
35
+ };
26
36
  export default class DocXmlContent extends LightningElementWithState<{
27
37
  isFetchingDocument: boolean;
28
38
  isFetchingContent: boolean;
29
39
  lastHighlightedSearch: string;
40
+ internalLinkClicked: boolean;
30
41
  }> {
31
42
  @api apiDomain = "https://developer.salesforce.com";
32
- @api coveoOrganizationId!: string;
33
- @api coveoPublicAccessToken!: string;
43
+ @api hideFooter = false;
44
+ @api displayReadingTime = false;
34
45
 
35
46
  @api
36
47
  get allLanguages(): Array<Language> {
@@ -43,31 +54,32 @@ export default class DocXmlContent extends LightningElementWithState<{
43
54
  }
44
55
  }
45
56
 
46
- @api
47
- get enableCoveo() {
48
- return this._enableCoveo;
49
- }
50
-
51
- set enableCoveo(value) {
52
- this._enableCoveo = normalizeBoolean(value);
53
- }
54
-
55
57
  private availableLanguages: Array<DocLanguage> = [];
56
- private availableVersions: Array<DocVersion> = [];
57
- private contentProvider: FetchContent;
58
+ @track private availableVersions: Array<DocVersion> = [];
59
+ private contentProvider?: FetchContent;
58
60
  private docContent = "";
59
- private language: DocLanguage = null;
60
- private loaded = false;
61
+ private language?: DocLanguage | null = null;
62
+ private displayContent = false;
63
+ private display404 = false;
64
+ private _pageHeader?: Header;
61
65
  private pdfUrl = "";
62
66
  private tocMap: TocMap = {};
63
- private sidebarContent: Array<TreeNode> = null;
64
- private version: DocVersion = null;
67
+ private sidebarContent: Array<TreeNode> | null = null;
68
+ private version: DocVersion | null = null;
65
69
  private docTitle = "";
66
- private analyticsEvent = "custEv_ctaLinkClick";
67
70
  private _pathName = "";
68
- private _pageHeader?: Header;
69
71
  private listenerAttached = false;
70
- private _enableCoveo?: boolean = false;
72
+ private sidebarFooterContent: SiderbarFooter = { ...defaultSidebarFooter };
73
+ private latestVersion = false;
74
+ private previewVersion = false;
75
+
76
+ private get enableFooter(): boolean {
77
+ return !this.hideFooter;
78
+ }
79
+
80
+ private get computedReadingTime(): number | undefined {
81
+ return this.displayReadingTime ? this.readingTime : undefined;
82
+ }
71
83
 
72
84
  private searchSyncer = new SearchSyncer({
73
85
  callbacks: {
@@ -107,8 +119,40 @@ export default class DocXmlContent extends LightningElementWithState<{
107
119
  });
108
120
  private _allLanguages: Array<Language> = [];
109
121
 
122
+ private get oldVersionInfo(): DocPhaseInfo | null {
123
+ let info = null;
124
+ if (!this.disableVersion) {
125
+ const currentGAVersion = this.versionOptions.find(
126
+ (version) => !version.url.includes(version.id)
127
+ );
128
+ if (currentGAVersion?.link?.href && this.version?.id) {
129
+ const versionNo = currentGAVersion.id;
130
+ /**
131
+ * Need to show old version doc banner only if the version is less than the current ga version
132
+ * We should not show it to the preview version whose version is more than ga
133
+ **/
134
+ try {
135
+ if (parseFloat(this.version.id) < parseFloat(versionNo)) {
136
+ info = oldVersionDocInfo(currentGAVersion.link.href);
137
+ } else if (
138
+ parseFloat(this.version.id) > parseFloat(versionNo)
139
+ ) {
140
+ this.previewVersion = true;
141
+ }
142
+ } catch (exception) {
143
+ /* Ideally this use case should not happen, but just added to not to break the page*/
144
+ console.warn(exception);
145
+ }
146
+ }
147
+ }
148
+ return info;
149
+ }
150
+
151
+ @track showVersionBanner = false;
152
+
110
153
  @track private pageReference: PageReference = {};
111
154
  @track breadcrumbs: Array<Breadcrumb> = [];
155
+ @track readingTime?: number;
112
156
 
113
157
  constructor() {
114
158
  super();
@@ -133,7 +177,13 @@ export default class DocXmlContent extends LightningElementWithState<{
133
177
  this.apiDomain,
134
178
  this.allLanguages
135
179
  );
136
- this.fetchDocument().then(() => (this.loaded = true));
180
+ this.fetchDocument().then((content: any) => {
181
+ if (content) {
182
+ this.displayContent = true;
183
+ } else {
184
+ this.display404 = true;
185
+ }
186
+ });
137
187
  window.addEventListener("popstate", this.handlePopState);
138
188
 
139
189
  this.searchSyncer.init();
@@ -142,8 +192,8 @@ export default class DocXmlContent extends LightningElementWithState<{
142
192
  renderedCallback(): void {
143
193
  this.setState({ internalLinkClicked: true });
144
194
  const urlSectionLink =
145
- this.pageReference?.hash?.split("#").length > 1
146
- ? this.pageReference.hash.split("#")[1]
195
+ this.pageReference?.hash?.split("#").length! > 1
196
+ ? this.pageReference.hash!.split("#")[1]
147
197
  : this.pageReference?.hash;
148
198
 
149
199
  const contentEl = this.template.querySelector("doc-content");
@@ -156,6 +206,7 @@ export default class DocXmlContent extends LightningElementWithState<{
156
206
 
157
207
  if (anchorEl) {
158
208
  anchorEl.scrollIntoView();
209
+
159
210
  this.setState({ internalLinkClicked: false });
160
211
  }
161
212
 
@@ -181,24 +232,17 @@ export default class DocXmlContent extends LightningElementWithState<{
181
232
  disconnectedCallback(): void {
182
233
  window.removeEventListener("popstate", this.handlePopState);
183
234
  this.searchSyncer.dispose();
184
- if (this.listenerAttached) {
185
- this.pageHeader.removeEventListener(
186
- "langchange",
187
- this.handleLanguageChange
188
- );
189
- this.listenerAttached = false;
190
- }
191
235
  }
192
236
 
193
- private get languageId(): string {
194
- return this.language.id.replace("-", "_");
237
+ private get languageId(): string | undefined {
238
+ return this.language?.id.replace("-", "_");
195
239
  }
196
240
 
197
- private get releaseVersionId(): string {
198
- return this.version.id;
241
+ private get releaseVersionId(): string | undefined {
242
+ return this.version?.id;
199
243
  }
200
244
 
201
- private get deliverable(): string {
245
+ private get deliverable(): string | undefined {
202
246
  return this.pageReference.deliverable;
203
247
  }
204
248
 
@@ -206,34 +250,16 @@ export default class DocXmlContent extends LightningElementWithState<{
206
250
  // Coveo is enabled and the version is greater than 51 (within the latest 3 versions)
207
251
  // TODO: we need a better fix for version number check
208
252
  return !(
209
- this.enableCoveo &&
210
- this.coveoOrganizationId &&
211
- this.coveoPublicAccessToken &&
212
- (!this.version?.releaseVersion ||
213
- (this.version?.releaseVersion &&
214
- parseInt(
215
- this.version.releaseVersion.replace("v", ""),
216
- 10
217
- ) >= 53))
253
+ !this.version?.releaseVersion ||
254
+ (this.version?.releaseVersion &&
255
+ parseInt(this.version.releaseVersion.replace("v", ""), 10) >=
256
+ 53)
218
257
  );
219
258
  }
220
259
 
221
- private get coveoAdvancedQueryConfig(): CoveoAdvancedQueryXMLConfig {
222
- const config: { locale: string; topicid: string; version?: string } = {
223
- locale: this.languageId,
224
- topicid: this.deliverable
225
- };
226
-
227
- if (this.releaseVersionId && this.releaseVersionId !== "noversion") {
228
- config.version = this.releaseVersionId;
229
- }
230
-
231
- return config;
232
- }
233
-
234
260
  private get pageHeader(): Header {
235
261
  if (!this._pageHeader) {
236
- this._pageHeader = document.querySelector("doc-header");
262
+ this._pageHeader = document.querySelector("doc-header")!;
237
263
  }
238
264
 
239
265
  return this._pageHeader;
@@ -276,13 +302,24 @@ export default class DocXmlContent extends LightningElementWithState<{
276
302
 
277
303
  private get breadcrumbPixelPerCharacter() {
278
304
  return (
279
- PIXEL_PER_CHARACTER_MAP[this.language.id] ||
305
+ PIXEL_PER_CHARACTER_MAP[this.language!.id] ||
280
306
  PIXEL_PER_CHARACTER_MAP.default
281
307
  );
282
308
  }
283
309
 
284
- private handlePopState = (): void =>
285
- this.updatePageReference(this.getReferenceFromUrl());
310
+ private get ANALYTICS_PAYLOAD() {
311
+ return {
312
+ element_title: "version picker",
313
+ content_category: "cta"
314
+ };
315
+ }
316
+
317
+ private handlePopState = (event: PopStateEvent): void =>
318
+ this.updatePageReference(this.getReferenceFromUrl(), event);
319
+
320
+ handleDismissVersionBanner() {
321
+ this.showVersionBanner = false;
322
+ }
286
323
 
287
324
  handleSelect(event: CustomEvent<{ name: string }>): void {
288
325
  event.stopPropagation();
@@ -314,7 +351,7 @@ export default class DocXmlContent extends LightningElementWithState<{
314
351
  this.updateUrl();
315
352
  }
316
353
 
317
- handleLanguageChange = (event: CustomEvent<string>): Promise<void> => {
354
+ handleLanguageChange = (event: any) => {
318
355
  if (this.language && this.language.id === event.detail) {
319
356
  return;
320
357
  }
@@ -322,12 +359,26 @@ export default class DocXmlContent extends LightningElementWithState<{
322
359
  this.language = this.availableLanguages.find(
323
360
  ({ id }) => id === event.detail
324
361
  );
325
- this.pageReference.docId = this.language.url;
362
+ this.pageReference.docId = this.language!.url;
363
+
364
+ trackGTM(event.target!, "custEv_linkClick", {
365
+ click_text: event.detail,
366
+ element_title: "language selector",
367
+ click_url: `${window.location.origin}${this.pageReferenceToString(
368
+ this.pageReference
369
+ )}`,
370
+ element_type: "link",
371
+ content_category: "cta"
372
+ });
373
+
326
374
  this.updateUrl();
327
375
  this.fetchDocument();
328
376
  };
329
377
 
330
- updatePageReference(newPageReference: PageReference): void {
378
+ updatePageReference(
379
+ newPageReference: PageReference,
380
+ event: PopStateEvent | undefined = undefined
381
+ ): void {
331
382
  this.pageReference.hash = newPageReference.hash;
332
383
  this.pageReference.search = newPageReference.search;
333
384
 
@@ -344,13 +395,26 @@ export default class DocXmlContent extends LightningElementWithState<{
344
395
  }
345
396
 
346
397
  this.fetchContent()
347
- .then(() => this.buildBreadcrumbs())
398
+ .then(() => {
399
+ this.buildBreadcrumbs();
400
+ document.body.scrollTop = event?.state?.scroll?.value || 0;
401
+ })
348
402
  .catch(handleContentError);
349
403
  }
350
404
 
405
+ private sanitizeUrlPart(part: string | undefined): string | undefined {
406
+ if (!part) {
407
+ return part;
408
+ }
409
+ return DOMPurify.sanitize(part);
410
+ }
411
+
351
412
  getReferenceFromUrl(): PageReference {
352
413
  const [page, docId, deliverable, contentDocumentId] =
353
- window.location.pathname.substr(1).split("/");
414
+ window.location.pathname
415
+ .substr(1)
416
+ .split("/")
417
+ .map(this.sanitizeUrlPart);
354
418
 
355
419
  const { origin: domain, hash, search } = window.location;
356
420
 
@@ -359,9 +423,9 @@ export default class DocXmlContent extends LightningElementWithState<{
359
423
  deliverable,
360
424
  docId,
361
425
  domain,
362
- hash,
426
+ hash: this.sanitizeUrlPart(hash),
363
427
  page,
364
- search
428
+ search: this.sanitizeUrlPart(search)
365
429
  };
366
430
  }
367
431
 
@@ -375,12 +439,13 @@ export default class DocXmlContent extends LightningElementWithState<{
375
439
  );
376
440
  }
377
441
 
378
- async fetchDocument(): Promise<void> {
442
+ async fetchDocument(): Promise<string> {
379
443
  this.setState({
380
444
  isFetchingDocument: true
381
445
  });
382
- const data = await this.contentProvider.fetchDocumentData(
383
- this.pageReference.docId
446
+
447
+ const data = await this.contentProvider!.fetchDocumentData(
448
+ this.pageReference.docId!
384
449
  );
385
450
 
386
451
  // This could be a 404 scenario.
@@ -388,7 +453,7 @@ export default class DocXmlContent extends LightningElementWithState<{
388
453
  this.setState({
389
454
  isFetchingDocument: false
390
455
  });
391
- return;
456
+ return "";
392
457
  }
393
458
 
394
459
  this.docTitle = data.docTitle;
@@ -400,7 +465,7 @@ export default class DocXmlContent extends LightningElementWithState<{
400
465
  this.availableVersions = data.availableVersions;
401
466
  this.pdfUrl = data.pdfUrl;
402
467
 
403
- this.updateHeader();
468
+ this.updateHeaderAndSidebarFooter();
404
469
 
405
470
  this.buildBreadcrumbs();
406
471
 
@@ -409,16 +474,22 @@ export default class DocXmlContent extends LightningElementWithState<{
409
474
  this.updateUrl(HistoryState.REPLACE_STATE);
410
475
  }
411
476
 
477
+ if (this.oldVersionInfo) {
478
+ this.showVersionBanner = true;
479
+ } else {
480
+ this.latestVersion = true;
481
+ }
482
+
412
483
  if (
413
484
  this.pageReference?.contentDocumentId?.replace(/\.htm$/, "") !==
414
485
  data.id
415
486
  ) {
416
487
  try {
417
- await this.fetchContent();
488
+ const moreData = await this.fetchContent();
418
489
  this.setState({
419
490
  isFetchingDocument: false
420
491
  });
421
- return;
492
+ return moreData.content;
422
493
  } catch (error) {
423
494
  this.pageReference.contentDocumentId = `${data.id}.htm`;
424
495
  this.pageReference.hash = "";
@@ -428,43 +499,69 @@ export default class DocXmlContent extends LightningElementWithState<{
428
499
  }
429
500
 
430
501
  this.docContent = data.content;
502
+ this.calculateReadingTime(data.content);
431
503
  this.addMetatags();
432
504
  this.setState({
433
505
  isFetchingDocument: false
434
506
  });
507
+
508
+ // Track page view when a new document is loaded
509
+ trackGTM(this.template.host, "doc_page_view", {
510
+ page_path: this.pageReferenceToString(this.pageReference),
511
+ doc_id: this.pageReference.docId,
512
+ content_document_id: this.pageReference.contentDocumentId,
513
+ deliverable: this.pageReference.deliverable,
514
+ language: this.language?.id,
515
+ version: this.version?.id,
516
+ article_name: this.getArticleName()
517
+ });
518
+
519
+ return data.content;
435
520
  }
436
521
 
437
- async fetchContent(): Promise<void> {
522
+ async fetchContent(): Promise<ContentData> {
438
523
  this.setState({
439
524
  isFetchingContent: true
440
525
  });
441
- const data = await this.contentProvider.fetchContent(
442
- this.pageReference.deliverable,
443
- this.pageReference.contentDocumentId,
526
+ const data = await this.contentProvider!.fetchContent(
527
+ this.pageReference.deliverable!,
528
+ this.pageReference.contentDocumentId!,
444
529
  {
445
- language: this.language.id,
446
- version: this.version.id
530
+ language: this.language!.id,
531
+ version: this.version!.id
447
532
  }
448
533
  );
449
534
 
450
535
  if (data) {
451
536
  this.docContent = data.content;
537
+ this.calculateReadingTime(data.content);
452
538
  this.addMetatags();
453
539
 
454
540
  if (!this.pageReference.hash) {
455
- document.querySelector("main")?.scrollIntoView({
456
- behavior: "smooth",
457
- block: "start",
458
- inline: "nearest"
459
- });
541
+ document.body.scrollIntoView();
460
542
  }
543
+
544
+ // Track page view when new content is loaded (navigation within same document)
545
+ // Note: breadcrumbs will be built after this in updatePageReference,
546
+ // but we need to build them here to get article_name
547
+ this.buildBreadcrumbs();
548
+ trackGTM(this.template.host, "doc_page_view", {
549
+ page_path: this.pageReferenceToString(this.pageReference),
550
+ doc_id: this.pageReference.docId,
551
+ content_document_id: this.pageReference.contentDocumentId,
552
+ deliverable: this.pageReference.deliverable,
553
+ language: this.language?.id,
554
+ version: this.version?.id,
555
+ article_name: this.getArticleName()
556
+ });
461
557
  }
462
558
  this.setState({
463
559
  isFetchingContent: false
464
560
  });
561
+ return data;
465
562
  }
466
563
 
467
- updateHeader(): void {
564
+ updateHeaderAndSidebarFooter(): void {
468
565
  if (!this.pageHeader) {
469
566
  return;
470
567
  }
@@ -474,20 +571,12 @@ export default class DocXmlContent extends LightningElementWithState<{
474
571
  }
475
572
 
476
573
  if (this.pdfUrl) {
477
- this.pageHeader.bailHref = this.pdfUrl;
478
- this.pageHeader.bailLabel = "PDF";
574
+ this.sidebarFooterContent.bailHref = this.pdfUrl;
575
+ this.sidebarFooterContent.bailLabel = "PDF";
479
576
  }
480
577
 
481
- if (!this.listenerAttached) {
482
- this.pageHeader.addEventListener(
483
- "langchange",
484
- this.handleLanguageChange
485
- );
486
- this.listenerAttached = true;
487
- }
488
-
489
- this.pageHeader.languages = this.availableLanguages;
490
- this.pageHeader.language = this.language?.id;
578
+ this.sidebarFooterContent.languages = this.availableLanguages;
579
+ this.sidebarFooterContent.language = this.language?.id;
491
580
 
492
581
  if (this.pageReference) {
493
582
  const { docId, deliverable, page } = this.pageReference;
@@ -511,23 +600,24 @@ export default class DocXmlContent extends LightningElementWithState<{
511
600
  }
512
601
 
513
602
  private updateSearchInput(searchParam: string): void {
514
- this.template
515
- .querySelector("doc-content-layout")
516
- ?.setSidebarInputValue(searchParam);
603
+ (this.refs.docContentLayout as any)?.setSidebarInputValue(searchParam);
517
604
  }
518
605
 
519
606
  private pageReferenceToString(reference: PageReference): string {
520
607
  const { page, docId, deliverable, contentDocumentId, hash, search } =
521
608
  reference;
522
609
  return `/${page}/${docId}/${deliverable}/${contentDocumentId}${this.normalizeSearch(
523
- search
610
+ search!
524
611
  )}${this.normalizeHash(hash)}`;
525
612
  }
526
613
 
527
- private normalizeUrlPart(part: string, sentinel: string): string {
614
+ private normalizeUrlPart(
615
+ part: string | undefined,
616
+ sentinel: string
617
+ ): string {
528
618
  return (
529
619
  (part &&
530
- (part.startsWith(sentinel) ? part : `${sentinel}${part}`)) ||
620
+ (part.startsWith(sentinel!) ? part : `${sentinel}${part}`)) ||
531
621
  ""
532
622
  );
533
623
  }
@@ -536,16 +626,16 @@ export default class DocXmlContent extends LightningElementWithState<{
536
626
  return this.normalizeUrlPart(search, "?");
537
627
  }
538
628
 
539
- private normalizeHash(hash: string): string {
629
+ private normalizeHash(hash?: string): string {
540
630
  return this.normalizeUrlPart(hash, "#");
541
631
  }
542
632
 
543
633
  private getComposedTitle(
544
- topicTitle: string | undefined,
634
+ topicTitle: string | null | undefined,
545
635
  docTitle: string | undefined
546
636
  ): string {
547
637
  // map to avoid duplicates
548
- const titleMap = {};
638
+ const titleMap: { [key: string]: any } = {};
549
639
  if (topicTitle) {
550
640
  // sometimes the h1 tag text (which is docSubTitle) contains text with new line character. For e.g, "Bulk API 2.0 Older\n Documentation",
551
641
  // here it contains \n in the text context which needs to be removed
@@ -580,8 +670,13 @@ export default class DocXmlContent extends LightningElementWithState<{
580
670
  return this.breadcrumbs && this.breadcrumbs.length > 1;
581
671
  }
582
672
 
583
- get docContentStyle(): string {
584
- return this.showBreadcrumbs ? "" : "margin-top: 48px";
673
+ private getArticleName(): string | undefined {
674
+ if (!this.breadcrumbs || this.breadcrumbs.length === 0) {
675
+ return undefined;
676
+ }
677
+ // Join all breadcrumb labels with "/" to create article name
678
+ // e.g., "Tooling API Objects/FieldDefinition"
679
+ return this.breadcrumbs.map((crumb) => crumb.label).join("/");
585
680
  }
586
681
 
587
682
  private buildBreadcrumbs(): void {
@@ -615,16 +710,30 @@ export default class DocXmlContent extends LightningElementWithState<{
615
710
  return [item];
616
711
  }
617
712
 
713
+ // This method take docId and drops the version from the docId.
714
+ // Example:
715
+ // Takes input string: docId = "atlas.en-us.238.0.b2b_b2c_comm_dev.meta"
716
+ // Output string: filteredDocId = "atlas.en-us.b2b_b2c_comm_dev.meta"
717
+ dropVersionFromDocId(docId: string): string {
718
+ if (!this.version?.id) {
719
+ return docId;
720
+ }
721
+
722
+ const curVersion = this.version.id + ".";
723
+ const filteredDocId = docId.replace(curVersion, "");
724
+ return filteredDocId;
725
+ }
726
+
618
727
  addMetatags(): void {
619
728
  const div = document.createElement("div");
620
- div.innerHTML = this.docContent;
729
+ div.innerHTML = DOMPurify.sanitize(this.docContent);
621
730
  const docDescription = div.querySelector(".shortdesc")?.textContent;
622
731
  const topicTitle = div.querySelector("h1")?.textContent;
623
732
 
624
733
  const title = document.querySelector("title");
625
734
  const composedTitle = this.getComposedTitle(topicTitle, this.docTitle);
626
735
 
627
- if (title) {
736
+ if (title && title.textContent) {
628
737
  title.textContent = composedTitle;
629
738
  }
630
739
  const metatitle = document.querySelector('meta[name="title"]');
@@ -646,14 +755,80 @@ export default class DocXmlContent extends LightningElementWithState<{
646
755
  'link[rel="canonical"]'
647
756
  );
648
757
  if (metadescription) {
758
+ const copyPageReference = { ...this.pageReference };
759
+ copyPageReference.docId = copyPageReference.docId
760
+ ? this.dropVersionFromDocId(copyPageReference.docId)
761
+ : copyPageReference.docId;
649
762
  metadescription.setAttribute(
650
763
  "href",
651
764
  window.location.protocol +
652
765
  "//" +
653
766
  window.location.host +
654
- this.pageReferenceToString(this.pageReference)
767
+ this.pageReferenceToString(copyPageReference)
655
768
  );
656
769
  }
657
770
  }
771
+
772
+ this.addNoIndexMetaForOlderDocVersions();
773
+ }
774
+
775
+ /**
776
+ * Method adds noindex, follow meta tag to the older Couch DB doc pages.
777
+ * Fixes W-12547462.
778
+ */
779
+ private addNoIndexMetaForOlderDocVersions() {
780
+ // eslint-disable-next-line @lwc/lwc/no-document-query
781
+ const headTag = document.getElementsByTagName("head");
782
+ // this checks if the selected version is not the latest version,
783
+ // then it adds the noindex, follow meta tag to the older version pages.
784
+ const versionId = this.version!.id;
785
+ const docId = this.pageReference.docId;
786
+
787
+ // SEO fix:
788
+ // Doc id without version id is always considered latest and should be used for SEO.
789
+ // Condition is to find a docId which includes version id,
790
+ // these docs are always considered as old and should not be indexed including the preview docs.
791
+ if (
792
+ headTag.length &&
793
+ docId?.includes(versionId) &&
794
+ !document.querySelector('meta[name="robots"]')
795
+ ) {
796
+ const robotsMeta = document.createElement("meta");
797
+ robotsMeta.setAttribute("name", "robots");
798
+ robotsMeta.setAttribute("content", "noindex, follow");
799
+ headTag[0].appendChild(robotsMeta);
800
+ }
801
+ }
802
+
803
+ private get showVersionPicker(): boolean {
804
+ return !this.disableVersion;
805
+ }
806
+
807
+ private calculateReadingTime(content: string): void {
808
+ if (!this.displayReadingTime) {
809
+ this.readingTime = undefined;
810
+ return;
811
+ }
812
+
813
+ if (!content) {
814
+ this.readingTime = undefined;
815
+ return;
816
+ }
817
+
818
+ // Strip HTML tags and get text content
819
+ const div = document.createElement("div");
820
+ div.innerHTML = DOMPurify.sanitize(content);
821
+ const textContent = div.textContent || div.innerText || "";
822
+
823
+ // Count words (split by whitespace and filter out empty strings)
824
+ const wordCount = textContent
825
+ .trim()
826
+ .split(/\s+/)
827
+ .filter((word) => word.length > 0).length;
828
+
829
+ // Calculate reading time: (wordCount / 240) + 1, rounded
830
+ const readingTimeMinutes = Math.round(wordCount / 240 + 1);
831
+ this.readingTime =
832
+ readingTimeMinutes > 0 ? readingTimeMinutes : undefined;
658
833
  }
659
834
  }