增加友情链接页面

This commit is contained in:
2026-09-16 23:53:27 +08:00
parent 651f860703
commit 481e76cb16
12 changed files with 145 additions and 70 deletions
+32
View File
@@ -0,0 +1,32 @@
(() => {
const canonicalLocale = (value) => {
const language = value?.toLowerCase().replaceAll("_", "-");
if (!language) return null;
if (/^zh(?:-|$)/.test(language)) return /^zh-(tw|hk|mo)(-|$)|-hant(?:-|$)/.test(language) ? "zh-Hant" : "zh-Hans";
if (/^en(?:-|$)/.test(language)) return "en";
if (/^(ja|jp)(?:-|$)/.test(language)) return "jp";
return null;
};
const parts = location.pathname.split("/");
const firstSegment = parts[1];
const pathLocale = canonicalLocale(firstSegment);
const isUnknownLanguagePath = !pathLocale && /^[a-z]{2,3}(?:-[a-z0-9]+)*$/i.test(firstSegment ?? "");
let locale = pathLocale;
if (!locale) {
try { locale = canonicalLocale(localStorage.getItem("kr-locale")); } catch {}
}
if (!locale) {
for (const language of navigator.languages?.length ? navigator.languages : [navigator.language]) {
locale = canonicalLocale(language);
if (locale) break;
}
}
locale ??= "en";
const path = pathLocale || isUnknownLanguagePath ? `/${parts.slice(2).join("/")}` : location.pathname;
const normalizedPath = path.replace(/\/$/, "");
const destination = `/${locale}${normalizedPath}/`;
const search = new URLSearchParams(location.search);
search.delete("lang");
const canonicalUrl = destination + (search.size ? `?${search}` : "") + location.hash;
if (location.pathname + location.search + location.hash !== canonicalUrl) location.replace(canonicalUrl);
})();
+12
View File
@@ -0,0 +1,12 @@
(() => {
const isTheme = (value) => value === "light" || value === "system" || value === "dark";
let preference = "system";
try {
const cookieTheme = document.cookie.split(";").map((entry) => entry.trim()).find((entry) => entry.startsWith("kr-theme="))?.slice("kr-theme=".length);
const savedTheme = isTheme(cookieTheme) ? cookieTheme : localStorage.getItem("kr-theme");
if (isTheme(savedTheme)) preference = savedTheme;
} catch {}
const theme = preference === "system" ? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : preference;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
})();
+4
View File
@@ -0,0 +1,4 @@
<svg width="430" height="430" viewBox="0 0 430 430" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M111.798 108.829C114.867 107 118.76 109.212 118.76 112.785V268.853C118.76 270.507 119.647 272.035 121.085 272.854L214.023 325.806C215.464 326.627 217.235 326.609 218.66 325.761L308.993 271.944C310.387 271.114 311.241 269.611 311.241 267.988V161.141C311.241 159.49 310.357 157.965 308.924 157.145L219.537 105.974C218.136 105.171 216.417 105.162 215.007 105.948L176.667 127.329C173.598 129.04 169.82 126.821 169.82 123.307V80.0176C169.82 78.3714 170.698 76.8504 172.125 76.0283L214.96 51.3372C216.394 50.5105 218.161 50.5173 219.589 51.3549L356.426 131.628C357.835 132.454 358.701 133.966 358.701 135.599V295.301C358.701 296.947 357.822 298.469 356.396 299.291L218.632 378.687C217.221 379.501 215.484 379.508 214.066 378.706L73.6378 299.284C72.1932 298.467 71.3 296.936 71.3 295.276V135.578C71.3 133.956 72.1538 132.453 73.5475 131.623L111.798 108.829Z" fill="#333333"/>
<path d="M261.536 241.829V188.171L215.001 161.309L168.466 188.171V241.829L215.001 268.691L261.536 241.829Z" fill="#FD6727"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

+13
View File
@@ -0,0 +1,13 @@
import type { Metadata } from "next";
import { FriendlyLinksPage } from "@/components/links/friendly-links-page";
import { getLocaleMessages, isLocale } from "@/lib/i18n";
export async function generateMetadata({ params }: { params: Promise<{ locale?: string }> }): Promise<Metadata> {
const { locale } = await params;
return { title: getLocaleMessages(isLocale(locale) ? locale : "zh-Hans").footer.legal[2] };
}
export default async function LinksPage({ params }: { params: Promise<{ locale?: string }> }) {
const { locale } = await params;
return <FriendlyLinksPage locale={isLocale(locale) ? locale : "zh-Hans"} />;
}
+1
View File
@@ -0,0 +1 @@
export { default, generateMetadata } from "@/app/(legacy)/links/page";
+26 -1
View File
@@ -55,6 +55,20 @@ a { color: inherit; text-decoration: none; }
.home-collage { position: relative; height: 648px; margin-top: 50px; }.home-collage-tile { position: absolute; border-radius: 13px; background: #d9d9d9; }.home-collage-wide { top: 0; left: 0; width: 41.4%; height: 359px; }.home-collage-medium { top: 0; left: 42.57%; width: 28.87%; height: 359px; }.home-collage-small { top: 376px; left: 27.28%; width: 44.3%; height: 272px; }.home-collage-bottom { top: 376px; left: 0; width: 25.83%; height: 272px; }.home-collage-tall { top: 0; right: 0; width: 27.1%; height: 648px; } .home-collage { position: relative; height: 648px; margin-top: 50px; }.home-collage-tile { position: absolute; border-radius: 13px; background: #d9d9d9; }.home-collage-wide { top: 0; left: 0; width: 41.4%; height: 359px; }.home-collage-medium { top: 0; left: 42.57%; width: 28.87%; height: 359px; }.home-collage-small { top: 376px; left: 27.28%; width: 44.3%; height: 272px; }.home-collage-bottom { top: 376px; left: 0; width: 25.83%; height: 272px; }.home-collage-tall { top: 0; right: 0; width: 27.1%; height: 648px; }
.news-page { padding: 30px 0 60px; } .news-page { padding: 30px 0 60px; }
.friendly-links-hero { background: var(--policy-hero-image) center 26.71% / 100% auto no-repeat; }
.friendly-links-page-inner { gap: 50px; }
.friendly-links-content { width: min(1214px, 100%); margin: 0 auto; }
.friendly-links-section + .friendly-links-section { margin-top: 50px; }
.friendly-links-section h2 { margin: 0 0 20px; color: #181c20; font-size: 40px; font-weight: 700; line-height: 48px; }
.friendly-links-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.friendly-link-card { position: relative; display: flex; align-items: stretch; min-width: 0; min-height: 186px; overflow: hidden; border: 1.1px solid #e3e6e8; border-radius: 16.42px; background: #fff; color: #181c20; transition: border-color .2s ease, box-shadow .2s ease, transform .2s ease; }
a.friendly-link-card:hover, a.friendly-link-card:focus-visible { border-color: #8f56a2; box-shadow: 0 8px 20px rgba(40, 29, 47, .1); transform: translateY(-2px); }
.friendly-link-card-content { display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 16.42px; padding: 32.83px 10.94px; }
.friendly-link-card-heading { display: flex; align-items: center; justify-content: center; gap: 16px; min-width: 0; font-size: 27.36px; font-weight: 400; line-height: normal; white-space: nowrap; }
.friendly-link-card-heading img { width: 65.67px; height: 65.67px; overflow: hidden; border-radius: 8px; object-fit: cover; }
.friendly-link-card-description { color: #8d949a; font-size: 16.42px; line-height: 21.89px; text-align: center; }
.friendly-link-card-archive { position: absolute; top: 13px; right: 5px; padding: 4px 6px; border-radius: 4px; background: #e72329; color: #fff; font-size: 10px; line-height: 12px; }
.friendly-links-page .page-bottom-nav { margin-top: 176px; }
.news-page-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; } .news-page-inner { max-width: 1200px; margin: 0 auto; padding: 0 24px; }
.news-page-header { display: flex; justify-content: center; align-items: center; margin: 0 0 30px; } .news-page-header { display: flex; justify-content: center; align-items: center; margin: 0 0 30px; }
.news-page-header h1 { margin: 0; font-size: clamp(52px, 5vw, 88px); letter-spacing: -0.07em; line-height: 1; font-weight: 800; } .news-page-header h1 { margin: 0; font-size: clamp(52px, 5vw, 88px); letter-spacing: -0.07em; line-height: 1; font-weight: 800; }
@@ -104,7 +118,7 @@ a { color: inherit; text-decoration: none; }
.news-detail-body a:hover, .policy-body a:hover { color: #8f56a2; } .news-detail-body a:hover, .policy-body a:hover { color: #8f56a2; }
.policy-page { padding: 0 0 48px; background: #fff; } .policy-page { padding: 0 0 48px; background: #fff; }
.policy-page-inner { display: flex; flex-direction: column; gap: 50px; align-items: center; padding: 0 40px 48px; } .policy-page-inner { display: flex; flex-direction: column; gap: 50px; align-items: center; padding: 0 40px 48px; }
.policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), url("/policy-hero.png") center / cover; overflow: hidden; } .policy-hero { width: min(1200px, 100%); min-height: 322px; display: flex; align-items: flex-start; padding: 156px 48px 0; border-radius: 10px; background: linear-gradient(110deg, rgba(143, 86, 162, 0.82), rgba(143, 86, 162, 0.18)), var(--policy-hero-image, url("/policy-hero.png")) center / cover; overflow: hidden; }
.policy-hero-inner { margin-left: -48px; padding: 10px 46px; background: #8f56a2; } .policy-hero-inner { margin-left: -48px; padding: 10px 46px; background: #8f56a2; }
.policy-hero h1 { margin: 0; color: #fff; font-size: clamp(38px, 4vw, 62px); line-height: 1.3; letter-spacing: 0.25px; } .policy-hero h1 { margin: 0; color: #fff; font-size: clamp(38px, 4vw, 62px); line-height: 1.3; letter-spacing: 0.25px; }
.policy-card { width: min(1200px, 100%); padding: 28px; border-radius: 26px; background: #fefdfa; box-shadow: 0 10px 15px rgba(15, 23, 42, 0.04); } .policy-card { width: min(1200px, 100%); padding: 28px; border-radius: 26px; background: #fefdfa; box-shadow: 0 10px 15px rgba(15, 23, 42, 0.04); }
@@ -188,6 +202,9 @@ html[data-theme="dark"] .theme-toggle button:hover, html[data-theme="dark"] .the
html[data-theme="dark"] .theme-toggle button.active:hover, html[data-theme="dark"] .theme-toggle button.active:focus-visible { background: #2b3746; } html[data-theme="dark"] .theme-toggle button.active:hover, html[data-theme="dark"] .theme-toggle button.active:focus-visible { background: #2b3746; }
html[data-theme="dark"] .theme-toggle img { filter: brightness(0) invert(1); } html[data-theme="dark"] .theme-toggle img { filter: brightness(0) invert(1); }
html[data-theme="dark"] .news-toolbar { border-color: rgba(255, 255, 255, 0.16); } html[data-theme="dark"] .news-toolbar { border-color: rgba(255, 255, 255, 0.16); }
html[data-theme="dark"] .friendly-links-section h2 { color: var(--ink); }
html[data-theme="dark"] .friendly-link-card { border-color: #394553; background: #19222d; color: var(--ink); }
html[data-theme="dark"] .friendly-link-card-description { color: #a9b5c1; }
html[data-theme="dark"] .news-filter-button, html[data-theme="dark"] .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); } html[data-theme="dark"] .news-filter-button, html[data-theme="dark"] .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); }
html[data-theme="dark"] .news-pagination-button, html[data-theme="dark"] .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; } html[data-theme="dark"] .news-pagination-button, html[data-theme="dark"] .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; }
html[data-theme="dark"] .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); } html[data-theme="dark"] .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
@@ -224,6 +241,9 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
html:not([data-theme]) .theme-toggle button.active:hover, html:not([data-theme]) .theme-toggle button.active:focus-visible { background: #2b3746; } html:not([data-theme]) .theme-toggle button.active:hover, html:not([data-theme]) .theme-toggle button.active:focus-visible { background: #2b3746; }
html:not([data-theme]) .theme-toggle img { filter: brightness(0) invert(1); } html:not([data-theme]) .theme-toggle img { filter: brightness(0) invert(1); }
html:not([data-theme]) .news-toolbar { border-color: rgba(255, 255, 255, 0.16); } html:not([data-theme]) .news-toolbar { border-color: rgba(255, 255, 255, 0.16); }
html:not([data-theme]) .friendly-links-section h2 { color: var(--ink); }
html:not([data-theme]) .friendly-link-card { border-color: #394553; background: #19222d; color: var(--ink); }
html:not([data-theme]) .friendly-link-card-description { color: #a9b5c1; }
html:not([data-theme]) .news-filter-button, html:not([data-theme]) .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); } html:not([data-theme]) .news-filter-button, html:not([data-theme]) .news-view-toggle { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.04); }
html:not([data-theme]) .news-pagination-button, html:not([data-theme]) .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; } html:not([data-theme]) .news-pagination-button, html:not([data-theme]) .news-share-button { color: var(--ink); border-color: rgba(255, 255, 255, 0.2); background: #1b2531; }
html:not([data-theme]) .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); } html:not([data-theme]) .news-detail-card { background: #19222d; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); }
@@ -277,6 +297,7 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
.home-collage-small { top: 202px; left: 39%; width: 61%; height: 158px; } .home-collage-small { top: 202px; left: 39%; width: 61%; height: 158px; }
.home-collage-tall { display: none; } .home-collage-tall { display: none; }
.news-page-inner { padding: 0 16px; } .news-page-inner { padding: 0 16px; }
.friendly-links-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.news-page-header h1 { font-size: clamp(46px, 12vw, 72px); } .news-page-header h1 { font-size: clamp(46px, 12vw, 72px); }
.news-toolbar { align-items: flex-start; flex-wrap: wrap; gap: 16px; } .news-toolbar { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
.news-toolbar-right { margin-left: auto; } .news-toolbar-right { margin-left: auto; }
@@ -337,6 +358,10 @@ html[data-theme="dark"] .site-footer-title, html[data-theme="dark"] .site-footer
.home-collage-bottom, .home-collage-small { top: 160px; height: 120px; } .home-collage-bottom, .home-collage-small { top: 160px; height: 120px; }
.news-page { padding-top: 20px; } .news-page { padding-top: 20px; }
.friendly-links-content { width: 100%; }
.friendly-links-section + .friendly-links-section { margin-top: 36px; }
.friendly-links-section h2 { margin-bottom: 16px; font-size: 30px; line-height: 1.35; }
.friendly-links-grid { grid-template-columns: 1fr; gap: 12px; }
.news-page-inner { padding: 0 12px; } .news-page-inner { padding: 0 12px; }
.news-page-header { margin-bottom: 20px; } .news-page-header { margin-bottom: 20px; }
.news-page-header h1 { font-size: 52px; } .news-page-header h1 { font-size: 52px; }
+11 -2
View File
@@ -1,7 +1,16 @@
import SiteDocument from "@/components/layout/site-document"; import Script from "next/script";
import "@/app/globals.css";
export { metadata, viewport } from "@/components/layout/site-document"; export { metadata, viewport } from "@/components/layout/site-document";
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return <SiteDocument>{children}</SiteDocument>; return (
<html lang="zh-Hans" suppressHydrationWarning>
<body>
{children}
<Script id="theme-preference" src="/init-theme.js" strategy="beforeInteractive" />
<Script id="locale-canonicalization" src="/init-locale.js" strategy="beforeInteractive" />
</body>
</html>
);
} }
+1 -67
View File
@@ -1,5 +1,4 @@
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import "@/app/globals.css";
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
@@ -7,75 +6,10 @@ export const metadata: Metadata = {
template: "%s | 基沃托斯铁道公团 KivotosRailway", template: "%s | 基沃托斯铁道公团 KivotosRailway",
}, },
description: "Kivotos Railway website", description: "Kivotos Railway website",
icons: { icons: { icon: "/icon/icon.svg" },
icon: "/icon/icon.svg",
},
}; };
export const viewport: Viewport = { export const viewport: Viewport = {
width: "device-width", width: "device-width",
initialScale: 1, initialScale: 1,
}; };
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="zh-Hans" suppressHydrationWarning>
<head>
{/* Run during HTML parsing, before the body can paint. */}
<script
id="theme-preference"
dangerouslySetInnerHTML={{ __html: `(() => {
const isTheme = (value) => value === 'light' || value === 'system' || value === 'dark';
let preference = 'system';
try {
const cookieTheme = document.cookie.split(';').map((entry) => entry.trim()).find((entry) => entry.startsWith('kr-theme='))?.slice('kr-theme='.length);
const savedTheme = isTheme(cookieTheme) ? cookieTheme : localStorage.getItem('kr-theme');
if (isTheme(savedTheme)) preference = savedTheme;
} catch {}
const theme = preference === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : preference;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
})();` }}
/>
<script
id="locale-canonicalization"
dangerouslySetInnerHTML={{ __html: `(() => {
const canonicalLocale = (value) => {
const language = value?.toLowerCase().replaceAll('_', '-');
if (!language) return null;
if (/^zh(?:-|$)/.test(language)) return /^zh-(tw|hk|mo)(-|$)|-hant(?:-|$)/.test(language) ? 'zh-Hant' : 'zh-Hans';
if (/^en(?:-|$)/.test(language)) return 'en';
if (/^(ja|jp)(?:-|$)/.test(language)) return 'jp';
return null;
};
const parts = location.pathname.split('/');
const firstSegment = parts[1];
const pathLocale = canonicalLocale(firstSegment);
const isUnknownLanguagePath = !pathLocale && /^[a-z]{2,3}(?:-[a-z0-9]+)*$/i.test(firstSegment ?? '');
let locale = pathLocale;
if (!locale) {
try { locale = canonicalLocale(localStorage.getItem('kr-locale')); } catch {}
}
if (!locale) {
for (const language of navigator.languages?.length ? navigator.languages : [navigator.language]) {
locale = canonicalLocale(language);
if (locale) break;
}
}
locale ??= 'en';
const path = pathLocale || isUnknownLanguagePath ? '/' + parts.slice(2).join('/') : location.pathname;
const normalizedPath = path.replace(/\\/$/, '');
const destination = '/' + locale + normalizedPath + '/';
const search = new URLSearchParams(location.search);
search.delete('lang');
const canonicalUrl = destination + (search.size ? '?' + search : '') + location.hash;
if (location.pathname + location.search + location.hash !== canonicalUrl) location.replace(canonicalUrl);
})();` }}
/>
</head>
<body>{children}</body>
</html>
);
}
@@ -0,0 +1,20 @@
import links from "@/data/links.json";
import type { CSSProperties } from "react";
import { PageBreadcrumb } from "@/components/layout/page-breadcrumb";
import { getLocaleMessages, type Locale } from "@/lib/i18n";
type LinkEntry = { name: string; description: string; logo: string; url?: string; archived?: boolean };
type LocalizedText = Record<Locale, string>;
type LinkSection = { id: string; title: LocalizedText; links: LinkEntry[] };
const linkSections = links.sections as LinkSection[];
function FriendlyLinkCard({ link }: { link: LinkEntry }) {
const content = <><span className="friendly-link-card-content"><span className="friendly-link-card-heading"><img src={link.logo} alt="" width={48} height={48} /><span>{link.name}</span></span><span className="friendly-link-card-description">{link.description}</span></span>{link.archived && <span className="friendly-link-card-archive">已归档</span>}</>;
return link.url ? <a className="friendly-link-card" href={link.url} target="_blank" rel="noreferrer" aria-label={`访问 ${link.name}`}>{content}</a> : <article className="friendly-link-card">{content}</article>;
}
export function FriendlyLinksPage({ locale }: { locale: Locale }) {
const messages = getLocaleMessages(locale);
const heroStyle = { "--policy-hero-image": 'url("/links/friendly-links-hero.png")' } as CSSProperties;
return <main className="policy-page friendly-links-page"><div className="policy-page-inner friendly-links-page-inner"><section className="policy-hero friendly-links-hero" aria-labelledby="friendly-links-title" style={heroStyle}><div className="policy-hero-inner"><h1 id="friendly-links-title">{messages.footer.legal[2]}</h1></div></section><div className="friendly-links-content">{linkSections.map((section) => <section className="friendly-links-section" key={section.id} aria-labelledby={`friendly-links-${section.id}`}><h2 id={`friendly-links-${section.id}`}>{section.title[locale]}</h2><div className="friendly-links-grid">{section.links.map((link, index) => <FriendlyLinkCard key={`${section.id}-${link.name}-${index}`} link={link} />)}</div></section>)}</div></div><div className="page-bottom-nav"><div className="page-bottom-nav-inner"><PageBreadcrumb items={[{ label: messages.common.home, href: "/" }, { label: messages.footer.legal[2] }]} /></div></div></main>;
}
+25
View File
@@ -0,0 +1,25 @@
{
"sections": [
{
"id": "services",
"title": { "zh-Hans": "服务提供", "zh-Hant": "服務提供", "en": "Service providers", "jp": "サービス提供" },
"links": [
{ "name": "德威云", "description": "Minecraft 服务器提供方", "logo": "/links/fetarute.png", "url": "https://www.deviy.cn/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "蓝色秋生", "description": "网页服务提供方,对象存储、CDN 维护方", "logo": "/links/blueakio.svg", "url": "https://www.blueakio.com/" }
]
},
{
"id": "friends",
"title": { "zh-Hans": "朋友们", "zh-Hant": "朋友們", "en": "Friends", "jp": "友だち" },
"links": [
{ "name": "德威云", "description": "Minecraft 服务器提供方", "logo": "/links/fetarute.png", "url": "https://deviy.cn/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "蓝色秋生", "description": "网页服务提供方,对象存储、CDN 维护方", "logo": "/links/fetarute.png", "url": "https://liqiusheng.cn/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" },
{ "name": "腾讯云", "description": "CDN、对象存储 提供方", "logo": "/links/fetarute.png", "url": "https://cloud.tencent.com/" }
]
}
]
}