Files
web_hgcg_h5/README.md
T
2026-10-27 17:39:27 +07:00

257 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 黄果吃瓜 · 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`。