Files
2026-10-27 21:28:04 +07:00

33 KiB
Raw Permalink Blame History

黄果吃瓜 · Next.js

Next.js App Router、React、TypeScript。保留 HTML 原稿布局和移动端导航,业务数据通过 Swagger API 动态获取。

运行

已使用 Node.js 22.17.1 验证;最低要求 20.9。

nvm use 22.17.1
npm install
npm run dev

开发、测试和生产启动的默认地址均为 http://localhost:21021,端口或环境变量修改后需要重启 Next.js。

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 写入:

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、静态资源和对应环境配置。

# 预览流程,不构建、不连接服务器
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。需要其他目标时可覆盖:

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 重新核对各页面,具体映射和接口缺项见 页面接口对照。短剧列表使用 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 官方说明。

媒体和 /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,不写入真实业务数据。

启动服务后可执行真实接口检查:

PREVIEW_URL=http://127.0.0.1:21021 npm run smoke:api

若游客登录服务暂时超时,可仅验证公开内容:

SMOKE_PUBLIC_ONLY=1 PREVIEW_URL=http://127.0.0.1:21021 npm run smoke:api

2026-10-21 的类型检查、生产构建和全部 258 项测试通过;真实接口检查通过游客会话、公开与个人只读接口及 29 个页面。页面检查确认账户、充值、搜索和工具页的首个响应包含完整正文。真实支付、付费解锁及浏览器视觉验收不在此次验证范围内,详情见 验证记录。

目录

  • 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:年龄确认、广告队列和会话进度,以及控制器与浏览器事件适配测试。

访问统计

生产环境(APP_ENV=production)在全部页面加载 Google Analytics 4(G-7ZYCTGDJ4F)和 Yandex.Metrika(113827715,含 webvisor、clickmap 及 noscript 像素),代码在 src/components/site/SiteAnalytics.tsx,由根布局引入;开发、测试环境不加载,避免污染统计数据。页面均为原生整页跳转,每次加载各自初始化,无需额外发送路由 hit。

已删除的功能

已移除原有的 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。