257 lines
33 KiB
Markdown
257 lines
33 KiB
Markdown
# 黄果吃瓜 · Next.js
|
||
|
||
Next.js App Router、React、TypeScript。保留 HTML 原稿布局和移动端导航,业务数据通过 Swagger API 动态获取。
|
||
|
||
## 运行
|
||
|
||
已使用 Node.js **22.17.1** 验证;最低要求 20.9。
|
||
|
||
```sh
|
||
nvm use 22.17.1
|
||
npm install
|
||
npm run dev
|
||
```
|
||
|
||
开发、测试和生产启动的默认地址均为 http://localhost:21021,端口或环境变量修改后需要重启 Next.js。
|
||
|
||
```sh
|
||
npm run typecheck
|
||
npm run build
|
||
npm test
|
||
npm start
|
||
```
|
||
|
||
现在需要运行 Next.js 服务以提供 API 代理与动态首页,不再导出静态 `out/`。旧的静态预览进程需要停止后用新命令启动。
|
||
|
||
## 环境配置
|
||
|
||
本项目使用 `config/*.env` 区分业务环境,启动脚本在运行 Next.js 前加载配置。
|
||
|
||
| 环境 | 配置文件 | 构建命令 | 启动命令 | 构建目录 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 开发 | `config/development.env` | `npm run build:dev` | `npm run dev`;构建后可用 `npm run start:dev` | `.next-dev/` |
|
||
| 测试 | `config/test.env` | `npm run build:test` | `npm run start:test` | `.next-test/` |
|
||
| 生产 | `config/production.env` | `npm run build` | `npm start` | `.next/` |
|
||
|
||
测试环境按生产模式构建、启动,使用 `APP_ENV=test` 和 `NODE_ENV=production`;`npm run dev:test` 则使用测试配置启动开发服务器。业务环境通过 `APP_ENV` 区分,不把测试部署误设为单元测试使用的 `NODE_ENV=test`。
|
||
|
||
浏览器始终访问当前站点的相对地址 `/api/*`,Next.js 服务端通过 `UPSTREAM_API_URL` 请求 Go 的 `/api/app/*`。开发、测试、生产环境均使用 `https://webhgcgapi.mfcg.me`。上游地址仅由服务端读取。默认应用端口均为 `21021`。公开接口缓存和 SEO 快照按解析后的上游域名隔离。修改服务器配置时,在对应目录新增 `config/test.local.env` 或 `config/production.local.env`;这些本地文件已忽略提交。优先级为:进程环境变量 > 对应 `*.local.env` > 对应基础配置。开发环境兼容旧 `.env.local`,其优先级低于 `config/development.local.env`;测试和生产不从旧 `.env.local` 加载本站业务配置。
|
||
|
||
原有 `API_BASE_URL` 仍作为兼容别名。在同一配置层内 `UPSTREAM_API_URL` 优先;较高配置层的旧变量仍可覆盖较低层的新变量,启动后统一为同一个上游地址。上游地址仅在服务端读取,不写入浏览器代码。
|
||
|
||
例如需要测试服务同时运行在另一端口,可在 `config/test.local.env` 写入:
|
||
|
||
```dotenv
|
||
PORT=21022
|
||
```
|
||
|
||
也可以临时运行 `npm run start:test -- --port 21022`。不同环境使用各自的构建产物及 SEO 快照缓存;先执行对应环境的构建命令,再执行其启动命令。`UPSTREAM_API_URL` 等服务端配置在启动时读取,修改后重启即可;`NEXT_PUBLIC_*` 在构建时写入浏览器代码,修改后应重新构建。
|
||
|
||
开发环境默认关闭公共接口缓存,测试和生产保留原有缓存上限;可用 `PUBLIC_DATA_CACHE_TTL_MS` 覆盖。`npm run check` 会先完成生产构建(含类型检查),再运行完整本地回归测试。
|
||
|
||
## 打包与部署
|
||
|
||
使用 Node.js 22.17.1;生产跨平台打包需要 npm >=10.9.2。两份脚本均先构建、运行回归测试,再整理 Next.js standalone、静态资源和对应环境配置。
|
||
|
||
```sh
|
||
# 预览流程,不构建、不连接服务器
|
||
bash deploy_test.sh --dry-run
|
||
bash deploy_prod.sh --dry-run
|
||
|
||
# 只生成本地压缩包,不上传
|
||
bash deploy_test.sh --package-only
|
||
bash deploy_prod.sh --package-only
|
||
```
|
||
|
||
测试产物为根目录的 `web_91dj_h5.tar.gz` 和 `web_91dj_h5.tar.gz.sha256`,上传相同文件及 `up_next_h5.sh`;同时刷新同名 `web_91dj_h5.md5` 供本地兼容校验。包内直接包含 `start.mjs`、`server.js`、`.next-test/`、`public/`、`config/test.env` 和 lockfile。测试包不带本机 `node_modules`,使用 `tar -xzf web_91dj_h5.tar.gz` 解压后,在 Linux 执行 `npm ci --omit=dev --no-audit --no-fund`,再执行 `node start.mjs`;默认端口为 21021。可通过 `DEPLOY_PACKAGE_NAME=自定义名称.tar.gz` 覆盖包名,校验文件随之生成。测试和生产使用同名压缩包,根目录文件对应最后一次执行的环境。
|
||
|
||
生产产物为 `web_91dj_h5.tar.gz` 和纯哈希格式的 `web_91dj_h5.md5`,包含 `.next/` 和 standalone 裁剪依赖,默认适配 Linux x64/glibc,可直接执行 `node start.mjs`。可通过 `DEPLOY_TARGET_ARCH=arm64`、`DEPLOY_TARGET_LIBC=musl` 选择目标架构和 libc。打包时仅从 lockfile 安装对应 Linux Sharp/libvips,并校验 ELF 架构,不携带 macOS 原生模块。
|
||
|
||
两种包仅包含对应的基础 `config/*.env`,不打包 `.env*`、`*.local.env` 和 SEO 缓存。服务器的私有覆盖配置放在发布目录下对应的 `config/test.local.env` / `config/production.local.env`;修改 `NEXT_PUBLIC_*` 必须重新构建。本站媒体密钥由上游接口代理提供,无需额外的本地 `m3u8.key` 文件。
|
||
|
||
部署默认值由两份脚本开头的变量定义,并支持环境变量覆盖;当前上传配置如下。本项目包名和 Supervisor 名使用 `91dj`:
|
||
|
||
| 环境 | SSH 目标 | 上传目录 | SCP / SSH 端口 |
|
||
| --- | --- | --- | --- |
|
||
| 测试 | `root@172.104.181.99` | `/server/web_91dj/h5` | `22889 / 22889` |
|
||
| 生产 | `server@34.92.204.175` | `/home/server/web_91dj/h5` | `22889 / 22889` |
|
||
|
||
直接执行脚本会使用上述地址;仅打包继续使用 `--package-only`。需要其他目标时可覆盖:
|
||
|
||
```sh
|
||
DEPLOY_HOST=user@test-host DEPLOY_DIR=/srv/91dj/h5 DEPLOY_RUN_DIR=/srv/91dj/h5 bash deploy_test.sh
|
||
DEPLOY_JUMP_HOST=user@jump-host DEPLOY_JUMP_DIR=/srv/91dj/h5 bash deploy_prod.sh
|
||
```
|
||
|
||
测试当前 SSH 端口为 22889,可通过 `DEPLOY_SSH_PORT` 修改;它与 `config/test.env` 中的应用监听端口 `PORT` 分别配置。`deploy_test.sh` 上传成功后执行 `ssh -p 22889 root@172.104.181.99 "cd /server/web_91dj/h5 && bash run.sh"`,由服务器现有的 `run.sh` 负责后续更新。上传目录和执行目录当前均为 `/server/web_91dj/h5`,分别可用 `DEPLOY_DIR`、`DEPLOY_RUN_DIR` 覆盖;脚本不上传或覆盖远端 `run.sh`。部署测试从脚本帮助信息读取当前默认执行目录,并单独验证目录覆盖及上传、执行失败时的退出行为,修改服务器目录无需同步修改测试断言。上传失败不会执行远端更新,远端命令失败会立即退出;`--package-only` 和 `--dry-run` 均不执行 SSH。可通过 `DEPLOY_CHECK_URL` 在远端命令成功后增加公网健康检查。
|
||
|
||
随包上传的 `up_next_h5.sh` 保留供服务器按需调用:该脚本校验 SHA-256、检查并解压 tar.gz、安装运行依赖、保留本地覆盖与 SEO 缓存,然后切换版本、启动服务并检查 `/healthz` 与首页,失败自动恢复旧版本。默认 Supervisor 程序为 `91dj_next_h5`,在服务器运行该脚本时可通过 `DEPLOY_PROGRAM` 覆盖;使用自定义包名时需设置对应的 `DEPLOY_PACKAGE_NAME`。服务器 `run.sh` 的解包、服务管理及包名规则以该脚本自身实现为准。
|
||
|
||
生产脚本将 `web_91dj_h5.tar.gz` 和 `web_91dj_h5.md5` 上传到 `server@34.92.204.175:/home/server/web_91dj/h5/`,SCP 与 SSH 默认端口均为 `22889`。上传成功后执行 `ssh -p 22889 server@34.92.204.175 "cd /home/server/web_91dj/h5 && bash rsupdate.sh"`。服务器现有的 `rsupdate.sh` 负责解包、保留生产私有配置和重启服务;本项目不上传或覆盖该脚本。上传失败不会触发远端更新,SSH 失败或返回失败状态会停止发布。可通过 `DEPLOY_JUMP_HOST`、`DEPLOY_JUMP_DIR`、`DEPLOY_SCP_PORT`、`DEPLOY_SSH_PORT` 覆盖目标,执行目录同步跟随上传目录。
|
||
|
||
生产 SSH 的远端命令文本与上述手动命令保持一致:通过路径校验后,`cd` 的目录不额外添加单引号,以兼容按原始命令文本匹配的 SSH 白名单。dry-run 与实际执行共用同一个命令,运行时会打印命令并保留远端日志。远端即使打印“发布成功”,只要此前输出含 `ERROR`(如 `curl` 报 HTTP 401)、`FATAL` 等失败状态,本地仍会报错;HTTP 401 的具体检查地址需核对服务器现有 `rsupdate.sh`,应用自身提供无需登录的 `/healthz`。
|
||
|
||
`npm run bundle:test` / `npm run bundle` 只整理已有构建;`npm run bundle:linux -- x64 glibc` 为生产产物准备 Linux 原生依赖。同一仓库的测试、生产脚本共用打包锁,压缩包及 `dist/` 已忽略提交。`/healthz` 是不触发游客登录、禁止缓存和索引的部署探针,不依赖上游接口。
|
||
|
||
## 页面路由
|
||
|
||
首页使用站点根地址,例如 `http://127.0.0.1:21021`,也支持 `http://127.0.0.1:21021/`;两者都直接显示首页。浏览器把纯域名的请求路径解析为 `/`,不需要跳转。短剧列表单独使用 `/shorts/`。
|
||
|
||
其他页面统一使用以 `/` 结尾的路径路由,可直接打开、刷新、在新标签页打开,并使用浏览器前进和后退。链接生成时就包含尾斜杠,不通过跳转或地址替换补齐。
|
||
|
||
- 首页 `/`;短剧 `/shorts/`;专题 `/topics/`;热播榜 `/rank/hot/`、新剧榜 `/rank/new/`、追剧榜 `/rank/follow/`;应用 `/apps/`;回家的路 `/home-road/`。榜单 tab 使用页面链接,数据类型由路径决定;原 `/rank/` 仍可直接访问热播榜,不重定向。
|
||
- 官方入口 `/navigation/`:独立展示线路、官方渠道与 App 下载,不加载首页导航、播放器或账户交互;首页和业务页面不再弹出入口导航页。
|
||
- 专题、题材、标签、栏目:`/topics/:id/`、`/theme/:id/`、`/tag/:id/`、`/config/:id/`。
|
||
- 详情 `/detail/:id/`;章节播放 `/play/:id/:chapterId/`。
|
||
- 内容路由优先使用接口返回的英文或拼音 `slug`(字母、数字、连字符、下划线),中文或无效 slug 回退到 ID,例如 `/detail/city-story/`、`/play/city-story/90001/`、`/topics/urban/`、`/tag/urban/`、`/config/editors-choice/`。章节段仍是章节 ID。卡片、轮播、选集、评论/列表分页和 SEO 地址使用同一规则;原 ID 地址继续直接访问。有配置的 `moreUrl` 优先级不变。
|
||
- 搜索 `/search/关键词/`;分页 `/search/关键词/page/2/`。关键词通过 `encodeURIComponent` 作为单个路径段编码,支持中文和特殊字符。
|
||
- 独立导航页 `/navigation/` 站名固定为“黄果吃瓜”,入口线路和永久入口固定为 `https://huangguocg.com/`(`src/lib/site.ts`),联系方式固定为官方 TG `@jbd125`;不使用后台 `navPageList`、`newUrlAddr*`、`foreverUrlAddr` 和联系方式字段。介绍和 App 下载地址仍读取 `config/info`,未配置或无效的下载链接不展示。
|
||
- 帮助与平台信息 `/info/:slug/`。个人中心(`/member/*`)、充值/VIP(`/info/deposit/*`)、`/info/user-center/`、`/info/settings/` 已删除,返回 404。
|
||
- 所有分页使用独立路径,例如 `/shorts/page/2/`、`/rank/new/page/2/`、`/topics/31/page/2/`。详情和播放页不分页(评论已删除),`/page/N/` 返回 404。页码、上一页和下一页均为真实链接,可直接访问、刷新和前进后退。
|
||
- 排序通过 `?sort=2` 保留。切换排序时链接指向第一页,翻页时保留当前条件;第一页链接使用原页面路径,不执行自动跳转或地址替换。
|
||
|
||
页面直接根据 URL 路径加载,不解析或转换旧 hash 路由。站内导航、动态卡片、播放选集和搜索入口均使用路径地址;无效路径返回 404。
|
||
|
||
页面地址统一使用尾斜杠。`src/proxy.ts` 对有效页面的无尾斜杠地址返回 `308` 永久重定向,例如 `/shorts` → `/shorts/`,分页、搜索关键词和查询参数完整保留;首页 `/` 直接响应。Next.js 保留 `trailingSlash: true` 和 `skipTrailingSlashRedirect: true`,由 Proxy 仅处理页面,API、播放列表、密钥、静态资源地址不受影响;无效路径直接返回 404。
|
||
|
||
## 接口配置
|
||
|
||
顶部、侧栏和移动导航由 `navigation/header.categoryList` 在服务端渲染,按接口顺序显示名称、地址和二级导航。URL 导航与二级导航显示在主导航,底部类型不混入;成功返回空列表时不注入固定菜单。`/hot` 直接生成本站 `/rank/hot/` 链接;已知系统入口的空地址按本站路由生成。导航公共数据缓存 60 秒,后台更新在缓存过期后的页面请求生效;页面高亮仍按当前路由判断。接口图标优先,缺失时沿用本站图标;外链添加 nofollow 等属性。
|
||
|
||
2026-10-21 已按当前 91av Swagger 重新核对各页面,具体映射和接口缺项见 [页面接口对照](docs/api-contract.md)。短剧列表使用 `playlet/all/list`;专题采用顶层详情字段;首页未配置 `moreUrl` 的栏目使用首页真实数据兜底。按用户要求移除独立剧集入口及 `/series/`、`/video/:id/` 路由,旧地址直接返回 404。
|
||
|
||
页面 head 由本项目(`JHA-1047`)的 `src/lib/seo-server.ts` 和 `src/lib/seo.ts` 生成:全站名称与首页 TDK 取 `config/info.seoInfo`,栏目 TDK 优先使用 `translation/category/list` 的导航配置,具体专题、剧集使用对应接口信息。兼容字符串形式的 SEO 配置及 `keyworld` 等旧字段。`/navigation/` 使用 `seoNavInfo`。
|
||
|
||
canonical、sitemap 和结构化数据的域名固定为 `https://huangguocg.com`(`src/lib/site.ts` 的 `SITE_ORIGIN`),不使用后台的 `foreverUrlAddr`,也不回退到请求 Host、API 或 CDN 域名。页面上展示的站点地址(导航页线路与永久入口、回家的路、详情页“官方地址”)同样固定为该域名。配置沿用 60 秒公共缓存,公开 SEO 信息保存到 `.next/cache/seo/`,按 API 地址隔离;接口超时或报错时沿用最近一次有效的站名与 TDK,服务重启也可读取已有快照。快照不包含账号或凭证;部署时需保留该缓存目录。同一浏览器文档沿用服务端给出的 canonical、site_name 和发布者身份。
|
||
|
||
canonical 不带查询参数,分页各自生成规范地址和有边界的 `prev/next`,第 1 页使用栏目地址。搜索、账户、充值和工具页按参考项目输出 `noindex`。服务端将 TDK、robots、Open Graph、Twitter 和图标信息直接写入 `<head>`,每次页面请求都返回对应页面的完整 head。本次未新增统计脚本。
|
||
|
||
`/robots.txt` 由 `src/app/robots.ts` 提供纯文本响应,不依赖账户会话或请求 Host。生产环境(`APP_ENV=production`)允许页面和 JS/CSS、图片、播放资源抓取,仅限制 `/api/` 和 `/healthz`;开发、测试环境禁止全站抓取。搜索、账户、充值、工具和重复页面继续使用页面级 `noindex`,保留爬虫读取这些标记的权限。生产环境声明 `Sitemap: https://永久域名/sitemap.xml`,域名与 canonical 共用配置及最近一次有效快照;完全没有有效域名时仅省略 Sitemap 行,抓取规则继续返回。开发、测试环境不声明 Sitemap。本项只添加声明,XML 文件仍由后续 sitemap 服务提供。
|
||
|
||
公开正文与 SEO 使用同一次服务端取数:短剧、三个榜单、专题列表/详情、题材/标签、首页更多栏目、短剧详情、播放页及各自分页直接输出当前页的标题、简介、内容卡片、集数和已有评论,分页与免费集数保留原生链接。搜索结果也直接输出首屏正文,继续保持 `noindex`。详情/播放和栏目模板在服务端与站内导航间共用,沿用现有 HTML 样式。
|
||
|
||
请求与渲染方式:服务端先完成当前页面取数,再返回页头和完整正文;福利应用和回家的路页面也直接输出内容。链接和搜索使用浏览器原生页面请求,旧页面保留到新响应到达,不清空正文或插入页面加载占位。浏览器复用首屏快照绑定轮播、播放、表单和支付等交互,不再重复获取整页数据;提交成功后重新请求当前页面。
|
||
|
||
公共配置、导航、分类、页脚和标签选项使用 60 秒进程内缓存,首页使用 30 秒;同一上游地址及查询参数的并发请求合并,返回独立数据副本,缓存有数量上限。配置及首页刷新失败可沿用已成功取得的数据;错误和无效响应不写入缓存。账户、余额、套餐及付费观看权限不使用公共缓存,HTML 继续保持私有响应。可用 `PUBLIC_DATA_CACHE_TTL_MS` 缩短公共缓存时间,设为 `0` 可关闭;多实例各自维护缓存。
|
||
|
||
详情、章节、选集和推荐按 URL 中的 ID 并行请求。浏览器复用首屏中的公开配置和页脚数据,下载与广告交互不再等待重复取数。支持浏览器前进后退缓存时,公开页保留 DOM、滚动位置和交互,并在后台恢复当前会话;账户、播放及打开弹窗的页面仍重新请求,防止恢复已处置的支付/权限状态。
|
||
|
||
有效业务页面在 Proxy 中恢复或建立游客会话,将同一身份的 Cookie 传给本次服务端渲染并写回浏览器。客户端复用已准备的用户信息,避免首屏再次初始化游客。正式账号过期显示登录入口,接口异常直接返回重试状态;账户页面保持 `private, no-store` 和 `noindex`。首屏账号、账户列表、套餐、流水和播放器数据均按字段白名单传递,未授权付费视频源、服务端账号凭证及 SDK 私密配置不写入 HTML;点击金币集数仍读取实时权限、价格并由用户确认解锁。当前 Next.js 动态参数在路由入口只解码一次,中文和含空格、斜杠、百分号的搜索词与浏览器保持一致。
|
||
|
||
JSON-LD 参考 `huangguo.com` 的品牌节点及参考项目 `seoser/jsonld.go`:收录页输出 `Organization`、`WebSite` 和对应 `WebPage` / `CollectionPage`,列表输出实际内容的 `ItemList`,详情使用 `TVSeries`,播放页关联 `TVEpisode` 与具备真实封面、发布时间的 `VideoObject`。关于页使用 `AboutPage`,常见问题直接从页面可见问答生成 `FAQPage`。URL、站名与分页序号沿用现有动态 SEO 配置;不使用参考站的品牌、域名、伪造日期或评分。每份页面响应包含本页的原生 JSON-LD script,不收录页不输出旧页面的数据。测试覆盖脚本安全转义和重复节点清理。
|
||
|
||
- 文档:https://91av-wz.80sygtm.com/swagger/index.html
|
||
- `UPSTREAM_API_URL`:三套环境均为 `https://webhgcgapi.mfcg.me`;兼容旧 `API_BASE_URL`。
|
||
- `API_CHANNEL`:可选游客会话的默认渠道码;已有首次渠道时优先保留。
|
||
- 项目编号:`JHA-1047`。接口读取与业务处理位于 `src/lib/api/server.ts`、`src/lib/client/config.ts`、`src/lib/client/channel.ts`、`src/lib/client/auth.ts` 和 `src/lib/client/account.ts`。
|
||
|
||
浏览器访问同源 `/api/*`,由 Next.js 按白名单代理到配置上游的 `/api/app/*`。Nginx 已有的 `/api/app/* → Go` 规则可以保留,其他 `/api/*` 交给 Next.js;不能将整个 `/api/` 都交给 Go。旧 Next.js `/api/app/*` 处理器仍保留供本地兼容访问。首页由服务端渲染,公开首页数据缓存 30 秒。公共接口不携带用户 token;用户接口使用 HttpOnly、SameSite Cookie。token 和密码不会写入浏览器本地存储或返回给前端脚本。
|
||
|
||
若显式配置为 `current`,则使用当前请求域名(默认配置不使用此模式)。该模式依赖 Nginx 将 `/api/app/*` 及 `/h5/*`、`/enkey`、`/newenkey` 等后端媒体路径转发到 Go,本站其他页面和 `/api/*` 由 Next.js 处理。上游请求若被再次转回 Next.js,会立即返回 `502` 和转发配置提示,避免递归请求。修改接口模式后重启服务即可,无须将当前域名写入前端构建产物。
|
||
|
||
请求处理参考 `jha-1045-hgdjnet-web`:Next.js 接口代理不根据 `Origin`、`Host` 或 `Sec-Fetch-Site` 拦截 POST,避免 CloudFront / Nginx 转发后域名或协议变化导致正常浏览器请求返回 403。浏览器继续请求当前站点的 `/api/*`,代理负责接口白名单、参数处理和会话传递,业务权限由上游校验。
|
||
|
||
## 游客会话
|
||
|
||
已删除登录、注册/绑定、找回密码和退出,全站只使用游客身份(点赞、收藏仍需要它)。实现位于 `src/lib/client/api.ts`、`src/lib/api/page-session.ts` 和 `src/app/api/app/[...path]/route.ts`。
|
||
|
||
- 首次访问自动建立游客会话;已有 Cookie 时先读取 `auth/getInfo` 恢复身份,接口未返回新 token 时继续使用原 token。
|
||
- 游客使用持久设备标识,发送 `devID` 和渠道;首次渠道取 `dc/ch/pc`,后续访问保持不变。服务端按参考项目发送设备头,并透传有效的客户端 IP。
|
||
- 会话失效时(包括旧版本遗留的已登录 Cookie)清理会话并重建游客后重试一次,不再提示登录。仅设备与渠道写本地存储,凭证保留在 HttpOnly Cookie。
|
||
|
||
## 已接入
|
||
|
||
- 首页轮播、公告、后台配置栏目、广告、题材及视频区块。
|
||
- 短剧运营分类、剧集视频列表、榜单、专题、题材/标签、分页和更多列表。
|
||
- 搜索、搜索联想、热搜、猜你喜欢;搜索记录单独保存在当前浏览器。
|
||
- 短剧详情、章节列表、章节权限和实际播放地址;HLS 播放、媒体代理和播放上报。
|
||
- 游客会话;点赞、收藏、分享。评论已删除,详情页和播放页的评论区换成“推荐视频”(`playlet/guess-like/list`,12 部)。
|
||
- 全部短剧和视频免费观看:前端不再识别 `needUnlock`/`needVip`/`payType`/`canWatch`,只要接口返回了视频地址就直接播放,选集里没有付费角标。接口没返回地址的章节显示“本集视频暂未开放”,有免费预告时可看预告。**付费章节目前后端不返回 `videoUrl`,需要在后台把这些章节设为免费后才能播放。**
|
||
- 应用列表、下载配置、固定平台说明页。
|
||
|
||
首页“推荐 / 最新 / 热门 / 随机”筛选项保持移除。栏目排序、列表数据、总数与观看权限由接口决定,失败时显示错误或空状态。
|
||
|
||
首页动态栏目沿用 HTML 的模块间距(桌面 48px、移动端 32px)与卡片网格,首个栏目不再复用原稿图文混排区域的选择器。首页封面统一为 3:4,图片居中裁切,避免接口方图、竖图混排时卡片高度不齐,并在图片解密加载前预留封面高度。
|
||
|
||
全站以原稿 HTML 的页面结构和样式为基准:短剧、剧集、专题、题材/标签、更多列表和搜索页沿用原稿网格、标题、筛选栏和封面框;排行榜使用原稿数字角标、五列布局及窄屏断点,排名按接口分页顺序连续计算,榜单 tab 保持独立路由。搜索关键词只在已转义的标题中高亮。
|
||
|
||
专题列表与专题详情按 HTML 直接展示卡片和分页,不增加“最新专题 / 热门专题”或“编辑推荐 / 最新 / 最热”等分类选项。
|
||
|
||
搜索结果由本项目(`JHA-1047`)的 `src/lib/client/search.ts` 和 `src/lib/seo-server.ts` 实现:服务端准备会话后调用 `playlet/search`,传入 `keyword/pageNum/pageSize`,默认每页 25 条;展示接口总数、关键词和实际 `pageSize`,剧名优先使用 `playName`,不再本地筛选接口结果。无结果优先展示 `everyoneWatching`,缺少推荐时请求 `playlet/hot/list`;推荐不计入搜索数量。`/search/` 可直接进入,空关键词跳过搜索接口,翻页使用 `/search/{关键词}/page/{页码}/`,搜索框、热搜、搜索历史和联想词入口均直接生成关键词路径,不通过重定向转换。标题高亮沿用 HTML 的 NFKC、大小写和空白处理;无结果/失败状态使用原稿插图,接口错误保留重试入口,不伪装为零结果。
|
||
|
||
“关于我们、用户协议、隐私政策、常见问题、2257、DMCA”六页固定使用 `src/lib/info-pages.ts` 中的原 HTML 文案与结构,不再请求富文本接口,底部入口也保持固定。每次访问均由服务端输出正文、标题和对应样式标记,无须等待首页或富文本接口。保留分节间距、折叠问答、编号段落和 `mailto:` 邮箱链接。
|
||
|
||
联系方式与其他黄果站点一致,全站只展示官方 TG `@jbd125`(`https://t.me/jbd125`,`src/lib/site.ts` 的 `OFFICIAL_TG_HANDLE`/`OFFICIAL_TG_URL`):关于我们的“联系我们”、DMCA 联系方式、页脚和侧栏图标、导航页“官方TG”、回家的路“保持联系”、详情页官方链接以及结构化数据 `sameAs`。不再读取后台的 `telegram`、`tgGroup`、`twitterAddr`、邮箱或页脚 `contactList`,也不展示 X、邮箱和 TG 群。
|
||
|
||
|
||
本站 UI 为了收录和与其他黄果站点区分,使用独立的 `hg-*` 组件(`src/lib/client/render.ts`、`detail.ts`、`player-markup.ts`,样式在 `globals.css` 末尾):
|
||
|
||
- 短剧卡片 `card()`:`<article class="hg-card">`,整卡一个链接,`title` 为“《剧名》全N集 在线观看”,海报 alt 为“《剧名》海报”,可见标题、题材标签、简介、热度和集数角标;榜单显示排名数字。
|
||
- 推荐视频条目 `videoItem()`:横向缩略图加 `<h3>` 标题链接、题材/集数/热度和三行简介,用于详情页和播放页的“推荐视频”区。
|
||
- 首页 banner `hero()`:主图区输出当前推荐的 `<h2>` 标题链接、题材/集数/热度、简介和“立即观看”;右侧 `<ol>` 列出全部 banner 的真实详情链接(首次点击切换主图,再次点击进入详情),只有 1 个 banner 时隐藏列表。
|
||
- 详情页:海报 + `<h1>`、“《剧名》全N集在线观看”导语、`<dl>` 信息表(类型链接、集数、热度、收藏、点赞)、操作区;随后是“《剧名》剧情简介”“《剧名》全集列表”“推荐视频”和官方链接。
|
||
- 播放页:播放器(桌面端限高居中)、“剧名 第N集” `<h1>`、同一信息表与操作、剧情简介、全集列表(链接带“《剧名》第N集” title)、推荐视频。
|
||
|
||
App 下载入口(顶部、悬浮图标、首页底部、详情页和 `/navigation/`)统一校验配置。配置返回前隐藏入口;没有有效的 HTTP(S) 下载链接时不展示。电脑端直接点击链接下载,已移除悬浮二维码面板和触发脚本;移动端保留点击下载弹窗,二维码只在实际点击时生成。
|
||
|
||
图片解密由本项目(`JHA-1047`)的 `src/lib/client/images.ts` 实现:使用相同密钥对前 100 字节做 XOR,识别 JPEG、PNG、GIF、WebP,并兼容未加密图片。封面、轮播、题材、广告、应用图标、用户/评论头像和视频 poster 统一处理;接口原图放在 `data-src` / `data-poster`,完成解密后展示 Blob URL。本地素材和下载二维码保持直接加载。移动端并发 2、桌面端并发 4,首屏主图和弹窗优先;共享请求与缓存、视口懒加载、12 秒超时及占位图回退,页面离开时释放 Blob URL。
|
||
|
||
播放逻辑由本项目(`JHA-1047`)的 `src/lib/client/player.ts`、`src/lib/client/playback.ts` 和 `src/lib/api/media.ts` 实现:绝对地址直接播放,相对地址走 `/h5/m3u8/`;Safari 优先原生 HLS,其余浏览器优先 hls.js,无 MSE 时再尝试原生 HLS。保留移动端内联属性,HLS 致命错误最多恢复 3 次,重试会重建播放器;切换预告、正片或离开路由时销毁旧实例。正片只要接口返回有效 `videoUrl` 即可播放(不判断付费字段),没有地址时可看配置的免费预告;预告不提交正片观看上报。上一集/下一集使用接口的真实章节 ID。
|
||
|
||
H.264/AVC 与 H.265/HEVC 使用 `hls.js` 1.7.3 完整版解析,保留 HEVC MPEG-2 TS 支持(参考项目为 1.6.13 完整版)。多编码清单按设备能力保留 H.264、`hvc1`、`hev1`,不强制改写编码或限制为 H.264。编码不兼容时在 hls.js 与原生 HLS 之间各尝试一次,保留签名与播放位置;普通网络/密钥错误仍走原有恢复逻辑。无可用解码方式时显示编码不支持提示。H.265 的实际解码依赖浏览器、系统及设备能力,前端不提供转码或软件解码器,参见 [hls.js 官方说明](https://github.com/video-dev/hls.js/blob/v1.7.3/README.md)。
|
||
|
||
媒体和 `/enkey`、`/newenkey` 使用 Route Handler 读取运行时 `UPSTREAM_API_URL`,保留完整签名查询参数,不转发应用登录 Cookie 或 Authorization。密钥上游先请求无尾斜杠路径,兼容不同服务的尾斜杠规则:只允许同一域名、同一密钥路径增加或删除尾斜杠的一次跳转,签名查询、IP 和 UA 保持不变,返回值必须是 16 字节 AES 密钥。媒体支持 Range,清单和密钥均不缓存,业务错误响应不会作为视频内容返回。
|
||
|
||
## 年龄确认与广告弹窗
|
||
|
||
弹窗逻辑由本项目(`JHA-1047`)的 `src/lib/client/startup.ts` 实现,保留本项目的弹窗样式。
|
||
|
||
- 年龄确认使用会话记录 `hg_age_ok`,当前标签页会话首次进入时展示;确认后刷新、切换业务页面不再重复展示。存储不可用时仍可正常确认并继续;退出行为与参考项目一致,打开 Google。
|
||
- 大图广告使用配置接口 `advitise.dialogList`,按接口顺序逐条展示;先通过年龄确认,再显示已加载完成的广告,配置晚于确认返回时也能继续展示。
|
||
- 点击关闭或按 Esc 进入下一条;点击广告在新标签页打开配置链接。全部关闭后,同一会话内不再展示相同广告队列;刷新时从当前广告恢复。广告队列签名优先使用 `advertiseCode`,队列变化后重新展示。
|
||
- 图片加载失败、解码失败或 12 秒超时会跳过当前广告;全部失败时不记为已完成,后续页面可以重试。
|
||
- 大图广告结束后展示 `advitise.nineGridList` 宫格弹窗,保留 HTML 的四列图标、名称、间距、模糊遮罩和底部关闭按钮;没有大图广告时,年龄确认后直接展示宫格。按配置顺序渲染,不补齐模拟数据,图片复用解密逻辑,失败项不展示;没有有效图片时不弹出。点击应用打开配置链接并关闭弹窗,关闭按钮和 Esc 均可关闭;`gridAdDismissed` 独立记录当前宫格配置,同一会话不重复展示,配置变化后可重新展示。
|
||
- 宫格弹窗最大高度为视口的 80%,预留底部关闭按钮空间,超出的广告在宫格区域内纵向滚动;每次展示从顶部开始。
|
||
|
||
`/navigation/` 保持独立入口页,不加载年龄确认和广告队列。
|
||
|
||
## 验证
|
||
|
||
`npm test` 使用独立本地模拟上游和生产构建验证代理与 SSR,不写入真实业务数据。
|
||
|
||
启动服务后可执行真实接口检查:
|
||
|
||
```sh
|
||
PREVIEW_URL=http://127.0.0.1:21021 npm run smoke:api
|
||
```
|
||
|
||
若游客登录服务暂时超时,可仅验证公开内容:
|
||
|
||
```sh
|
||
SMOKE_PUBLIC_ONLY=1 PREVIEW_URL=http://127.0.0.1:21021 npm run smoke:api
|
||
```
|
||
|
||
2026-10-21 的类型检查、生产构建和全部 258 项测试通过;真实接口检查通过游客会话、公开与个人只读接口及 29 个页面。页面检查确认账户、充值、搜索和工具页的首个响应包含完整正文。真实支付、付费解锁及浏览器视觉验收不在此次验证范围内,详情见 [验证记录](docs/verification.md)。
|
||
|
||
## 目录
|
||
|
||
- `src/app/api/app/[...path]/route.ts`:业务代理、会话 Cookie 和接口白名单。
|
||
- `src/app/[...path]/page.tsx` 与 `src/lib/routing.ts`:页面路径入口、校验与解析。
|
||
- `src/app/navigation/page.tsx`:独立入口导航页,服务端读取线路、渠道和下载配置。
|
||
- `src/lib/api/`:接口类型、响应兼容和服务端请求。
|
||
- `src/lib/seo-server.ts`:服务端页面取数、正文模板和 SEO。
|
||
- `src/lib/api/page-session.ts`:页面会话准备及同一次请求的 Cookie 传递。
|
||
- `src/lib/api/public-cache.ts`:有上限的公共数据缓存、过期刷新及并发合并。
|
||
- `src/lib/site-shared-data.ts`:首屏公开配置和页脚数据白名单。
|
||
- `src/lib/client/`:共享页面模板及轮播、播放、下载等浏览器交互。
|
||
- `public/scripts/site-runtime.js`:原稿布局、移动端菜单和播放器控件。
|
||
- `public/assets/`:本地品牌、图标和空状态素材。黄果吃瓜 logo 为 `hgcg-logo.png`(500×176 比例,左侧图标、右侧字标),悬浮 App 图标为 `hgcg-app-icon.png`;`favicon.png`、`apple-touch-icon.png`、`src/app/icon.png` 和分享图 `og-default.png` 均由同一黄果图标生成。
|
||
- `tests/api-integration.test.mjs`:SSR、鉴权隔离和代理边界测试。
|
||
- `tests/routing.test.mjs`:路径解析、搜索关键词编码、hash 地址不转换与原生页面导航测试。
|
||
- `src/lib/client/startup.ts` 与 `tests/startup.test.mjs`:年龄确认、广告队列和会话进度,以及控制器与浏览器事件适配测试。
|
||
|
||
## 已删除的功能
|
||
|
||
已移除 Web SDK 埋点(含 `X-SDK-Common`、`X-Tracker-*` 请求头透传)、意见反馈/在线客服、充值与 VIP、个人中心(收藏/历史/点赞/已购/设置)、登录注册以及付费章节解锁。对应页面返回 404,代理白名单不再放行 `auth/login`、`auth/register`、`auth/resetPassword`、`auth/profile`、`coin/*`、`vip/*`、`recharge/*`、`playlet/chapter/unlock`、个人列表和 `upload/image`。
|
||
|