@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.
- package/.env.example +22 -0
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/app/api/[[...path]]/route.ts +69 -0
- package/app/archives/page.tsx +28 -0
- package/app/article/[id]/page.tsx +92 -0
- package/app/category/[category]/page.tsx +60 -0
- package/app/globals.css +4969 -0
- package/app/knowledge/[pId]/[id]/page.tsx +54 -0
- package/app/knowledge/[pId]/page.tsx +47 -0
- package/app/knowledge/page.tsx +29 -0
- package/app/layout.tsx +76 -0
- package/app/llms.txt/route.ts +26 -0
- package/app/login/page.tsx +17 -0
- package/app/nav/[id]/page.tsx +44 -0
- package/app/nav/page.tsx +5 -0
- package/app/not-found.tsx +21 -0
- package/app/page/[id]/page.tsx +57 -0
- package/app/page.tsx +61 -0
- package/app/register/page.tsx +17 -0
- package/app/robots.ts +14 -0
- package/app/rss/route.ts +82 -0
- package/app/search/page.tsx +46 -0
- package/app/sitemap.ts +38 -0
- package/app/suggestions/page.tsx +47 -0
- package/app/tag/[tag]/page.tsx +58 -0
- package/app/tags/page.tsx +18 -0
- package/app/template.tsx +6 -0
- package/components/article/ArchiveBanner.tsx +40 -0
- package/components/article/ArticleActionsBar.tsx +53 -0
- package/components/article/ArticleCarousel.tsx +237 -0
- package/components/article/ArticleCopyrightBar.tsx +128 -0
- package/components/article/ArticleEndBar.tsx +19 -0
- package/components/article/ArticleFeedSection.tsx +59 -0
- package/components/article/ArticleList.tsx +170 -0
- package/components/article/ArticleReadingChrome.tsx +104 -0
- package/components/article/ArticleReadingProgress.tsx +75 -0
- package/components/article/ArticleRecommend.tsx +362 -0
- package/components/article/ArticleTocPanel.tsx +19 -0
- package/components/article/ArticleViewClient.tsx +221 -0
- package/components/article/CategoryMenu.tsx +67 -0
- package/components/article/CmsPageClient.tsx +92 -0
- package/components/article/LoadMore.tsx +59 -0
- package/components/article/ReadingContent.tsx +51 -0
- package/components/auth/AuthPageClient.tsx +314 -0
- package/components/comment/CommentEditor.tsx +290 -0
- package/components/comment/CommentEmoji.tsx +110 -0
- package/components/comment/CommentIconButton.tsx +37 -0
- package/components/comment/CommentItem.tsx +135 -0
- package/components/comment/CommentSection.tsx +123 -0
- package/components/comment/emojis.ts +152 -0
- package/components/knowledge/KnowledgeList.tsx +70 -0
- package/components/layout/BackToTop.tsx +27 -0
- package/components/layout/ConditionalSiteFooter.tsx +29 -0
- package/components/layout/ContentStylesLoader.tsx +9 -0
- package/components/layout/DoubleColumnLayout.tsx +171 -0
- package/components/layout/HeaderSearch.tsx +23 -0
- package/components/layout/LocaleSwitcher.tsx +24 -0
- package/components/layout/PageContainer.tsx +11 -0
- package/components/layout/PageLoadProgress.tsx +169 -0
- package/components/layout/SiteFooter.tsx +75 -0
- package/components/layout/SiteHeader.tsx +312 -0
- package/components/layout/SystemNotification.tsx +176 -0
- package/components/layout/ThemeSwitch.tsx +19 -0
- package/components/layout/UserAuth.tsx +130 -0
- package/components/search/SearchHero.tsx +323 -0
- package/components/search/SearchQuickLinks.tsx +285 -0
- package/components/shared/DevChunkRecovery.tsx +8 -0
- package/components/shared/Link.tsx +30 -0
- package/components/shared/SectionHeading.tsx +38 -0
- package/components/shared/ThemeImage.tsx +13 -0
- package/components/shared/logo.svg +7 -0
- package/components/views/ArchivesClient.tsx +137 -0
- package/components/views/CategoryClient.tsx +87 -0
- package/components/views/HomeClient.tsx +79 -0
- package/components/views/KnowledgeBookClient.tsx +127 -0
- package/components/views/KnowledgeChapterClient.tsx +175 -0
- package/components/views/KnowledgeClient.tsx +69 -0
- package/components/views/LoginClient.tsx +80 -0
- package/components/views/NavDetailClient.tsx +177 -0
- package/components/views/SearchClient.tsx +100 -0
- package/components/views/TagArchiveClient.tsx +105 -0
- package/components/views/TagsClient.tsx +46 -0
- package/components/widgets/AboutUs.tsx +180 -0
- package/components/widgets/ArticleActionButton.tsx +95 -0
- package/components/widgets/BookmarkWidget.tsx +61 -0
- package/components/widgets/CategoriesWidget.tsx +47 -0
- package/components/widgets/HomeSidebar.tsx +40 -0
- package/components/widgets/LikesWidget.tsx +77 -0
- package/components/widgets/ShareWidget.tsx +120 -0
- package/components/widgets/TagCloud.tsx +94 -0
- package/components/widgets/TagsWidget.tsx +160 -0
- package/eslint.config.mjs +80 -0
- package/lib/auth/githubOAuth.ts +26 -0
- package/lib/auth/sessionUser.ts +41 -0
- package/lib/mock-api/auth.ts +129 -0
- package/lib/mock-api/config.ts +11 -0
- package/lib/mock-api/cors.ts +53 -0
- package/lib/mock-api/data.ts +998 -0
- package/lib/mock-api/match-route.ts +202 -0
- package/lib/mock-api/proxy.ts +28 -0
- package/lib/providers/client.ts +58 -0
- package/lib/providers/materializeProvider.ts +9 -0
- package/lib/providers/server.ts +8 -0
- package/lib/reactpress/appearance.ts +69 -0
- package/lib/reactpress/bootstrap.ts +48 -0
- package/lib/reactpress/coercePublishedPage.ts +12 -0
- package/lib/reactpress/contentSeo.ts +123 -0
- package/lib/reactpress/env.ts +31 -0
- package/lib/reactpress/feedFooterPlacement.ts +16 -0
- package/lib/reactpress/fetchSetting.ts +19 -0
- package/lib/reactpress/homeFooter.tsx +27 -0
- package/lib/reactpress/layout-shell.tsx +19 -0
- package/lib/reactpress/materializeBootstrapCatalog.ts +31 -0
- package/lib/reactpress/normalizeApiResponse.ts +25 -0
- package/lib/reactpress/providers.tsx +199 -0
- package/lib/reactpress/resolveSiteNav.ts +56 -0
- package/lib/reactpress/seoMetadata.ts +82 -0
- package/lib/reactpress/serverLocale.ts +43 -0
- package/lib/reactpress/siteMetadata.ts +151 -0
- package/lib/reactpress/staticParams.ts +46 -0
- package/lib/reactpress/suggestionsContent.ts +48 -0
- package/lib/reactpress/useFeedFooterPlacement.ts +22 -0
- package/lib/reactpress/useSiteNavConfig.ts +39 -0
- package/lib/utils/archiveBanner.ts +8 -0
- package/lib/utils/colors.ts +119 -0
- package/lib/utils/comment.ts +46 -0
- package/lib/utils/headerPosition.ts +55 -0
- package/lib/utils/icons.tsx +238 -0
- package/lib/utils/reading.ts +16 -0
- package/lib/utils/scroll.ts +8 -0
- package/lib/utils/tagCloud.ts +222 -0
- package/next.config.js +103 -0
- package/package.json +98 -0
- package/postcss.config.js +5 -0
- package/prettier.config.js +10 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/cli.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/home-dark.png +0 -0
- package/public/home-light.png +0 -0
- package/public/icon-192.png +0 -0
- package/public/icon-512.png +0 -0
- package/public/logo-400.png +0 -0
- package/public/logo.png +0 -0
- package/public/mobile.png +0 -0
- package/public/pad.png +0 -0
- package/public/site.webmanifest +19 -0
- package/scripts/build-mock.mjs +139 -0
- package/scripts/dev.mjs +221 -0
- package/scripts/ensure-env.mjs +100 -0
- package/scripts/remote-dev.mjs +143 -0
- package/scripts/resolve-api-env.mjs +114 -0
- package/scripts/set-publish-version.mjs +31 -0
- package/scripts/smoke-pages.mjs +113 -0
- package/scripts/start-mock.mjs +79 -0
- package/scripts/verify-toolkit.mjs +37 -0
- package/svg.d.ts +6 -0
- package/theme.json +123 -0
- package/theme.manifest.schema.json +265 -0
- package/tsconfig.json +41 -0
- 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
|
+
}
|
package/app/nav/page.tsx
ADDED
|
@@ -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
|
+
}
|
package/app/rss/route.ts
ADDED
|
@@ -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, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''')
|
|
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
|
+
}
|