diff --git a/app/Services/ArticleRecommendations.php b/app/Services/ArticleRecommendations.php
new file mode 100644
index 00000000..329ec637
--- /dev/null
+++ b/app/Services/ArticleRecommendations.php
@@ -0,0 +1,66 @@
+';
+
+ public static function providers(): array
+ {
+ if (! config('airport_recommendations.enabled')) return [];
+ $providers = array_filter(config('airport_recommendations.providers', []), fn ($p) =>
+ ($p['enabled'] ?? false) && filter_var($p['url'] ?? '', FILTER_VALIDATE_URL)
+ && in_array(parse_url($p['url'], PHP_URL_SCHEME), ['https', 'http'], true));
+ uasort($providers, fn ($a, $b) => ($a['order'] ?? 0) <=> ($b['order'] ?? 0));
+ return $providers;
+ }
+
+ /** Presentation only: article storage, API receipts and subscription bytes stay intact. */
+ public static function prepare(Articles $article): array
+ {
+ $html = $article->sContent ?? '';
+ $category = $article->oCategory;
+ $slug = $category?->sSlug;
+ $scope = in_array($slug, ['freenode', 'shadowrocketids', 'advanced', 'client'], true)
+ || $category?->oParent?->sSlug === 'client';
+ $enabled = $scope && self::providers() !== [];
+ if (! $enabled) return ['body' => $html, 'enabled' => false, 'inline' => false];
+
+ $inline = false;
+ if (in_array($slug, ['freenode', 'shadowrocketids'], true)) {
+ // Match the known legacy heading and its own Fuyun link, bounded by the next heading.
+ if (preg_match('~(?:
]*>\s*)?]*>\s*机场推荐\s*[-—-::]*\s*福云\s*[::]?\s*~u', $html, $head, PREG_OFFSET_CAPTURE)) {
+ $start = $head[0][1];$from = $start + strlen($head[0][0]);
+ $tail = substr($html, $from);
+ if (preg_match('~]*>.*?~si', $tail, $links, PREG_OFFSET_CAPTURE);
+ foreach ($links[0] as [$link, $offset]) {
+ $text = html_entity_decode(strip_tags($link), ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ if (! str_contains($text, '机场官网') || ! str_contains($text, '福云')) continue;
+ $end = $from + $offset + strlen($link);
+ if (preg_match('~\A(?:\s|(?:b|strong|p)>)*~i', substr($html, $end), $closing)) $end += strlen($closing[0]);
+ $html = substr($html, 0, $start).self::SLOT.substr($html, $end);
+ $inline = true;
+ break;
+ }
+ }
+ }
+ if ($slug === 'advanced') {
+ $anchor = '#'.config('airport_recommendations.anchor');
+ if (! str_contains($html, $anchor)) {
+ $jump = '如果需要更稳定的线路,可以从文末的机场推荐中选择。
';
+ $inserted = false;
+ $html = preg_replace_callback('~]*>.*?
~si', function ($m) use (&$inserted, $jump) {
+ if ($inserted || ! preg_match('/免费节点|订阅|稳定|机场|线路/u', strip_tags($m[0]))) return $m[0];
+ $inserted = true;
+ return $m[0].$jump;
+ }, $html);
+ if (! $inserted) $html .= $jump;
+ }
+ }
+ return ['body' => $html, 'enabled' => true, 'inline' => $inline];
+ }
+}
diff --git a/app/Services/FreeNodeArticleLayout.php b/app/Services/FreeNodeArticleLayout.php
new file mode 100644
index 00000000..b46c54a1
--- /dev/null
+++ b/app/Services/FreeNodeArticleLayout.php
@@ -0,0 +1,93 @@
+]*>\s*)?]*>\s*免费节点订阅链接\s*[::]?\s*(?:\s*
)?~u', $html, $heading, PREG_OFFSET_CAPTURE)) return null;
+ if (!preg_match('~]*>\s*温馨提示\s*[::]?\s*
~u', $html, $help, PREG_OFFSET_CAPTURE, $heading[0][1])) return null;
+ $start = $heading[0][1];$from = $start + strlen($heading[0][0]);$end = $help[0][1];
+ $intro = self::fragment(str_replace(ArticleRecommendations::SLOT, '', substr($html, 0, $start)));
+ $subscriptions = self::fragment(substr($html, $from, $end - $from));
+ if (!$intro || !$subscriptions) return null;
+ $result = ['count' => null, 'speed' => null, 'regions' => [], 'community' => [], 'intro' => '', 'notes' => '', 'subscriptions' => [], 'usage' => substr($html, $end + strlen($help[0][0]))];
+ foreach (iterator_to_array($intro->childNodes) as $node) {
+ $text = self::text($node->textContent);
+ if ($text === '') continue;
+ if (preg_match('/^本次更新共\s*(\d+)\s*个可用节点[,,]\s*最高速度\s*([^。]+)[。]?$/u', $text, $m)) {
+ $result['count'] = (int)$m[1];$result['speed'] = trim($m[2]);continue;
+ }
+ if (preg_match('/^覆盖(.+?)等多个区域[。]?$/u', $text, $m)) {
+ $result['regions'] = array_values(array_unique(array_filter(array_map('trim', preg_split('/[、,,]/u', $m[1])))));continue;
+ }
+ if (str_starts_with($text, '复制下方订阅链接')) continue;
+ if ($node instanceof DOMElement && preg_match('/点击加入我们的tg频道|点击关注我们的github仓库/u', $text)) {
+ $recognized = false;
+ foreach ($node->getElementsByTagName('a') as $a) {
+ $url = $a->getAttribute('href');$host = strtolower(parse_url($url, PHP_URL_HOST) ?? '');
+ if (parse_url($url, PHP_URL_SCHEME) !== 'https' || !in_array($host, ['t.me', 'github.com'], true)) continue;
+ $result['community'][] = ['url' => $url, 'label' => $host === 't.me' ? 'Telegram 更新频道' : 'GitHub 项目仓库'];$recognized = true;
+ }
+ if ($recognized) continue;
+ }
+ $result['intro'] .= $intro->ownerDocument->saveHTML($node);
+ }
+ $urls = [];
+ foreach (iterator_to_array($subscriptions->getElementsByTagName('a')) as $a) {
+ if (!$a->hasAttribute('data-clipboard-text')) continue;
+ $url = $a->getAttribute('data-clipboard-text');
+ if (!filter_var($url, FILTER_VALIDATE_URL) || !in_array(parse_url($url, PHP_URL_SCHEME), ['http', 'https'], true)) return null;
+ $path = parse_url($url, PHP_URL_PATH) ?? '';
+ $type = match (true) {
+ (bool)preg_match('/-clash\.(yaml|yml)$/i', $path) => ['key'=>'clash', 'title'=>'Clash / Mihomo', 'format'=>'YAML', 'clients'=>'Clash Verge Rev、Mihomo 等客户端', 'symbol'=>'C'],
+ (bool)preg_match('/-singbox\.(txt|json)$/i', $path) => ['key'=>'singbox', 'title'=>'Sing-box / Hiddify', 'format'=>'SING-BOX', 'clients'=>'Sing-box、Hiddify 等客户端', 'symbol'=>'S'],
+ (bool)preg_match('/-v2ray\.txt$/i', $path) => ['key'=>'v2ray', 'title'=>'V2ray / Shadowrocket', 'format'=>'通用订阅', 'clients'=>'v2rayNG、Shadowrocket 等客户端', 'symbol'=>'V'],
+ default => null,
+ };
+ // Unknown formats keep the original article rather than dropping a subscription.
+ if (!$type || isset($result['subscriptions'][$type['key']])) return null;
+ $result['subscriptions'][$type['key']] = $type + ['url'=>$url];$urls[] = $url;
+ }
+ if ($result['subscriptions'] === []) return null;
+ // Keep every supplementary note while removing only labels/URLs now shown in cards.
+ foreach (iterator_to_array($subscriptions->childNodes) as $node) {
+ $text = self::text($node->textContent);
+ if ($text === '' || in_array($text, $urls, true) || preg_match('/订阅链接\s*[::]$/u', $text)) continue;
+ if ($node instanceof DOMElement && ($node->hasAttribute('data-clipboard-text') || $node->getElementsByTagName('a')->length)) {
+ foreach (iterator_to_array($node->getElementsByTagName('a')) as $link) {
+ if ($link->hasAttribute('data-clipboard-text')) $link->parentNode->removeChild($link);
+ }
+ if (self::text($node->textContent) === '' || in_array(self::text($node->textContent), $urls, true)) continue;
+ }
+ $result['notes'] .= $subscriptions->ownerDocument->saveHTML($node);
+ }
+ $order = ['clash'=>0, 'v2ray'=>1, 'singbox'=>2];
+ uasort($result['subscriptions'], fn ($a, $b) => $order[$a['key']] <=> $order[$b['key']]);
+ $result['subscriptions'] = array_values($result['subscriptions']);
+ return $result;
+ }
+
+ private static function text(string $text): string
+ {
+ return trim(preg_replace('/\s+/u', ' ', $text));
+ }
+
+ private static function fragment(string $html): ?DOMElement
+ {
+ $dom = new DOMDocument('1.0', 'UTF-8');$before = libxml_use_internal_errors(true);
+ try {
+ $dom->loadHTML(''.$html.'
', LIBXML_NONET);
+ return $dom->getElementById('fn-root');
+ } catch (\Throwable) {
+ return null;
+ } finally {
+ libxml_clear_errors();libxml_use_internal_errors($before);
+ }
+ }
+}
diff --git a/config/airport_recommendations.php b/config/airport_recommendations.php
new file mode 100644
index 00000000..53a086c8
--- /dev/null
+++ b/config/airport_recommendations.php
@@ -0,0 +1,33 @@
+ env('AIRPORT_RECOMMENDATIONS_ENABLED', true),
+ 'anchor' => 'airport-recommendations',
+ // User-supplied commercial copy. Edit here once, then clear config/response caches.
+ 'providers' => [
+ 'fuc' => [
+ 'enabled' => true, 'order' => 10, 'name' => '福云', 'tagline' => '日常之选',
+ 'url' => env('AIRPORT_FUC_URL', 'https://xn--123-9i9dj97q.com/'),
+ 'price' => '18', 'period' => '元 / 月',
+ 'features' => ['流媒体解锁', 'AI 解锁'],
+ 'description' => '老牌机场,套餐性价比高,适合日常娱乐办公',
+ 'coupon' => 'clashfreenode.com', 'discount' => '9 折',
+ ],
+ 'dpl' => [
+ 'enabled' => true, 'order' => 20, 'name' => 'DP-LINK.NET', 'tagline' => '专线服务',
+ 'url' => env('AIRPORT_DPL_URL', 'https://dp-link.net/'),
+ 'price' => '26', 'period' => '元 / 月',
+ 'features' => ['流媒体解锁', 'AI 解锁'],
+ 'description' => '专线机场,适合对网络质量有要求的用户',
+ 'coupon' => 'clashfreenode.com', 'discount' => '9 折',
+ ],
+ 'hn' => [
+ 'enabled' => true, 'order' => 30, 'name' => 'hardnerd', 'tagline' => '灵活套餐',
+ 'url' => env('AIRPORT_HN_URL', 'https://hardnerd.net/'),
+ 'price' => '28', 'period' => '元 / 月',
+ 'features' => ['流媒体解锁', 'AI 解锁'],
+ 'description' => '专线机场,适合对网络质量有要求的用户,有一次性套餐',
+ 'coupon' => null, 'discount' => null,
+ ],
+ ],
+];
diff --git a/public/css/airport-recommendations.css b/public/css/airport-recommendations.css
new file mode 100644
index 00000000..d784fe3a
--- /dev/null
+++ b/public/css/airport-recommendations.css
@@ -0,0 +1,27 @@
+.airport-recommendations{margin:28px 0 24px;scroll-margin-top:100px;color:#424852;clear:both;font-size:14px;line-height:1.65}
+.airport-recommendations__heading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:15px}
+.post-content .airport-recommendations__heading h2{font-size:21px;line-height:1.4;margin:0;padding:0;border:0;color:#29323d;font-weight:700}
+.airport-recommendations__heading>span{font-size:11px;color:#8e98a5;letter-spacing:.06em}
+.airport-recommendations__table-wrap{border:1px solid #e8edf2;border-radius:10px;overflow:hidden;background:#fff}
+.post-content .airport-recommendations__table{width:100%;table-layout:fixed;border:0;border-collapse:collapse;margin:0;font-size:13px;line-height:1.65}
+.post-content .airport-recommendations__table thead th{background:#f7f9fc;color:#7b8795;font-weight:500;font-size:12px;border:0;border-bottom:1px solid #e8edf2;padding:12px 11px;text-align:left}
+.post-content .airport-recommendations__table thead th:nth-child(1){width:19%}.post-content .airport-recommendations__table thead th:nth-child(2){width:15%}.post-content .airport-recommendations__table thead th:nth-child(3){width:18%}.post-content .airport-recommendations__table thead th:nth-child(4){width:25%}.post-content .airport-recommendations__table thead th:nth-child(5){width:23%}
+.post-content .airport-recommendations__table tbody th,.post-content .airport-recommendations__table tbody td{padding:18px 11px;border:0;border-bottom:1px solid #edf0f5;vertical-align:middle;background:transparent;text-align:left;overflow-wrap:anywhere}
+.post-content .airport-recommendations__table tbody tr:last-child>*{border-bottom:0}
+.post-content .airport-provider a{font-size:14px;font-weight:700;text-decoration:none;color:#2f4057;display:inline-flex;align-items:center;gap:5px;white-space:nowrap}
+.post-content .airport-provider a:hover{color:#238dce}.airport-provider a>span{font-weight:400;color:#96a4b5;font-size:12px}.airport-provider__label{display:block;font-size:10px;color:#9ba6b3;font-weight:400;margin-top:4px}
+.airport-price strong{font-size:22px;font-weight:650;color:#35465d;letter-spacing:-.6px;display:block;line-height:1.15}.airport-price>span{font-size:10px;color:#8b96a6;white-space:nowrap}
+.airport-features{display:grid;gap:5px;font-size:12px;white-space:nowrap}.airport-features i{font-style:normal;color:#27a781;margin-right:4px}.airport-description{color:#76818e;font-size:12px}
+.airport-coupon__discount{font-size:10px;color:#248cca;background:#edf7fd;border-radius:4px;padding:2px 6px;display:inline-block;margin-bottom:5px}
+.airport-coupon__copy{display:flex;align-items:center;gap:4px;background:none;border:0;border-bottom:1px dashed #c9d5e1;padding:0 0 2px;font:inherit;font-size:11px;color:#53667f;text-align:left;cursor:pointer;max-width:100%;white-space:nowrap}.airport-coupon__copy svg{flex:none;color:#9aaabd}.airport-coupon__copy.is-copied{color:#19856a;border-color:#19856a}.airport-coupon__copy:focus-visible,.airport-provider a:focus-visible{outline:2px solid #319bc9;outline-offset:4px}.airport-coupon__none{font-size:12px;color:#a3aab5}
+.post-content p.airport-recommendations__note{font-size:10px;color:#a0a8b4;margin:10px 0 0;line-height:1.6}.post-content p.airport-recommendation-jump{font-size:14px;color:#6f7d8c;margin:14px 0 20px}.airport-recommendation-jump a{color:#238dce;text-underline-offset:3px}
+@media(max-width:767.98px){
+.airport-recommendations{margin:25px 0}.post-content .airport-recommendations__heading h2{font-size:20px}.airport-recommendations__heading>span{font-size:10px}
+.airport-recommendations__table-wrap{border:0;border-radius:0;overflow:visible;background:transparent}.post-content .airport-recommendations__table,.post-content .airport-recommendations__table tbody{display:block;width:100%}.airport-recommendations__table thead{display:none}
+.post-content .airport-recommendations__table tbody tr{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:13px 12px;border:1px solid #e8edf2;border-radius:10px;padding:18px;margin-bottom:12px;background:#fff}
+.post-content .airport-recommendations__table tbody th,.post-content .airport-recommendations__table tbody td{display:block;border:0;padding:0;min-width:0}.post-content .airport-provider a{font-size:16px}.airport-provider__label{font-size:11px}.airport-price{grid-column:2;grid-row:1;text-align:right!important}.airport-price strong{display:inline;font-size:25px}.airport-price>span{display:inline;margin-left:5px;font-size:10px}
+.post-content .airport-recommendations__table tbody td.airport-description{grid-column:1/-1;grid-row:2;font-size:12px;line-height:1.75}.airport-features{display:flex;gap:12px;font-size:11px;flex-wrap:wrap}
+.post-content .airport-recommendations__table tbody td.airport-coupon{grid-column:1/-1;border-top:1px solid #edf0f5;padding-top:11px;display:flex;align-items:center;gap:8px}.airport-coupon:before{content:attr(data-label);font-size:11px;color:#9aa4b0;margin-right:auto}.airport-coupon__discount{margin:0}.airport-coupon__copy{font-size:11px}.post-content p.airport-recommendations__note{font-size:10px}
+}
+
+.airport-copy-status{display:block;color:#24846f;font-size:11px;margin-top:8px}.airport-copy-status:empty{display:none}
diff --git a/public/css/cfn-brand.css b/public/css/cfn-brand.css
new file mode 100644
index 00000000..da803530
--- /dev/null
+++ b/public/css/cfn-brand.css
@@ -0,0 +1,16 @@
+.header .navbar-brand.cfn-brand{display:inline-flex;align-items:center;gap:10px;max-width:none;padding:0;line-height:1;text-decoration:none}
+.header .navbar-brand.cfn-brand .cfn-brand__cat{display:block;width:44px;height:44px;max-height:none;flex:none;border-radius:0}
+.cfn-brand__copy{display:flex;flex-direction:column;gap:5px;white-space:nowrap}
+.cfn-brand__name{font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:23px;font-weight:700;letter-spacing:-.65px;color:#555555;line-height:1.08}
+.cfn-brand__accent{color:inherit;font-weight:650}
+.cfn-brand__tagline{font-size:11px;font-weight:400;letter-spacing:1.25px;color:#938798;line-height:1.25}
+body.dark .cfn-brand__tagline{color:#b7acbc}body.dark .cfn-brand__name{color:#eeeaf1}
+.mobile-sidebar-social-links{display:flex;align-items:center;gap:12px;margin:20px 24px 0;padding-top:16px;border-top:1px solid rgba(127,127,127,.16)}
+@media(max-width:1199.98px){
+.header #header>.container{position:relative;flex-wrap:nowrap;min-height:44px}
+.header .navbar-brand.cfn-brand{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);gap:8px;margin:0;flex-shrink:0}
+.header .navbar-brand.cfn-brand .cfn-brand__cat{width:38px;height:38px}
+.cfn-brand__name{font-size:21px;letter-spacing:-.5px}
+.cfn-brand__tagline{font-size:10px;letter-spacing:1px}
+.header .navbar-toggler{flex-shrink:0}
+}
diff --git a/public/css/freenode-article.css b/public/css/freenode-article.css
new file mode 100644
index 00000000..4d11b180
--- /dev/null
+++ b/public/css/freenode-article.css
@@ -0,0 +1,37 @@
+.free-node-page{--fn-purple:#86608e;--fn-text:#303641;--fn-muted:#9299a5;--fn-line:#ececf1}.free-node-page .post.card{overflow:hidden}.free-node-page .post>.card-body{padding:28px}.free-node-page .breadcrumbs .current{max-width:540px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
+.fn-header{display:flex;gap:24px;align-items:flex-start;margin-bottom:20px}.fn-header__main{flex:1;min-width:0}.free-node-page .fn-header h1{font-size:32px;line-height:1.3;letter-spacing:-.45px;color:#30333c;margin:0;font-weight:700}.fn-header__lead{font-size:14px;color:#7c7a87;margin:8px 0 0;line-height:1.6}.fn-header__art{width:112px;height:80px;object-fit:cover;border-radius:10px;flex:none}
+.fn-overview{border:1px solid var(--fn-line);border-radius:10px;display:flex;padding:14px 18px;gap:20px;background:#fbfafc;margin-bottom:20px}.fn-metric{flex:none;padding-right:22px;border-right:1px solid #e9e5ed}.fn-metric>span,.fn-overview__regions>span{display:block;font-size:14px;color:#756e7c;margin-bottom:7px}.fn-metric>div{display:flex;gap:5px;align-items:baseline}.fn-metric strong{font-size:23px;font-weight:650;color:#6d4e76;letter-spacing:-.5px;line-height:1.25}.fn-metric>div>span{font-size:13px;color:#a19aa6}.fn-overview__regions{min-width:0}.fn-overview__regions>div{display:flex;flex-wrap:wrap;gap:4px 10px;font-size:14px;color:#767181;line-height:1.8}
+.fn-article-nav{display:flex;gap:8px;padding-bottom:18px;border-bottom:1px solid var(--fn-line);margin-bottom:22px}.fn-article-nav a{font-size:14px;color:#8c8794;background:#f7f6f9;padding:7px 14px;border-radius:6px;line-height:1.5;text-decoration:none}.fn-article-nav a:first-child{background:#ede6f1;color:#785282}.fn-article-nav a span{margin-left:20px}.fn-article-nav a:hover{color:#6e4d78;background:#eee7f2}
+.free-node-page .post-content{font-size:16px;line-height:1.95}.fn-subscriptions,.fn-usage{scroll-margin-top:92px}.fn-section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin-bottom:14px}.free-node-page .post-content .fn-section-heading h2{font-size:22.72px;line-height:1.55;margin:0;padding:0;border:0;color:#38343e}.fn-subscription-list{display:grid;grid-template-columns:minmax(0,1fr);gap:11px}.fn-subscription{min-width:0;border:1px solid #e8e9ee;border-radius:10px;padding:17px 17px 14px;background:white}.fn-subscription__top{display:flex;gap:11px;align-items:center;margin-bottom:9px}.fn-subscription__name{flex:1;min-width:0}.free-node-page .post-content .fn-subscription__name h3{font-size:18.08px;line-height:1.5;color:#40404a;margin:0;font-weight:600}
+.fn-subscription__address{display:flex;align-items:center;gap:12px;background:#f7f7fa;border:1px solid #f0eff4;padding:8px 9px 8px 11px;border-radius:6px}.free-node-page .post-content .fn-subscription__address code{color:#777180;background:none;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;padding:0;display:block;min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fn-copy{border:0;border-radius:5px;background:#86618f;color:white;padding:8px 12px;display:inline-flex;align-items:center;justify-content:center;gap:5px;font-size:14px;line-height:1.5;white-space:nowrap;flex:none;cursor:pointer;min-height:40px}.fn-copy:hover{background:#765280}.fn-copy:focus-visible,.fn-article-nav a:focus-visible{outline:2px solid #9572b0;outline-offset:3px}
+.free-node-page .post-content .fn-subscription-note{font-size:14px;line-height:1.8;color:#7d7686;margin-top:13px}.free-node-page .post-content .fn-subscription-note p{margin:0!important}.fn-subscription-note span{color:inherit!important}.fn-subscription-note br{display:none}.fn-usage{margin:32px 0 0;padding-top:27px;border-top:1px solid var(--fn-line)}.fn-steps{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr));list-style:none!important;padding:0!important;margin:0 0 18px!important;gap:15px}.fn-steps>li{display:flex;gap:9px;padding:0!important;margin:0!important}.fn-steps>li>span{font-size:14px;font-weight:600;color:#bca5c5;line-height:1.8}.fn-steps strong{font-size:16px;font-weight:500;color:#6a6271}.free-node-page .post-content .fn-steps p{font-size:14px;color:#807788;margin:3px 0 0;line-height:1.8}
+.fn-original-help{background:#faf9fb;border:1px solid #efecf2;border-radius:8px;padding:13px 16px;color:#786c84;font-size:12px;line-height:1.9}.free-node-page .post-content .fn-original-help p{font-size:12px;line-height:1.9;margin:0 0 7px}.free-node-page .post-content .fn-original-help p:last-child{margin-bottom:0}.fn-original-help p:empty,.fn-original-help br{display:none}.fn-tutorial-links{display:flex;align-items:center;gap:15px;flex-wrap:wrap;margin-top:13px;font-size:14px;color:#b0a4b5}.fn-tutorial-links a{color:#8a6b95;text-decoration:none!important}.fn-community{margin:24px 0 0;border-top:1px solid var(--fn-line);padding-top:18px;font-size:14px;color:#7f7386;display:flex;align-items:center;justify-content:space-between;gap:10px}.fn-community>div{display:flex;gap:14px}.fn-community a{color:#8c7996;text-decoration:none!important}.fn-empty-note{font-size:14px!important;color:#a58a62!important;padding:9px 12px;background:#fdf9f1;border-radius:6px;margin:0 0 13px!important}.fn-empty-note a{color:inherit;text-decoration:underline}.fn-extra-intro{font-size:16px}.free-node-page .post-content>.airport-recommendations{border-top:1px solid var(--fn-line);padding-top:25px;margin-top:28px}.free-node-page .post-tags{border-top:1px solid var(--fn-line);padding-top:18px}.free-node-page .post-actions{margin-bottom:0!important}
+@media(min-width:992px){.free-node-page .sidebar{top:0}}
+@media(max-width:767.98px){.free-node-page .post>.card-body{padding:22px}.fn-header{gap:12px;align-items:flex-start;margin-bottom:16px}.fn-header__art{width:68px;height:76px;object-fit:cover;border-radius:8px}.free-node-page .fn-header h1{font-size:26px;line-height:1.35;letter-spacing:-.3px}.fn-header__lead{font-size:14px;line-height:1.7;margin-top:6px}.fn-overview{flex-wrap:wrap;padding:15px;gap:15px}.fn-metric{padding-right:17px}.fn-metric strong{font-size:22px}.fn-overview__regions{flex-basis:100%;border-top:1px solid #ebe6ef;padding-top:10px}.fn-overview__regions>span{margin-bottom:4px}.fn-article-nav{gap:6px;padding-bottom:20px;margin-bottom:23px}.fn-article-nav a{padding:8px 12px;font-size:14px}.fn-article-nav a span{margin-left:7px}.free-node-page .post-content .fn-section-heading h2{font-size:19.5px}.fn-subscription{padding:15px 13px 12px}.fn-subscription__top{gap:9px}.free-node-page .post-content .fn-subscription__name h3{font-size:17px}.fn-subscription__address{gap:8px;padding:7px}.fn-copy{font-size:14px;padding:8px 10px}.fn-copy svg{display:none}.fn-steps{grid-template-columns:1fr;gap:12px}.fn-steps>li>span{width:22px}.free-node-page .post-content .fn-steps p{margin-top:0}.fn-community{align-items:flex-start;flex-direction:column;gap:7px}.fn-tutorial-links{gap:8px 14px}.fn-original-help{padding:12px}.fn-usage{margin-top:26px;padding-top:22px}}
+@media(prefers-reduced-motion:no-preference){html:has(.free-node-page){scroll-behavior:smooth}}
+
+.free-node-page .post-content .fn-subscription h3::before{display:none}
+
+/* Match the existing tutorial type scale; shared recommendation overrides stay local to this page. */
+.free-node-page .post-content .airport-recommendations__heading h2{font-size:22.72px}
+.free-node-page .post-content .airport-recommendations__table{font-size:14px}
+.free-node-page .post-content .airport-recommendations__table thead th{font-size:13px}
+.free-node-page .post-content .airport-provider a{font-size:14px}
+.free-node-page .airport-provider__label,.free-node-page .airport-coupon__discount{font-size:12px}
+.free-node-page .airport-features,.free-node-page .airport-description{font-size:14px}
+.free-node-page .airport-coupon__copy,.free-node-page .airport-price>span{font-size:12px}
+.free-node-page .post-content p.airport-recommendations__note{font-size:12px}
+@media(max-width:600px){
+.free-node-page .post-content{font-size:15px;line-height:1.95}
+.free-node-page .post-content .fn-original-help p,.free-node-page .fn-original-help{font-size:12px;line-height:1.9}
+.free-node-page .fn-extra-intro{font-size:15px;line-height:1.95}
+.free-node-page .post-content .fn-subscription__address code{font-size:11px}
+.free-node-page .post-content .airport-recommendations__heading h2{font-size:19.5px}
+.fn-steps{grid-template-columns:1fr}
+}
+
+.fn-header h1>span{display:inline-block}
+.fn-article-nav,.fn-community>div{flex-wrap:wrap}
+.free-node-page .post-content .airport-recommendations__table tbody td.airport-description{font-size:14px}
+
+@media(max-width:360px){.fn-article-nav a{padding-left:8px;padding-right:8px}}
diff --git a/public/css/random-article-ad.css b/public/css/random-article-ad.css
index 60f8c579..1a865230 100644
--- a/public/css/random-article-ad.css
+++ b/public/css/random-article-ad.css
@@ -1,5 +1,8 @@
-.random-article-ad { position: relative; min-width: 0; aspect-ratio: 2.45; margin: 20px 0; overflow: hidden; border-radius: 12px; background: #181b1e; }
+.random-article-ad { position: relative; min-width: 0; aspect-ratio: 3.675; margin: 20px 0; overflow: hidden; border-radius: 12px; background: #181b1e; }
.random-article-ad[hidden] { display: none !important; }
.random-article-ad > a { display: block; position: absolute; inset: 0; }
-.random-article-ad img { display: block; width: 100%; height: 100%; object-fit: cover; }
+.random-article-ad img { display: block; width: 100%; height: 100%; object-fit: contain; }
.random-article-ad > a:focus-visible { outline: 3px solid #8ab4ff; outline-offset: -4px; }
+
+.random-article-ad[data-ad-selected="dpl"] { background: #0a0f1a; }
+.random-article-ad[data-ad-selected="fuc"] { background: #f8f8fc; }
diff --git a/public/js/airport-recommendations.js b/public/js/airport-recommendations.js
new file mode 100644
index 00000000..980a2998
--- /dev/null
+++ b/public/js/airport-recommendations.js
@@ -0,0 +1,34 @@
+document.addEventListener('click', async (event) => {
+ const button = event.target.closest('[data-airport-coupon]');
+ if (!button) return;
+ const status = button.closest('.airport-recommendations').querySelector('.airport-copy-status');
+ try {
+ let copied = false;
+ if (navigator.clipboard?.writeText) {
+ try { await navigator.clipboard.writeText(button.dataset.airportCoupon); copied = true; } catch { /* Try the user-activated fallback below. */ }
+ }
+ if (!copied) {
+ const field = document.createElement('textarea');
+ field.value = button.dataset.airportCoupon;
+ field.setAttribute('readonly', '');
+ field.style.cssText = 'position:fixed;left:-9999px;top:0';
+ document.body.append(field);
+ try { field.select(); copied = document.execCommand('copy'); }
+ finally { field.remove(); button.focus({preventScroll:true}); }
+ }
+ if (!copied) throw new Error('Copy unavailable');
+ status.textContent = '优惠码已复制';
+ button.classList.add('is-copied');
+ button.setAttribute('aria-label', '优惠码已复制');
+ window.setTimeout(() => {
+ button.classList.remove('is-copied');
+ button.setAttribute('aria-label', `复制优惠码 ${button.dataset.airportCoupon}`);
+ }, 2000);
+ } catch {
+ const selection = window.getSelection();
+ const range = document.createRange();
+ range.selectNodeContents(button.querySelector('span'));
+ selection.removeAllRanges(); selection.addRange(range);
+ status.textContent = '已选中优惠码,请长按或使用复制快捷键';
+ }
+});
diff --git a/public/sites/cfn/images/logo/cat.svg b/public/sites/cfn/images/logo/cat.svg
new file mode 100644
index 00000000..d1b3ab1f
--- /dev/null
+++ b/public/sites/cfn/images/logo/cat.svg
@@ -0,0 +1 @@
+
diff --git a/resources/views/components/airport-recommendations.blade.php b/resources/views/components/airport-recommendations.blade.php
new file mode 100644
index 00000000..8024f0e8
--- /dev/null
+++ b/resources/views/components/airport-recommendations.blade.php
@@ -0,0 +1,42 @@
+@php($providers = \App\Services\ArticleRecommendations::providers())
+@if($providers)
+@once
+ @push('styles')
+
+ @endpush
+ @push('scripts')
+
+ @endpush
+@endonce
+
+
+
机场推荐
+
+
+
+ 机场套餐、解锁情况与优惠码对比
+ | 机场名 | 套餐起售价 | 解锁情况 | 适合使用场景 | 优惠码 |
+
+ @foreach($providers as $key => $provider)
+
+ | {{ $provider['name'] }}↗{{ $provider['tagline'] ?? '' }} |
+ {{ $provider['price'] }}{{ $provider['period'] }}起 |
+ @foreach($provider['features'] as $feature)✓{{ $feature }}@endforeach |
+ {{ $provider['description'] }} |
+
+ @if($provider['coupon'])
+ {{ $provider['discount'] }}
+
+ @else
+ 无
+ @endif
+ |
+
+ @endforeach
+
+
+
+ 价格、套餐及优惠以服务商官网为准。
+
+
+@endif
diff --git a/resources/views/components/app-layout.blade.php b/resources/views/components/app-layout.blade.php
index 62dbf3f6..107be3a9 100644
--- a/resources/views/components/app-layout.blade.php
+++ b/resources/views/components/app-layout.blade.php
@@ -69,6 +69,10 @@
@vite(['resources/css/app.css', "resources/sites/".config("app.site_code_short")."/css/app.css",'resources/js/app.js'])
+ @if(config('app.site_code_short') === 'cfn')
+
+ @endif
+
@stack('styles')
@if(app()->environment('production') && ! $bEditorialPreview && config('app.site_code_short') == 'cfn')
diff --git a/resources/views/components/cfn-brand.blade.php b/resources/views/components/cfn-brand.blade.php
new file mode 100644
index 00000000..cf59dcde
--- /dev/null
+++ b/resources/views/components/cfn-brand.blade.php
@@ -0,0 +1,2 @@
+
+ClashFreeNode每天分享免费节点
diff --git a/resources/views/components/nav-bar.blade.php b/resources/views/components/nav-bar.blade.php
index 0d417aaf..b1985f4a 100644
--- a/resources/views/components/nav-bar.blade.php
+++ b/resources/views/components/nav-bar.blade.php
@@ -1,19 +1,23 @@