Restore original FNA interface while retaining internal SEO
This commit is contained in:
parent
fa2bc2dafe
commit
bc48be39e6
@ -17,3 +17,6 @@ ## AI 文章写作(必读)
|
||||
## SEO统一合同(2026-10-31)
|
||||
|
||||
首页、归档、教程、分页、索引、sitemap、内容更新时间和IndexNow统一按[SEO实施维护](docs/maintenance/SEO_IMPLEMENTATION_2026-10-31.md)。禁止恢复全query robots屏蔽、分页canonical第一页、按180天批量删正文、用点赞updated_at作为lastmod,或让源图删除影响本站独立副本。当前状态和未取得的搜索平台证据见该文档;实施不等于收录/排名已验证。
|
||||
|
||||
|
||||
2026-10-31用户纠正:SEO只做内部优化,用户看到的布局、样式、可见标题/说明、图片呈现、导航和广告位置必须保持原界面;不因SEO增加说明块、分页条或改版。后续可见改动须用户另行明确要求。恢复基线与验证见[界面保留修复](docs/maintenance/SEO_UI_RESTORE_2026-10-31.md)。
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
'brand' => env('SEO_BRAND', env('site_code_short') === 'cfn' ? 'ClashFreeNode' : env('APP_NAME', '网站')),
|
||||
'home_title' => env('SEO_HOME_TITLE', '免费节点与订阅使用指南'),
|
||||
'home_description' => '获取最近更新的免费节点与订阅,查看 Clash、V2ray 客户端入门教程和连接排错指南。资源状态随时间和网络环境变化。',
|
||||
'page_size' => 20,
|
||||
'page_size' => env('page_limit', 10),
|
||||
];
|
||||
|
||||
@ -4,7 +4,9 @@ # FNA_V3 SEO 实施与发布 · 2026-10-31
|
||||
|
||||
搜索渠道按用户此前与雷欧确认的 Yandex / Bing 优先,保留通用搜索兼容;两者均支持本批采用的规范URL、sitemap与IndexNow。依据:[Bing指南](https://www.bing.com/webmasters/help/webmaster-guidelines-30fba23a)、[Yandex IndexNow](https://www.yandex.com/support/webmaster/en/indexing-options/index-now.html)、[IndexNow协议](https://www.indexnow.org/documentation)(2026-10-31核对)。
|
||||
|
||||
## 当前实现
|
||||
> 用户于本批上线后明确纠正:只做内部SEO,界面必须与优化前一致。可见布局、说明、分页、标题和图片改造已进入定点恢复;现行约束及验证以[界面保留修复](SEO_UI_RESTORE_2026-10-31.md)为准,下面保留首批发布历史。
|
||||
|
||||
## 首批实现记录
|
||||
|
||||
- 首页 `/` 使用稳定标题/H1、WebSite、独立 canonical,显示最近可用记录全文及真实记录时间;明确零结果或失败不顶替上次记录,无可展示记录返回503/Retry-After。`/freenode` 是带历史说明的分页归档。旧的 `/?page=N` 定向到对应归档页;首页不是分页第一页。
|
||||
- 新免费节点记录创建即 `sSeoPolicy=archive`,可浏览、noindex、退出sitemap;已有资源仍保留分类7天策略,教程不继承资源期限。没有批量删除、合并、301到首页或恢复历史收录。取消 ClearDb 的180天正文删除,30天点赞明细清理保留。
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
> 2026-10-31界面约束更新:只做内部SEO,保持优化前外观。可见分页、说明/H1及列表图片改造撤回;以[界面恢复](SEO_UI_RESTORE_2026-10-31.md)为准,不能沿旧清单重新改版。
|
||||
|
||||
> 2026-10-31最新状态:本批技术实现已发布正式cfn,189篇服务器侧验收通过、原数据保持;sitemap 35项,IndexNow首页/栏目通知HTTP200。实现、限制及精确发布证据见 [SEO实施记录](SEO_IMPLEMENTATION_2026-10-31.md)。原审计的“未实施/未发布”仅描述其当时状态。
|
||||
|
||||
# FNA_V3 SEO 优化建议与执行清单
|
||||
|
||||
初始评估:2026-10-22,Asia/Shanghai;执行状态更新:2026-10-31。**本批技术优化已正式部署,24项按当前技术范围完成,12项仍有分项或长期证据缺口。** 本文件是唯一执行清单;下方审计证据保留原日期与原采样值,当前状态看第5节和[实施记录](SEO_IMPLEMENTATION_2026-10-31.md),不把旧审计的“未发布”当作当前状态。
|
||||
初始评估:2026-10-22,Asia/Shanghai;执行状态更新:2026-10-31。**本批技术优化已正式部署,20项按当前技术范围完成,16项仍有分项或长期证据缺口。** 本文件是唯一执行清单;下方审计证据保留原日期与原采样值,当前状态看第5节和[实施记录](SEO_IMPLEMENTATION_2026-10-31.md),不把旧审计的“未发布”当作当前状态。
|
||||
|
||||
初始审计依据当时本地代码、数据库、HTTP及受限浏览器形成,只读未改站;当时的DPL广告纳入基准。cfn已于2026-10-29正式切换,本批SEO于2026-10-31增量部署,具体上线范围不从初始快照推断。
|
||||
|
||||
@ -176,16 +178,16 @@ ## 5 执行总表
|
||||
| [~] | SEO-002 | 确定搜索意图、目标用户和效果基线 | P1 | D06 | 页面意图已区分;Yandex/Bing流量基线未取得 |
|
||||
| [x] | SEO-003 | 确定内容生命周期与索引资格 | P0 策略 | D03、D04 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-004 | 统一规范 URL 和历史别名 | P0 | D02 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-005 | 修复分页发现与滚动加载 | P0 | 004 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [~] | SEO-005 | 修复分页发现与滚动加载 | P0 | 004 | 滚动与canonical修复保留;原界面不新增分页条,无JS翻页未提供 |
|
||||
| [x] | SEO-006 | 协调 robots、参数和 noindex | P0 | 003、005 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-007 | 修复无效分页、空结果与错误状态 | P0 | 004、005 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-008 | 统一公开可见性与发布时间判断 | P0 发布前 | 003 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-009 | 修复面包屑和全站内链地址 | P1 | 004 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-010 | 分类、标签、搜索的标题与语义层级 | P1 | 001、004 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [~] | SEO-010 | 分类、标签、搜索的标题与语义层级 | P1 | 001、004 | head元信息保留;可见H1/栏名完全沿用原界面 |
|
||||
| [x] | SEO-011 | 生成真实、干净的页面简介 | P1 | 010 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-012 | 分离真实内容日期并统一时间基准 | P1 | 003 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-013 | 统一 sitemap 的集合和 lastmod | P1 | 003、004、008、012 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-014 | 建立长期内链和内容导航 | P1 | 005、009、018 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [~] | SEO-014 | 建立长期内链和内容导航 | P1 | 005、009、018 | 内链地址修复保留;新增说明和导航撤销 |
|
||||
| [~] | SEO-015 | 整理标签与重复主题页 | P1 | 002、003 | 普通标签noindex已上线;历史主题合并待流量证据 |
|
||||
| [x] | SEO-016 | 隔离并同步本站搜索索引 | P1 | 008、032 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [~] | SEO-017 | 建立空资源与检测失败的发布门槛 | P0 内容 | 003 | 零结果/失败门槛已上线;上游结构化实测字段待接入 |
|
||||
@ -197,7 +199,7 @@ ## 5 执行总表
|
||||
| [~] | SEO-023 | 推广链接、广告与正文体验 | P1 | D07 | 推广链接sponsored已上线;既有广告保留,完整体验待实测 |
|
||||
| [x] | SEO-024 | 添加匹配真实内容的结构化数据 | P2 | 009、010、012、022 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-025 | 修复分享地址和社交预览信息 | P2 | 001、004、011 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [x] | SEO-026 | 图片语义、尺寸和加载方式 | P2 | 020、028 | 已发布;本地回归及服务器侧验收通过 |
|
||||
| [~] | SEO-026 | 图片语义、尺寸和加载方式 | P2 | 020、028 | 图片呈现恢复原样;列表img及新增宽高改造撤销 |
|
||||
| [~] | SEO-027 | 手机端与无障碍完整验收 | P2 | 005、010 | 320/390及键盘/无JS样本通过;全设备无障碍未覆盖 |
|
||||
| [~] | SEO-028 | 建立正式构建性能基线并按瓶颈优化 | P2 | 026、027 | 构建体积基线已建立;真实用户CWV未取得 |
|
||||
| [x] | SEO-029 | 缓存与发布、撤稿、到期一致性 | P1 启用前 | 003、008、012 | 已发布;本地回归及服务器侧验收通过 |
|
||||
@ -209,7 +211,7 @@ ## 5 执行总表
|
||||
| [~] | SEO-035 | 搜索平台与业务效果监测 | P1 上线后 | 001、002、036 | 通知回执已上线;Yandex/Bing实际索引和流量报告未取得 |
|
||||
| [x] | SEO-036 | 发布、迁移、回滚与复核 | P0 发布时 | 前述必需项 | 已发布;本地回归及服务器侧验收通过 |
|
||||
|
||||
本批已完成24项技术范围;12项保留部分验收或长期证据缺口。`[x]`只表示当前明确技术合同已交付,不表示排名、真实收录或所有设备验证通过。搜索改为本站数据库实时查询,未清空共享Meilisearch;整页缓存明确禁用,不再以未来启用缓存为完成条件。
|
||||
按最新界面约束保留20项技术范围;16项保留部分验收或长期证据缺口。`[x]`只表示当前明确技术合同已交付,不表示排名、真实收录或所有设备验证通过。搜索改为本站数据库实时查询,未清空共享Meilisearch;整页缓存明确禁用,不再以未来启用缓存为完成条件。
|
||||
|
||||
## 6 各项优化的实施说明
|
||||
|
||||
|
||||
21
docs/maintenance/SEO_UI_RESTORE_2026-10-31.md
Normal file
21
docs/maintenance/SEO_UI_RESTORE_2026-10-31.md
Normal file
@ -0,0 +1,21 @@
|
||||
# SEO界面保留修复 · 2026-10-31
|
||||
|
||||
用户明确:先不改变网站布局,用户看到的界面必须与SEO优化前完全一样,只做内部SEO。本修复撤销前批7c11ca77引入的可见改变;以SEO前生产`a820755e7e914180178e6928f73d5b42b0b24c86`的页面模板为外观基线,增量接续`fa2bc2dafec4392c20004cef840ce54bc59ea41c`,不整站回退、不恢复旧广告或业务数据。
|
||||
|
||||
## 实施边界
|
||||
|
||||
- 恢复栏目横幅在左侧内容区的位置、旧卡片结构和CSS背景封面、原空结果样式、原导航/页脚文字。移除新增栏目说明、首页/文章历史说明块、可见分页条及额外导航。
|
||||
- 首页可见标题恢复当前文章标题,发布时间恢复原相对时间文字;不再添加可见更新时间。去掉seo.css和其全局图片/焦点样式覆盖。生产HN广告、此前已撤下的旧侧栏广告、本地DPL/FUC/HN组合保持各自现状。
|
||||
- 内部meta、canonical、robots、JSON-LD、sitemap、公开性/无效路径校验、内容时钟、发布门槛和IndexNow保留。面包屑地址与复制链接修正保留,不改变可见文字。原每页数量从既有page_limit配置读取。
|
||||
- 翻页辅助地址移入head的rel=prev/next,原滚动交互读取实际next并去重;不再增加可见分页,也不声称有无JS翻页能力。head关系仅为辅助元数据,不能保证搜索引擎将它当作完整发现链路;不使用隐藏关键词或给机器人单独渲染页面。
|
||||
|
||||
## 已取得的本地证据
|
||||
|
||||
- 完整64项测试、533断言通过;Vite发布构建通过。
|
||||
- 同一份隔离合成数据,首页、栏目、文章、空结果各在1280桌面及390手机视口与优化前模板对照,8组PNG字节/哈希完全一致。测试图、结果存项目output/seo-20261031/ui-comparison;不包含线上页面截图,不作为生产浏览器验收。
|
||||
- 正文DOM在仅归一内部URL/属性和语义time标签后四类一致;滚动从第2页追加到第3页,共26条/26唯一ID,末页next消失,无可见分页条。
|
||||
- 临时测试不访问生产网站,不往业务数据库插入文章。页面原样要求高于上一批关于可见H1、说明块、img改造或分页UI的实施记录。
|
||||
|
||||
## 发布与恢复
|
||||
|
||||
部署按Git显式提交,保留当前代码、构建、数据摘要及回滚点;本批不迁移数据库、不改内容或通知回执。实际SHA、服务器验收及恢复点由本节发布后回填。若补丁失败只回退本批代码,不能回灌整库覆盖新文章。
|
||||
9
public/build/assets/app-BJweg-u1.js
Normal file
9
public/build/assets/app-BJweg-u1.js
Normal file
File diff suppressed because one or more lines are too long
@ -28,7 +28,7 @@
|
||||
"src": "resources/fonts/iconfont/iconfont.woff"
|
||||
},
|
||||
"resources/js/app.js": {
|
||||
"file": "assets/app-VvJM4xzJ.js",
|
||||
"file": "assets/app-BJweg-u1.js",
|
||||
"name": "app",
|
||||
"src": "resources/js/app.js",
|
||||
"isEntry": true
|
||||
|
||||
@ -1 +0,0 @@
|
||||
.seo-card-image{display:block;width:100%;height:100%;object-fit:cover}.seo-pagination{display:flex;gap:1.5rem;align-items:center;justify-content:center}.resource-context p:last-child{margin-bottom:0}.seo-noscript-nav{display:flex;flex-wrap:wrap;gap:1rem;padding:1rem}.post-content img{height:auto;max-width:100%}a:focus-visible,button:focus-visible{outline:2px solid currentColor;outline-offset:3px}
|
||||
@ -64,8 +64,12 @@ const initArticleLoader = (sContainerSelector) => {
|
||||
if (seen.has(item.dataset.articleId)) item.remove(); else seen.add(item.dataset.articleId);
|
||||
});
|
||||
const nextDoc = typeof xResponse === 'string' ? new DOMParser().parseFromString(xResponse, 'text/html') : xResponse;
|
||||
const nav = nextDoc?.querySelector('.seo-pagination');
|
||||
if (nav) document.querySelector('.seo-pagination').replaceWith(nav.cloneNode(true));
|
||||
// Pagination remains metadata; the existing infinite-scroll interface stays unchanged.
|
||||
for (const rel of ['next', 'prev']) {
|
||||
document.querySelector(`link.pagination-${rel}`)?.remove();
|
||||
const link = nextDoc?.querySelector(`link.pagination-${rel}`);
|
||||
if (link) document.head.appendChild(link.cloneNode(true));
|
||||
}
|
||||
const $aNewItems = $(aItems);
|
||||
|
||||
$aNewItems.addClass('ajax-item-fade-in');
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
'sRobots' => 'index, follow',
|
||||
'bEditorialPreview' => false,
|
||||
'seo' => [],
|
||||
'pagination' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
@ -52,7 +53,10 @@
|
||||
<script type="application/ld+json">{!! json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) !!}</script>
|
||||
@endforeach
|
||||
@endunless
|
||||
<link rel="stylesheet" href="/css/seo.css">
|
||||
@if($pagination)
|
||||
@if($pagination->previousPageUrl())<link class="pagination-prev" rel="prev" href="{{ $pagination->currentPage() === 2 ? $pagination->path() : $pagination->previousPageUrl() }}">@endif
|
||||
@if($pagination->nextPageUrl())<link class="pagination-next" rel="next" href="{{ $pagination->nextPageUrl() }}">@endif
|
||||
@endif
|
||||
|
||||
{{-- 声明它是 PNG,并告诉浏览器它的实际尺寸 --}}
|
||||
<link rel="icon" type="image/png" href="/sites/{{ config('app.site_code_short') }}/icon.png" sizes="166x166">
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
@props(['oArticle'])
|
||||
<div class="list-item default block ajax-item" data-article-id="{{ $oArticle->id }}">
|
||||
<div class="media media-3x2 col-4 col-md-3 col-xl-4">
|
||||
<a class="media-content" href="{{ url('/a/'.$oArticle->sSlug).'.html' }}" title="{{ $oArticle->sTitle }}" >@if($oArticle->sThumb)<img class="seo-card-image" src="{{ $oArticle->sThumb }}" alt="" @foreach(\App\Services\Seo::imageSize($oArticle->sThumb) as $dimension => $value) {{ $dimension }}="{{ $value }}" @endforeach loading="lazy" decoding="async">@endif</a>
|
||||
<a class="media-content" href="{{ url('/a/'.$oArticle->sSlug).'.html' }}" title="{{ $oArticle->sTitle }}" style="background-image:url('{{ $oArticle->sThumb }}')"></a>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<div class="list-body">
|
||||
<h2 class="h5 m-0"><a href="{{ url('/a/'.$oArticle->sSlug.'.html') }}" title="{{$oArticle->sTitle}}" class="list-title h5 h-2x">
|
||||
<a href=" {{ url('/a/'.$oArticle->sSlug.'.html') }} " title="{{$oArticle->sTitle}}" class="list-title h5 h-2x">
|
||||
{{ $oArticle->sTitle }}
|
||||
</a></h2>
|
||||
</a>
|
||||
<div class="list-desc d-none d-xl-block text-secondary text-sm mt-2">
|
||||
<div class="h-2x">
|
||||
{!! $oArticle->sTitleSub !!}
|
||||
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div class="list-footer">
|
||||
<div class="d-flex flex-fill align-items-center text-xs text-muted">
|
||||
<a href="{{ \App\Services\Seo::url(\App\Services\Seo::categoryPath($oArticle->oCategory)) }}" class="me-2 me-md-3"><i class="text-sm iconfont icon-dots-circle me-1"></i>{{ \App\Services\Seo::categoryName($oArticle->oCategory) }}</a>
|
||||
<a href="{{ \App\Services\Seo::url(\App\Services\Seo::categoryPath($oArticle->oCategory)) }}" class="me-2 me-md-3"><i class="text-sm iconfont icon-dots-circle me-1"></i>{!! $oArticle->oCategory->sName !!}</a>
|
||||
<div class="flex-fill"></div>
|
||||
@if ($oArticle->iIsTop)
|
||||
<span class="d-none d-md-inline-block ms-2 ms-md-3" style="color: var(--color-primary);">置顶</span>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div id="sentinel" style="height: 20px;"></div>
|
||||
<footer class="footer bg-white py-3 py-md-4">
|
||||
<div class="container text-center text-lg-start text-xs text-muted">
|
||||
Copyright © 2024 <a href="/" title="{{config('seo.brand')}}">{{config('seo.brand')}}</a>.
|
||||
Copyright © 2024 <a href="/" title="{{config('app.name')}}">{{env('APP_NAME')}}</a>.
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
@ -3,18 +3,18 @@
|
||||
<div class="container">
|
||||
<a href="/" rel="home" class="logo navbar-brand order-2 order-xl-1">
|
||||
<img style="border-radius:8px;" src="/sites/{{ config('app.site_code_short') }}/images/logo/logo.png"
|
||||
@foreach(\App\Services\Seo::imageSize("/sites/".config("app.site_code_short")."/images/logo/logo.png") as $dimension => $value) {{ $dimension }}="{{ $value }}" @endforeach class="d-inline-block logo-light nc-no-lazy"
|
||||
alt="{{ config('seo.brand') }}">
|
||||
class="d-inline-block logo-light nc-no-lazy"
|
||||
alt="{{ config('app.name') }}">
|
||||
<img style="border-radius:8px;" src="/sites/{{ config('app.site_code_short') }}/images/logo/logo.png"
|
||||
@foreach(\App\Services\Seo::imageSize("/sites/".config("app.site_code_short")."/images/logo/logo.png") as $dimension => $value) {{ $dimension }}="{{ $value }}" @endforeach class="d-none logo-dark nc-no-lazy"
|
||||
alt="{{ config('seo.brand') }}">
|
||||
class="d-none logo-dark nc-no-lazy"
|
||||
alt="{{ config('app.name') }}">
|
||||
</a>
|
||||
<button class="btn btn-link btn-icon btn-sm btn-rounded navbar-toggler order-1" type="button"
|
||||
id="sidebarCollapse" aria-label="打开导航菜单" aria-controls="mobile-navigation" aria-expanded="false">
|
||||
<span><i class="iconfont icon-menu"></i></span>
|
||||
</button>
|
||||
<button class="btn btn-link btn-icon btn-sm btn-rounded navbar-toggler nav-search order-3 collapsed"
|
||||
aria-label="搜索本站" data-bs-target="#navbar-search" data-bs-toggle="collapse" aria-expanded="false"
|
||||
data-bs-target="#navbar-search" data-bs-toggle="collapse" aria-expanded="false"
|
||||
aria-controls="navbar-search">
|
||||
<span>
|
||||
<i class="iconfont icon-search"></i>
|
||||
@ -34,14 +34,14 @@
|
||||
<li class=" menu-item menu-item-type-taxonomy menu-item-object-category current-post-ancestor current-menu-parent current-post-parent {{ $oNav->aChildren->isNotEmpty() ? 'menu-item-has-children' : '' }}">
|
||||
<a class="menua {{ $sActiveAttr }}" href="{{ url($oNav->sSlug) }}">
|
||||
<i class="{{ $oNav->sHtmlClass }}"></i>
|
||||
{{ \App\Services\Seo::categoryName($oNav) }}
|
||||
{{ $oNav->sName }}
|
||||
</a>
|
||||
|
||||
@if($oNav->aChildren->isNotEmpty())
|
||||
<div class="submenu-arrow iconfont icon-jiantou1"></div>
|
||||
<ul class="sub-menu">
|
||||
@foreach($oNav->aChildren as $oSub)
|
||||
<li class="menu-item menu-item-type-taxonomy menu-item-object-category"><a class="menuaSub" href="{{ url($oNav->sSlug.'/'.$oSub->sSlug) }}">{{ \App\Services\Seo::categoryName($oSub) }}</a></li>
|
||||
<li class="menu-item menu-item-type-taxonomy menu-item-object-category"><a class="menuaSub" href="{{ url($oNav->sSlug.'/'.$oSub->sSlug) }}">{{ $oSub->sName }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@ -60,7 +60,7 @@
|
||||
</a>
|
||||
|
||||
<button class="btn btn-link btn-icon btn-sm btn-rounded nav-search collapsed"
|
||||
data-bs-toggle="collapse" aria-label="搜索本站" data-bs-target="#navbar-search" aria-expanded="false"
|
||||
data-bs-toggle="collapse" data-bs-target="#navbar-search" aria-expanded="false"
|
||||
aria-controls="navbar-search">
|
||||
<span>
|
||||
<i class="iconfont icon-search"></i>
|
||||
@ -85,12 +85,12 @@
|
||||
<li class="menu-item menu-item-type-taxonomy menu-item-object-category {{ $oNav->aChildren->isNotEmpty() ? 'menu-item-has-children' : '' }}">
|
||||
<a href="{{ url($oNav->sSlug) }}">
|
||||
<i class="{{ $oNav->sHtmlClass }}"></i>
|
||||
{{ \App\Services\Seo::categoryName($oNav) }}
|
||||
{{ $oNav->sName }}
|
||||
</a>
|
||||
@if($oNav->aChildren->isNotEmpty())
|
||||
<ul class="sub-menu">
|
||||
@foreach($oNav->aChildren as $oSub)
|
||||
<li class="menu-item menu-item-type-taxonomy menu-item-object-category"><a class="menuaSub" href="{{ url($oNav->sSlug.'/'.$oSub->sSlug) }}">{{ \App\Services\Seo::categoryName($oSub) }}</a></li>
|
||||
<li class="menu-item menu-item-type-taxonomy menu-item-object-category"><a class="menuaSub" href="{{ url($oNav->sSlug.'/'.$oSub->sSlug) }}">{{ $oSub->sName }}</a></li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
@ -106,11 +106,11 @@
|
||||
<form method="get" role="search" id="searchform" action="/">
|
||||
<div class="search-form">
|
||||
<input type="text" class="search-input form-control form-control-lg" name="s" id="s"
|
||||
aria-label="搜索关键词" placeholder="请输入搜索关键词并按回车键…">
|
||||
placeholder="请输入搜索关键词并按回车键…">
|
||||
<i class="search-toggle iconfont icon-search"></i>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<noscript><nav class="seo-noscript-nav" aria-label="栏目导航"><a href="/">当前节点</a>@foreach($aNavs as $oNav)<a href="{{ \App\Services\Seo::url(\App\Services\Seo::categoryPath($oNav)) }}">{{ \App\Services\Seo::categoryName($oNav) }}</a>@endforeach<form action="/" method="get"><label>搜索 <input name="s" maxlength="100" required></label><button type="submit">搜索</button></form></nav></noscript></header>
|
||||
</header>
|
||||
|
||||
|
||||
@ -1,30 +1,81 @@
|
||||
<x-app-layout :seo="$seo" :sSlug="$oCategory->sSlug ?? ''">
|
||||
<main class="site-main"><div class="container">
|
||||
@if(isset($oCategory) && in_array($oCategory->sSlug, ['client','advanced']))
|
||||
<link rel="stylesheet" href="/css/tutorials.css">
|
||||
@endif
|
||||
@if(isset($oCategory) && !empty($oCategory->aBanner['img_file']))
|
||||
@php($banner = $oCategory->aBanner)
|
||||
<div class="list-cover rounded mb-3"><div class="media media-21x9"><div class="media-content" style="background-image:url('/sites/{{ config('app.site_code_short') }}/images/banner/{{ $banner['img_file'] }}');background-position:center {{ $banner['img_top'] ?? 'center' }};"><div class="overlay-grad"></div></div></div><div class="cover-body"><h1 class="cover-title">{{ $seo['heading'] }}</h1></div></div>
|
||||
<p>{{ $seo['description'] }}</p>
|
||||
@else
|
||||
<section class="archive-header block p-4 mb-3"><h1>{{ $seo['heading'] }}</h1><p>{{ $seo['description'] }}</p></section>
|
||||
@endif
|
||||
@if(isset($oCategory) && $oCategory->sSlug === 'freenode')<p><a href="/">查看当前免费节点与订阅</a></p>@endif
|
||||
<div class="row listAjax"><div class="content-wrapper col-lg-8">
|
||||
@if(count($oArticles) === 0)
|
||||
<p class="p-4">{{ isset($sKeywords) ? '没有找到匹配的文章,请尝试其他关键词。' : '这个栏目暂时没有文章。' }}</p>
|
||||
@else
|
||||
<div class="list-home list-grid list-grid-padding">
|
||||
@foreach($oArticles as $oArticle)<x-article-item :oArticle="$oArticle" />@endforeach
|
||||
<x-app-layout
|
||||
:seo="$seo"
|
||||
:pagination="$oArticles"
|
||||
:sSlug="$oCategory->sSlug ?? ''"
|
||||
:sTitle="isset($oCategory) && in_array($oCategory->sSlug, ['client', 'advanced']) ? $oCategory->sName . ' - ' . config('app.name') : config('app.title') . ' - ' . config('app.name')"
|
||||
:sDesc="isset($oCategory) && in_array($oCategory->sSlug, ['client', 'advanced']) ? $oCategory->sDescription : config('app.desc')"
|
||||
>
|
||||
<main class="site-main">
|
||||
<div class="container">
|
||||
@if (isset($oCategory) && in_array($oCategory->sSlug, ['client', 'advanced']))
|
||||
<link rel="stylesheet" href="/css/tutorials.css">
|
||||
@if (!isset($oCategory->aBanner) || count($oArticles) == 0)
|
||||
<section class="tutorial-category" aria-label="教程栏目介绍">
|
||||
<h1>{{ $oCategory->sName }}</h1>
|
||||
<p>{{ $oCategory->sDescription }}</p>
|
||||
</section>
|
||||
@endif
|
||||
@endif
|
||||
@if (count($oArticles) == 0)
|
||||
{{-- Empty State --}}
|
||||
<div class="content-error d-flex flex-fill align-items-center h-v-80">
|
||||
<div class="text-center p-5 mx-auto">
|
||||
<div class="d-inline-block svg-lg svg-empty"></div>
|
||||
<p class="text-lg text-muted mt-3 mt-md-4">看起来这里没有任何东西…</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="row listAjax">
|
||||
<div class="content-wrapper col-lg-8">
|
||||
@if (@$aTag['name'])
|
||||
<div class="archive-header block p-4 p-xl-5 mb-3">
|
||||
<div class="archive-icon btn btn-primary btn-icon btn-md btn-rounded">
|
||||
<span><i class="iconfont icon-tag-fill"></i></span>
|
||||
</div>
|
||||
<div class="archive-body">
|
||||
<h1>{{@$aTag['name']}}<sup class="text-muted mx-1">{{@$aTag['count']}}</sup></h1>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($oCategory->aBanner)
|
||||
@php
|
||||
$sBannerPath = '/sites/' . config('app.site_code_short') . '/images/banner/' . $oCategory->aBanner['img_file'];
|
||||
@endphp
|
||||
|
||||
<div class="list-cover rounded mb-3">
|
||||
<div class="media media-21x9">
|
||||
<div class="media-content" style="background-image:url({{ $sBannerPath }}); background-position: center {{ $oCategory->aBanner['img_top'] }};">
|
||||
<div class="overlay-grad"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cover-body">
|
||||
<h1 class="cover-title">{{ in_array($oCategory->sSlug, ['client', 'advanced']) ? $oCategory->sName : $oCategory->sSlug }}<sup class="text-light mx-1">{{ $iArticleCount ?? count($oArticles) }}</sup></h1>
|
||||
</div>
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
<div class="list-home list-grid list-grid-padding">
|
||||
@foreach ($oArticles as $oArticle)
|
||||
<x-article-item :oArticle="$oArticle" />
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="page-load-status" style="display:none; padding: 20px; text-align: center;">
|
||||
<div class="infinite-scroll-request">正在加载更多...</div>
|
||||
<div class="infinite-scroll-last text-muted"> 已经到底了 </div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sidebar col-lg-4 d-none d-lg-block ms-lg-auto">
|
||||
@include('components.order-likes')
|
||||
<!-- @include('components.generated-tags-cloud') -->
|
||||
@include('components.banners')
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="page-load-status" style="display:none"><span class="infinite-scroll-request">正在加载更多…</span><span class="infinite-scroll-last">已经到底了</span></div>
|
||||
@endif
|
||||
<nav class="seo-pagination my-4" aria-label="文章分页">
|
||||
@if($oArticles->previousPageUrl())<a rel="prev" href="{{ $oArticles->currentPage() === 2 ? $oArticles->path() : $oArticles->previousPageUrl() }}">上一页</a>@endif
|
||||
<span>第 {{ $oArticles->currentPage() }} / {{ $oArticles->lastPage() }} 页</span>
|
||||
@if($oArticles->nextPageUrl())<a class="pagination-next" rel="next" href="{{ $oArticles->nextPageUrl() }}">下一页</a>@endif
|
||||
</nav>
|
||||
</div><div class="sidebar col-lg-4 d-none d-lg-block ms-lg-auto">@include('components.order-likes')</div></div>
|
||||
</div></main>
|
||||
</main>
|
||||
</x-app-layout>
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
</span>
|
||||
<span itemprop="itemListElement">
|
||||
<a href="{{ \App\Services\Seo::url(\App\Services\Seo::categoryPath($oArticle->oCategory)) }}" itemprop="item">
|
||||
<span itemprop="name">{{ \App\Services\Seo::categoryName($oArticle->oCategory) }}</span>
|
||||
<span itemprop="name">{{ $oArticle->oCategory->sName }}</span>
|
||||
</a>
|
||||
</span>
|
||||
<span class="sep">
|
||||
@ -59,31 +59,17 @@
|
||||
<div class="post card card-md">
|
||||
<div class="card-body">
|
||||
<div class="post-title mb-3">
|
||||
<h1 class="mb-3">{{ $seo['heading'] ?? $oArticle->sTitle }}</h1>
|
||||
<h1 class="mb-3">{{ $oArticle->sTitle }}</h1>
|
||||
<div class="post-meta text-xs">
|
||||
<a href="/"><i class="text-sm iconfont icon-user1 me-1"></i>{{ config('seo.brand') }}</a>
|
||||
<a href="/"><i class="text-sm iconfont icon-user1 me-1"></i>{{ config('app.name') }}</a>
|
||||
<i class="text-primary iconfont icon-dot mx-2"></i>
|
||||
<time class="text-muted" datetime="{{ $oArticle->created_at?->toAtomString() }}">{{ $bIsHome ? '最近资源记录' : '发布于' }} {{ $oArticle->created_at?->format('Y-m-d H:i') }} UTC</time>
|
||||
@if($modified = \App\Services\Seo::modified($oArticle))
|
||||
<time class="text-muted ms-2" datetime="{{ $modified }}">内容更新于 {{ \Carbon\Carbon::parse($modified)->format('Y-m-d H:i') }} UTC</time>
|
||||
@endif
|
||||
<time class="text-muted" datetime="{{ $oArticle->created_at?->toAtomString() }}">发布于 {{ $oArticle->sCreatedAtDiff }}</time>
|
||||
</div>
|
||||
</div>
|
||||
@if($oArticle->oCategory->sSlug === 'freenode')
|
||||
<div class="alert alert-light resource-context">
|
||||
@if($bIsHome)
|
||||
<p>以下为最近一条资源记录。连接情况会随时间、地区和网络变化,请以客户端实际连接为准。</p>
|
||||
<a href="{{ \App\Services\Seo::articleUrl($oArticle) }}">查看这次更新记录</a> · <a href="/freenode">全部历史记录</a>
|
||||
@else
|
||||
<p>这是 {{ $oArticle->created_at?->format('Y-m-d') }} 的历史记录,订阅与节点可能已经变化。</p><a href="/">查看当前免费节点与订阅</a>
|
||||
@endif
|
||||
· <a href="/client">入门教程</a> · <a href="/advanced">连接与配置排错</a>
|
||||
</div>
|
||||
@endif
|
||||
<div class="post-content">
|
||||
@if ($oArticle->bPageThumb && $oArticle->sThumb)
|
||||
<p>
|
||||
<img decoding="async" alt="" @foreach(\App\Services\Seo::imageSize($oArticle->sThumb) as $dimension => $value) {{ $dimension }}="{{ $value }}" @endforeach class="size-full wp-image-28 aligncenter" src="{{ $oArticle->sThumb }}" style="border-radius: 6px;">
|
||||
<img decoding="async" alt="" class="size-full wp-image-28 aligncenter" src="{{ $oArticle->sThumb }}" style="border-radius: 6px;">
|
||||
</p>
|
||||
@endif
|
||||
{!! nl2br(\App\Services\Seo::body($oArticle->sContent)) !!}
|
||||
@ -103,13 +89,13 @@
|
||||
@unless($bEditorialPreview ?? false)
|
||||
<div class="post-actions d-flex align-items-center my-3 my-md-4">
|
||||
<a href="javascript:;"
|
||||
aria-label="喜欢这篇文章" class="post-like-button btn btn-light btn-w-md btn-rounded me-2"
|
||||
class="post-like-button btn btn-light btn-w-md btn-rounded me-2"
|
||||
data-action="like" data-id="{{ $oArticle->id }}">
|
||||
<i class="text-lg iconfont icon-like"></i>
|
||||
<span class="like-count">{{ $oArticle->iLikeCount }}</span>
|
||||
</a>
|
||||
<div class="flex-fill"></div>
|
||||
<a href="javascript:;" aria-label="分享这篇文章" class="btn btn-light btn-icon btn-rounded btn-share-toggler">
|
||||
<a href="javascript:;" class="btn btn-light btn-icon btn-rounded btn-share-toggler">
|
||||
<span><i class="iconfont icon-share-square"></i></span>
|
||||
</a>
|
||||
<template id="single-share-template">
|
||||
|
||||
@ -48,6 +48,10 @@ public function test_category_aliases_pagination_and_parameters(): void
|
||||
$this->assertStringContainsString('rel="next" href="https://example.org/client/windows?page=3"',$html);
|
||||
$this->assertStringContainsString('rel="prev" href="https://example.org/client/windows"',$html);
|
||||
$this->assertStringNotContainsString('guide5.html',$html);
|
||||
$dom=new \DOMDocument();@$dom->loadHTML($html);$xp=new \DOMXPath($dom);
|
||||
$this->assertSame(1,$xp->query('//head/link[@class="pagination-next"]')->length);
|
||||
$this->assertSame(0,$xp->query('//body/nav[@class="seo-pagination"]')->length);
|
||||
$this->assertSame(0,$xp->query('//*[contains(@class,"seo-card-image")]')->length);
|
||||
$this->get('/client/windows?page=3')->assertOk()->assertDontSee('class="pagination-next"',false);
|
||||
$this->get('/robots.txt')->assertOk()->assertDontSee('Disallow: /*?*',false);
|
||||
}
|
||||
@ -57,7 +61,7 @@ public function test_hidden_and_future_content_are_absent_from_every_public_quer
|
||||
foreach(['draft'=>['iStatus'=>0],'future'=>['created_at'=>'2026-11-01 00:00:00']] as $slug=>$extra){$id=$this->article($slug,$extra);DB::table('article_tag')->insert(['iArticleId'=>$id,'iTagId'=>1]);$this->get('/a/'.$slug.'.html')->assertNotFound();}
|
||||
$this->get('/search/'.rawurlencode('教程'))->assertOk()->assertSee('good.html',false)->assertDontSee('draft.html',false)->assertDontSee('future.html',false)->assertSee('noindex, follow',false);
|
||||
$this->get('/?s='.rawurlencode('教程'))->assertStatus(301)->assertRedirect(Seo::url('/search/'.rawurlencode('教程')));
|
||||
$this->get('/search/'.rawurlencode('不存在的内容'))->assertOk()->assertSee('没有找到匹配的文章');
|
||||
$this->get('/search/'.rawurlencode('不存在的内容'))->assertOk()->assertSee('看起来这里没有任何东西…');
|
||||
$this->get('/tag/clash')->assertOk()->assertDontSee('draft.html',false);
|
||||
DB::table('categories')->where('id',2)->update(['iStatus'=>0]);
|
||||
$this->get('/a/good.html')->assertNotFound();$this->get('/client/windows')->assertNotFound();
|
||||
@ -70,12 +74,15 @@ public function test_home_is_stable_and_failed_refresh_does_not_replace_it(): vo
|
||||
$this->article('fn-zero',['iCategoryId'=>1,'sTitleSub'=>'本次更新共0个可用节点','created_at'=>'2026-10-31 11:00:00']);
|
||||
$this->assertSame('fn-good',app(ArticlesService::class)->getLatestFreeNodeSlug());
|
||||
$html=$this->get('/')->assertOk()->getContent();
|
||||
$this->assertStringContainsString('<h1 class="mb-3">免费节点与订阅使用指南</h1>',$html);
|
||||
$this->assertStringContainsString('<h1 class="mb-3">日期标题</h1>',$html);
|
||||
$this->assertStringContainsString('rel="canonical" href="https://example.org/"',$html);
|
||||
$this->assertStringContainsString('property="og:type" content="website"',$html);
|
||||
$this->assertStringContainsString('fn-good.html',$html);
|
||||
$this->assertStringContainsString('示例正文',$html);
|
||||
$this->assertStringNotContainsString('resource-context',$html);
|
||||
$this->assertStringNotContainsString('/css/seo.css',$html);
|
||||
$this->assertStringContainsString('发布于',$html);
|
||||
$this->travel(10)->days();$this->get('/')->assertOk()->assertSee('content="index, follow"',false);
|
||||
$this->get('/a/fn-good.html')->assertOk()->assertSee('content="noindex, follow"',false)->assertSee('历史记录');
|
||||
$this->get('/a/fn-good.html')->assertOk()->assertSee('content="noindex, follow"',false)->assertDontSee('resource-context',false);
|
||||
DB::table('articles')->delete();$this->get('/')->assertStatus(503)->assertHeader('Retry-After','3600');
|
||||
}
|
||||
public function test_new_snapshots_are_archives_and_content_clock_ignores_likes(): void
|
||||
|
||||
Loading…
Reference in New Issue
Block a user