@conduction/docusaurus-preset 1.5.1 → 1.7.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/docusaurus-preset",
3
- "version": "1.5.1",
3
+ "version": "1.7.0",
4
4
  "scripts": {
5
5
  "prepack": "node scripts/prepack-bundle-css.js"
6
6
  },
@@ -10,6 +10,7 @@
10
10
  ".": "./src/index.js",
11
11
  "./components": "./src/components/index.js",
12
12
  "./data": "./src/data/app-downloads.js",
13
+ "./data/apps-registry": "./src/data/apps-registry.js",
13
14
  "./css/brand.css": "./src/css/brand.css",
14
15
  "./css/tokens.css": "./src/css/tokens.css",
15
16
  "./theme": "./src/theme.js",
@@ -0,0 +1,148 @@
1
+ /**
2
+ * <AppCrossLinks />
3
+ *
4
+ * Cross-links between the three surfaces a visitor lands on while
5
+ * learning about a Conduction app:
6
+ *
7
+ * 1. /apps/<slug> product page
8
+ * 2. https://docs.conduction.nl/<slug> documentation
9
+ * 3. /academy?app=<slug> academy filtered
10
+ *
11
+ * URLs come from the shared apps-registry so all three surfaces stay
12
+ * in lockstep without per-page constants.
13
+ *
14
+ * Two variants:
15
+ * - rail sticky right-rail card. Used on /apps/<slug>.mdx, paired
16
+ * with a 2-col grid (see partners/<slug>.mdx for the same
17
+ * layout pattern).
18
+ * - inline page-bottom block, three link tiles in a row. Used at
19
+ * the bottom of an academy post and on each app's docs
20
+ * overview page (or auto-injected via the DocItemFooter
21
+ * swizzle when the docs site sets themeConfig.conduction
22
+ * .appId in docusaurus.config.js).
23
+ *
24
+ * `surface` tells the component which row to omit, so we never link
25
+ * the user to the page they're already on:
26
+ * - surface="product" hides the "Open product page" row
27
+ * - surface="academy" hides the "Read in the academy" row
28
+ * - surface="docs" hides the "Read the documentation" row
29
+ * - surface omitted all three rows render
30
+ *
31
+ * Usage:
32
+ *
33
+ * import {AppCrossLinks} from '@conduction/docusaurus-preset/components';
34
+ *
35
+ * <AppCrossLinks
36
+ * variant="rail"
37
+ * apps={['opencatalogi']}
38
+ * surface="product"
39
+ * />
40
+ *
41
+ * <AppCrossLinks
42
+ * variant="inline"
43
+ * apps={frontMatter.apps}
44
+ * surface="academy"
45
+ * />
46
+ */
47
+
48
+ import React from 'react';
49
+ import {APPS_REGISTRY} from '../../data/apps-registry';
50
+ import styles from './AppCrossLinks.module.css';
51
+
52
+ const ROWS = [
53
+ {key: 'product', surfaceKey: 'product', hrefKey: 'productHref', label: 'Open product page', hint: 'Capabilities, pricing, install link'},
54
+ {key: 'docs', surfaceKey: 'docs', hrefKey: 'docsHref', label: 'Read the documentation', hint: 'Reference, guides, API'},
55
+ {key: 'academy', surfaceKey: 'academy', hrefKey: 'academyHref', label: 'Read in the academy', hint: 'Tutorials, case studies, blog'},
56
+ ];
57
+
58
+ const ICONS = {
59
+ product: (
60
+ <svg viewBox="0 0 24 24" aria-hidden="true">
61
+ <path d="M3 7l9-4 9 4-9 4-9-4z" />
62
+ <path d="M3 12l9 4 9-4" />
63
+ <path d="M3 17l9 4 9-4" />
64
+ </svg>
65
+ ),
66
+ docs: (
67
+ <svg viewBox="0 0 24 24" aria-hidden="true">
68
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
69
+ <path d="M14 2v6h6" />
70
+ <path d="M9 13h6M9 17h6M9 9h2" />
71
+ </svg>
72
+ ),
73
+ academy: (
74
+ <svg viewBox="0 0 24 24" aria-hidden="true">
75
+ <path d="M3 7l9-4 9 4-9 4-9-4z" />
76
+ <path d="M5 11v5a7 7 0 0 0 14 0v-5" />
77
+ <path d="M21 7v5" />
78
+ </svg>
79
+ ),
80
+ };
81
+
82
+ function AppCard({app, surface, variant}) {
83
+ const reg = APPS_REGISTRY[app];
84
+ if (!reg) return null;
85
+
86
+ const rows = ROWS.filter((row) => row.surfaceKey !== surface);
87
+ const tagId = `app-cross-links-${reg.slug}`;
88
+
89
+ return (
90
+ <div className={styles.card} aria-labelledby={tagId}>
91
+ <div className={styles.head}>
92
+ <span className={styles.hex} aria-hidden="true" />
93
+ <div>
94
+ <p className={styles.eyebrow}>About this app</p>
95
+ <h4 className={styles.title} id={tagId}>{reg.name}</h4>
96
+ </div>
97
+ </div>
98
+
99
+ <ul className={styles.rows}>
100
+ {rows.map((row) => (
101
+ <li key={row.key} className={styles.row}>
102
+ <a
103
+ href={reg[row.hrefKey]}
104
+ className={styles.rowLink}
105
+ target={row.key === 'docs' ? '_blank' : undefined}
106
+ rel={row.key === 'docs' ? 'noopener noreferrer' : undefined}
107
+ >
108
+ <span className={styles.rowIcon} aria-hidden="true">{ICONS[row.key]}</span>
109
+ <span className={styles.rowText}>
110
+ <span className={styles.rowLabel}>{row.label}</span>
111
+ <span className={styles.rowHint}>{row.hint}</span>
112
+ </span>
113
+ <span className={styles.rowArrow} aria-hidden="true">→</span>
114
+ </a>
115
+ </li>
116
+ ))}
117
+ </ul>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ export default function AppCrossLinks({
123
+ apps = [],
124
+ variant = 'inline',
125
+ surface,
126
+ className,
127
+ heading,
128
+ }) {
129
+ const known = apps.filter((slug) => APPS_REGISTRY[slug]);
130
+ if (known.length === 0) return null;
131
+
132
+ const composed = [
133
+ styles.root,
134
+ styles[`variant-${variant}`],
135
+ className,
136
+ ].filter(Boolean).join(' ');
137
+
138
+ return (
139
+ <aside className={composed} aria-label="Related app links">
140
+ {heading && <h3 className={styles.heading}>{heading}</h3>}
141
+ <div className={styles.list}>
142
+ {known.map((slug) => (
143
+ <AppCard key={slug} app={slug} surface={surface} variant={variant} />
144
+ ))}
145
+ </div>
146
+ </aside>
147
+ );
148
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * <AppCrossLinks /> styles. Rail variant mirrors PartnerSidecard's
3
+ * card chrome (white card, cobalt-100 hairline, 1-shadow). Inline
4
+ * variant lays the same cards out horizontally as a page-bottom
5
+ * cross-link block on academy posts and docs overview pages.
6
+ */
7
+
8
+ .root {
9
+ font-family: var(--conduction-typography-font-family-body);
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: var(--space-4);
13
+ }
14
+
15
+ .heading {
16
+ font-size: 22px;
17
+ font-weight: 700;
18
+ letter-spacing: -0.01em;
19
+ color: var(--c-cobalt-900);
20
+ margin: 0;
21
+ }
22
+
23
+ .list {
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--space-4);
27
+ }
28
+
29
+ .variant-rail {
30
+ width: 100%;
31
+ max-width: 320px;
32
+ position: sticky;
33
+ top: var(--space-8);
34
+ }
35
+
36
+ .variant-inline .list {
37
+ display: grid;
38
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
39
+ gap: var(--space-4);
40
+ }
41
+
42
+ .card {
43
+ background: white;
44
+ border: 1px solid var(--c-cobalt-100);
45
+ border-radius: var(--radius-lg);
46
+ padding: var(--space-5);
47
+ box-shadow: var(--shadow-1);
48
+ }
49
+
50
+ /* ---------- Card head ---------- */
51
+
52
+ .head {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: var(--space-3);
56
+ margin-bottom: var(--space-4);
57
+ }
58
+
59
+ .hex {
60
+ width: 28px;
61
+ height: 32px;
62
+ clip-path: var(--hex-pointy-top);
63
+ background: var(--c-blue-cobalt);
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .eyebrow {
68
+ font-family: var(--conduction-typography-font-family-code);
69
+ font-size: 10px;
70
+ letter-spacing: 0.12em;
71
+ text-transform: uppercase;
72
+ color: var(--c-cobalt-400);
73
+ margin: 0 0 2px;
74
+ font-weight: 600;
75
+ }
76
+
77
+ .title {
78
+ font-size: 17px;
79
+ font-weight: 700;
80
+ letter-spacing: -0.01em;
81
+ color: var(--c-cobalt-900);
82
+ margin: 0;
83
+ line-height: 1.2;
84
+ }
85
+
86
+ /* ---------- Rows ---------- */
87
+
88
+ .rows {
89
+ list-style: none;
90
+ margin: 0;
91
+ padding: 0;
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 2px;
95
+ }
96
+
97
+ .row {
98
+ margin: 0;
99
+ }
100
+
101
+ .rowLink {
102
+ display: grid;
103
+ grid-template-columns: 24px 1fr auto;
104
+ align-items: center;
105
+ gap: var(--space-3);
106
+ padding: var(--space-3);
107
+ border-radius: var(--radius-md);
108
+ background: var(--c-cobalt-50);
109
+ color: var(--c-cobalt-900);
110
+ text-decoration: none;
111
+ transition: background 160ms ease, color 160ms ease, transform 160ms ease;
112
+ }
113
+ .rowLink:hover {
114
+ background: var(--c-blue-cobalt);
115
+ color: white;
116
+ text-decoration: none;
117
+ transform: translateX(2px);
118
+ }
119
+ .rowLink:hover .rowHint,
120
+ .rowLink:hover .rowArrow {
121
+ color: rgba(255, 255, 255, 0.85);
122
+ }
123
+ .rowLink:hover .rowIcon {
124
+ color: white;
125
+ }
126
+
127
+ .rowIcon {
128
+ width: 22px;
129
+ height: 22px;
130
+ display: inline-flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ color: var(--c-blue-cobalt);
134
+ }
135
+ .rowIcon svg {
136
+ width: 22px;
137
+ height: 22px;
138
+ stroke: currentColor;
139
+ stroke-width: 1.6;
140
+ fill: none;
141
+ stroke-linecap: round;
142
+ stroke-linejoin: round;
143
+ }
144
+
145
+ .rowText {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 1px;
149
+ min-width: 0;
150
+ }
151
+
152
+ .rowLabel {
153
+ font-size: 14px;
154
+ font-weight: 600;
155
+ line-height: 1.3;
156
+ }
157
+
158
+ .rowHint {
159
+ font-size: 12px;
160
+ color: var(--c-cobalt-400);
161
+ line-height: 1.3;
162
+ }
163
+
164
+ .rowArrow {
165
+ font-size: 16px;
166
+ color: var(--c-cobalt-400);
167
+ font-weight: 600;
168
+ flex-shrink: 0;
169
+ }
170
+
171
+ /* ---------- Responsive ---------- */
172
+
173
+ @media (max-width: 900px) {
174
+ .variant-rail {
175
+ max-width: 100%;
176
+ position: static;
177
+ }
178
+ }
@@ -1,9 +1,15 @@
1
1
  /**
2
2
  * <ContentTypeFilter />
3
3
  *
4
- * Top-of-page chip row that filters academy content by type. "All" is
5
- * the default chip and is always present. Driven by a `?type=` query
6
- * parameter so the filter survives reload and copy-paste.
4
+ * Top-of-page chip row that filters a feed by a single key. Defaults
5
+ * to academy content types ("Blogs", "Guides", "Case studies", …) but
6
+ * is reused on the academy landing as the product filter row by
7
+ * passing a different `types` + `labels` map (see APP_LABELS in
8
+ * @conduction/docusaurus-preset/data/apps-registry).
9
+ *
10
+ * "All" is the default chip and is always present. Driven by a query
11
+ * parameter (`?type=` for content types, `?app=` for products) so the
12
+ * filter survives reload and copy-paste.
7
13
  *
8
14
  * Mirrors the chip row in preview/components/academy.html.
9
15
  *
@@ -29,6 +35,19 @@
29
35
  *
30
36
  * const [type, setType] = useState(null);
31
37
  * <ContentTypeFilter value={type} onChange={setType} />
38
+ *
39
+ * Usage as a product filter (reuses the same component):
40
+ *
41
+ * import {APP_LABELS, APP_SLUGS} from '@conduction/docusaurus-preset/data/apps-registry';
42
+ * <ContentTypeFilter
43
+ * value={app}
44
+ * onChange={setApp}
45
+ * types={APP_SLUGS}
46
+ * labels={APP_LABELS}
47
+ * counts={appCounts}
48
+ * allLabel="All apps"
49
+ * allCount={totalCount}
50
+ * />
32
51
  */
33
52
 
34
53
  import React from 'react';
@@ -48,9 +67,14 @@ export default function ContentTypeFilter({
48
67
  allLabel = 'Everything',
49
68
  allCount,
50
69
  types = CONTENT_TYPES,
70
+ labels,
51
71
  className,
52
72
  }) {
53
73
  const isLinkMode = typeof hrefForType === 'function';
74
+ const labelFor = (key) => {
75
+ if (labels && labels[key]) return labels[key];
76
+ return CONTENT_TYPE_PLURAL_LABELS[key] || key;
77
+ };
54
78
 
55
79
  const renderChip = (key, label, count) => {
56
80
  const active = (key === ALL && value == null) || key === value;
@@ -94,7 +118,7 @@ export default function ContentTypeFilter({
94
118
  <div className={[styles.row, className].filter(Boolean).join(' ')}>
95
119
  {renderChip(ALL, allLabel, allCount)}
96
120
  {types.map((t) =>
97
- renderChip(t, CONTENT_TYPE_PLURAL_LABELS[t] || t, counts && counts[t])
121
+ renderChip(t, labelFor(t), counts && counts[t])
98
122
  )}
99
123
  </div>
100
124
  );
@@ -94,6 +94,8 @@ export {
94
94
  export {default as NewsletterCta} from './NewsletterCta/NewsletterCta.jsx';
95
95
  export {default as RelatedPosts} from './RelatedPosts/RelatedPosts.jsx';
96
96
  export {default as ContentDetailHero} from './ContentDetailHero/ContentDetailHero.jsx';
97
+ export {default as AppCrossLinks} from './AppCrossLinks/AppCrossLinks.jsx';
98
+ export {APPS_REGISTRY, APP_SLUGS, APP_LABELS, getApp, getApps} from '../data/apps-registry';
97
99
 
98
100
  /* Tutorial-body components. Drop-in replacements for the ad-hoc
99
101
  "What you need", "Troubleshooting", and "Next steps" h2 + bullet
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @conduction/docusaurus-preset/data/apps-registry
3
+ *
4
+ * URL-only registry of every public Conduction app. Single source of
5
+ * truth for cross-linking between the three surfaces a visitor lands
6
+ * on while learning about an app:
7
+ *
8
+ * 1. /apps/<slug> product detail page
9
+ * 2. https://docs.conduction.nl/<slug> documentation site (lives
10
+ * in the app's own repo, served from a per-app subfolder under
11
+ * the central docs.conduction.nl Docusaurus install)
12
+ * 3. /academy?app=<slug> academy posts filtered by app
13
+ *
14
+ * The registry is consumed by:
15
+ * - <AppCrossLinks/> renders the three links per app.
16
+ * - <ProductFilter/> renders the chip row on /academy.
17
+ * - sites/www/src/data/apps-catalog.js the live Conduction.nl
18
+ * site adds icons, taglines, and category metadata on top of
19
+ * this registry, so display data and URLs stay in lockstep.
20
+ *
21
+ * Adding a new app: add an entry here. The url shape is conventional:
22
+ * - productHref: /apps/<slug>
23
+ * - docsHref: https://docs.conduction.nl/<slug>
24
+ * - academyHref: /academy?app=<slug>
25
+ * Override any of the three when an app deviates from the convention
26
+ * (none today; the convention holds).
27
+ */
28
+
29
+ export const APPS_REGISTRY = {
30
+ opencatalogi: {slug: 'opencatalogi', name: 'OpenCatalogi', productHref: '/apps/opencatalogi', docsHref: 'https://docs.conduction.nl/opencatalogi', academyHref: '/academy?app=opencatalogi'},
31
+ openregister: {slug: 'openregister', name: 'OpenRegister', productHref: '/apps/openregister', docsHref: 'https://docs.conduction.nl/openregister', academyHref: '/academy?app=openregister'},
32
+ openconnector: {slug: 'openconnector', name: 'OpenConnector', productHref: '/apps/openconnector', docsHref: 'https://docs.conduction.nl/openconnector', academyHref: '/academy?app=openconnector'},
33
+ docudesk: {slug: 'docudesk', name: 'DocuDesk', productHref: '/apps/docudesk', docsHref: 'https://docs.conduction.nl/docudesk', academyHref: '/academy?app=docudesk'},
34
+ mydash: {slug: 'mydash', name: 'MyDash', productHref: '/apps/mydash', docsHref: 'https://docs.conduction.nl/mydash', academyHref: '/academy?app=mydash'},
35
+ openwoo: {slug: 'openwoo', name: 'OpenWoo', productHref: '/apps/openwoo', docsHref: 'https://docs.conduction.nl/openwoo', academyHref: '/academy?app=openwoo'},
36
+ zaakafhandelapp: {slug: 'zaakafhandelapp', name: 'ZaakAfhandelApp', productHref: '/apps/zaakafhandelapp', docsHref: 'https://docs.conduction.nl/zaakafhandelapp', academyHref: '/academy?app=zaakafhandelapp'},
37
+ pipelinq: {slug: 'pipelinq', name: 'PipelinQ', productHref: '/apps/pipelinq', docsHref: 'https://docs.conduction.nl/pipelinq', academyHref: '/academy?app=pipelinq'},
38
+ procest: {slug: 'procest', name: 'Procest', productHref: '/apps/procest', docsHref: 'https://docs.conduction.nl/procest', academyHref: '/academy?app=procest'},
39
+ decidesk: {slug: 'decidesk', name: 'DeciDesk', productHref: '/apps/decidesk', docsHref: 'https://docs.conduction.nl/decidesk', academyHref: '/academy?app=decidesk'},
40
+ softwarecatalog: {slug: 'softwarecatalog', name: 'SoftwareCatalog', productHref: '/apps/softwarecatalog', docsHref: 'https://docs.conduction.nl/softwarecatalog', academyHref: '/academy?app=softwarecatalog'},
41
+ larpingapp: {slug: 'larpingapp', name: 'LarpingApp', productHref: '/apps/larpingapp', docsHref: 'https://docs.conduction.nl/larpingapp', academyHref: '/academy?app=larpingapp'},
42
+ nldesign: {slug: 'nldesign', name: 'NLDesign', productHref: '/apps/nldesign', docsHref: 'https://docs.conduction.nl/nldesign', academyHref: '/academy?app=nldesign'},
43
+ };
44
+
45
+ export const APP_SLUGS = Object.keys(APPS_REGISTRY);
46
+
47
+ /** Build a label map keyed by slug, suitable for <ContentTypeFilter labels=…/>. */
48
+ export const APP_LABELS = APP_SLUGS.reduce((acc, slug) => {
49
+ acc[slug] = APPS_REGISTRY[slug].name;
50
+ return acc;
51
+ }, {});
52
+
53
+ /** Resolve a slug to its registry entry; returns undefined for unknown slugs. */
54
+ export function getApp(slug) {
55
+ return APPS_REGISTRY[slug];
56
+ }
57
+
58
+ /** Resolve an array of slugs, dropping any that aren't in the registry. */
59
+ export function getApps(slugs = []) {
60
+ return slugs.map(getApp).filter(Boolean);
61
+ }
package/src/index.js CHANGED
@@ -86,8 +86,8 @@ const baseFooterLinks = () => [
86
86
  {
87
87
  title: 'Documentatie',
88
88
  items: [
89
- { label: 'Brand book', href: 'https://connext.conduction.nl/' },
90
- { label: 'Diagram set', href: 'https://connext.conduction.nl/diagrams/' },
89
+ { label: 'Brand book', href: 'https://identity.conduction.nl/' },
90
+ { label: 'Diagram set', href: 'https://identity.conduction.nl/diagrams/' },
91
91
  ],
92
92
  },
93
93
  ];
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Brand DocItem/Footer swizzle.
3
+ *
4
+ * Wraps Docusaurus's default doc-item footer (tags row + edit-meta
5
+ * row) and appends a Conduction <AppCrossLinks/> block so every
6
+ * documentation page on docs.conduction.nl/<app> finishes with a
7
+ * cross-link to the product page and the academy filter.
8
+ *
9
+ * Activation is per-site, opt-in: a docs site enables the block by
10
+ * adding to its docusaurus.config.js:
11
+ *
12
+ * themeConfig: {
13
+ * conduction: {
14
+ * appId: 'opencatalogi'
15
+ * },
16
+ * ...
17
+ * }
18
+ *
19
+ * Apps that don't set `themeConfig.conduction.appId` keep the default
20
+ * Docusaurus footer behaviour, untouched.
21
+ *
22
+ * Per-page opt-out: a doc page can suppress the block via frontmatter
23
+ *
24
+ * ---
25
+ * hide_app_cross_links: true
26
+ * ---
27
+ *
28
+ * which is useful for changelogs, license pages, or other docs that
29
+ * shouldn't end with a cross-link.
30
+ */
31
+
32
+ import React from 'react';
33
+ import clsx from 'clsx';
34
+ import {ThemeClassNames} from '@docusaurus/theme-common';
35
+ import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
36
+ import {useDoc} from '@docusaurus/plugin-content-docs/client';
37
+ import TagsListInline from '@theme/TagsListInline';
38
+ import EditMetaRow from '@theme/EditMetaRow';
39
+ import {AppCrossLinks} from '@conduction/docusaurus-preset/components';
40
+ import {APPS_REGISTRY} from '@conduction/docusaurus-preset/data/apps-registry';
41
+
42
+ export default function DocItemFooter() {
43
+ const {siteConfig} = useDocusaurusContext();
44
+ const {metadata, frontMatter} = useDoc();
45
+ const {editUrl, lastUpdatedAt, lastUpdatedBy, tags} = metadata;
46
+
47
+ /* Per-page opt-out and per-page override (`apps:` in frontmatter
48
+ wins over the site-wide appId). Lets a single doc page point at
49
+ a different app without touching siteConfig. */
50
+ const hide = !!frontMatter?.hide_app_cross_links;
51
+ const fmApps = Array.isArray(frontMatter?.apps) ? frontMatter.apps : null;
52
+ const siteAppId = siteConfig?.themeConfig?.conduction?.appId;
53
+ const apps = (fmApps && fmApps.length > 0)
54
+ ? fmApps
55
+ : (siteAppId ? [siteAppId] : []);
56
+ const knownApps = apps.filter((slug) => APPS_REGISTRY[slug]);
57
+
58
+ const canDisplayTagsRow = tags.length > 0;
59
+ const canDisplayEditMetaRow = !!(editUrl || lastUpdatedAt || lastUpdatedBy);
60
+ const canDisplayCrossLinks = !hide && knownApps.length > 0;
61
+
62
+ const canDisplayFooter = canDisplayTagsRow || canDisplayEditMetaRow || canDisplayCrossLinks;
63
+ if (!canDisplayFooter) return null;
64
+
65
+ return (
66
+ <footer
67
+ className={clsx(ThemeClassNames.docs.docFooter, 'docusaurus-mt-lg')}>
68
+ {canDisplayTagsRow && (
69
+ <div
70
+ className={clsx(
71
+ 'row margin-top--sm',
72
+ ThemeClassNames.docs.docFooterTagsRow,
73
+ )}>
74
+ <div className="col">
75
+ <TagsListInline tags={tags} />
76
+ </div>
77
+ </div>
78
+ )}
79
+ {canDisplayEditMetaRow && (
80
+ <EditMetaRow
81
+ className={clsx(
82
+ 'margin-top--sm',
83
+ ThemeClassNames.docs.docFooterEditMetaRow,
84
+ )}
85
+ editUrl={editUrl}
86
+ lastUpdatedAt={lastUpdatedAt}
87
+ lastUpdatedBy={lastUpdatedBy}
88
+ />
89
+ )}
90
+ {canDisplayCrossLinks && (
91
+ <div className="margin-top--lg">
92
+ <AppCrossLinks
93
+ variant="inline"
94
+ apps={knownApps}
95
+ surface="docs"
96
+ heading={knownApps.length === 1
97
+ ? 'About this app'
98
+ : 'About these apps'}
99
+ />
100
+ </div>
101
+ )}
102
+ </footer>
103
+ );
104
+ }