@raystack/chronicle 0.1.0-canary.6b53016 → 0.1.0-canary.9b7d924

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 (79) hide show
  1. package/dist/cli/index.js +501 -9822
  2. package/package.json +16 -12
  3. package/src/cli/__tests__/config.test.ts +25 -0
  4. package/src/cli/__tests__/scaffold.test.ts +10 -0
  5. package/src/cli/commands/build.ts +54 -18
  6. package/src/cli/commands/dev.ts +10 -22
  7. package/src/cli/commands/init.ts +28 -29
  8. package/src/cli/commands/serve.ts +38 -36
  9. package/src/cli/commands/start.ts +12 -21
  10. package/src/cli/utils/config.ts +1 -1
  11. package/src/cli/utils/index.ts +1 -2
  12. package/src/cli/utils/resolve.ts +2 -0
  13. package/src/cli/utils/scaffold.ts +4 -115
  14. package/src/components/mdx/code.tsx +10 -1
  15. package/src/components/mdx/details.module.css +1 -24
  16. package/src/components/mdx/details.tsx +2 -3
  17. package/src/components/mdx/image.tsx +5 -19
  18. package/src/components/mdx/index.tsx +3 -3
  19. package/src/components/mdx/link.tsx +10 -11
  20. package/src/components/ui/footer.tsx +3 -2
  21. package/src/components/ui/search.module.css +7 -0
  22. package/src/components/ui/search.tsx +58 -87
  23. package/src/lib/config.ts +1 -0
  24. package/src/lib/head.tsx +45 -0
  25. package/src/lib/page-context.tsx +95 -0
  26. package/src/lib/source.ts +92 -21
  27. package/src/{app/apis/[[...slug]]/layout.tsx → pages/ApiLayout.tsx} +10 -7
  28. package/src/pages/ApiPage.tsx +68 -0
  29. package/src/pages/DocsLayout.tsx +18 -0
  30. package/src/pages/DocsPage.tsx +43 -0
  31. package/src/pages/NotFound.tsx +10 -0
  32. package/src/pages/__tests__/head.test.tsx +57 -0
  33. package/src/server/App.tsx +59 -0
  34. package/src/server/__tests__/entry-server.test.tsx +35 -0
  35. package/src/server/__tests__/handlers.test.ts +77 -0
  36. package/src/server/__tests__/og.test.ts +23 -0
  37. package/src/server/__tests__/router.test.ts +72 -0
  38. package/src/server/__tests__/vite-config.test.ts +25 -0
  39. package/src/server/adapters/vercel.ts +133 -0
  40. package/src/server/dev.ts +156 -0
  41. package/src/server/entry-client.tsx +74 -0
  42. package/src/server/entry-prod.ts +97 -0
  43. package/src/server/entry-server.tsx +35 -0
  44. package/src/server/entry-vercel.ts +28 -0
  45. package/src/server/handlers/apis-proxy.ts +52 -0
  46. package/src/{app/api/health/route.ts → server/handlers/health.ts} +1 -1
  47. package/src/server/handlers/llms.ts +58 -0
  48. package/src/server/handlers/og.ts +87 -0
  49. package/src/server/handlers/robots.ts +11 -0
  50. package/src/server/handlers/search.ts +140 -0
  51. package/src/server/handlers/sitemap.ts +39 -0
  52. package/src/server/handlers/specs.ts +9 -0
  53. package/src/server/index.html +12 -0
  54. package/src/server/prod.ts +18 -0
  55. package/src/server/request-handler.ts +63 -0
  56. package/src/server/router.ts +42 -0
  57. package/src/server/vite-config.ts +71 -0
  58. package/src/themes/default/Layout.tsx +9 -10
  59. package/src/themes/default/Page.module.css +60 -0
  60. package/src/themes/default/font.ts +4 -6
  61. package/src/themes/paper/ChapterNav.tsx +5 -6
  62. package/src/themes/paper/Page.tsx +8 -9
  63. package/src/types/config.ts +11 -0
  64. package/src/types/content.ts +1 -0
  65. package/tsconfig.json +2 -3
  66. package/next.config.mjs +0 -10
  67. package/source.config.ts +0 -50
  68. package/src/app/[[...slug]]/layout.tsx +0 -15
  69. package/src/app/[[...slug]]/page.tsx +0 -57
  70. package/src/app/api/apis-proxy/route.ts +0 -59
  71. package/src/app/api/search/route.ts +0 -90
  72. package/src/app/apis/[[...slug]]/page.tsx +0 -57
  73. package/src/app/layout.tsx +0 -26
  74. package/src/app/llms-full.txt/route.ts +0 -18
  75. package/src/app/llms.txt/route.ts +0 -15
  76. package/src/app/providers.tsx +0 -8
  77. package/src/cli/utils/process.ts +0 -7
  78. package/src/lib/get-llm-text.ts +0 -10
  79. /package/src/{app/apis/[[...slug]]/layout.module.css → pages/ApiLayout.module.css} +0 -0
@@ -1,33 +1,7 @@
1
- import { execSync } from 'child_process'
2
- import { createRequire } from 'module'
3
1
  import fs from 'fs'
4
2
  import path from 'path'
5
- import chalk from 'chalk'
6
3
  import { PACKAGE_ROOT } from './resolve'
7
4
 
8
- const COPY_FILES = ['src', 'source.config.ts', 'tsconfig.json']
9
-
10
- function copyRecursive(src: string, dest: string) {
11
- const stat = fs.statSync(src)
12
- if (stat.isDirectory()) {
13
- fs.mkdirSync(dest, { recursive: true })
14
- for (const entry of fs.readdirSync(src)) {
15
- copyRecursive(path.join(src, entry), path.join(dest, entry))
16
- }
17
- } else {
18
- fs.copyFileSync(src, dest)
19
- }
20
- }
21
-
22
- function ensureRemoved(targetPath: string) {
23
- try {
24
- fs.lstatSync(targetPath)
25
- fs.rmSync(targetPath, { recursive: true, force: true })
26
- } catch {
27
- // nothing exists, proceed
28
- }
29
- }
30
-
31
5
  export function detectPackageManager(): string {
32
6
  if (process.env.npm_config_user_agent) {
33
7
  return process.env.npm_config_user_agent.split('/')[0]
@@ -39,93 +13,8 @@ export function detectPackageManager(): string {
39
13
  return 'npm'
40
14
  }
41
15
 
42
- function generateNextConfig(scaffoldPath: string) {
43
- const config = `import { createMDX } from 'fumadocs-mdx/next'
44
-
45
- const withMDX = createMDX()
46
-
47
- /** @type {import('next').NextConfig} */
48
- const nextConfig = {
49
- reactStrictMode: true,
50
- }
51
-
52
- export default withMDX(nextConfig)
53
- `
54
- fs.writeFileSync(path.join(scaffoldPath, 'next.config.mjs'), config)
55
- }
56
-
57
- function createPackageJson(): Record<string, unknown> {
58
- return {
59
- name: 'chronicle-docs',
60
- private: true,
61
- dependencies: {
62
- '@raystack/chronicle': 'latest',
63
- },
64
- devDependencies: {
65
- '@raystack/tools-config': '0.56.0',
66
- 'openapi-types': '^12.1.3',
67
- typescript: '5.9.3',
68
- '@types/react': '^19.2.10',
69
- '@types/node': '^25.1.0',
70
- },
71
- }
72
- }
73
-
74
- function ensureDeps() {
75
- const cwd = process.cwd()
76
- const cwdPkgJson = path.join(cwd, 'package.json')
77
- const cwdNodeModules = path.join(cwd, 'node_modules')
78
-
79
- if (fs.existsSync(cwdPkgJson) && fs.existsSync(cwdNodeModules)) {
80
- // Case 1: existing project with deps installed
81
- return
82
- }
83
-
84
- // Case 2: no package.json — create in cwd and install
85
- if (!fs.existsSync(cwdPkgJson)) {
86
- fs.writeFileSync(cwdPkgJson, JSON.stringify(createPackageJson(), null, 2) + '\n')
87
- }
88
-
89
- if (!fs.existsSync(cwdNodeModules)) {
90
- const pm = detectPackageManager()
91
- console.log(chalk.cyan(`Installing dependencies with ${pm}...`))
92
- execSync(`${pm} install`, { cwd, stdio: 'inherit' })
93
- }
94
- }
95
-
96
- export function resolveNextCli(): string {
97
- const cwdRequire = createRequire(path.join(process.cwd(), 'package.json'))
98
- return cwdRequire.resolve('next/dist/bin/next')
99
- }
100
-
101
- export function scaffoldDir(contentDir: string): string {
102
- const scaffoldPath = path.join(process.cwd(), '.chronicle')
103
-
104
- // Create .chronicle/ if not exists
105
- if (!fs.existsSync(scaffoldPath)) {
106
- fs.mkdirSync(scaffoldPath, { recursive: true })
107
- }
108
-
109
- // Copy package files
110
- for (const name of COPY_FILES) {
111
- const src = path.join(PACKAGE_ROOT, name)
112
- const dest = path.join(scaffoldPath, name)
113
- ensureRemoved(dest)
114
- copyRecursive(src, dest)
115
- }
116
-
117
- // Generate next.config.mjs
118
- generateNextConfig(scaffoldPath)
119
-
120
- // Symlink content dir
121
- const contentLink = path.join(scaffoldPath, 'content')
122
- ensureRemoved(contentLink)
123
- fs.symlinkSync(path.resolve(contentDir), contentLink)
124
-
125
- // Ensure dependencies are available
126
- ensureDeps()
127
-
128
- console.log(chalk.gray(`Scaffold: ${scaffoldPath}`))
129
-
130
- return scaffoldPath
16
+ export function getChronicleVersion(): string {
17
+ const pkgPath = path.join(PACKAGE_ROOT, 'package.json')
18
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
19
+ return pkg.version
131
20
  }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
- import type { ComponentProps } from 'react'
3
+ import { type ComponentProps, isValidElement, Children } from 'react'
4
+ import { Mermaid } from './mermaid'
4
5
  import styles from './code.module.css'
5
6
 
6
7
  type PreProps = ComponentProps<'pre'> & {
@@ -16,6 +17,14 @@ export function MdxCode({ children, className, ...props }: ComponentProps<'code'
16
17
  }
17
18
 
18
19
  export function MdxPre({ children, title, className, ...props }: PreProps) {
20
+ // Detect mermaid code blocks
21
+ if (isValidElement(children)) {
22
+ const childProps = children.props as { className?: string; children?: string }
23
+ if (childProps.className?.includes('language-mermaid') && typeof childProps.children === 'string') {
24
+ return <Mermaid chart={childProps.children} />
25
+ }
26
+ }
27
+
19
28
  return (
20
29
  <div className={styles.codeBlock}>
21
30
  {title && <div className={styles.codeHeader}>{title}</div>}
@@ -4,32 +4,9 @@
4
4
  margin: var(--rs-space-5) 0;
5
5
  }
6
6
 
7
- .summary {
8
- padding: var(--rs-space-4) var(--rs-space-5);
9
- cursor: pointer;
7
+ .trigger {
10
8
  font-weight: 500;
11
9
  font-size: var(--rs-font-size-small);
12
- color: var(--rs-color-text-base-primary);
13
- background: var(--rs-color-background-base-secondary);
14
- list-style: none;
15
- display: flex;
16
- align-items: center;
17
- gap: var(--rs-space-3);
18
- }
19
-
20
- .summary::-webkit-details-marker {
21
- display: none;
22
- }
23
-
24
- .summary::before {
25
- content: '▶';
26
- font-size: 10px;
27
- transition: transform 0.2s ease;
28
- color: var(--rs-color-text-base-secondary);
29
- }
30
-
31
- .details[open] > .summary::before {
32
- transform: rotate(90deg);
33
10
  }
34
11
 
35
12
  .content {
@@ -1,9 +1,8 @@
1
1
  import type { ComponentProps } from 'react'
2
- import styles from './details.module.css'
3
2
 
4
3
  export function MdxDetails({ children, className, ...props }: ComponentProps<'details'>) {
5
4
  return (
6
- <details className={`${styles.details} ${className ?? ''}`} {...props}>
5
+ <details className={className} {...props}>
7
6
  {children}
8
7
  </details>
9
8
  )
@@ -11,7 +10,7 @@ export function MdxDetails({ children, className, ...props }: ComponentProps<'de
11
10
 
12
11
  export function MdxSummary({ children, className, ...props }: ComponentProps<'summary'>) {
13
12
  return (
14
- <summary className={`${styles.summary} ${className ?? ''}`} {...props}>
13
+ <summary className={className} {...props}>
15
14
  {children}
16
15
  </summary>
17
16
  )
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
- import NextImage from 'next/image'
4
3
  import type { ComponentProps } from 'react'
5
4
 
6
5
  type ImageProps = Omit<ComponentProps<'img'>, 'src'> & {
@@ -12,27 +11,14 @@ type ImageProps = Omit<ComponentProps<'img'>, 'src'> & {
12
11
  export function Image({ src, alt, width, height, ...props }: ImageProps) {
13
12
  if (!src || typeof src !== 'string') return null
14
13
 
15
- const isExternal = src.startsWith('http://') || src.startsWith('https://')
16
-
17
- if (isExternal) {
18
- return (
19
- // eslint-disable-next-line @next/next/no-img-element
20
- <img
21
- src={src}
22
- alt={alt ?? ''}
23
- width={width}
24
- height={height}
25
- {...props}
26
- />
27
- )
28
- }
29
-
30
14
  return (
31
- <NextImage
15
+ <img
32
16
  src={src}
33
17
  alt={alt ?? ''}
34
- width={typeof width === 'string' ? parseInt(width, 10) : (width ?? 800)}
35
- height={typeof height === 'string' ? parseInt(height, 10) : (height ?? 400)}
18
+ width={width}
19
+ height={height}
20
+ loading="lazy"
21
+ {...props}
36
22
  />
37
23
  )
38
24
  }
@@ -1,6 +1,6 @@
1
1
  import type { MDXComponents } from 'mdx/types'
2
2
  import { Image } from './image'
3
- import { Link } from './link'
3
+ import { MdxLink } from './link'
4
4
  import { MdxTable, MdxThead, MdxTbody, MdxTr, MdxTh, MdxTd } from './table'
5
5
  import { MdxPre, MdxCode } from './code'
6
6
  import { MdxDetails, MdxSummary } from './details'
@@ -12,7 +12,7 @@ import { Tabs } from '@raystack/apsara'
12
12
  export const mdxComponents: MDXComponents = {
13
13
  p: MdxParagraph,
14
14
  img: Image,
15
- a: Link,
15
+ a: MdxLink,
16
16
  table: MdxTable,
17
17
  thead: MdxThead,
18
18
  tbody: MdxTbody,
@@ -32,4 +32,4 @@ export const mdxComponents: MDXComponents = {
32
32
  }
33
33
 
34
34
  export { Image } from './image'
35
- export { Link } from './link'
35
+ export { MdxLink } from './link'
@@ -1,12 +1,11 @@
1
1
  'use client'
2
2
 
3
- import NextLink from 'next/link'
4
- import { Link as ApsaraLink } from '@raystack/apsara'
3
+ import { Link } from 'react-router-dom'
5
4
  import type { ComponentProps } from 'react'
6
5
 
7
6
  type LinkProps = ComponentProps<'a'>
8
7
 
9
- export function Link({ href, children, ...props }: LinkProps) {
8
+ export function MdxLink({ href, children, ...props }: LinkProps) {
10
9
  if (!href) {
11
10
  return <span {...props}>{children}</span>
12
11
  }
@@ -14,25 +13,25 @@ export function Link({ href, children, ...props }: LinkProps) {
14
13
  const isExternal = href.startsWith('http://') || href.startsWith('https://')
15
14
  const isAnchor = href.startsWith('#')
16
15
 
17
- if (isAnchor) {
16
+ if (isExternal) {
18
17
  return (
19
- <ApsaraLink href={href} {...props}>
18
+ <a href={href} target="_blank" rel="noopener noreferrer" {...props}>
20
19
  {children}
21
- </ApsaraLink>
20
+ </a>
22
21
  )
23
22
  }
24
23
 
25
- if (isExternal) {
24
+ if (isAnchor) {
26
25
  return (
27
- <ApsaraLink href={href} target="_blank" rel="noopener noreferrer" {...props}>
26
+ <a href={href} {...props}>
28
27
  {children}
29
- </ApsaraLink>
28
+ </a>
30
29
  )
31
30
  }
32
31
 
33
32
  return (
34
- <NextLink href={href} className={props.className}>
33
+ <Link to={href} className={props.className}>
35
34
  {children}
36
- </NextLink>
35
+ </Link>
37
36
  )
38
37
  }
@@ -1,4 +1,5 @@
1
- import { Flex, Link, Text } from "@raystack/apsara";
1
+ import { Link } from "react-router-dom";
2
+ import { Flex, Text } from "@raystack/apsara";
2
3
  import type { FooterConfig } from "@/types";
3
4
  import styles from "./footer.module.css";
4
5
 
@@ -18,7 +19,7 @@ export function Footer({ config }: FooterProps) {
18
19
  {config?.links && config.links.length > 0 && (
19
20
  <Flex gap="medium" className={styles.links}>
20
21
  {config.links.map((link) => (
21
- <Link key={link.href} href={link.href} className={styles.link}>
22
+ <Link key={link.href} to={link.href} className={styles.link}>
22
23
  {link.label}
23
24
  </Link>
24
25
  ))}
@@ -102,3 +102,10 @@
102
102
  .item[data-selected="true"] .icon {
103
103
  color: var(--rs-color-foreground-accent-primary-hover);
104
104
  }
105
+
106
+ .pageText :global(mark),
107
+ .headingText :global(mark) {
108
+ background: transparent;
109
+ color: var(--rs-color-foreground-accent-primary);
110
+ font-weight: 600;
111
+ }
@@ -1,21 +1,50 @@
1
1
  "use client";
2
2
 
3
- import { useState, useEffect, useCallback } from "react";
4
- import { useRouter } from "next/navigation";
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import { useNavigate } from "react-router-dom";
5
5
  import { Button, Command, Dialog, Text } from "@raystack/apsara";
6
6
  import { cx } from "class-variance-authority";
7
- import { useDocsSearch } from "fumadocs-core/search/client";
8
- import type { SortedResult } from "fumadocs-core/search";
9
7
  import { DocumentIcon, HashtagIcon } from "@heroicons/react/24/outline";
10
- import { isMacOs } from "react-device-detect";
11
8
  import { MethodBadge } from "@/components/api/method-badge";
12
9
  import styles from "./search.module.css";
13
10
 
11
+ interface SearchResult {
12
+ id: string;
13
+ url: string;
14
+ type: "page" | "api";
15
+ content: string;
16
+ }
17
+
18
+ function useSearch(query: string) {
19
+ const [results, setResults] = useState<SearchResult[]>([]);
20
+ const [isLoading, setIsLoading] = useState(false);
21
+ const timerRef = useRef<ReturnType<typeof setTimeout>>();
22
+
23
+ useEffect(() => {
24
+ clearTimeout(timerRef.current);
25
+ timerRef.current = setTimeout(async () => {
26
+ setIsLoading(true);
27
+ try {
28
+ const params = new URLSearchParams();
29
+ if (query) params.set("query", query);
30
+ const res = await fetch(`/api/search?${params}`);
31
+ setResults(await res.json());
32
+ } catch {
33
+ setResults([]);
34
+ }
35
+ setIsLoading(false);
36
+ }, 100);
37
+ return () => clearTimeout(timerRef.current);
38
+ }, [query]);
39
+
40
+ return { results, isLoading };
41
+ }
42
+
14
43
  function SearchShortcutKey({ className }: { className?: string }) {
15
- const [key, setKey] = useState("");
44
+ const [key, setKey] = useState("\u2318");
16
45
 
17
46
  useEffect(() => {
18
- setKey(isMacOs ? "" : "Ctrl");
47
+ setKey(navigator.platform?.startsWith("Mac") ? "\u2318" : "Ctrl");
19
48
  }, []);
20
49
 
21
50
  return (
@@ -26,26 +55,21 @@ function SearchShortcutKey({ className }: { className?: string }) {
26
55
  }
27
56
 
28
57
  interface SearchProps {
29
- className?: string
58
+ className?: string;
30
59
  }
31
60
 
32
61
  export function Search({ className }: SearchProps) {
33
62
  const [open, setOpen] = useState(false);
34
- const router = useRouter();
35
-
36
- const { search, setSearch, query } = useDocsSearch({
37
- type: "fetch",
38
- api: "/api/search",
39
- delayMs: 100,
40
- allowEmpty: true,
41
- });
63
+ const navigate = useNavigate();
64
+ const [search, setSearch] = useState("");
65
+ const { results, isLoading } = useSearch(search);
42
66
 
43
67
  const onSelect = useCallback(
44
68
  (url: string) => {
45
69
  setOpen(false);
46
- router.push(url);
70
+ navigate(url);
47
71
  },
48
- [router],
72
+ [navigate],
49
73
  );
50
74
 
51
75
  useEffect(() => {
@@ -60,10 +84,6 @@ export function Search({ className }: SearchProps) {
60
84
  return () => document.removeEventListener("keydown", down);
61
85
  }, []);
62
86
 
63
- const results = deduplicateByUrl(
64
- query.data === "empty" ? [] : (query.data ?? []),
65
- );
66
-
67
87
  return (
68
88
  <>
69
89
  <Button
@@ -91,17 +111,17 @@ export function Search({ className }: SearchProps) {
91
111
  />
92
112
 
93
113
  <Command.List className={styles.list}>
94
- {query.isLoading && <Command.Empty>Loading...</Command.Empty>}
95
- {!query.isLoading &&
114
+ {isLoading && <Command.Empty>Loading...</Command.Empty>}
115
+ {!isLoading &&
96
116
  search.length > 0 &&
97
117
  results.length === 0 && (
98
118
  <Command.Empty>No results found.</Command.Empty>
99
119
  )}
100
- {!query.isLoading &&
120
+ {!isLoading &&
101
121
  search.length === 0 &&
102
122
  results.length > 0 && (
103
123
  <Command.Group heading="Suggestions">
104
- {results.slice(0, 8).map((result: SortedResult) => (
124
+ {results.slice(0, 8).map((result) => (
105
125
  <Command.Item
106
126
  key={result.id}
107
127
  value={result.id}
@@ -111,7 +131,7 @@ export function Search({ className }: SearchProps) {
111
131
  <div className={styles.itemContent}>
112
132
  {getResultIcon(result)}
113
133
  <Text className={styles.pageText}>
114
- {stripMethod(result.content)}
134
+ {result.content}
115
135
  </Text>
116
136
  </div>
117
137
  </Command.Item>
@@ -119,7 +139,7 @@ export function Search({ className }: SearchProps) {
119
139
  </Command.Group>
120
140
  )}
121
141
  {search.length > 0 &&
122
- results.map((result: SortedResult) => (
142
+ results.map((result) => (
123
143
  <Command.Item
124
144
  key={result.id}
125
145
  value={result.id}
@@ -128,23 +148,9 @@ export function Search({ className }: SearchProps) {
128
148
  >
129
149
  <div className={styles.itemContent}>
130
150
  {getResultIcon(result)}
131
- <div className={styles.resultText}>
132
- {result.type === "heading" ? (
133
- <>
134
- <Text className={styles.headingText}>
135
- {stripMethod(result.content)}
136
- </Text>
137
- <Text className={styles.separator}>-</Text>
138
- <Text className={styles.pageText}>
139
- {getPageTitle(result.url)}
140
- </Text>
141
- </>
142
- ) : (
143
- <Text className={styles.pageText}>
144
- {stripMethod(result.content)}
145
- </Text>
146
- )}
147
- </div>
151
+ <Text className={styles.pageText}>
152
+ {result.content}
153
+ </Text>
148
154
  </div>
149
155
  </Command.Item>
150
156
  ))}
@@ -156,47 +162,12 @@ export function Search({ className }: SearchProps) {
156
162
  );
157
163
  }
158
164
 
159
- function deduplicateByUrl(results: SortedResult[]): SortedResult[] {
160
- const seen = new Set<string>();
161
- return results.filter((r) => {
162
- const base = r.url.split("#")[0];
163
- if (seen.has(base)) return false;
164
- seen.add(base);
165
- return true;
166
- });
167
- }
168
-
169
- const API_METHODS = new Set(["GET", "POST", "PUT", "DELETE", "PATCH"]);
170
-
171
- function extractMethod(content: string): string | null {
172
- const first = content.split(" ")[0];
173
- return API_METHODS.has(first) ? first : null;
174
- }
175
-
176
- function stripMethod(content: string): string {
177
- const first = content.split(" ")[0];
178
- return API_METHODS.has(first) ? content.slice(first.length + 1) : content;
179
- }
180
-
181
- function getResultIcon(result: SortedResult): React.ReactNode {
182
- if (!result.url.startsWith("/apis/")) {
183
- return result.type === "page" ? (
184
- <DocumentIcon className={styles.icon} />
185
- ) : (
186
- <HashtagIcon className={styles.icon} />
187
- );
165
+ function getResultIcon(result: SearchResult): React.ReactNode {
166
+ if (result.type === "api") {
167
+ const method = result.content.split(" ")[0];
168
+ return ["GET", "POST", "PUT", "DELETE", "PATCH"].includes(method)
169
+ ? <MethodBadge method={method} size="micro" />
170
+ : null;
188
171
  }
189
- const method = extractMethod(result.content);
190
- return method ? <MethodBadge method={method} size="micro" /> : null;
191
- }
192
-
193
- function getPageTitle(url: string): string {
194
- const path = url.split("#")[0];
195
- const segments = path.split("/").filter(Boolean);
196
- const lastSegment = segments[segments.length - 1];
197
- if (!lastSegment) return "Home";
198
- return lastSegment
199
- .split("-")
200
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
201
- .join(" ");
172
+ return <DocumentIcon className={styles.icon} />;
202
173
  }
package/src/lib/config.ts CHANGED
@@ -51,5 +51,6 @@ export function loadConfig(): ChronicleConfig {
51
51
  footer: userConfig.footer,
52
52
  api: userConfig.api,
53
53
  llms: { enabled: false, ...userConfig.llms },
54
+ analytics: { enabled: false, ...userConfig.analytics },
54
55
  }
55
56
  }
@@ -0,0 +1,45 @@
1
+ import type { ChronicleConfig } from '@/types'
2
+
3
+ export interface HeadProps {
4
+ title: string
5
+ description?: string
6
+ config: ChronicleConfig
7
+ jsonLd?: Record<string, unknown>
8
+ }
9
+
10
+ export function Head({ title, description, config, jsonLd }: HeadProps) {
11
+ const fullTitle = `${title} | ${config.title}`
12
+ const ogParams = new URLSearchParams({ title })
13
+ if (description) ogParams.set('description', description)
14
+
15
+ return (
16
+ <>
17
+ <title>{fullTitle}</title>
18
+ {description && <meta name="description" content={description} />}
19
+
20
+ {config.url && (
21
+ <>
22
+ <meta property="og:title" content={title} />
23
+ {description && <meta property="og:description" content={description} />}
24
+ <meta property="og:site_name" content={config.title} />
25
+ <meta property="og:type" content="website" />
26
+ <meta property="og:image" content={`/og?${ogParams.toString()}`} />
27
+ <meta property="og:image:width" content="1200" />
28
+ <meta property="og:image:height" content="630" />
29
+
30
+ <meta name="twitter:card" content="summary_large_image" />
31
+ <meta name="twitter:title" content={title} />
32
+ {description && <meta name="twitter:description" content={description} />}
33
+ <meta name="twitter:image" content={`/og?${ogParams.toString()}`} />
34
+ </>
35
+ )}
36
+
37
+ {jsonLd && (
38
+ <script
39
+ type="application/ld+json"
40
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd, null, 2) }}
41
+ />
42
+ )}
43
+ </>
44
+ )
45
+ }