@raystack/chronicle 0.2.0 → 0.3.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": "@raystack/chronicle",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Config-driven documentation framework",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
package/source.config.ts CHANGED
@@ -11,6 +11,7 @@ export const docs = defineDocs({
11
11
  docs: {
12
12
  schema: frontmatterSchema.extend({
13
13
  order: z.number().optional(),
14
+ lastModified: z.string().optional(),
14
15
  }),
15
16
  postprocess: {
16
17
  includeProcessedMarkdown: true,
@@ -1,3 +1,4 @@
1
+ import type { Metadata, ResolvingMetadata } from 'next'
1
2
  import { notFound } from 'next/navigation'
2
3
  import type { MDXContent } from 'mdx/types'
3
4
  import { loadConfig } from '@/lib/config'
@@ -16,6 +17,41 @@ interface PageData {
16
17
  toc: { title: string; url: string; depth: number }[]
17
18
  }
18
19
 
20
+ export async function generateMetadata(
21
+ { params }: PageProps,
22
+ parent: ResolvingMetadata,
23
+ ): Promise<Metadata> {
24
+ const { slug } = await params
25
+ const page = source.getPage(slug)
26
+ if (!page) return {}
27
+ const config = loadConfig()
28
+ const data = page.data as PageData
29
+ const parentMetadata = await parent
30
+
31
+ const metadata: Metadata = {
32
+ title: data.title,
33
+ description: data.description,
34
+ }
35
+
36
+ if (config.url) {
37
+ const ogParams = new URLSearchParams({ title: data.title })
38
+ if (data.description) ogParams.set('description', data.description)
39
+ metadata.openGraph = {
40
+ ...parentMetadata.openGraph,
41
+ title: data.title,
42
+ description: data.description,
43
+ images: [{ url: `/og?${ogParams.toString()}`, width: 1200, height: 630 }],
44
+ }
45
+ metadata.twitter = {
46
+ ...parentMetadata.twitter,
47
+ title: data.title,
48
+ description: data.description,
49
+ }
50
+ }
51
+
52
+ return metadata
53
+ }
54
+
19
55
  export default async function DocsPage({ params }: PageProps) {
20
56
  const { slug } = await params
21
57
  const config = loadConfig()
@@ -33,20 +69,33 @@ export default async function DocsPage({ params }: PageProps) {
33
69
 
34
70
  const tree = buildPageTree()
35
71
 
72
+ const pageUrl = config.url ? `${config.url}/${(slug ?? []).join('/')}` : undefined
73
+
36
74
  return (
37
- <Page
38
- page={{
39
- slug: slug ?? [],
40
- frontmatter: {
41
- title: data.title,
75
+ <>
76
+ <script type="application/ld+json">
77
+ {JSON.stringify({
78
+ '@context': 'https://schema.org',
79
+ '@type': 'Article',
80
+ headline: data.title,
42
81
  description: data.description,
43
- },
44
- content: <MDXBody components={mdxComponents} />,
45
- toc: data.toc ?? [],
46
- }}
47
- config={config}
48
- tree={tree}
49
- />
82
+ ...(pageUrl && { url: pageUrl }),
83
+ }, null, 2)}
84
+ </script>
85
+ <Page
86
+ page={{
87
+ slug: slug ?? [],
88
+ frontmatter: {
89
+ title: data.title,
90
+ description: data.description,
91
+ },
92
+ content: <MDXBody components={mdxComponents} />,
93
+ toc: data.toc ?? [],
94
+ }}
95
+ config={config}
96
+ tree={tree}
97
+ />
98
+ </>
50
99
  )
51
100
  }
52
101
 
@@ -1,3 +1,4 @@
1
+ import type { Metadata, ResolvingMetadata } from 'next'
1
2
  import { notFound } from 'next/navigation'
2
3
  import type { OpenAPIV3 } from 'openapi-types'
3
4
  import { Flex, Headline, Text } from '@raystack/apsara'
@@ -10,6 +11,65 @@ interface PageProps {
10
11
  params: Promise<{ slug?: string[] }>
11
12
  }
12
13
 
14
+ export async function generateMetadata(
15
+ { params }: PageProps,
16
+ parent: ResolvingMetadata,
17
+ ): Promise<Metadata> {
18
+ const { slug } = await params
19
+ const config = loadConfig()
20
+ const specs = loadApiSpecs(config.api ?? [])
21
+ const parentMetadata = await parent
22
+
23
+ if (!slug || slug.length === 0) {
24
+ const apiDescription = `API documentation for ${config.title}`
25
+ const metadata: Metadata = {
26
+ title: 'API Reference',
27
+ description: apiDescription,
28
+ }
29
+ if (config.url) {
30
+ metadata.openGraph = {
31
+ ...parentMetadata.openGraph,
32
+ title: 'API Reference',
33
+ description: apiDescription,
34
+ images: [{ url: `/og?title=${encodeURIComponent('API Reference')}&description=${encodeURIComponent(apiDescription)}`, width: 1200, height: 630 }],
35
+ }
36
+ metadata.twitter = {
37
+ ...parentMetadata.twitter,
38
+ title: 'API Reference',
39
+ description: apiDescription,
40
+ }
41
+ }
42
+ return metadata
43
+ }
44
+
45
+ const match = findApiOperation(specs, slug)
46
+ if (!match) return {}
47
+
48
+ const operation = match.operation as OpenAPIV3.OperationObject
49
+ const title = operation.summary ?? `${match.method.toUpperCase()} ${match.path}`
50
+ const description = operation.description
51
+
52
+ const metadata: Metadata = { title, description }
53
+
54
+ if (config.url) {
55
+ const ogParams = new URLSearchParams({ title })
56
+ if (description) ogParams.set('description', description)
57
+ metadata.openGraph = {
58
+ ...parentMetadata.openGraph,
59
+ title,
60
+ description,
61
+ images: [{ url: `/og?${ogParams.toString()}`, width: 1200, height: 630 }],
62
+ }
63
+ metadata.twitter = {
64
+ ...parentMetadata.twitter,
65
+ title,
66
+ description,
67
+ }
68
+ }
69
+
70
+ return metadata
71
+ }
72
+
13
73
  export default async function ApiPage({ params }: PageProps) {
14
74
  const { slug } = await params
15
75
  const config = loadConfig()
@@ -7,8 +7,28 @@ import { Providers } from './providers'
7
7
  const config = loadConfig()
8
8
 
9
9
  export const metadata: Metadata = {
10
- title: config.title,
10
+ title: {
11
+ default: config.title,
12
+ template: `%s | ${config.title}`,
13
+ },
11
14
  description: config.description,
15
+ ...(config.url && {
16
+ metadataBase: new URL(config.url),
17
+ openGraph: {
18
+ title: config.title,
19
+ description: config.description,
20
+ url: config.url,
21
+ siteName: config.title,
22
+ type: 'website',
23
+ images: [{ url: '/og?title=' + encodeURIComponent(config.title), width: 1200, height: 630 }],
24
+ },
25
+ twitter: {
26
+ card: 'summary_large_image',
27
+ title: config.title,
28
+ description: config.description,
29
+ images: ['/og?title=' + encodeURIComponent(config.title)],
30
+ },
31
+ }),
12
32
  }
13
33
 
14
34
  export default function RootLayout({
@@ -19,6 +39,17 @@ export default function RootLayout({
19
39
  return (
20
40
  <html lang="en" suppressHydrationWarning>
21
41
  <body suppressHydrationWarning>
42
+ {config.url && (
43
+ <script type="application/ld+json">
44
+ {JSON.stringify({
45
+ '@context': 'https://schema.org',
46
+ '@type': 'WebSite',
47
+ name: config.title,
48
+ description: config.description,
49
+ url: config.url,
50
+ }, null, 2)}
51
+ </script>
52
+ )}
22
53
  <Providers>{children}</Providers>
23
54
  </body>
24
55
  </html>
@@ -0,0 +1,62 @@
1
+ import { ImageResponse } from 'next/og'
2
+ import type { NextRequest } from 'next/server'
3
+ import { loadConfig } from '@/lib/config'
4
+
5
+ export async function GET(request: NextRequest) {
6
+ const { searchParams } = request.nextUrl
7
+ const title = searchParams.get('title') ?? loadConfig().title
8
+ const description = searchParams.get('description') ?? ''
9
+ const siteName = loadConfig().title
10
+
11
+ return new ImageResponse(
12
+ (
13
+ <div
14
+ style={{
15
+ height: '100%',
16
+ width: '100%',
17
+ display: 'flex',
18
+ flexDirection: 'column',
19
+ justifyContent: 'center',
20
+ padding: '60px 80px',
21
+ backgroundColor: '#0a0a0a',
22
+ color: '#fafafa',
23
+ }}
24
+ >
25
+ <div
26
+ style={{
27
+ fontSize: 24,
28
+ color: '#888',
29
+ marginBottom: 16,
30
+ }}
31
+ >
32
+ {siteName}
33
+ </div>
34
+ <div
35
+ style={{
36
+ fontSize: 56,
37
+ fontWeight: 700,
38
+ lineHeight: 1.2,
39
+ marginBottom: 24,
40
+ }}
41
+ >
42
+ {title}
43
+ </div>
44
+ {description && (
45
+ <div
46
+ style={{
47
+ fontSize: 24,
48
+ color: '#999',
49
+ lineHeight: 1.4,
50
+ }}
51
+ >
52
+ {description}
53
+ </div>
54
+ )}
55
+ </div>
56
+ ),
57
+ {
58
+ width: 1200,
59
+ height: 630,
60
+ }
61
+ )
62
+ }
@@ -0,0 +1,10 @@
1
+ import type { MetadataRoute } from 'next'
2
+ import { loadConfig } from '@/lib/config'
3
+
4
+ export default function robots(): MetadataRoute.Robots {
5
+ const config = loadConfig()
6
+ return {
7
+ rules: { userAgent: '*', allow: '/' },
8
+ ...(config.url && { sitemap: `${config.url}/sitemap.xml` }),
9
+ }
10
+ }
@@ -0,0 +1,29 @@
1
+ import type { MetadataRoute } from 'next'
2
+ import { loadConfig } from '@/lib/config'
3
+ import { source } from '@/lib/source'
4
+ import { loadApiSpecs } from '@/lib/openapi'
5
+ import { buildApiRoutes } from '@/lib/api-routes'
6
+
7
+ export default function sitemap(): MetadataRoute.Sitemap {
8
+ const config = loadConfig()
9
+ if (!config.url) return []
10
+
11
+ const baseUrl = config.url.replace(/\/$/, '')
12
+
13
+ const docPages = source.getPages().map((page) => ({
14
+ url: `${baseUrl}/${page.slugs.join('/')}`,
15
+ ...(page.data.lastModified && { lastModified: new Date(page.data.lastModified) }),
16
+ }))
17
+
18
+ const apiPages = config.api?.length
19
+ ? buildApiRoutes(loadApiSpecs(config.api)).map((route) => ({
20
+ url: `${baseUrl}/apis/${route.slug.join('/')}`,
21
+ }))
22
+ : []
23
+
24
+ return [
25
+ { url: baseUrl },
26
+ ...docPages,
27
+ ...apiPages,
28
+ ]
29
+ }
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
  }
@@ -1,6 +1,7 @@
1
1
  export interface ChronicleConfig {
2
2
  title: string
3
3
  description?: string
4
+ url?: string
4
5
  logo?: LogoConfig
5
6
  theme?: ThemeConfig
6
7
  navigation?: NavigationConfig
@@ -8,12 +9,22 @@ export interface ChronicleConfig {
8
9
  footer?: FooterConfig
9
10
  api?: ApiConfig[]
10
11
  llms?: LlmsConfig
12
+ analytics?: AnalyticsConfig
11
13
  }
12
14
 
13
15
  export interface LlmsConfig {
14
16
  enabled?: boolean
15
17
  }
16
18
 
19
+ export interface AnalyticsConfig {
20
+ enabled?: boolean
21
+ googleAnalytics?: GoogleAnalyticsConfig
22
+ }
23
+
24
+ export interface GoogleAnalyticsConfig {
25
+ measurementId: string
26
+ }
27
+
17
28
  export interface ApiConfig {
18
29
  name: string
19
30
  spec: string
@@ -5,6 +5,7 @@ export interface Frontmatter {
5
5
  description?: string
6
6
  order?: number
7
7
  icon?: string
8
+ lastModified?: string
8
9
  }
9
10
 
10
11
  export interface Page {