@levino/shipyard-blog 0.7.4 → 0.7.5

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.
@@ -3,36 +3,30 @@
3
3
  * Blog archive page - shows all posts organized by year.
4
4
  */
5
5
  import { i18n } from 'astro:config/server'
6
- import { type CollectionEntry, getCollection } from 'astro:content'
7
- import blogConfig from 'virtual:shipyard-blog/config'
8
- import type { GetStaticPaths } from 'astro'
6
+ import {
7
+ type CollectionEntry,
8
+ type CollectionKey,
9
+ getCollection,
10
+ } from 'astro:content'
11
+ import blogConfigDefault from 'virtual:shipyard-blog/config'
9
12
  import { groupBy, map, pipe, prop, reverse, sortBy, toPairs } from 'ramda'
10
13
  import Layout from './Layout.astro'
11
14
 
15
+ const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
16
+ const collectionName =
17
+ Astro.props.collectionName ?? blogConfigDefault.collectionName
18
+
12
19
  const { includeDraftsInDev, routeBasePath } = blogConfig
13
20
 
14
21
  // Filter out draft and unlisted posts
15
22
  const isDev = import.meta.env.DEV
16
- const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
23
+ const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
17
24
  if (post.data.unlisted) return false
18
25
  if (post.data.draft && !(isDev && includeDraftsInDev)) return false
19
26
  return true
20
27
  }
21
28
 
22
- export const getStaticPaths = (() => {
23
- if (i18n) {
24
- return i18n.locales.map((locale) => {
25
- if (typeof locale !== 'string') {
26
- throw new Error('shipyard does only support strings as locales.')
27
- }
28
- return { params: { locale } }
29
- })
30
- } else {
31
- return [{ params: {} }]
32
- }
33
- }) satisfies GetStaticPaths
34
-
35
- const allPosts = await getCollection('blog')
29
+ const allPosts = await getCollection(collectionName as CollectionKey)
36
30
  const filteredPosts = allPosts.filter(shouldIncludePost).filter(({ id }) => {
37
31
  if (i18n) {
38
32
  const [postLocale] = id.split('/')
@@ -49,10 +43,10 @@ const sortedPosts = filteredPosts.toSorted(
49
43
  // Group posts by year
50
44
  type YearPosts = {
51
45
  year: string
52
- posts: CollectionEntry<'blog'>[]
46
+ posts: CollectionEntry<CollectionKey>[]
53
47
  }
54
48
 
55
- const getYearFromPost = (post: CollectionEntry<'blog'>): string =>
49
+ const getYearFromPost = (post: CollectionEntry<CollectionKey>): string =>
56
50
  post.data.date.getFullYear().toString()
57
51
 
58
52
  const groupPostsByYear = pipe(
@@ -61,7 +55,7 @@ const groupPostsByYear = pipe(
61
55
  map(([year, posts]): YearPosts => ({ year, posts: posts ?? [] })),
62
56
  sortBy(prop('year')),
63
57
  reverse,
64
- ) as (posts: CollectionEntry<'blog'>[]) => YearPosts[]
58
+ ) as (posts: CollectionEntry<CollectionKey>[]) => YearPosts[]
65
59
 
66
60
  const postsByYear = groupPostsByYear(sortedPosts)
67
61
 
@@ -79,7 +73,7 @@ const getBlogPostLink = (id: string, locale?: string) => {
79
73
  }
80
74
  ---
81
75
 
82
- <Layout title="Archive">
76
+ <Layout title="Archive" blogConfig={blogConfig} collectionName={collectionName}>
83
77
  <div class="max-w-4xl mx-auto">
84
78
  <h1 class="text-4xl font-bold mb-8">Archive</h1>
85
79
  <p class="text-base-content/60 mb-8">
@@ -1,149 +1,29 @@
1
1
  ---
2
- /**
3
- * Individual author page showing author info and their posts.
4
- */
5
2
  import { i18n } from 'astro:config/server'
6
- import { type CollectionEntry, getCollection } from 'astro:content'
7
- import blogConfig from 'virtual:shipyard-blog/config'
8
- import Layout from '@levino/shipyard-base/layouts/Page.astro'
9
3
  import {
10
- filter,
11
- flatten,
12
- map,
13
- pipe,
14
- prop,
15
- reverse,
16
- sortBy,
17
- uniqBy,
18
- } from 'ramda'
4
+ type CollectionEntry,
5
+ type CollectionKey,
6
+ getCollection,
7
+ } from 'astro:content'
8
+ import blogConfigDefault from 'virtual:shipyard-blog/config'
9
+ import Layout from '@levino/shipyard-base/layouts/Page.astro'
10
+ import { filter, pipe, reverse, sortBy } from 'ramda'
19
11
  import type { Author } from '../src/index'
20
12
  import { getReadingTime } from '../src/readingTime'
21
13
 
14
+ const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
15
+ const collectionName =
16
+ Astro.props.collectionName ?? blogConfigDefault.collectionName
17
+
22
18
  const {
23
19
  showReadingTime,
24
20
  includeDraftsInDev: includeDraftsInDevConfig,
25
21
  routeBasePath,
26
22
  } = blogConfig
27
23
 
28
- export async function getStaticPaths() {
29
- // Must inline all helper functions and values because Astro hoists getStaticPaths to a separate module
30
- const { authorsEnabled, includeDraftsInDev } = blogConfig
31
- const isDev = import.meta.env.DEV
32
-
33
- const normalizeAuthorInner = (
34
- author:
35
- | string
36
- | {
37
- name: string
38
- title?: string
39
- url?: string
40
- image_url?: string
41
- email?: string
42
- },
43
- ): {
44
- name: string
45
- title?: string
46
- url?: string
47
- image_url?: string
48
- email?: string
49
- } => {
50
- if (typeof author === 'string') {
51
- return { name: author }
52
- }
53
- return author
54
- }
55
-
56
- const getAuthorsFromPostInner = (
57
- post: CollectionEntry<'blog'>,
58
- ): {
59
- name: string
60
- title?: string
61
- url?: string
62
- image_url?: string
63
- email?: string
64
- }[] => {
65
- const authors = post.data.authors
66
- if (!authors) return []
67
- if (Array.isArray(authors)) {
68
- return authors.map(normalizeAuthorInner)
69
- }
70
- return [
71
- normalizeAuthorInner(
72
- authors as
73
- | string
74
- | {
75
- name: string
76
- title?: string
77
- url?: string
78
- image_url?: string
79
- email?: string
80
- },
81
- ),
82
- ]
83
- }
84
-
85
- const getAuthorSlugInner = (name: string): string =>
86
- name.toLowerCase().replace(/\s+/g, '-')
87
-
88
- const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
89
- if (post.data.unlisted) return false
90
- if (post.data.draft && !(isDev && includeDraftsInDev)) return false
91
- return true
92
- }
93
-
94
- if (!authorsEnabled) {
95
- return []
96
- }
97
-
98
- const allPosts = await getCollection('blog')
99
-
100
- // Get all unique authors
101
- const getAllAuthors = pipe(
102
- filter(shouldIncludePost),
103
- map(getAuthorsFromPostInner),
104
- flatten,
105
- uniqBy(prop('name')),
106
- )
107
-
108
- if (i18n) {
109
- const paths: {
110
- params: { locale: string; author: string }
111
- props: { author: Author }
112
- }[] = []
113
-
114
- for (const locale of i18n.locales) {
115
- if (typeof locale !== 'string') {
116
- throw new Error('shipyard does only support strings as locales.')
117
- }
118
-
119
- const localePosts = allPosts.filter((post) => {
120
- const [postLocale] = post.id.split('/')
121
- return postLocale === locale
122
- })
123
-
124
- const authors = getAllAuthors(localePosts) as Author[]
125
-
126
- for (const author of authors) {
127
- paths.push({
128
- params: { locale, author: getAuthorSlugInner(author.name) },
129
- props: { author },
130
- })
131
- }
132
- }
133
-
134
- return paths
135
- } else {
136
- const authors = getAllAuthors(allPosts) as Author[]
137
- return authors.map((author) => ({
138
- params: { author: getAuthorSlugInner(author.name) },
139
- props: { author },
140
- }))
141
- }
142
- }
143
-
144
24
  const { author } = Astro.props
145
25
 
146
- const allPosts = await getCollection('blog')
26
+ const allPosts = await getCollection(collectionName as CollectionKey)
147
27
  const isDev = import.meta.env.DEV
148
28
 
149
29
  // Component-level helper functions
@@ -154,7 +34,7 @@ const normalizeAuthor = (authorArg: string | Author): Author => {
154
34
  return authorArg
155
35
  }
156
36
 
157
- const getAuthorsFromPost = (post: CollectionEntry<'blog'>): Author[] => {
37
+ const getAuthorsFromPost = (post: CollectionEntry<CollectionKey>): Author[] => {
158
38
  const authors = post.data.authors
159
39
  if (!authors) return []
160
40
  if (Array.isArray(authors)) {
@@ -164,7 +44,7 @@ const getAuthorsFromPost = (post: CollectionEntry<'blog'>): Author[] => {
164
44
  }
165
45
 
166
46
  // Filter posts (component-level)
167
- const shouldIncludePostComponent = (post: CollectionEntry<'blog'>) => {
47
+ const shouldIncludePostComponent = (post: CollectionEntry<CollectionKey>) => {
168
48
  if (post.data.unlisted) return false
169
49
  if (post.data.draft && !(isDev && includeDraftsInDevConfig)) return false
170
50
  return true
@@ -173,23 +53,23 @@ const shouldIncludePostComponent = (post: CollectionEntry<'blog'>) => {
173
53
  // Get posts by this author
174
54
  const authorPosts = pipe(
175
55
  filter(shouldIncludePostComponent),
176
- filter((post: CollectionEntry<'blog'>) => {
56
+ filter((post: CollectionEntry<CollectionKey>) => {
177
57
  if (i18n) {
178
58
  const [postLocale] = post.id.split('/')
179
59
  return postLocale === Astro.currentLocale
180
60
  }
181
61
  return true
182
62
  }),
183
- filter((post: CollectionEntry<'blog'>) => {
63
+ filter((post: CollectionEntry<CollectionKey>) => {
184
64
  const authors = getAuthorsFromPost(post)
185
65
  return authors.some((a) => a.name === author.name)
186
66
  }),
187
- sortBy((post: CollectionEntry<'blog'>) => post.data.date.getTime()),
67
+ sortBy((post: CollectionEntry<CollectionKey>) => post.data.date.getTime()),
188
68
  reverse,
189
- )(allPosts) as CollectionEntry<'blog'>[]
69
+ )(allPosts) as CollectionEntry<CollectionKey>[]
190
70
 
191
71
  // Generate post URL
192
- const getPostUrl = (post: CollectionEntry<'blog'>): string => {
72
+ const getPostUrl = (post: CollectionEntry<CollectionKey>): string => {
193
73
  if (i18n && Astro.currentLocale) {
194
74
  const slug = post.id.replace(`${Astro.currentLocale}/`, '')
195
75
  return `/${Astro.currentLocale}/${routeBasePath}/${slug}`
@@ -3,48 +3,26 @@
3
3
  * Authors index page showing all blog authors.
4
4
  */
5
5
  import { i18n } from 'astro:config/server'
6
- import { type CollectionEntry, getCollection } from 'astro:content'
7
- import blogConfig from 'virtual:shipyard-blog/config'
8
- import Layout from '@levino/shipyard-base/layouts/Page.astro'
9
6
  import {
10
- filter,
11
- flatten,
12
- groupBy,
13
- map,
14
- pipe,
15
- prop,
16
- reverse,
17
- sortBy,
18
- uniqBy,
19
- values,
20
- } from 'ramda'
7
+ type CollectionEntry,
8
+ type CollectionKey,
9
+ getCollection,
10
+ } from 'astro:content'
11
+ import blogConfigDefault from 'virtual:shipyard-blog/config'
12
+ import Layout from '@levino/shipyard-base/layouts/Page.astro'
13
+ import { filter, flatten, map, pipe, prop, sortBy, uniqBy } from 'ramda'
21
14
  import type { Author } from '../src/index'
22
15
 
23
- export async function getStaticPaths() {
24
- const { authorsEnabled } = blogConfig
25
-
26
- if (!authorsEnabled) {
27
- return []
28
- }
29
-
30
- if (i18n) {
31
- return i18n.locales.map((locale) => {
32
- if (typeof locale !== 'string') {
33
- throw new Error('shipyard does only support strings as locales.')
34
- }
35
- return { params: { locale } }
36
- })
37
- } else {
38
- return [{ params: {} }]
39
- }
40
- }
16
+ const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
17
+ const collectionName =
18
+ Astro.props.collectionName ?? blogConfigDefault.collectionName
41
19
 
42
- const allPosts = await getCollection('blog')
20
+ const allPosts = await getCollection(collectionName as CollectionKey)
43
21
  const { includeDraftsInDev, routeBasePath } = blogConfig
44
22
  const isDev = import.meta.env.DEV
45
23
 
46
24
  // Filter posts
47
- const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
25
+ const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
48
26
  if (post.data.unlisted) return false
49
27
  if (post.data.draft && !(isDev && includeDraftsInDev)) return false
50
28
  return true
@@ -52,14 +30,14 @@ const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
52
30
 
53
31
  const posts = pipe(
54
32
  filter(shouldIncludePost),
55
- filter((post: CollectionEntry<'blog'>) => {
33
+ filter((post: CollectionEntry<CollectionKey>) => {
56
34
  if (i18n) {
57
35
  const [postLocale] = post.id.split('/')
58
36
  return postLocale === Astro.currentLocale
59
37
  }
60
38
  return true
61
39
  }),
62
- )(allPosts) as CollectionEntry<'blog'>[]
40
+ )(allPosts) as CollectionEntry<CollectionKey>[]
63
41
 
64
42
  // Extract authors from posts
65
43
  const normalizeAuthor = (author: string | Author): Author => {
@@ -69,7 +47,7 @@ const normalizeAuthor = (author: string | Author): Author => {
69
47
  return author
70
48
  }
71
49
 
72
- const getAuthorsFromPost = (post: CollectionEntry<'blog'>): Author[] => {
50
+ const getAuthorsFromPost = (post: CollectionEntry<CollectionKey>): Author[] => {
73
51
  const authors = post.data.authors
74
52
  if (!authors) return []
75
53
  if (Array.isArray(authors)) {
@@ -1,7 +1,12 @@
1
1
  ---
2
2
  import { i18n } from 'astro:config/server'
3
- import { type CollectionEntry, getCollection, render } from 'astro:content'
4
- import blogConfig from 'virtual:shipyard-blog/config'
3
+ import {
4
+ type CollectionEntry,
5
+ type CollectionKey,
6
+ getCollection,
7
+ render,
8
+ } from 'astro:content'
9
+ import blogConfigDefault from 'virtual:shipyard-blog/config'
5
10
  import { TableOfContents } from '@levino/shipyard-base/components'
6
11
  import { getEditUrl, getGitMetadata } from '../src/gitMetadata'
7
12
  import { type Author, getReadingTime } from '../src/index'
@@ -12,96 +17,21 @@ import BlogReadingTime from './BlogReadingTime.astro'
12
17
  import BlogTags from './BlogTags.astro'
13
18
  import Layout from './Layout.astro'
14
19
 
15
- export const getStaticPaths = async () => {
16
- const allPosts = await getCollection('blog')
17
-
18
- // Filter out draft posts for production - defined inside getStaticPaths
19
- const isDev = import.meta.env.DEV
20
- const { includeDraftsInDev } = blogConfig
21
- const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
22
- if (post.data.draft && !(isDev && includeDraftsInDev)) {
23
- return false
24
- }
25
- return true
26
- }
27
-
28
- const blogPosts = allPosts.filter(shouldIncludePost)
29
-
30
- // Sort posts by date (newest first)
31
- const sortedPosts = blogPosts.toSorted(
32
- (a, b) => b.data.date.getTime() - a.data.date.getTime(),
33
- )
34
-
35
- const getParams = (slug: string) => {
36
- if (i18n) {
37
- const [locale, ...rest] = slug.split('/')
38
- return {
39
- slug: rest.join('/'),
40
- locale,
41
- }
42
- } else {
43
- // For non-i18n, treat the entire slug as the path
44
- return {
45
- slug: slug,
46
- }
47
- }
48
- }
49
-
50
- const getPostUrl = (post: CollectionEntry<'blog'>) => {
51
- const { routeBasePath } = blogConfig
52
- if (i18n) {
53
- const [locale, ...rest] = post.id.split('/')
54
- return `/${locale}/${routeBasePath}/${rest.join('/')}`
55
- }
56
- return `/${routeBasePath}/${post.id}`
57
- }
58
-
59
- return sortedPosts.map((entry) => {
60
- // Filter posts by locale for pagination
61
- let localePosts = sortedPosts
62
- if (i18n) {
63
- const [locale] = entry.id.split('/')
64
- localePosts = sortedPosts.filter((post) =>
65
- post.id.startsWith(`${locale}/`),
66
- )
67
- }
68
-
69
- // Find position within locale-filtered posts
70
- const localeIndex = localePosts.findIndex((post) => post.id === entry.id)
71
-
72
- // Newer is previous in the array (lower index), older is next (higher index)
73
- const newerPost = localeIndex > 0 ? localePosts[localeIndex - 1] : undefined
74
- const olderPost =
75
- localeIndex < localePosts.length - 1
76
- ? localePosts[localeIndex + 1]
77
- : undefined
78
-
79
- return {
80
- params: getParams(entry.id),
81
- props: {
82
- entry,
83
- older: olderPost
84
- ? { href: getPostUrl(olderPost), title: olderPost.data.title }
85
- : undefined,
86
- newer: newerPost
87
- ? { href: getPostUrl(newerPost), title: newerPost.data.title }
88
- : undefined,
89
- },
90
- }
91
- })
92
- }
20
+ const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
21
+ const collectionName =
22
+ Astro.props.collectionName ?? blogConfigDefault.collectionName
93
23
 
94
24
  // In SSR mode (prerender: false), getStaticPaths is not called so Astro.props will be empty.
95
25
  // We need to fetch the entry from the collection based on URL params.
96
26
  let { entry, older, newer } = Astro.props
97
27
 
98
28
  if (!entry) {
99
- const allPosts = await getCollection('blog')
29
+ const allPosts = await getCollection(collectionName as CollectionKey)
100
30
 
101
31
  // Filter out draft posts for production
102
32
  const isDev = import.meta.env.DEV
103
33
  const { includeDraftsInDev } = blogConfig
104
- const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
34
+ const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
105
35
  if (post.data.draft && !(isDev && includeDraftsInDev)) {
106
36
  return false
107
37
  }
@@ -134,7 +64,7 @@ if (!entry) {
134
64
 
135
65
  // Calculate pagination for SSR mode
136
66
  const { routeBasePath } = blogConfig
137
- const getPostUrl = (post: CollectionEntry<'blog'>) => {
67
+ const getPostUrl = (post: CollectionEntry<CollectionKey>) => {
138
68
  if (i18n) {
139
69
  const [postLocale, ...rest] = post.id.split('/')
140
70
  return `/${postLocale}/${routeBasePath}/${rest.join('/')}`
@@ -279,7 +209,7 @@ const formatDate = new Intl.DateTimeFormat(Astro.currentLocale || 'en', {
279
209
  const showToc = !hide_table_of_contents && filteredHeadings.length > 0
280
210
  ---
281
211
 
282
- <Layout title={seoTitle} canonicalUrl={canonical_url}>
212
+ <Layout title={seoTitle} canonicalUrl={canonical_url} blogConfig={blogConfig} collectionName={collectionName}>
283
213
  <div class="grid grid-cols-12 gap-6 max-w-7xl mx-auto">
284
214
  <div class:list={['col-span-12', { 'xl:col-span-9': showToc }]}>
285
215
  {/* Post header */}
@@ -1,14 +1,21 @@
1
1
  ---
2
2
  import { Image } from 'astro:assets'
3
3
  import { i18n } from 'astro:config/server'
4
- import { type CollectionEntry, getCollection } from 'astro:content'
5
- import blogConfig from 'virtual:shipyard-blog/config'
6
- import type { GetStaticPaths } from 'astro'
4
+ import {
5
+ type CollectionEntry,
6
+ type CollectionKey,
7
+ getCollection,
8
+ } from 'astro:content'
9
+ import blogConfigDefault from 'virtual:shipyard-blog/config'
7
10
  import { getReadingTime } from '../src/index'
8
11
  import BlogReadingTime from './BlogReadingTime.astro'
9
12
  import BlogTags from './BlogTags.astro'
10
13
  import Layout from './Layout.astro'
11
14
 
15
+ const blogConfig = Astro.props.blogConfig ?? blogConfigDefault
16
+ const collectionName =
17
+ Astro.props.collectionName ?? blogConfigDefault.collectionName
18
+
12
19
  const {
13
20
  postsPerPage,
14
21
  showReadingTime,
@@ -19,7 +26,7 @@ const {
19
26
 
20
27
  // Filter out draft and unlisted posts for production
21
28
  const isDev = import.meta.env.DEV
22
- const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
29
+ const shouldIncludePost = (post: CollectionEntry<CollectionKey>) => {
23
30
  // Exclude unlisted posts from listings
24
31
  if (post.data.unlisted) return false
25
32
  // Include drafts only in dev mode if configured
@@ -27,45 +34,29 @@ const shouldIncludePost = (post: CollectionEntry<'blog'>) => {
27
34
  return true
28
35
  }
29
36
 
30
- export const getStaticPaths = (() => {
31
- if (i18n) {
32
- return i18n.locales.map((locale) => {
33
- if (typeof locale !== 'string') {
34
- throw new Error('shipyard does only support strings as locales.')
35
- }
36
- return {
37
- params: {
38
- locale,
39
- },
40
- }
41
- })
42
- } else {
43
- // For non-i18n, return a single path without locale
44
- return [{ params: {} }]
45
- }
46
- }) satisfies GetStaticPaths
47
-
48
37
  // Page 1 is always the index page (no page param)
49
38
  const currentPage = 1
50
39
 
51
- const entries = await getCollection('blog').then((posts) => {
52
- const filteredPosts = posts.filter(shouldIncludePost)
40
+ const entries = await getCollection(collectionName as CollectionKey).then(
41
+ (posts) => {
42
+ const filteredPosts = posts.filter(shouldIncludePost)
53
43
 
54
- if (i18n) {
55
- // With i18n: filter by locale
56
- return filteredPosts
57
- .filter(({ id }) => {
58
- const [postLocale] = id.split('/')
59
- return postLocale === Astro.currentLocale
60
- })
61
- .toSorted((a, b) => b.data.date.getTime() - a.data.date.getTime())
62
- } else {
63
- // Without i18n: return all posts
64
- return filteredPosts.toSorted(
65
- (a, b) => b.data.date.getTime() - a.data.date.getTime(),
66
- )
67
- }
68
- })
44
+ if (i18n) {
45
+ // With i18n: filter by locale
46
+ return filteredPosts
47
+ .filter(({ id }) => {
48
+ const [postLocale] = id.split('/')
49
+ return postLocale === Astro.currentLocale
50
+ })
51
+ .toSorted((a, b) => b.data.date.getTime() - a.data.date.getTime())
52
+ } else {
53
+ // Without i18n: return all posts
54
+ return filteredPosts.toSorted(
55
+ (a, b) => b.data.date.getTime() - a.data.date.getTime(),
56
+ )
57
+ }
58
+ },
59
+ )
69
60
 
70
61
  // Calculate pagination
71
62
  const totalPosts = entries.length
@@ -134,7 +125,7 @@ const tagBaseUrl = i18n
134
125
  : `/${routeBasePath}/tags`
135
126
  ---
136
127
 
137
- <Layout title={blogConfig.blogTitle}>
128
+ <Layout title={blogConfig.blogTitle} blogConfig={blogConfig} collectionName={collectionName}>
138
129
  <div class="max-w-7xl mx-auto">
139
130
  <h1 class="text-4xl font-bold mb-10">{blogConfig.blogTitle}</h1>
140
131
  <div class="space-y-6">