@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
package/.env.example
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# ReactPress API (SSR — Next.js server requests)
|
|
2
|
+
REACTPRESS_API_URL=http://localhost:3002/api
|
|
3
|
+
|
|
4
|
+
# Browser CSR requests (same as SSR, or use same-origin /api via rewrites)
|
|
5
|
+
NEXT_PUBLIC_REACTPRESS_API_URL=http://localhost:3002/api
|
|
6
|
+
|
|
7
|
+
# Public site URL (SEO / sitemap / Open Graph)
|
|
8
|
+
CLIENT_SITE_URL=http://localhost:3001
|
|
9
|
+
|
|
10
|
+
# Subpath deployment (optional)
|
|
11
|
+
# BASE_PATH=/blog
|
|
12
|
+
|
|
13
|
+
# ── Mock mode (offline demo / CI / Vercel starter deploy) ───────────────────
|
|
14
|
+
# REACTPRESS_MOCK_API=1
|
|
15
|
+
|
|
16
|
+
# ── Remote API during dev (equivalent to --remote-origin) ───────────────────
|
|
17
|
+
# REACTPRESS_DEV_REMOTE_ORIGIN=https://api.yoursite.com
|
|
18
|
+
|
|
19
|
+
# ── Production (Vercel / self-hosted) ───────────────────────────────────────
|
|
20
|
+
# REACTPRESS_API_URL=https://api.yoursite.com/api
|
|
21
|
+
# NEXT_PUBLIC_REACTPRESS_API_URL=/api
|
|
22
|
+
# CLIENT_SITE_URL=https://www.yoursite.com
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ReactPress / FECommunity
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<a href="https://reactpress.surge.sh/">
|
|
4
|
+
<img src="./public/logo.png" alt="ReactPress" width="480" />
|
|
5
|
+
</a>
|
|
6
|
+
|
|
7
|
+
<br />
|
|
8
|
+
|
|
9
|
+
**ReactPress Theme Starter**
|
|
10
|
+
|
|
11
|
+
A lightweight, simple, and easy-to-use Next.js theme, powered by [ReactPress](https://github.com/fecommunity/reactpress).
|
|
12
|
+
|
|
13
|
+
<a href="https://reactpress-theme-starter.vercel.app">
|
|
14
|
+
<img src="./public/home-dark.png" alt="ReactPress Theme Starter preview" width="100%" />
|
|
15
|
+
</a>
|
|
16
|
+
|
|
17
|
+
<br />
|
|
18
|
+
|
|
19
|
+
[Live Demo](https://reactpress-theme-starter.vercel.app) · [Documentation](https://reactpress.surge.sh/) · [简体中文](./README_zh.md)
|
|
20
|
+
|
|
21
|
+
<br />
|
|
22
|
+
|
|
23
|
+
<img src="https://img.shields.io/badge/Next.js_15-000?style=flat-square&logo=next.js&logoColor=white" alt="Next.js 15" />
|
|
24
|
+
<img src="https://img.shields.io/badge/React_19-61dafb?style=flat-square&logo=react&logoColor=white" alt="React 19" />
|
|
25
|
+
<img src="https://img.shields.io/badge/Tailwind_4-38bdf8?style=flat-square&logo=tailwindcss&logoColor=white" alt="Tailwind CSS 4" />
|
|
26
|
+
<img src="https://img.shields.io/badge/TypeScript-0074c1?style=flat-square&logo=typescript&logoColor=white" alt="TypeScript" />
|
|
27
|
+
<img src="https://img.shields.io/badge/License-MIT-22c55e?style=flat-square" alt="MIT License" />
|
|
28
|
+
<a href="https://github.com/fecommunity/reactpress-theme-starter/stargazers">
|
|
29
|
+
<img src="https://img.shields.io/github/stars/fecommunity/reactpress-theme-starter?style=flat-square&logo=github" alt="GitHub stars" />
|
|
30
|
+
</a>
|
|
31
|
+
|
|
32
|
+
<br />
|
|
33
|
+
|
|
34
|
+
**If this project helps you, a ⭐ on GitHub helps others discover it.**
|
|
35
|
+
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Try in 60 seconds (no backend)
|
|
41
|
+
|
|
42
|
+
Preview the full theme UI with built-in sample data — no ReactPress install required.
|
|
43
|
+
|
|
44
|
+
### Option A — `create-next-app` (recommended)
|
|
45
|
+
|
|
46
|
+
Bootstrap from this repository with the [official Next.js CLI](https://nextjs.org/docs/app/api-reference/cli/create-next-app):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx create-next-app@latest reactpress-theme-starter --example "https://github.com/fecommunity/reactpress-theme-starter" --use-pnpm
|
|
50
|
+
cd reactpress-theme-starter
|
|
51
|
+
pnpm dev:mock
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Option B — clone manually
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/fecommunity/reactpress-theme-starter.git
|
|
58
|
+
cd reactpress-theme-starter
|
|
59
|
+
pnpm install
|
|
60
|
+
pnpm dev:mock
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Open **http://localhost:3001** — same mode as the [live demo](https://reactpress-theme-starter.vercel.app).
|
|
64
|
+
|
|
65
|
+
### Deploy to Vercel (zero env vars)
|
|
66
|
+
|
|
67
|
+
One-click deploy uses [`vercel.json`](./vercel.json) with mock data — no backend required.
|
|
68
|
+
|
|
69
|
+
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ffecommunity%2Freactpress-theme-starter&project-name=reactpress-theme&repository-name=reactpress-theme&demo-title=ReactPress%20Theme%20Starter&demo-description=Next.js%2015%20headless%20blog%20theme%20with%20mock%20mode%20%E2%80%94%20no%20backend%20required&demo-url=https%3A%2F%2Freactpress-theme-starter.vercel.app&demo-image=https%3A%2F%2Fraw.githubusercontent.com%2Ffecommunity%2Freactpress-theme-starter%2Fmaster%2Fpublic%2Fhome-dark.png)
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## What is this?
|
|
74
|
+
|
|
75
|
+
This repository is the **public-facing theme** for ReactPress: routing, layout, SEO, and UI. It does **not** include a CMS or admin panel — all content comes from a ReactPress backend via [`@fecommunity/reactpress-toolkit`](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit).
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
ReactPress API ──REST──▶ Theme Starter (Next.js) ──▶ Public Site
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Stack:** Next.js 15 · React 19 · Tailwind CSS 4 · Node.js 20+ · pnpm 9
|
|
82
|
+
|
|
83
|
+
**Includes:** articles, archives, search, CMS pages, knowledge base, comments, light/dark mode, RSS/sitemap, and an optional embedded mock API for offline development.
|
|
84
|
+
|
|
85
|
+
### Why this theme?
|
|
86
|
+
|
|
87
|
+
| | ReactPress Theme Starter | Generic Next.js blog template |
|
|
88
|
+
| :------------------------ | :----------------------------------------------------------------------- | :---------------------------- |
|
|
89
|
+
| CMS & admin | ✅ [ReactPress](https://github.com/fecommunity/reactpress) backend + CLI | ❌ Build or wire your own |
|
|
90
|
+
| Try without backend | ✅ `pnpm dev:mock` | ❌ Usually needs content/API |
|
|
91
|
+
| Knowledge base & comments | ✅ Built-in | ❌ Roll your own |
|
|
92
|
+
| Customizable appearance | ✅ Admin console + `theme.json` | ❌ Hard-coded or manual |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Quick Start
|
|
97
|
+
|
|
98
|
+
**Requirements:** Node.js 20+, pnpm 9+
|
|
99
|
+
|
|
100
|
+
### Recommended — ReactPress CLI
|
|
101
|
+
|
|
102
|
+
The fastest way to run a full stack (visitor site, admin console, and API) is the [ReactPress CLI](https://reactpress.surge.sh/). No manual `.env` wiring for a standard setup.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm i -g @fecommunity/reactpress@3
|
|
106
|
+
reactpress init
|
|
107
|
+
reactpress dev
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
<div align="center">
|
|
111
|
+
|
|
112
|
+
<img src="./public/cli.png" alt="ReactPress CLI interactive menu" width="100%" />
|
|
113
|
+
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
After startup, open the URLs printed in the terminal (typically the public site, admin, and API). Use `reactpress doctor` if something fails to start.
|
|
117
|
+
|
|
118
|
+
**Use this theme with the CLI**
|
|
119
|
+
|
|
120
|
+
1. Clone this repository and install dependencies (`pnpm install`).
|
|
121
|
+
2. Start the API with `reactpress dev --api-only` (or run the full stack and point the theme at the API).
|
|
122
|
+
3. Copy [`.env.example`](./.env.example) to `.env` if needed — default local API is `http://localhost:3002/api`.
|
|
123
|
+
4. In the theme directory, run:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pnpm dev
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Open **http://localhost:3001** — the theme loads live content from your ReactPress API.
|
|
130
|
+
|
|
131
|
+
Remote API during development:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pnpm dev -- --remote-origin api.yoursite.com
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Local theme preview (mock — no backend)
|
|
138
|
+
|
|
139
|
+
To work on UI, layout, or styling **without** installing ReactPress or running the API, use the built-in mock server:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git clone https://github.com/fecommunity/reactpress-theme-starter.git
|
|
143
|
+
cd reactpress-theme-starter
|
|
144
|
+
pnpm install
|
|
145
|
+
pnpm dev:mock
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Open **http://localhost:3001** — sample data comes from [`lib/mock-api/data.ts`](./lib/mock-api/data.ts). This is the same mode used by the [Vercel demo](https://reactpress-theme-starter.vercel.app).
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Commands
|
|
153
|
+
|
|
154
|
+
| Command | Purpose |
|
|
155
|
+
| :------------------- | :------------------------------------------------------------ |
|
|
156
|
+
| `pnpm dev` | Theme dev server + live ReactPress API (CLI or remote) |
|
|
157
|
+
| `pnpm dev:mock` | Theme dev server + built-in mock API (no backend) |
|
|
158
|
+
| `pnpm build:mock` | Production build with mock data (Vercel demo / offline build) |
|
|
159
|
+
| `pnpm build` | Production build (API must be reachable at build time) |
|
|
160
|
+
| `pnpm start` | Run production server on port **3001** |
|
|
161
|
+
| `pnpm run check` | ESLint + Prettier |
|
|
162
|
+
| `pnpm run typecheck` | TypeScript check |
|
|
163
|
+
|
|
164
|
+
For full-stack local development (site + admin + API), prefer **`reactpress dev`** from the ReactPress CLI — see [Quick Start](#quick-start).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
When using **`pnpm dev`** against a live API, copy [`.env.example`](./.env.example) to `.env`. Mock mode (`pnpm dev:mock`) and the ReactPress CLI handle most setup for you.
|
|
171
|
+
|
|
172
|
+
| Variable | Description |
|
|
173
|
+
| :------------------------------- | :---------------------------------------------- |
|
|
174
|
+
| `REACTPRESS_API_URL` | Server-side API base URL (include `/api`) |
|
|
175
|
+
| `NEXT_PUBLIC_REACTPRESS_API_URL` | Client-side API URL; use `/api` for same-origin |
|
|
176
|
+
| `CLIENT_SITE_URL` | Public site URL for SEO and Open Graph |
|
|
177
|
+
| `REACTPRESS_MOCK_API` | Set to `1` to enable the embedded mock API |
|
|
178
|
+
|
|
179
|
+
See `.env.example` for optional variables (subpath deploy, GitHub OAuth, remote dev origin).
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Deployment
|
|
184
|
+
|
|
185
|
+
### Demo (Vercel)
|
|
186
|
+
|
|
187
|
+
Import this repo as-is — [`vercel.json`](./vercel.json) uses `build:mock` with mock data. See [Deploy to Vercel](#deploy-to-vercel-zero-env-vars) above for the one-click button with live demo preview.
|
|
188
|
+
|
|
189
|
+
> The [live demo](https://reactpress-theme-starter.vercel.app) runs in mock mode (UI + sample data only).
|
|
190
|
+
|
|
191
|
+
### Production
|
|
192
|
+
|
|
193
|
+
Deploy as a headless frontend pointed at your ReactPress API:
|
|
194
|
+
|
|
195
|
+
1. Set `REACTPRESS_API_URL`, `NEXT_PUBLIC_REACTPRESS_API_URL`, and `CLIENT_SITE_URL`
|
|
196
|
+
2. Build with `pnpm build` (not `build:mock`)
|
|
197
|
+
3. Start with `pnpm start` or your platform's Next.js runtime
|
|
198
|
+
|
|
199
|
+
`pnpm build` prefetches pages from the API — ensure the API is reachable at build time.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Project Layout
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
app/ App Router pages, feeds, sitemap, mock API route
|
|
207
|
+
components/ Layout, article, search, widgets
|
|
208
|
+
lib/ mock-api/, reactpress/ utilities
|
|
209
|
+
scripts/ dev, build, and smoke-test scripts
|
|
210
|
+
theme.json Theme manifest (routes, appearance schema)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Route templates are declared in [`theme.json`](./theme.json). Appearance (colors, logo, navigation) is configured in the ReactPress admin console.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## FAQ
|
|
218
|
+
|
|
219
|
+
<details>
|
|
220
|
+
<summary><strong>Do I need ReactPress to preview the theme?</strong></summary>
|
|
221
|
+
|
|
222
|
+
No. Run `pnpm dev:mock` for a full UI preview with sample data. Install [ReactPress CLI](https://reactpress.surge.sh/) when you want a real CMS, admin panel, and API.
|
|
223
|
+
|
|
224
|
+
</details>
|
|
225
|
+
|
|
226
|
+
<details>
|
|
227
|
+
<summary><strong>How is this different from WordPress or Ghost themes?</strong></summary>
|
|
228
|
+
|
|
229
|
+
This is a **headless frontend** — content lives in ReactPress (REST API + admin). You deploy the Next.js theme separately and point it at your API. See [ReactPress docs](https://reactpress.surge.sh/) for the full stack.
|
|
230
|
+
|
|
231
|
+
</details>
|
|
232
|
+
|
|
233
|
+
<details>
|
|
234
|
+
<summary><strong>Can I use my own Next.js components or fork the theme?</strong></summary>
|
|
235
|
+
|
|
236
|
+
Yes. MIT licensed. Customize components under `app/` and `components/`, or declare routes in [`theme.json`](./theme.json). Pull requests welcome — see [Contributing](./CONTRIBUTING.md).
|
|
237
|
+
|
|
238
|
+
</details>
|
|
239
|
+
|
|
240
|
+
<details>
|
|
241
|
+
<summary><strong>Where do I report bugs in the CMS or CLI?</strong></summary>
|
|
242
|
+
|
|
243
|
+
Theme issues → [this repo](https://github.com/fecommunity/reactpress-theme-starter/issues). Core ReactPress (API, admin, CLI) → [fecommunity/reactpress](https://github.com/fecommunity/reactpress/issues).
|
|
244
|
+
|
|
245
|
+
</details>
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Community
|
|
250
|
+
|
|
251
|
+
| Channel | Link |
|
|
252
|
+
| :-------------- | :---------------------------------------------------------------------------------------- |
|
|
253
|
+
| Q&A & showcase | [GitHub Discussions](https://github.com/fecommunity/reactpress-theme-starter/discussions) |
|
|
254
|
+
| Bug reports | [Issues](https://github.com/fecommunity/reactpress-theme-starter/issues) |
|
|
255
|
+
| ReactPress core | [github.com/fecommunity/reactpress](https://github.com/fecommunity/reactpress) |
|
|
256
|
+
| Documentation | [reactpress.surge.sh](https://reactpress.surge.sh/) |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Contributing
|
|
261
|
+
|
|
262
|
+
| | |
|
|
263
|
+
| :------------------------------------------ | :-------------------------------- |
|
|
264
|
+
| [Contributing](./CONTRIBUTING.md) | Setup, conventions, pull requests |
|
|
265
|
+
| [Contributing (中文)](./CONTRIBUTING_zh.md) | 中文贡献指南 |
|
|
266
|
+
| [Code of Conduct](./CODE_OF_CONDUCT.md) | Community standards |
|
|
267
|
+
| [Security](./SECURITY.md) | Report vulnerabilities |
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Links
|
|
272
|
+
|
|
273
|
+
| | |
|
|
274
|
+
| :---------------- | :----------------------------------------------------------------------------------------------- |
|
|
275
|
+
| ReactPress docs | [reactpress.surge.sh](https://reactpress.surge.sh/) |
|
|
276
|
+
| ReactPress source | [github.com/fecommunity/reactpress](https://github.com/fecommunity/reactpress) |
|
|
277
|
+
| Toolkit | [@fecommunity/reactpress-toolkit](https://www.npmjs.com/package/@fecommunity/reactpress-toolkit) |
|
|
278
|
+
| Theme schema | [theme.manifest.schema.json](./theme.manifest.schema.json) |
|
|
279
|
+
|
|
280
|
+
<br />
|
|
281
|
+
|
|
282
|
+
<div align="center">
|
|
283
|
+
|
|
284
|
+
[MIT License](./LICENSE) · © ReactPress / FECommunity
|
|
285
|
+
|
|
286
|
+
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { isMockApiEnabled } from '@/lib/mock-api/config'
|
|
2
|
+
import { corsPreflightResponse, withCors } from '@/lib/mock-api/cors'
|
|
3
|
+
import { matchMockRoute } from '@/lib/mock-api/match-route'
|
|
4
|
+
import { proxyToUpstreamApi } from '@/lib/mock-api/proxy'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
type RouteContext = {
|
|
9
|
+
params: Promise<{ path?: string[] }>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function readJsonBody(request: Request): Promise<unknown> {
|
|
13
|
+
if (request.method === 'GET' || request.method === 'HEAD') return undefined
|
|
14
|
+
try {
|
|
15
|
+
const text = await request.text()
|
|
16
|
+
if (!text.trim()) return undefined
|
|
17
|
+
return JSON.parse(text) as unknown
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function handleRequest(request: Request, context: RouteContext): Promise<Response> {
|
|
24
|
+
if (request.method === 'OPTIONS') {
|
|
25
|
+
return corsPreflightResponse(request)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { path = [] } = await context.params
|
|
29
|
+
const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
|
|
30
|
+
const body = await readJsonBody(request)
|
|
31
|
+
|
|
32
|
+
let response: Response
|
|
33
|
+
if (isMockApiEnabled()) {
|
|
34
|
+
const mockResponse = matchMockRoute(request.method, path, { keyword, body })
|
|
35
|
+
response = mockResponse ?? Response.json({ success: true, data: null })
|
|
36
|
+
} else {
|
|
37
|
+
response = await proxyToUpstreamApi(request, path)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return withCors(request, response)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function GET(request: Request, context: RouteContext) {
|
|
44
|
+
return handleRequest(request, context)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function POST(request: Request, context: RouteContext) {
|
|
48
|
+
return handleRequest(request, context)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function PUT(request: Request, context: RouteContext) {
|
|
52
|
+
return handleRequest(request, context)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function PATCH(request: Request, context: RouteContext) {
|
|
56
|
+
return handleRequest(request, context)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function DELETE(request: Request, context: RouteContext) {
|
|
60
|
+
return handleRequest(request, context)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function HEAD(request: Request, context: RouteContext) {
|
|
64
|
+
return handleRequest(request, context)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function OPTIONS(request: Request, context: RouteContext) {
|
|
68
|
+
return handleRequest(request, context)
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import ArchivesClient from '@/components/views/ArchivesClient'
|
|
2
|
+
import { buildLocalizedListPageMetadata } from '@/lib/reactpress/siteMetadata'
|
|
3
|
+
import {
|
|
4
|
+
fetchArchivesPageProps,
|
|
5
|
+
themeApi,
|
|
6
|
+
withApiRetry,
|
|
7
|
+
type ArchiveTree,
|
|
8
|
+
} from '@fecommunity/reactpress-toolkit/theme/server'
|
|
9
|
+
import type { Metadata } from 'next'
|
|
10
|
+
|
|
11
|
+
export const revalidate = 60
|
|
12
|
+
|
|
13
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
14
|
+
return buildLocalizedListPageMetadata('archives', undefined, { path: '/archives' })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default async function ArchivesPage() {
|
|
18
|
+
let articles: ArchiveTree = {}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const data = await withApiRetry(() => fetchArchivesPageProps(themeApi))
|
|
22
|
+
articles = data.articles
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('[reactpress-theme-starter] archives fetch failed', error)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return <ArchivesClient articles={articles} />
|
|
28
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import ArticleViewClient from '@/components/article/ArticleViewClient'
|
|
2
|
+
import {
|
|
3
|
+
buildArticleJsonLd,
|
|
4
|
+
buildArticleMetadata,
|
|
5
|
+
parseSiteSeoContext,
|
|
6
|
+
} from '@/lib/reactpress/contentSeo'
|
|
7
|
+
import { generateArticleStaticParams } from '@/lib/reactpress/staticParams'
|
|
8
|
+
import {
|
|
9
|
+
fetchArticleDetailProps,
|
|
10
|
+
fetchSiteMeta,
|
|
11
|
+
resolveImageUrl,
|
|
12
|
+
themeApi,
|
|
13
|
+
unwrapSetting,
|
|
14
|
+
withApiRetry,
|
|
15
|
+
} from '@fecommunity/reactpress-toolkit/theme/server'
|
|
16
|
+
import type { Metadata } from 'next'
|
|
17
|
+
import { notFound } from 'next/navigation'
|
|
18
|
+
|
|
19
|
+
export const revalidate = 60
|
|
20
|
+
|
|
21
|
+
export async function generateStaticParams() {
|
|
22
|
+
return generateArticleStaticParams()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PageProps {
|
|
26
|
+
params: Promise<{ id: string }>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveAbsoluteImageUrl(cover: string | undefined, siteUrl?: string) {
|
|
30
|
+
if (!cover) return undefined
|
|
31
|
+
const imageUrl = resolveImageUrl(cover, 'large')
|
|
32
|
+
if (!siteUrl) return imageUrl
|
|
33
|
+
try {
|
|
34
|
+
return new URL(imageUrl, siteUrl).href
|
|
35
|
+
} catch {
|
|
36
|
+
return imageUrl
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
41
|
+
const { id } = await params
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const [{ article }, siteMeta, settingRow] = await Promise.all([
|
|
45
|
+
withApiRetry(() => fetchArticleDetailProps(themeApi, id)),
|
|
46
|
+
withApiRetry(() => fetchSiteMeta(themeApi)),
|
|
47
|
+
withApiRetry(() => themeApi.setting.findAll()).then(unwrapSetting),
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
if (!article?.id || article.status !== 'publish') {
|
|
51
|
+
return {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const site = parseSiteSeoContext(settingRow, siteMeta)
|
|
55
|
+
const imageUrl = resolveAbsoluteImageUrl(article.cover, site.siteUrl)
|
|
56
|
+
return buildArticleMetadata(article, site, imageUrl)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('[reactpress-theme-starter] article metadata fetch failed', error)
|
|
59
|
+
return {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default async function ArticlePage({ params }: PageProps) {
|
|
64
|
+
const { id } = await params
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const [{ article }, siteMeta] = await Promise.all([
|
|
68
|
+
withApiRetry(() => fetchArticleDetailProps(themeApi, id)),
|
|
69
|
+
withApiRetry(() => fetchSiteMeta(themeApi)),
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
if (!article?.id || article.status !== 'publish') {
|
|
73
|
+
notFound()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const coverUrl = resolveAbsoluteImageUrl(article.cover, siteMeta.siteUrl)
|
|
77
|
+
const jsonLd = buildArticleJsonLd(article, siteMeta.siteUrl, coverUrl)
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<script
|
|
82
|
+
type="application/ld+json"
|
|
83
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
84
|
+
/>
|
|
85
|
+
<ArticleViewClient article={article} />
|
|
86
|
+
</>
|
|
87
|
+
)
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('[reactpress-theme-starter] article page fetch failed', error)
|
|
90
|
+
notFound()
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import CategoryClient from '@/components/views/CategoryClient'
|
|
2
|
+
import { buildLocalizedListPageMetadata } from '@/lib/reactpress/siteMetadata'
|
|
3
|
+
import { generateCategoryStaticParams } from '@/lib/reactpress/staticParams'
|
|
4
|
+
import {
|
|
5
|
+
fetchCategoryArchivePageProps,
|
|
6
|
+
themeApi,
|
|
7
|
+
withApiRetry,
|
|
8
|
+
} from '@fecommunity/reactpress-toolkit/theme/server'
|
|
9
|
+
import type { Metadata } from 'next'
|
|
10
|
+
import { notFound } from 'next/navigation'
|
|
11
|
+
|
|
12
|
+
export const revalidate = 60
|
|
13
|
+
|
|
14
|
+
export async function generateStaticParams() {
|
|
15
|
+
return generateCategoryStaticParams()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PageProps {
|
|
19
|
+
params: Promise<{ category: string }>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
|
23
|
+
const { category } = await params
|
|
24
|
+
const categoryValue = decodeURIComponent(category)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const data = await withApiRetry(() => fetchCategoryArchivePageProps(themeApi, categoryValue))
|
|
28
|
+
const label = data.category?.label || categoryValue
|
|
29
|
+
return buildLocalizedListPageMetadata(
|
|
30
|
+
'pageTitleCategory',
|
|
31
|
+
{ label },
|
|
32
|
+
{
|
|
33
|
+
path: `/category/${encodeURIComponent(categoryValue)}`,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
} catch {
|
|
37
|
+
return buildLocalizedListPageMetadata(
|
|
38
|
+
'pageTitleCategory',
|
|
39
|
+
{ label: categoryValue },
|
|
40
|
+
{
|
|
41
|
+
path: `/category/${encodeURIComponent(categoryValue)}`,
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default async function CategoryArchivePage({ params }: PageProps) {
|
|
48
|
+
const { category } = await params
|
|
49
|
+
const categoryValue = decodeURIComponent(category)
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const data = await withApiRetry(() => fetchCategoryArchivePageProps(themeApi, categoryValue))
|
|
53
|
+
return (
|
|
54
|
+
<CategoryClient initialArticles={data.articles} total={data.total} category={data.category} />
|
|
55
|
+
)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('[reactpress-theme-starter] category archive fetch failed', error)
|
|
58
|
+
notFound()
|
|
59
|
+
}
|
|
60
|
+
}
|