@chassis-ui/docs 0.5.0 → 0.6.0-next.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.
@@ -1,5 +1,7 @@
1
1
  ---
2
2
  import fs from 'node:fs'
3
+ import { getSourceFsPath } from '../../site'
4
+ import { extractDocsSnippet } from '../../libs/snippet'
3
5
  import Code from './Code.astro'
4
6
 
5
7
  // Prints everything between `// scss-docs-start "name"` and `// scss-docs-end "name"`
@@ -11,7 +13,8 @@ interface Props {
11
13
  */
12
14
  name: string
13
15
  /**
14
- * File path that contains the content to display relative to the root of the repository.
16
+ * File path that contains the content to display, relative to the `sourceDir` of the site's
17
+ * `config.yml` (the site's root when it is not set).
15
18
  */
16
19
  file: string
17
20
  }
@@ -29,19 +32,17 @@ if (!name || !file) {
29
32
  let content: string
30
33
 
31
34
  try {
32
- const fileContent = fs.readFileSync(file, 'utf8')
35
+ const fileContent = fs.readFileSync(getSourceFsPath(file), 'utf8')
33
36
 
34
- const matches = fileContent.match(
35
- new RegExp(`// scss-docs-start ${name}\n((?:.|\n)*)// scss-docs-end ${name}`, 'm')
36
- )
37
+ const snippet = extractDocsSnippet(fileContent, 'scss', name)
37
38
 
38
- if (!matches || !matches[1]) {
39
+ if (!snippet) {
39
40
  throw new Error(
40
41
  `Failed to find the content named '${name}', make sure that '// scss-docs-start ${name}' and '// scss-docs-end ${name}' are defined.`
41
42
  )
42
43
  }
43
44
 
44
- content = matches[1].replaceAll(' !default', '')
45
+ content = snippet.replaceAll(' !default', '')
45
46
 
46
47
  // Fix the identation by removing extra spaces at the beginning of each line
47
48
  const lines = content.split('\n')
@@ -0,0 +1,271 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { AstroIntegration, AstroUserConfig, ViteUserConfig } from 'astro'
5
+ import type { z } from 'astro/zod'
6
+ import { loadConfig, loadSidebar } from './libs/config'
7
+ import { joinDocsPath } from './libs/docs-path'
8
+ import { getDocsMarkdownConfig, type DocsMarkdownConfigOptions } from './libs/markdown'
9
+ import { getInstalledPackageFsPath, getPackageRoot } from './libs/paths'
10
+ import { remarkCxConfig, remarkCxDocsref } from './libs/remark'
11
+ import { configSchema, type ChassisConfig, type Sidebar } from './libs/schema'
12
+ import { chassisAutoImport, type ChassisAutoImportOptions } from './libs/shortcodes'
13
+ import { getSiteUrl } from './libs/site'
14
+
15
+ export interface ChassisDocsOptions<TSchema extends z.ZodType = typeof configSchema> {
16
+ /**
17
+ * The config, for a site that has loaded it with `loadConfig()` to use it in
18
+ * `astro.config.ts`. `configFile` is then only watched, and `configSchema` is not used.
19
+ */
20
+ config?: ChassisConfig
21
+ /**
22
+ * Path of the config file, from the site's root.
23
+ * @default 'config.yml'
24
+ */
25
+ configFile?: string
26
+ /**
27
+ * Schema of the config file: `configSchema` from `@chassis-ui/docs/schema`, or an
28
+ * extension of it when the site has keys of its own.
29
+ */
30
+ configSchema?: TSchema
31
+ /**
32
+ * Path of the sidebar data file, from the site's root. `false` for a site that does not
33
+ * render `<DocsSidebar>`.
34
+ * @default 'data/sidebar.yml' when the file exists
35
+ */
36
+ sidebarFile?: string | false
37
+ /**
38
+ * Stylesheets that every page loads, from the site's root. A site's stylesheet is expected
39
+ * to `@use "@chassis-ui/docs/scss/main"`.
40
+ * @default ['src/scss/docs.scss'] when the file exists, the package's styles otherwise
41
+ */
42
+ styles?: string[]
43
+ /** Which shortcodes are imported into every MDX file. */
44
+ shortcodes?: Pick<ChassisAutoImportOptions, 'dir' | 'include' | 'exclude'>
45
+ /** Plugins of the site. They run after the plugins of the package. */
46
+ markdown?: Pick<DocsMarkdownConfigOptions, 'rehypePlugins' | 'remarkPlugins' | 'remarkRehype'>
47
+ /**
48
+ * What happens when a `[[docsref:]]` link points to a page that was not built.
49
+ * @default 'error'
50
+ */
51
+ brokenDocsrefs?: 'error' | 'warn' | 'ignore'
52
+ }
53
+
54
+ type VitePlugin = Extract<NonNullable<ViteUserConfig['plugins']>[number], { name: string }>
55
+
56
+ const virtualPrefix = 'virtual:chassis-docs/'
57
+ const resolvedPrefix = '\0' + virtualPrefix
58
+
59
+ function virtualModules(modules: Record<string, string>): VitePlugin {
60
+ return {
61
+ name: 'chassis-docs:virtual',
62
+ resolveId(id) {
63
+ if (id.startsWith(virtualPrefix) && id.slice(virtualPrefix.length) in modules) {
64
+ return '\0' + id
65
+ }
66
+ },
67
+ load(id) {
68
+ if (id.startsWith(resolvedPrefix)) {
69
+ return modules[id.slice(resolvedPrefix.length)]
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ // The declarations of `virtual.d.ts`, for a site.
76
+ const virtualTypes = `declare module 'virtual:chassis-docs/config' {
77
+ const config: import('@chassis-ui/docs/schema').ChassisConfig
78
+ export default config
79
+ }
80
+
81
+ declare module 'virtual:chassis-docs/sidebar' {
82
+ const sidebar: import('@chassis-ui/docs/schema').Sidebar
83
+ export default sidebar
84
+ }
85
+
86
+ declare module 'virtual:chassis-docs/paths' {
87
+ export const root: string
88
+ export const publicDir: string
89
+ export const sourceDir: string
90
+ export const packageRoot: string
91
+ }
92
+
93
+ declare module 'virtual:chassis-docs/styles' {}
94
+ `
95
+
96
+ // The folder of `@chassis-ui/css` with its default `_chassis-tokens.scss`, when the site has
97
+ // the package installed.
98
+ function getDefaultTokensLoadPath(root: string): string | undefined {
99
+ try {
100
+ return path.join(getInstalledPackageFsPath('@chassis-ui/css', root), 'scss/vendor')
101
+ } catch {
102
+ return undefined
103
+ }
104
+ }
105
+
106
+ // Append `index.html` when the path names a page, and drop the hash.
107
+ function toBuiltFilePath(docsPath: string): string {
108
+ const withoutHash = docsPath.split('#')[0]
109
+
110
+ return withoutHash.includes('.') ? withoutHash : path.join(withoutHash, 'index.html')
111
+ }
112
+
113
+ /**
114
+ * The Astro integration of `@chassis-ui/docs`. A site adds it in `astro.config.ts`.
115
+ *
116
+ * It reads `config.yml` and `data/sidebar.yml`, gives them to the layouts and components of
117
+ * the package, sets `site` and `markdown` of the Astro config, and imports the shortcodes
118
+ * into every MDX file.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * import { defineConfig } from 'astro/config'
123
+ * import { chassisDocs } from '@chassis-ui/docs/integration'
124
+ *
125
+ * export default defineConfig({
126
+ * integrations: [chassisDocs()]
127
+ * })
128
+ * ```
129
+ */
130
+ export function chassisDocs<TSchema extends z.ZodType = typeof configSchema>(
131
+ options: ChassisDocsOptions<TSchema> = {}
132
+ ): AstroIntegration {
133
+ const { brokenDocsrefs = 'error' } = options
134
+
135
+ // Docs paths that `[[docsref:]]` links name, collected while the pages are rendered.
136
+ const linkedDocsPaths = new Set<string>()
137
+ let config: ChassisConfig
138
+ let typeDefinitions = ''
139
+
140
+ return {
141
+ name: '@chassis-ui/docs',
142
+ hooks: {
143
+ 'astro:config:setup': ({ config: astroConfig, command, updateConfig, addWatchFile }) => {
144
+ const root = path.resolve(fileURLToPath(astroConfig.root))
145
+ const configFile = path.resolve(root, options.configFile ?? 'config.yml')
146
+
147
+ config =
148
+ options.config ??
149
+ loadConfig({
150
+ file: configFile,
151
+ schema: (options.configSchema ?? configSchema) as z.ZodType<ChassisConfig>
152
+ })
153
+ addWatchFile(configFile)
154
+
155
+ let sidebar: Sidebar = []
156
+
157
+ if (options.sidebarFile !== false) {
158
+ const sidebarFile = path.resolve(root, options.sidebarFile ?? 'data/sidebar.yml')
159
+
160
+ // The default file is optional. A file that the site names must exist.
161
+ if (options.sidebarFile !== undefined || fs.existsSync(sidebarFile)) {
162
+ sidebar = loadSidebar({ file: sidebarFile })
163
+ addWatchFile(sidebarFile)
164
+ }
165
+ }
166
+
167
+ const defaultStyles = path.join(root, 'src/scss/docs.scss')
168
+ const styles = options.styles
169
+ ? options.styles.map((file) => path.resolve(root, file))
170
+ : fs.existsSync(defaultStyles)
171
+ ? [defaultStyles]
172
+ : ['@chassis-ui/docs/scss/main.scss']
173
+
174
+ const autoImport = chassisAutoImport({ root, ...options.shortcodes })
175
+ typeDefinitions = autoImport.typeDefinitions
176
+
177
+ const paths = {
178
+ root,
179
+ publicDir: path.resolve(fileURLToPath(astroConfig.publicDir)),
180
+ sourceDir: path.resolve(root, config.sourceDir),
181
+ packageRoot: getPackageRoot()
182
+ }
183
+
184
+ const tokensLoadPath = getDefaultTokensLoadPath(root)
185
+
186
+ const update: AstroUserConfig = {
187
+ // A `site` that the Astro config sets itself is kept.
188
+ ...(astroConfig.site ? {} : { site: getSiteUrl(config) }),
189
+ markdown: getDocsMarkdownConfig({
190
+ anchors: config.anchors,
191
+ remarkPlugins: [
192
+ autoImport.plugin(),
193
+ [remarkCxConfig, { config }],
194
+ [
195
+ remarkCxDocsref,
196
+ {
197
+ docsPath: config.docsPath,
198
+ onDocsPath:
199
+ command === 'build'
200
+ ? (docsPath: string) => linkedDocsPaths.add(docsPath)
201
+ : undefined
202
+ }
203
+ ],
204
+ ...(options.markdown?.remarkPlugins ?? [])
205
+ ],
206
+ rehypePlugins: options.markdown?.rehypePlugins,
207
+ remarkRehype: options.markdown?.remarkRehype
208
+ }),
209
+ vite: {
210
+ // Scripts of the package and of the site both import `@chassis-ui/css`, and two
211
+ // copies of it register every listener twice. In the dev server, a script of the
212
+ // package would be pre-bundled with a copy of its own, and a script of the site
213
+ // would get a pre-bundled copy while the package gets the source. Both packages
214
+ // are served from source, and every import resolves to the site's copy.
215
+ optimizeDeps: {
216
+ exclude: ['@chassis-ui/docs', '@chassis-ui/css'],
217
+ // The one CommonJS dependency of the package's scripts. It has no default export
218
+ // unless it is pre-bundled.
219
+ include: ['@chassis-ui/docs > clipboard']
220
+ },
221
+ resolve: {
222
+ dedupe: ['@chassis-ui/css']
223
+ },
224
+ // The styles of `@chassis-ui/css` load the design tokens as `chassis-tokens`. This
225
+ // adds its default as the last load path, after the site's own load paths, so a
226
+ // site with a `_chassis-tokens.scss` of its own puts that folder in its config.
227
+ ...(tokensLoadPath
228
+ ? { css: { preprocessorOptions: { scss: { loadPaths: [tokensLoadPath] } } } }
229
+ : {}),
230
+ plugins: [
231
+ virtualModules({
232
+ config: `export default ${JSON.stringify(config)}`,
233
+ sidebar: `export default ${JSON.stringify(sidebar)}`,
234
+ paths: Object.entries(paths)
235
+ .map(([name, value]) => `export const ${name} = ${JSON.stringify(value)}`)
236
+ .join('\n'),
237
+ styles: styles.map((file) => `import ${JSON.stringify(file)}`).join('\n')
238
+ })
239
+ ]
240
+ }
241
+ }
242
+
243
+ // The user config is wider than what `updateConfig()` declares for `markdown`.
244
+ updateConfig(update as Parameters<typeof updateConfig>[0])
245
+ },
246
+ 'astro:config:done': ({ injectTypes }) => {
247
+ injectTypes({ filename: 'virtual.d.ts', content: virtualTypes })
248
+ injectTypes({ filename: 'auto-import.d.ts', content: typeDefinitions })
249
+ },
250
+ 'astro:build:done': ({ dir, logger }) => {
251
+ if (brokenDocsrefs === 'ignore') return
252
+
253
+ const broken = [...linkedDocsPaths].filter((docsPath) => {
254
+ const builtPath = toBuiltFilePath(joinDocsPath(config.docsPath, docsPath))
255
+
256
+ return !fs.existsSync(path.join(fileURLToPath(dir), builtPath))
257
+ })
258
+
259
+ if (broken.length === 0) return
260
+
261
+ const message = `These [[docsref:]] links point to a page or file that was not built:\n${broken.map((docsPath) => ` - ${docsPath}`).join('\n')}`
262
+
263
+ if (brokenDocsrefs === 'error') {
264
+ throw new Error(message)
265
+ }
266
+
267
+ logger.warn(message)
268
+ }
269
+ }
270
+ }
271
+ }
@@ -0,0 +1,65 @@
1
+ import ClipboardJS from 'clipboard'
2
+ import { Tooltip } from '@chassis-ui/css'
3
+
4
+ const btnTitle = 'Copy'
5
+
6
+ export function initCopyButtons(
7
+ selector: string,
8
+ textFn: (_element: Element) => string
9
+ ): ClipboardJS {
10
+ document.querySelectorAll(selector).forEach((btn) => {
11
+ Tooltip.getOrCreateInstance(btn, { title: btnTitle })
12
+ })
13
+
14
+ const clipboard = new ClipboardJS(selector, { text: textFn })
15
+
16
+ clipboard.on('success', (event) => {
17
+ const useEl = event.trigger.querySelector('.icon use')
18
+ const tooltipBtn = Tooltip.getInstance(event.trigger)
19
+ const originalHref = useEl?.getAttribute('href')
20
+
21
+ if (originalHref === '#check-solid') {
22
+ return
23
+ }
24
+
25
+ tooltipBtn?.setContent({ '.tooltip-inner': 'Copied!' })
26
+
27
+ event.trigger.addEventListener(
28
+ 'hidden.cx.tooltip',
29
+ () => {
30
+ tooltipBtn?.setContent({ '.tooltip-inner': btnTitle })
31
+ },
32
+ { once: true }
33
+ )
34
+
35
+ event.clearSelection()
36
+
37
+ if (useEl) {
38
+ useEl.setAttribute('href', '#check-solid')
39
+ }
40
+
41
+ setTimeout(() => {
42
+ if (useEl && originalHref) {
43
+ useEl.setAttribute('href', originalHref)
44
+ }
45
+ }, 2000)
46
+ })
47
+
48
+ clipboard.on('error', (event) => {
49
+ const modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-'
50
+ const fallbackMsg = `Press ${modifierKey}C to copy`
51
+ const tooltipBtn = Tooltip.getInstance(event.trigger)
52
+
53
+ tooltipBtn?.setContent({ '.tooltip-inner': fallbackMsg })
54
+
55
+ event.trigger.addEventListener(
56
+ 'hidden.cx.tooltip',
57
+ () => {
58
+ tooltipBtn?.setContent({ '.tooltip-inner': btnTitle })
59
+ },
60
+ { once: true }
61
+ )
62
+ })
63
+
64
+ return clipboard
65
+ }
@@ -5,9 +5,9 @@
5
5
  * Main layout wrapper for all pages with configurable HTML attributes and layout modes.
6
6
  */
7
7
 
8
- import type { CollectionEntry } from 'astro:content'
9
- import { getConfig } from '@libs/config'
10
8
  import type { Layout, LayoutOverridesHTMLAttributes } from '../libs/layout'
9
+ import type { DocsFrontmatter } from '../libs/schema'
10
+ import { getConfig } from '../site'
11
11
  import Head from './head/Head.astro'
12
12
  import Header from './header/Header.astro'
13
13
  import Scripts from './footer/Scripts.astro'
@@ -42,11 +42,11 @@ type Props = {
42
42
  // The following props are automatically set by Astro (if defined) based on the markdown frontmatter when rendering a
43
43
  // markdown page. They can be accessed through the `Astro.props.frontmatter` object but note that they won't be set when
44
44
  // not rendering a markdown page.
45
- type MarkdownProps = { frontmatter?: Partial<CollectionEntry<'docs'>['data']> }
45
+ type MarkdownProps = { frontmatter?: DocsFrontmatter }
46
46
 
47
- // This type is used to define the shape of the `extra_js` frontmatter property which allows adding extra JavaScript
47
+ // This type is used to define the shape of the `extraJs` frontmatter property which allows adding extra JavaScript
48
48
  // files to a page by passing an array of objects containing the `src` and `async` properties.
49
- type ExtraJS = NonNullable<CollectionEntry<'docs'>['data']['extra_js']>[number]
49
+ type ExtraJS = NonNullable<DocsFrontmatter['extraJs']>[number]
50
50
 
51
51
  const { frontmatter, layout, overrides, robots } = Astro.props
52
52
 
@@ -96,10 +96,8 @@ const mainProps = overrides?.main ?? {}
96
96
  </Scripts>
97
97
 
98
98
  {
99
- frontmatter?.extra_js &&
100
- frontmatter.extra_js.map((js: ExtraJS) => (
101
- <script is:inline async={js.async} src={js.src} />
102
- ))
99
+ frontmatter?.extraJs &&
100
+ frontmatter.extraJs.map((js: ExtraJS) => <script is:inline async={js.async} src={js.src} />)
103
101
  }
104
102
 
105
103
  {
@@ -6,11 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { MarkdownHeading } from 'astro'
9
- import type { CollectionEntry } from 'astro:content'
10
- import { getConfig } from '@libs/config'
11
- import { getChassisDocsPath } from '@libs/path'
12
9
  import type { LayoutOverridesHTMLAttributes } from '../libs/layout'
10
+ import type { DocsFrontmatter } from '../libs/schema'
13
11
  import { getSlug, processMarkdownToHtml } from '../libs/utils'
12
+ import { getConfig, getDocsPages, getDocsPath, getSiteFileUrl } from '../site'
14
13
  import BaseLayout from './BaseLayout.astro'
15
14
  import DocsSidebar from '../components/DocsSidebar.astro'
16
15
  import TableOfContents from '../components/TableOfContents.astro'
@@ -18,15 +17,26 @@ import Icon from '../components/shortcodes/Icon.astro'
18
17
 
19
18
  interface Props {
20
19
  /** Frontmatter data from the docs collection */
21
- frontmatter: CollectionEntry<'docs'>['data']
20
+ frontmatter: DocsFrontmatter
22
21
  /** Markdown headings for table of contents */
23
22
  headings?: MarkdownHeading[]
24
23
  /** Document ID from the docs collection */
25
- id: CollectionEntry<'docs'>['id']
24
+ id: string
25
+ /**
26
+ * Path of the page's source file from the site's root. Looked up from the docs collection
27
+ * by `id` when omitted.
28
+ */
29
+ filePath?: string
26
30
  }
27
31
 
28
32
  const { frontmatter, headings, id } = Astro.props
29
33
 
34
+ // "View on GitHub" links to the page's own source file. The old link, which assumed the
35
+ // file sat at `<id>.mdx` in the repository root, is kept only when no file path is known.
36
+ const filePath =
37
+ Astro.props.filePath ?? (await getDocsPages()).find((page) => page.id === id)?.filePath
38
+ const githubUrl = filePath ? getSiteFileUrl(filePath) : `${getConfig().repo}/blob/${id}.mdx`
39
+
30
40
  type DocSection = NonNullable<typeof frontmatter.sections>[number]
31
41
 
32
42
  // Extract the directory/section from the ID (format: "directory/filename.mdx")
@@ -71,16 +81,14 @@ if (frontmatter.toc) {
71
81
  <main class="cxd-main">
72
82
  <div class="cxd-intro">
73
83
  <slot name="intro">
74
- <div
75
- class="md:d-flex md:flex-row-reverse justify-content-between align-items-start"
76
- >
84
+ <div class="md:d-flex md:flex-row-reverse justify-content-between align-items-start">
77
85
  <div class="mb-md md:mb-0 d-flex text-nowrap">
78
86
  {
79
- // This is needed because we want to show the badge if show_badge isn't present or is set to false
87
+ // This is needed because we want to show the badge if showBadge isn't present or is set to false
80
88
  frontmatter.added &&
81
- ((frontmatter.added.show_badge !== undefined &&
82
- frontmatter.added.show_badge === true) ||
83
- frontmatter.added.show_badge === undefined) && (
89
+ ((frontmatter.added.showBadge !== undefined &&
90
+ frontmatter.added.showBadge === true) ||
91
+ frontmatter.added.showBadge === undefined) && (
84
92
  <span class="badge success smooth rounded-md me-xs">
85
93
  Added in v{frontmatter.added.version}
86
94
  </span>
@@ -88,7 +96,7 @@ if (frontmatter.toc) {
88
96
  }
89
97
  <a
90
98
  class="button default sm"
91
- href={`${getConfig().repo}/blob/${id}.mdx`}
99
+ href={githubUrl}
92
100
  title="View and edit this file on GitHub"
93
101
  target="_blank"
94
102
  rel="noopener"
@@ -143,7 +151,7 @@ if (frontmatter.toc) {
143
151
  <div class="md:col-6">
144
152
  <a
145
153
  class="d-block text-decoration-none"
146
- href={getChassisDocsPath(
154
+ href={getDocsPath(
147
155
  `${parentDirectory ? parentDirectory + '/' : ''}${section.slug ? section.slug : getSlug(section.title)}/`
148
156
  )}
149
157
  >
@@ -5,7 +5,7 @@
5
5
  * Minimal layout for redirect pages with meta refresh.
6
6
  */
7
7
 
8
- import { getConfig } from '@libs/config'
8
+ import { getConfig } from '../site'
9
9
 
10
10
  interface Props {
11
11
  /** Destination path for the redirect */
@@ -2,16 +2,18 @@
2
2
  /**
3
3
  * Analytics
4
4
  *
5
- * Analytics script tags for Fathom and Google Analytics.
5
+ * Google Analytics script tags. Rendered in production builds, when `analytics.googleId` is
6
+ * set in `config.yml`.
6
7
  */
7
8
 
8
- import { getConfig } from '@libs/config'
9
+ import { getConfig } from '../../site'
10
+
9
11
  const isProd = import.meta.env.PROD
10
- const googleId = getConfig().analytics.google_id
12
+ const { googleId } = getConfig().analytics
11
13
  ---
12
14
 
13
15
  {
14
- isProd && (
16
+ isProd && googleId && (
15
17
  <>
16
18
  <script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${googleId}`} />
17
19
  <script
@@ -5,13 +5,13 @@
5
5
  * HTML head section with meta tags, stylesheets, and SEO configuration.
6
6
  */
7
7
 
8
- import { getConfig } from '@libs/config'
8
+ import { getConfig } from '../../site'
9
9
  import type { Layout } from '../../libs/layout'
10
10
  import Stylesheet from './Stylesheet.astro'
11
11
  import Favicons from './Favicons.astro'
12
12
  import Social from './Social.astro'
13
13
  import Analytics from './Analytics.astro'
14
- import '@scss/docs.scss'
14
+ import 'virtual:chassis-docs/styles'
15
15
 
16
16
  interface Props {
17
17
  /** Page description for meta tags */
@@ -5,11 +5,9 @@
5
5
  * Social media meta tags (Open Graph, Twitter Cards).
6
6
  */
7
7
 
8
- import path from 'node:path'
9
- import { getConfig } from '@libs/config'
10
- import { getDocsPublicFsPath } from '@libs/path'
11
8
  import { getRemoteImageSize, getStaticImageSize } from '../../libs/image'
12
9
  import type { Layout } from '../../libs/layout'
10
+ import { getConfig, getPublicFsPath } from '../../site'
13
11
 
14
12
  interface Props {
15
13
  /** Page description */
@@ -28,12 +26,13 @@ const socialImageUrl = new URL(thumbnail, Astro.site)
28
26
  const isRemoteThumbnail = /^https?:\/\//i.test(thumbnail)
29
27
  const socialImageSize = isRemoteThumbnail
30
28
  ? await getRemoteImageSize(thumbnail)
31
- : await getStaticImageSize(path.join(getDocsPublicFsPath(), thumbnail))
29
+ : await getStaticImageSize(getPublicFsPath(thumbnail))
30
+ const { xUsername } = getConfig()
32
31
  ---
33
32
 
34
33
  <meta name="twitter:card" content="summary_lg_image" />
35
- <meta name="twitter:site" content=`@${getConfig().x_username}` />
36
- <meta name="twitter:creator" content=`@${getConfig().x_username}` />
34
+ {xUsername && <meta name="twitter:site" content={`@${xUsername}`} />}
35
+ {xUsername && <meta name="twitter:creator" content={`@${xUsername}`} />}
37
36
  <meta name="twitter:title" content={title} />
38
37
  <meta name="twitter:description" content={description} />
39
38
  <meta name="twitter:image" content={socialImageUrl} />
@@ -5,8 +5,8 @@
5
5
  * Main site navigation bar with menu items and theme toggler.
6
6
  */
7
7
 
8
- import { getConfig } from '@libs/config'
9
8
  import type { Layout } from '../../libs/layout'
9
+ import { getConfig } from '../../site'
10
10
  import NavLink from '../../components/NavLink.astro'
11
11
  import ThemeToggler from '../../components/ThemeToggler.astro'
12
12
  import SearchTrigger from './SearchTrigger.astro'
@@ -19,6 +19,7 @@ interface Props {
19
19
  }
20
20
 
21
21
  const { layout } = Astro.props
22
+ const { figmaHandle, githubOrg } = getConfig()
22
23
  const { pathname } = Astro.url
23
24
  const isActive = (path: string) => {
24
25
  if (path === '/') {
@@ -106,24 +107,32 @@ const isActive = (path: string) => {
106
107
  <hr class="lg:d-none m-0" />
107
108
 
108
109
  <ul class="navbar-nav flex-row flex-wrap lg:ms-auto align-items-center">
109
- <NavLink
110
- class="px-0 lg:px-xs"
111
- href={`https://figma.com/@${getConfig().figma_handle}`}
112
- target="_blank"
113
- rel="noopener"
114
- >
115
- <Icon name="figma-brand" title="Figma" />
116
- <small class="lg:d-none ms-xs">Figma Community</small>
117
- </NavLink>
118
- <NavLink
119
- class="px-0 lg:px-xs"
120
- href={`https://github.com/${getConfig().github_org}`}
121
- target="_blank"
122
- rel="noopener"
123
- >
124
- <Icon name="github-brand" title="GitHub" />
125
- <small class="lg:d-none ms-xs">GitHub</small>
126
- </NavLink>
110
+ {
111
+ figmaHandle && (
112
+ <NavLink
113
+ class="px-0 lg:px-xs"
114
+ href={`https://figma.com/@${figmaHandle}`}
115
+ target="_blank"
116
+ rel="noopener"
117
+ >
118
+ <Icon name="figma-brand" title="Figma" />
119
+ <small class="lg:d-none ms-xs">Figma Community</small>
120
+ </NavLink>
121
+ )
122
+ }
123
+ {
124
+ githubOrg && (
125
+ <NavLink
126
+ class="px-0 lg:px-xs"
127
+ href={`https://github.com/${githubOrg}`}
128
+ target="_blank"
129
+ rel="noopener"
130
+ >
131
+ <Icon name="github-brand" title="GitHub" />
132
+ <small class="lg:d-none ms-xs">GitHub</small>
133
+ </NavLink>
134
+ )
135
+ }
127
136
  <li class="nav-item col-12 lg:col-auto">
128
137
  <div class="vr d-none lg:d-flex h-100 lg:mx-2xs"></div>
129
138
  <hr class="lg:d-none" />