@fecommunity/reactpress-theme-starter 1.0.0-beta.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.
Files changed (163) hide show
  1. package/.env.example +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +286 -0
  4. package/app/api/[[...path]]/route.ts +69 -0
  5. package/app/archives/page.tsx +28 -0
  6. package/app/article/[id]/page.tsx +92 -0
  7. package/app/category/[category]/page.tsx +60 -0
  8. package/app/globals.css +4969 -0
  9. package/app/knowledge/[pId]/[id]/page.tsx +54 -0
  10. package/app/knowledge/[pId]/page.tsx +47 -0
  11. package/app/knowledge/page.tsx +29 -0
  12. package/app/layout.tsx +76 -0
  13. package/app/llms.txt/route.ts +26 -0
  14. package/app/login/page.tsx +17 -0
  15. package/app/nav/[id]/page.tsx +44 -0
  16. package/app/nav/page.tsx +5 -0
  17. package/app/not-found.tsx +21 -0
  18. package/app/page/[id]/page.tsx +57 -0
  19. package/app/page.tsx +61 -0
  20. package/app/register/page.tsx +17 -0
  21. package/app/robots.ts +14 -0
  22. package/app/rss/route.ts +82 -0
  23. package/app/search/page.tsx +46 -0
  24. package/app/sitemap.ts +38 -0
  25. package/app/suggestions/page.tsx +47 -0
  26. package/app/tag/[tag]/page.tsx +58 -0
  27. package/app/tags/page.tsx +18 -0
  28. package/app/template.tsx +6 -0
  29. package/components/article/ArchiveBanner.tsx +40 -0
  30. package/components/article/ArticleActionsBar.tsx +53 -0
  31. package/components/article/ArticleCarousel.tsx +237 -0
  32. package/components/article/ArticleCopyrightBar.tsx +128 -0
  33. package/components/article/ArticleEndBar.tsx +19 -0
  34. package/components/article/ArticleFeedSection.tsx +59 -0
  35. package/components/article/ArticleList.tsx +170 -0
  36. package/components/article/ArticleReadingChrome.tsx +104 -0
  37. package/components/article/ArticleReadingProgress.tsx +75 -0
  38. package/components/article/ArticleRecommend.tsx +362 -0
  39. package/components/article/ArticleTocPanel.tsx +19 -0
  40. package/components/article/ArticleViewClient.tsx +221 -0
  41. package/components/article/CategoryMenu.tsx +67 -0
  42. package/components/article/CmsPageClient.tsx +92 -0
  43. package/components/article/LoadMore.tsx +59 -0
  44. package/components/article/ReadingContent.tsx +51 -0
  45. package/components/auth/AuthPageClient.tsx +314 -0
  46. package/components/comment/CommentEditor.tsx +290 -0
  47. package/components/comment/CommentEmoji.tsx +110 -0
  48. package/components/comment/CommentIconButton.tsx +37 -0
  49. package/components/comment/CommentItem.tsx +135 -0
  50. package/components/comment/CommentSection.tsx +123 -0
  51. package/components/comment/emojis.ts +152 -0
  52. package/components/knowledge/KnowledgeList.tsx +70 -0
  53. package/components/layout/BackToTop.tsx +27 -0
  54. package/components/layout/ConditionalSiteFooter.tsx +29 -0
  55. package/components/layout/ContentStylesLoader.tsx +9 -0
  56. package/components/layout/DoubleColumnLayout.tsx +171 -0
  57. package/components/layout/HeaderSearch.tsx +23 -0
  58. package/components/layout/LocaleSwitcher.tsx +24 -0
  59. package/components/layout/PageContainer.tsx +11 -0
  60. package/components/layout/PageLoadProgress.tsx +169 -0
  61. package/components/layout/SiteFooter.tsx +75 -0
  62. package/components/layout/SiteHeader.tsx +312 -0
  63. package/components/layout/SystemNotification.tsx +176 -0
  64. package/components/layout/ThemeSwitch.tsx +19 -0
  65. package/components/layout/UserAuth.tsx +130 -0
  66. package/components/search/SearchHero.tsx +323 -0
  67. package/components/search/SearchQuickLinks.tsx +285 -0
  68. package/components/shared/DevChunkRecovery.tsx +8 -0
  69. package/components/shared/Link.tsx +30 -0
  70. package/components/shared/SectionHeading.tsx +38 -0
  71. package/components/shared/ThemeImage.tsx +13 -0
  72. package/components/shared/logo.svg +7 -0
  73. package/components/views/ArchivesClient.tsx +137 -0
  74. package/components/views/CategoryClient.tsx +87 -0
  75. package/components/views/HomeClient.tsx +79 -0
  76. package/components/views/KnowledgeBookClient.tsx +127 -0
  77. package/components/views/KnowledgeChapterClient.tsx +175 -0
  78. package/components/views/KnowledgeClient.tsx +69 -0
  79. package/components/views/LoginClient.tsx +80 -0
  80. package/components/views/NavDetailClient.tsx +177 -0
  81. package/components/views/SearchClient.tsx +100 -0
  82. package/components/views/TagArchiveClient.tsx +105 -0
  83. package/components/views/TagsClient.tsx +46 -0
  84. package/components/widgets/AboutUs.tsx +180 -0
  85. package/components/widgets/ArticleActionButton.tsx +95 -0
  86. package/components/widgets/BookmarkWidget.tsx +61 -0
  87. package/components/widgets/CategoriesWidget.tsx +47 -0
  88. package/components/widgets/HomeSidebar.tsx +40 -0
  89. package/components/widgets/LikesWidget.tsx +77 -0
  90. package/components/widgets/ShareWidget.tsx +120 -0
  91. package/components/widgets/TagCloud.tsx +94 -0
  92. package/components/widgets/TagsWidget.tsx +160 -0
  93. package/eslint.config.mjs +80 -0
  94. package/lib/auth/githubOAuth.ts +26 -0
  95. package/lib/auth/sessionUser.ts +41 -0
  96. package/lib/mock-api/auth.ts +129 -0
  97. package/lib/mock-api/config.ts +11 -0
  98. package/lib/mock-api/cors.ts +53 -0
  99. package/lib/mock-api/data.ts +998 -0
  100. package/lib/mock-api/match-route.ts +202 -0
  101. package/lib/mock-api/proxy.ts +28 -0
  102. package/lib/providers/client.ts +58 -0
  103. package/lib/providers/materializeProvider.ts +9 -0
  104. package/lib/providers/server.ts +8 -0
  105. package/lib/reactpress/appearance.ts +69 -0
  106. package/lib/reactpress/bootstrap.ts +48 -0
  107. package/lib/reactpress/coercePublishedPage.ts +12 -0
  108. package/lib/reactpress/contentSeo.ts +123 -0
  109. package/lib/reactpress/env.ts +31 -0
  110. package/lib/reactpress/feedFooterPlacement.ts +16 -0
  111. package/lib/reactpress/fetchSetting.ts +19 -0
  112. package/lib/reactpress/homeFooter.tsx +27 -0
  113. package/lib/reactpress/layout-shell.tsx +19 -0
  114. package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
  115. package/lib/reactpress/normalizeApiResponse.ts +25 -0
  116. package/lib/reactpress/providers.tsx +199 -0
  117. package/lib/reactpress/resolveSiteNav.ts +56 -0
  118. package/lib/reactpress/seoMetadata.ts +82 -0
  119. package/lib/reactpress/serverLocale.ts +43 -0
  120. package/lib/reactpress/siteMetadata.ts +151 -0
  121. package/lib/reactpress/staticParams.ts +46 -0
  122. package/lib/reactpress/suggestionsContent.ts +48 -0
  123. package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
  124. package/lib/reactpress/useSiteNavConfig.ts +39 -0
  125. package/lib/utils/archiveBanner.ts +8 -0
  126. package/lib/utils/colors.ts +119 -0
  127. package/lib/utils/comment.ts +46 -0
  128. package/lib/utils/headerPosition.ts +55 -0
  129. package/lib/utils/icons.tsx +238 -0
  130. package/lib/utils/reading.ts +16 -0
  131. package/lib/utils/scroll.ts +8 -0
  132. package/lib/utils/tagCloud.ts +222 -0
  133. package/next.config.js +103 -0
  134. package/package.json +98 -0
  135. package/postcss.config.js +5 -0
  136. package/prettier.config.js +10 -0
  137. package/public/apple-touch-icon.png +0 -0
  138. package/public/cli.png +0 -0
  139. package/public/favicon-32.png +0 -0
  140. package/public/favicon.ico +0 -0
  141. package/public/home-dark.png +0 -0
  142. package/public/home-light.png +0 -0
  143. package/public/icon-192.png +0 -0
  144. package/public/icon-512.png +0 -0
  145. package/public/logo-400.png +0 -0
  146. package/public/logo.png +0 -0
  147. package/public/mobile.png +0 -0
  148. package/public/pad.png +0 -0
  149. package/public/site.webmanifest +19 -0
  150. package/scripts/build-mock.mjs +139 -0
  151. package/scripts/dev.mjs +221 -0
  152. package/scripts/ensure-env.mjs +100 -0
  153. package/scripts/remote-dev.mjs +143 -0
  154. package/scripts/resolve-api-env.mjs +114 -0
  155. package/scripts/set-publish-version.mjs +31 -0
  156. package/scripts/smoke-pages.mjs +113 -0
  157. package/scripts/start-mock.mjs +79 -0
  158. package/scripts/verify-toolkit.mjs +37 -0
  159. package/svg.d.ts +6 -0
  160. package/theme.json +123 -0
  161. package/theme.manifest.schema.json +265 -0
  162. package/tsconfig.json +41 -0
  163. package/vercel.json +12 -0
@@ -0,0 +1,54 @@
1
+ import KnowledgeChapterClient from '@/components/views/KnowledgeChapterClient'
2
+ import { buildKnowledgeChapterMetadata, parseSiteSeoContext } from '@/lib/reactpress/contentSeo'
3
+ import {
4
+ fetchKnowledgeChapterPageProps,
5
+ fetchSiteMeta,
6
+ themeApi,
7
+ unwrapSetting,
8
+ withApiRetry,
9
+ } from '@fecommunity/reactpress-toolkit/theme/server'
10
+ import type { Metadata } from 'next'
11
+ import { notFound } from 'next/navigation'
12
+
13
+ export const revalidate = 60
14
+
15
+ interface PageProps {
16
+ params: Promise<{ pId: string; id: string }>
17
+ }
18
+
19
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
20
+ const { pId, id } = await params
21
+
22
+ try {
23
+ const [data, siteMeta, settingRow] = await Promise.all([
24
+ withApiRetry(() => fetchKnowledgeChapterPageProps(themeApi, pId, id)),
25
+ withApiRetry(() => fetchSiteMeta(themeApi)),
26
+ withApiRetry(() => themeApi.setting.findAll()).then(unwrapSetting),
27
+ ])
28
+
29
+ if (!data.book?.id || !data.chapter?.id) {
30
+ return {}
31
+ }
32
+
33
+ const site = parseSiteSeoContext(settingRow, siteMeta)
34
+ return buildKnowledgeChapterMetadata(data.chapter, data.book, site)
35
+ } catch (error) {
36
+ console.error('[reactpress-theme-starter] knowledge chapter metadata fetch failed', error)
37
+ return {}
38
+ }
39
+ }
40
+
41
+ export default async function KnowledgeChapterPage({ params }: PageProps) {
42
+ const { pId, id } = await params
43
+
44
+ try {
45
+ const data = await withApiRetry(() => fetchKnowledgeChapterPageProps(themeApi, pId, id))
46
+ if (!data.book?.id || !data.chapter?.id) {
47
+ notFound()
48
+ }
49
+ return <KnowledgeChapterClient pId={pId} id={id} book={data.book} chapter={data.chapter} />
50
+ } catch (error) {
51
+ console.error('[reactpress-theme-starter] knowledge chapter page fetch failed', error)
52
+ notFound()
53
+ }
54
+ }
@@ -0,0 +1,47 @@
1
+ import KnowledgeBookClient from '@/components/views/KnowledgeBookClient'
2
+ import {
3
+ buildListPageMetadata,
4
+ buildLocalizedListPageMetadata,
5
+ } from '@/lib/reactpress/siteMetadata'
6
+ import {
7
+ fetchKnowledgeBookPageProps,
8
+ themeApi,
9
+ withApiRetry,
10
+ } from '@fecommunity/reactpress-toolkit/theme/server'
11
+ import type { Metadata } from 'next'
12
+ import { notFound } from 'next/navigation'
13
+
14
+ export const revalidate = 60
15
+
16
+ interface PageProps {
17
+ params: Promise<{ pId: string }>
18
+ }
19
+
20
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
21
+ const { pId } = await params
22
+
23
+ try {
24
+ const data = await withApiRetry(() => fetchKnowledgeBookPageProps(themeApi, pId))
25
+ if (data.book?.title) {
26
+ return buildListPageMetadata(data.book.title, data.book.summary, `/knowledge/${pId}`)
27
+ }
28
+ } catch {
29
+ // fall through
30
+ }
31
+ return buildLocalizedListPageMetadata('knowledge', undefined, { path: '/knowledge' })
32
+ }
33
+
34
+ export default async function KnowledgeBookPage({ params }: PageProps) {
35
+ const { pId } = await params
36
+
37
+ try {
38
+ const data = await withApiRetry(() => fetchKnowledgeBookPageProps(themeApi, pId))
39
+ if (!data.book?.id) {
40
+ notFound()
41
+ }
42
+ return <KnowledgeBookClient pId={pId} book={data.book} otherBooks={data.otherBooks} />
43
+ } catch (error) {
44
+ console.error('[reactpress-theme-starter] knowledge book page fetch failed', error)
45
+ notFound()
46
+ }
47
+ }
@@ -0,0 +1,29 @@
1
+ import KnowledgeClient from '@/components/views/KnowledgeClient'
2
+ import { buildLocalizedListPageMetadata } from '@/lib/reactpress/siteMetadata'
3
+ import {
4
+ fetchKnowledgeIndexPageProps,
5
+ themeApi,
6
+ withApiRetry,
7
+ } from '@fecommunity/reactpress-toolkit/theme/server'
8
+ import type { Metadata } from 'next'
9
+
10
+ export const revalidate = 60
11
+
12
+ export async function generateMetadata(): Promise<Metadata> {
13
+ return buildLocalizedListPageMetadata('knowledge', undefined, { path: '/knowledge' })
14
+ }
15
+
16
+ export default async function KnowledgePage() {
17
+ let books = []
18
+ let total = 0
19
+
20
+ try {
21
+ const data = await withApiRetry(() => fetchKnowledgeIndexPageProps(themeApi, 12))
22
+ books = data.books
23
+ total = data.total
24
+ } catch (error) {
25
+ console.error('[reactpress-theme-starter] knowledge page fetch failed', error)
26
+ }
27
+
28
+ return <KnowledgeClient initialBooks={books} total={total} />
29
+ }
package/app/layout.tsx ADDED
@@ -0,0 +1,76 @@
1
+ import './globals.css'
2
+
3
+ import DevChunkRecovery from '@/components/shared/DevChunkRecovery'
4
+ import SiteHeader from '@/components/layout/SiteHeader'
5
+ import BackToTop from '@/components/layout/BackToTop'
6
+ import ConditionalSiteFooter from '@/components/layout/ConditionalSiteFooter'
7
+ import PageContainer from '@/components/layout/PageContainer'
8
+ import { loadAppBootstrap } from '@/lib/reactpress/bootstrap'
9
+ import { buildThemeStarterAppearanceCss } from '@/lib/reactpress/appearance'
10
+ import { getApiPreconnectOrigin } from '@/lib/reactpress/env'
11
+ import { ReactPressAppProviders } from '@/lib/reactpress/providers'
12
+ import { buildRootMetadata } from '@/lib/reactpress/siteMetadata'
13
+ import { DEFAULT_DESCRIPTION } from '@/lib/reactpress/seoMetadata'
14
+ import { colorModeInitScript } from '@fecommunity/reactpress-toolkit/theme/server'
15
+ import type { Metadata, Viewport } from 'next'
16
+
17
+ export const revalidate = 60
18
+ export const dynamic = 'force-dynamic'
19
+
20
+ export async function generateMetadata(): Promise<Metadata> {
21
+ return buildRootMetadata()
22
+ }
23
+
24
+ export const viewport: Viewport = {
25
+ width: 'device-width',
26
+ initialScale: 1,
27
+ viewportFit: 'cover',
28
+ }
29
+
30
+ export default async function RootLayout({ children }: { children: React.ReactNode }) {
31
+ const bootstrap = await loadAppBootstrap('/')
32
+ const appearanceCss = buildThemeStarterAppearanceCss(bootstrap.themeMods)
33
+ const basePath = process.env.BASE_PATH || ''
34
+ const apiPreconnect = getApiPreconnectOrigin()
35
+ const rootMetadata = await buildRootMetadata()
36
+ const siteDescription =
37
+ typeof rootMetadata.description === 'string' && rootMetadata.description.trim()
38
+ ? rootMetadata.description
39
+ : DEFAULT_DESCRIPTION
40
+
41
+ return (
42
+ <html lang={bootstrap.initialLocale} suppressHydrationWarning>
43
+ <head>
44
+ <meta name="description" content={siteDescription} />
45
+ <script
46
+ dangerouslySetInnerHTML={{
47
+ __html: `document.documentElement.setAttribute('data-rp-loading','');`,
48
+ }}
49
+ />
50
+ {apiPreconnect ? (
51
+ <link rel="preconnect" href={apiPreconnect} crossOrigin="anonymous" />
52
+ ) : null}
53
+ <link rel="icon" href={`${basePath}/favicon.ico`} sizes="any" />
54
+ <link rel="icon" type="image/png" sizes="32x32" href={`${basePath}/favicon-32.png`} />
55
+ <link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
56
+ <link rel="manifest" href={`${basePath}/site.webmanifest`} />
57
+ </head>
58
+ <body
59
+ className="flex min-h-dvh flex-col bg-[var(--bg-body)] text-[var(--main-text-color)] antialiased"
60
+ suppressHydrationWarning
61
+ >
62
+ <script dangerouslySetInnerHTML={{ __html: colorModeInitScript }} />
63
+ {appearanceCss ? <style dangerouslySetInnerHTML={{ __html: appearanceCss }} /> : null}
64
+ <ReactPressAppProviders bootstrap={bootstrap}>
65
+ <DevChunkRecovery />
66
+ <SiteHeader />
67
+ <main id="main-content" className="flex flex-1 flex-col">
68
+ <PageContainer className="flex flex-1 flex-col">{children}</PageContainer>
69
+ </main>
70
+ <ConditionalSiteFooter />
71
+ <BackToTop />
72
+ </ReactPressAppProviders>
73
+ </body>
74
+ </html>
75
+ )
76
+ }
@@ -0,0 +1,26 @@
1
+ import { getClientSiteUrl } from '@/lib/reactpress/env'
2
+
3
+ export async function GET() {
4
+ const siteUrl = getClientSiteUrl()
5
+
6
+ const body = `# ReactPress
7
+
8
+ > One package. Your CMS in about a minute. Modern publishing platform for blogs, company sites, and content-driven products.
9
+
10
+ ## Links
11
+
12
+ - [Home](${siteUrl}/): Latest ReactPress articles and guides
13
+ - [Archives](${siteUrl}/archives): Browse all published articles
14
+ - [Search](${siteUrl}/search): Search ReactPress documentation articles
15
+ - [Tags](${siteUrl}/tags): Browse articles by tag
16
+ - [GitHub](https://github.com/fecommunity/reactpress): Official open-source repository
17
+ - [Official Site](https://reactpress.surge.sh/): Product overview and documentation
18
+ `
19
+
20
+ return new Response(body, {
21
+ headers: {
22
+ 'Content-Type': 'text/plain; charset=utf-8',
23
+ 'Cache-Control': 'public, max-age=3600, s-maxage=3600',
24
+ },
25
+ })
26
+ }
@@ -0,0 +1,17 @@
1
+ import LoginClient from '@/components/views/LoginClient'
2
+ import { buildNoIndexMetadata } from '@/lib/reactpress/siteMetadata'
3
+ import { tServer } from '@/lib/reactpress/serverLocale'
4
+ import type { Metadata } from 'next'
5
+
6
+ export async function generateMetadata(): Promise<Metadata> {
7
+ return buildNoIndexMetadata(await tServer('pageTitleLogin'), '/login')
8
+ }
9
+
10
+ interface PageProps {
11
+ searchParams: Promise<{ code?: string; from?: string }>
12
+ }
13
+
14
+ export default async function LoginPage({ searchParams }: PageProps) {
15
+ const { code = '', from = '/' } = await searchParams
16
+ return <LoginClient code={code} from={from} />
17
+ }
@@ -0,0 +1,44 @@
1
+ import NavDetailClient from '@/components/views/NavDetailClient'
2
+ import { buildLocalizedListPageMetadata } from '@/lib/reactpress/siteMetadata'
3
+ import { getServerTranslator } from '@/lib/reactpress/serverLocale'
4
+ import { fetchSiteNavConfig } from '@fecommunity/reactpress-toolkit/theme/server'
5
+ import { SettingProvider } from '@/lib/providers/server'
6
+ import themeManifest from '../../../theme.json'
7
+ import type { Metadata } from 'next'
8
+
9
+ export const revalidate = 60
10
+
11
+ interface PageProps {
12
+ params: Promise<{ id: string }>
13
+ }
14
+
15
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
16
+ const { id } = await params
17
+ const siteKey = id.split('.')[0]
18
+ return buildLocalizedListPageMetadata(
19
+ 'pageTitleNavDetail',
20
+ { label: siteKey },
21
+ {
22
+ path: `/nav/${encodeURIComponent(id)}`,
23
+ }
24
+ )
25
+ }
26
+
27
+ export default async function NavDetailPage({ params }: PageProps) {
28
+ const { id } = await params
29
+ const siteKey = id.split('.')[0]
30
+ const { locale } = await getServerTranslator()
31
+
32
+ let navConfig
33
+ try {
34
+ navConfig = await fetchSiteNavConfig({
35
+ locale,
36
+ manifest: themeManifest,
37
+ getSetting: () => SettingProvider.getSetting(),
38
+ })
39
+ } catch (error) {
40
+ console.error('[reactpress-theme-starter] nav detail config fetch failed', error)
41
+ }
42
+
43
+ return <NavDetailClient siteKey={siteKey} navConfig={navConfig} />
44
+ }
@@ -0,0 +1,5 @@
1
+ import { redirect } from 'next/navigation'
2
+
3
+ export default function NavPage() {
4
+ redirect('/search')
5
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import Link from '@/components/shared/Link'
4
+ import { useLocale } from '@fecommunity/reactpress-toolkit/ui'
5
+
6
+ export default function NotFound() {
7
+ const { t } = useLocale()
8
+
9
+ return (
10
+ <div className="flex min-h-[50vh] flex-col items-center justify-center py-16 text-center">
11
+ <h1 className="m-0 text-6xl font-bold text-[var(--primary-color)]">404</h1>
12
+ <p className="mt-4 text-lg text-[var(--main-text-color)]">{t('pageMissing')}</p>
13
+ <Link
14
+ href="/"
15
+ className="rp-primary-action mt-6 inline-block rounded-xl bg-[var(--primary-color)] px-6 py-2.5 text-white no-underline hover:opacity-95"
16
+ >
17
+ {t('backHome')}
18
+ </Link>
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,57 @@
1
+ import CmsPageClient from '@/components/article/CmsPageClient'
2
+ import { buildCmsPageMetadata, parseSiteSeoContext } from '@/lib/reactpress/contentSeo'
3
+ import { coercePublishedPage } from '@/lib/reactpress/coercePublishedPage'
4
+ import {
5
+ fetchCmsPageProps,
6
+ fetchSiteMeta,
7
+ themeApi,
8
+ unwrapSetting,
9
+ withApiRetry,
10
+ } from '@fecommunity/reactpress-toolkit/theme/server'
11
+ import type { Metadata } from 'next'
12
+ import { notFound } from 'next/navigation'
13
+
14
+ export const revalidate = 60
15
+
16
+ interface PageProps {
17
+ params: Promise<{ id: string }>
18
+ }
19
+
20
+ export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
21
+ const { id } = await params
22
+
23
+ try {
24
+ const [{ page: rawPage }, siteMeta, settingRow] = await Promise.all([
25
+ withApiRetry(() => fetchCmsPageProps(themeApi, id)),
26
+ withApiRetry(() => fetchSiteMeta(themeApi)),
27
+ withApiRetry(() => themeApi.setting.findAll()).then(unwrapSetting),
28
+ ])
29
+
30
+ const page = coercePublishedPage(rawPage)
31
+ if (!page) {
32
+ return {}
33
+ }
34
+
35
+ const site = parseSiteSeoContext(settingRow, siteMeta)
36
+ return buildCmsPageMetadata(page, site)
37
+ } catch (error) {
38
+ console.error('[reactpress-theme-starter] cms page metadata fetch failed', error)
39
+ return {}
40
+ }
41
+ }
42
+
43
+ export default async function CmsPage({ params }: PageProps) {
44
+ const { id } = await params
45
+
46
+ try {
47
+ const { page: rawPage } = await withApiRetry(() => fetchCmsPageProps(themeApi, id))
48
+ const page = coercePublishedPage(rawPage)
49
+ if (!page) {
50
+ notFound()
51
+ }
52
+ return <CmsPageClient page={page} />
53
+ } catch (error) {
54
+ console.error('[reactpress-theme-starter] cms page fetch failed', error)
55
+ notFound()
56
+ }
57
+ }
package/app/page.tsx ADDED
@@ -0,0 +1,61 @@
1
+ import HomeClient from '@/components/views/HomeClient'
2
+ import { buildHomePageMetadata } from '@/lib/reactpress/siteMetadata'
3
+ import {
4
+ fetchHomePageProps,
5
+ resolveImageUrl,
6
+ themeApi,
7
+ withApiRetry,
8
+ type CarouselArticle,
9
+ type ListArticle,
10
+ } from '@fecommunity/reactpress-toolkit/theme/server'
11
+ import type { Metadata } from 'next'
12
+
13
+ export const revalidate = 60
14
+
15
+ export async function generateMetadata(): Promise<Metadata> {
16
+ try {
17
+ const data = await withApiRetry(() => fetchHomePageProps(themeApi))
18
+ const lcpCover = data.recommendedArticles.find((article) => article.cover)?.cover
19
+ const imageUrl = lcpCover ? resolveImageUrl(lcpCover, 'medium') : undefined
20
+ const base = await buildHomePageMetadata()
21
+ if (!imageUrl) return base
22
+ return {
23
+ ...base,
24
+ openGraph: {
25
+ ...base.openGraph,
26
+ images: [{ url: imageUrl }],
27
+ },
28
+ twitter: {
29
+ ...base.twitter,
30
+ card: 'summary_large_image',
31
+ images: [imageUrl],
32
+ },
33
+ }
34
+ } catch {
35
+ return buildHomePageMetadata()
36
+ }
37
+ }
38
+
39
+ export default async function Page() {
40
+ let articles: ListArticle[] = []
41
+ let total = 0
42
+ let recommendedArticles: CarouselArticle[] = []
43
+ try {
44
+ const data = await withApiRetry(() => fetchHomePageProps(themeApi))
45
+ articles = data.articles
46
+ total = data.total
47
+ recommendedArticles = data.recommendedArticles
48
+ } catch (error) {
49
+ console.error('[reactpress-theme-starter] home page fetch failed', error)
50
+ }
51
+
52
+ return (
53
+ <>
54
+ <HomeClient
55
+ initialArticles={articles}
56
+ total={total}
57
+ recommendedArticles={recommendedArticles}
58
+ />
59
+ </>
60
+ )
61
+ }
@@ -0,0 +1,17 @@
1
+ import AuthPageClient from '@/components/auth/AuthPageClient'
2
+ import { buildNoIndexMetadata } from '@/lib/reactpress/siteMetadata'
3
+ import { tServer } from '@/lib/reactpress/serverLocale'
4
+ import type { Metadata } from 'next'
5
+
6
+ export async function generateMetadata(): Promise<Metadata> {
7
+ return buildNoIndexMetadata(await tServer('commentNamespace.userRegister'), '/register')
8
+ }
9
+
10
+ interface PageProps {
11
+ searchParams: Promise<{ from?: string }>
12
+ }
13
+
14
+ export default async function RegisterPage({ searchParams }: PageProps) {
15
+ const { from = '/' } = await searchParams
16
+ return <AuthPageClient mode="register" from={from} />
17
+ }
package/app/robots.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { getClientSiteUrl } from '@/lib/reactpress/env'
2
+ import { MetadataRoute } from 'next'
3
+
4
+ export default function robots(): MetadataRoute.Robots {
5
+ const siteUrl = getClientSiteUrl()
6
+
7
+ return {
8
+ rules: {
9
+ userAgent: '*',
10
+ allow: '/',
11
+ },
12
+ sitemap: `${siteUrl}/sitemap.xml`,
13
+ }
14
+ }
@@ -0,0 +1,82 @@
1
+ import { ArticleProvider, CategoryProvider, SettingProvider } from '@/lib/providers/server'
2
+ import { getClientSiteUrl } from '@/lib/reactpress/env'
3
+
4
+ function escapeXml(value: string) {
5
+ return value
6
+ .replace(/&/g, '&amp;')
7
+ .replace(/</g, '&lt;')
8
+ .replace(/>/g, '&gt;')
9
+ .replace(/"/g, '&quot;')
10
+ .replace(/'/g, '&apos;')
11
+ }
12
+
13
+ function resolveUrl(base: string, path: string) {
14
+ const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base
15
+ const normalizedPath = path.startsWith('/') ? path : `/${path}`
16
+ return `${normalizedBase}${normalizedPath}`
17
+ }
18
+
19
+ export async function GET() {
20
+ let articles: Awaited<ReturnType<typeof ArticleProvider.getArticles>>[0] = []
21
+ let setting: Awaited<ReturnType<typeof SettingProvider.getSetting>> = {
22
+ systemTitle: 'Blog',
23
+ seoDesc: 'Blog',
24
+ systemUrl: getClientSiteUrl(),
25
+ } as Awaited<ReturnType<typeof SettingProvider.getSetting>>
26
+ let categories: Awaited<ReturnType<typeof CategoryProvider.getCategory>> = []
27
+
28
+ try {
29
+ const [articlesResult, settingResult, categoriesResult] = await Promise.all([
30
+ ArticleProvider.getArticles({
31
+ page: 1,
32
+ pageSize: 99999,
33
+ status: 'publish',
34
+ }),
35
+ SettingProvider.getSetting(),
36
+ CategoryProvider.getCategory({ articleStatus: 'publish' }),
37
+ ])
38
+ ;[articles] = articlesResult
39
+ setting = settingResult
40
+ categories = categoriesResult
41
+ } catch (error) {
42
+ console.error('[reactpress-theme-starter] rss fetch failed', error)
43
+ }
44
+
45
+ const siteUrl = setting.systemUrl || getClientSiteUrl()
46
+ const items = articles
47
+ .map((article) => {
48
+ const categoryLabel =
49
+ typeof article.category === 'object' && article.category
50
+ ? ((article.category as { label?: string }).label ?? '')
51
+ : ''
52
+ return `
53
+ <item>
54
+ <title>${escapeXml(article.title)}</title>
55
+ <description><![CDATA[${article.html || article.content || ''}]]></description>
56
+ <link>${escapeXml(resolveUrl(siteUrl, `article/${article.id}`))}</link>
57
+ <guid>${escapeXml(resolveUrl(siteUrl, `article/${article.id}`))}</guid>
58
+ <pubDate>${new Date(article.publishAt).toUTCString()}</pubDate>
59
+ ${categoryLabel ? `<category>${escapeXml(categoryLabel)}</category>` : ''}
60
+ </item>`
61
+ })
62
+ .join('')
63
+
64
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
65
+ <rss version="2.0">
66
+ <channel>
67
+ <title>${escapeXml(setting.systemTitle || 'Blog')}</title>
68
+ <description>${escapeXml(setting.seoDesc || setting.systemTitle || 'Blog')}</description>
69
+ <link>${escapeXml(siteUrl)}</link>
70
+ <generator>ReactPress</generator>
71
+ <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
72
+ ${categories.map((c) => `<category>${escapeXml((c as { label?: string }).label || '')}</category>`).join('')}
73
+ ${items}
74
+ </channel>
75
+ </rss>`
76
+
77
+ return new Response(xml, {
78
+ headers: {
79
+ 'Content-Type': 'text/xml; charset=utf-8',
80
+ },
81
+ })
82
+ }
@@ -0,0 +1,46 @@
1
+ import SearchClient from '@/components/views/SearchClient'
2
+ import { buildLocalizedListPageMetadata } from '@/lib/reactpress/siteMetadata'
3
+ import { getServerTranslator } from '@/lib/reactpress/serverLocale'
4
+ import { SettingProvider } from '@/lib/providers/server'
5
+ import {
6
+ fetchSearchPageProps,
7
+ fetchSiteNavConfig,
8
+ themeApi,
9
+ } from '@fecommunity/reactpress-toolkit/theme/server'
10
+ import themeManifest from '../../theme.json'
11
+ import type { Metadata } from 'next'
12
+
13
+ export const revalidate = 60
14
+
15
+ export async function generateMetadata(): Promise<Metadata> {
16
+ return buildLocalizedListPageMetadata('search', undefined, { path: '/search' })
17
+ }
18
+
19
+ interface PageProps {
20
+ searchParams: Promise<{ keyword?: string }>
21
+ }
22
+
23
+ export default async function SearchPage({ searchParams }: PageProps) {
24
+ const { keyword = '' } = await searchParams
25
+ const trimmed = keyword.trim()
26
+ const { locale } = await getServerTranslator()
27
+
28
+ let articles = []
29
+ let navConfig
30
+ try {
31
+ const [searchData, navData] = await Promise.all([
32
+ fetchSearchPageProps(themeApi, trimmed),
33
+ fetchSiteNavConfig({
34
+ locale,
35
+ manifest: themeManifest,
36
+ getSetting: () => SettingProvider.getSetting(),
37
+ }),
38
+ ])
39
+ articles = searchData.articles
40
+ navConfig = navData
41
+ } catch (error) {
42
+ console.error('[reactpress-theme-starter] search page fetch failed', error)
43
+ }
44
+
45
+ return <SearchClient keyword={trimmed} articles={articles} navConfig={navConfig} />
46
+ }
package/app/sitemap.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { getClientSiteUrl } from '@/lib/reactpress/env'
2
+ import { MetadataRoute } from 'next'
3
+ import {
4
+ themeApi,
5
+ unpackPaginatedPair,
6
+ withApiRetry,
7
+ } from '@fecommunity/reactpress-toolkit/theme/server'
8
+ import type { IArticle } from '@fecommunity/reactpress-toolkit/types'
9
+
10
+ export const revalidate = 3600
11
+
12
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
13
+ const siteUrl = getClientSiteUrl()
14
+
15
+ const staticRoutes = ['', 'blog', 'tags', 'archives', 'search'].map((route) => ({
16
+ url: route ? `${siteUrl}/${route}` : siteUrl,
17
+ lastModified: new Date(),
18
+ }))
19
+
20
+ let articleRoutes: MetadataRoute.Sitemap = []
21
+
22
+ try {
23
+ const response = await withApiRetry(() =>
24
+ themeApi.article.findAll({
25
+ query: { page: 1, pageSize: 500, status: 'publish' },
26
+ } as never)
27
+ )
28
+ const [articles] = unpackPaginatedPair<IArticle>(response)
29
+ articleRoutes = articles.map((article) => ({
30
+ url: `${siteUrl}/article/${article.id}`,
31
+ lastModified: article.updateAt ?? article.publishAt ?? new Date(),
32
+ }))
33
+ } catch (error) {
34
+ console.error('[reactpress-theme-starter] sitemap fetch failed', error)
35
+ }
36
+
37
+ return [...staticRoutes, ...articleRoutes]
38
+ }