Commit 581157d2 authored by fisherdaddy's avatar fisherdaddy

chore: 优化移动端展示和SEO

parent d44aafbd
......@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="robots" content="index, follow" />
<meta name="author" content="fisherdaddy" />
<link rel="canonical" href="https://fishersama.com" />
......
User-agent: *
Disallow:
Allow: /
Disallow: /api/
Disallow: /admin/
Disallow: /private/
# Allow Google Images to index images
User-agent: Googlebot-Image
Allow: /assets/
Allow: /images/
# Allow Google Mobile to index mobile version
User-agent: Googlebot-Mobile
Allow: /
# Sitemap
Sitemap: https://fishersama.com/sitemap.xml
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://fishersama.com/</loc>
<lastmod>2024-01-01</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://fishersama.com/dev-tools</loc>
<lastmod>2024-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://fishersama.com/image-tools</loc>
<lastmod>2024-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://fishersama.com/blog</loc>
<lastmod>2024-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>https://fishersama.com/ai-products</loc>
<lastmod>2024-01-01</lastmod>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
</urlset>
\ No newline at end of file
......@@ -11,15 +11,11 @@ function Header() {
const navigate = useNavigate();
const user = JSON.parse(localStorage.getItem('user'));
const [menuOpen, setMenuOpen] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const menuRef = useRef(null);
const handleLogout = () => {
localStorage.removeItem('user');
navigate('/login');
};
const toggleMenu = () => {
setMenuOpen((prev) => !prev);
setMenuOpen(!menuOpen);
};
useEffect(() => {
......@@ -28,69 +24,97 @@ function Header() {
setMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
}, [menuRef]);
const handleLogout = () => {
localStorage.removeItem('user');
navigate('/login');
setMobileMenuOpen(false);
};
const toggleMobileMenu = () => {
setMobileMenuOpen(!mobileMenuOpen);
};
const handleNavClick = () => {
setMobileMenuOpen(false);
};
return (
<header>
<nav>
<div className="logo-title-container">
<NavLink to="/" className="title no-underline">
<NavLink to="/" className="title no-underline" onClick={handleNavClick}>
<img src={logo} alt="Logo" className="logo" />
{t('title')}
</NavLink>
</div>
<div className="menu-items">
<NavLink to="/dev-tools" className={({ isActive }) => (isActive ? 'active' : '')}>
{t('dev-tools')}
</NavLink>
<NavLink to="/image-tools" className={({ isActive }) => (isActive ? 'active' : '')}>
{t('image-tools')}
</NavLink>
<NavLink to="/blog" className={({ isActive }) => (isActive ? 'active' : '')}>
{t('blog')}
</NavLink>
<NavLink to="/ai-products" className={({ isActive }) => (isActive ? 'active' : '')}>
{t('ai-products')}
</NavLink>
</div>
<div className="right-container">
<LanguageSelector />
<div className="auth-container">
{user ? (
<div className="user-info">
<div className="avatar-container" ref={menuRef}>
<img
src={user.picture}
alt="User Avatar"
className="avatar"
onClick={toggleMenu}
/>
<div className={`dropdown-menu ${menuOpen ? 'active' : ''}`}>
<button onClick={handleLogout}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
{t('logout')}
</button>
</div>
</div>
</div>
<button className="mobile-menu-button" onClick={toggleMobileMenu}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
{mobileMenuOpen ? (
<path d="M6 18L18 6M6 6l12 12" />
) : (
<NavLink to="/login" className="login-button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12" />
</svg>
{t('login')}
</NavLink>
<path d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
<div className={`menu-container ${mobileMenuOpen ? 'mobile-menu-open' : ''}`}>
<div className="menu-items">
<NavLink to="/dev-tools" onClick={handleNavClick}>
{t('dev-tools')}
</NavLink>
<NavLink to="/image-tools" onClick={handleNavClick}>
{t('image-tools')}
</NavLink>
<NavLink to="/blog" onClick={handleNavClick}>
{t('blog')}
</NavLink>
<NavLink to="/ai-products" onClick={handleNavClick}>
{t('ai-products')}
</NavLink>
</div>
<div className="right-container">
<LanguageSelector />
<div className="auth-container">
{user ? (
<div className="user-info">
<div className="avatar-container" ref={menuRef}>
<img
src={user.picture}
alt="User Avatar"
className="avatar"
onClick={toggleMenu}
/>
<div className={`dropdown-menu ${menuOpen ? 'active' : ''}`}>
<button onClick={handleLogout}>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
{t('logout')}
</button>
</div>
</div>
</div>
) : (
<NavLink to="/login" className="login-button">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />
<line x1="15" y1="12" x2="3" y2="12" />
</svg>
{t('login')}
</NavLink>
)}
</div>
</div>
</div>
</nav>
......
......@@ -7,7 +7,8 @@ function SEO({ title, description, lang = 'en', meta = [] }) {
const { t } = useTranslation();
const defaultTitle = t('title');
const defaultDescription = t('description'); // 确保在i18n配置中添加'description'
const defaultDescription = t('description');
const defaultKeywords = t('keywords');
const languages = ['en', 'zh', 'ja', 'ko'];
const hostname = 'https://fishersama.com'; // 替换为您的网站域名
......@@ -20,20 +21,23 @@ function SEO({ title, description, lang = 'en', meta = [] }) {
const structuredData = {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"@type": "WebApplication",
"name": defaultTitle,
"url": "https://fishersama.com/", // 请替换为您的网站URL
"url": "https://fishersama.com/",
"description": defaultDescription,
"potentialAction": {
"@type": "SearchAction",
"target": "https://fishersama.com/search?q={search_term}",
"query-input": "required name=search_term"
},
"applicationCategory": "AI Tools",
"operatingSystem": "Web Browser",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
},
"author": {
"@type": "Person",
"name": "Fisher"
},
"datePublished": "2024-01-01",
"dateModified": new Date().toISOString().split('T')[0]
};
return (
......@@ -50,11 +54,7 @@ function SEO({ title, description, lang = 'en', meta = [] }) {
},
{
name: 'keywords',
content: t('keywords'), // 确保在 i18n 配置中添加 'keywords'
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
content: defaultKeywords,
},
{
property: 'og:title',
......@@ -68,9 +68,17 @@ function SEO({ title, description, lang = 'en', meta = [] }) {
property: 'og:type',
content: 'website',
},
{
property: 'og:image',
content: 'https://fishersama.com/og-image.jpg',
},
{
name: 'twitter:card',
content: 'summary',
content: 'summary_large_image',
},
{
name: 'twitter:creator',
content: '@fun000001',
},
{
name: 'twitter:title',
......@@ -81,12 +89,30 @@ function SEO({ title, description, lang = 'en', meta = [] }) {
content: description || defaultDescription,
},
{
name: 'robots',
content: 'index,follow',
name: 'twitter:image',
content: 'https://fishersama.com/twitter-card.jpg',
},
{
name: 'application-name',
content: defaultTitle,
},
{
name: 'apple-mobile-web-app-title',
content: defaultTitle,
},
{
name: 'format-detection',
content: 'telephone=no',
},
// 可以根据需要添加更多元数据
{
name: 'theme-color',
content: '#6366F1',
}
].concat(meta)}
link={links}
link={[
...links,
{ rel: 'canonical', href: `https://fishersama.com${window.location.pathname}` }
]}
>
<script type="application/ld+json">
{JSON.stringify(structuredData)}
......
......@@ -6,6 +6,7 @@
"keywords": "AI Toolbox, AI tools, text cards, JSON formatter, URL decoder, OpenAI products, model price comparison, online tools, free tools",
"welcome": "Welcome",
"login": "Login",
"loginSubtitle": "Welcome to AI Toolbox, please log in for the full experience",
"logout": "Logout",
"dev-tools": "Development Tools",
"image-tools": "Image Tools",
......@@ -231,6 +232,7 @@
"keywords": "AI工具箱,AI 工具,文字卡片,JSON 格式化,URL 解码器,OpenAI 产品,模型价格对比,在线工具,免费工具",
"welcome": "欢迎",
"login": "登录",
"loginSubtitle": "欢迎使用 AI 工具箱,请登录以获得完整体验",
"logout": "退出登录",
"dev-tools": "开发工具",
"image-tools": "图片工具",
......@@ -447,10 +449,7 @@
"Image": "图像",
"AudioVideo": "音视频",
"Productivity": "效率办公"
},
"login": "登录",
"loginSubtitle": "欢迎使用 AI 工具箱,请登录以获得完整体验",
"or": "或"
}
},
"ja": {
"title": "AIツールボックス",
......@@ -459,6 +458,7 @@
"keywords": "AIツールボックス、AIツール、テキストカード、JSONフォーマッター、URLデコーダー、OpenAI製品、モデル価格比較、オンラインツール、無料ツール",
"welcome": "ようこそ",
"login": "ログイン",
"loginSubtitle": "AIツールボックスへようこそ。フル体験のためにログインしてください",
"logout": "ログアウト",
"dev-tools": "開発ツール",
"image-tools": "画像ツール",
......@@ -686,6 +686,7 @@
"keywords": "AI 도구상자, AI 도구, 텍스트 카드, JSON 포매터, URL 디코더, OpenAI 제품, 모델 가격 비교, 온라인 도구, 무료 도구",
"welcome": "환영합니다",
"login": "로그인",
"loginSubtitle": "AI 툴박스에 오신 것을 환영합니다. 전체 경험을 위해 로그인해 주세요",
"logout": "로그아웃",
"dev-tools": "개발 도구",
"image-tools": "이미지 도구",
......
......@@ -18,6 +18,38 @@ const tools = [
const Home = () => {
const { t } = useTranslation();
const renderToolLink = (tool) => {
const content = (
<>
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
</div>
</>
);
return tool.external ? (
<a
href={tool.path}
className="tool-card"
target="_blank"
rel="noopener noreferrer"
>
{content}
</a>
) : (
<Link to={tool.path} className="tool-card">
{content}
</Link>
);
};
return (
<>
<SEO
......@@ -28,18 +60,9 @@ const Home = () => {
<section className="tools-section">
<div className="tools-grid">
{tools.map(tool => (
<Link to={tool.path} key={tool.id} className="tool-card">
<img
src={tool.icon}
alt={`${t(`tools.${tool.id}.title`)} icon`}
className="tool-icon"
loading="lazy"
/>
<div className="tool-content">
<h3 className="tool-title">{t(`tools.${tool.id}.title`)}</h3>
<p className="tool-description">{t(`tools.${tool.id}.description`)}</p>
</div>
</Link>
<React.Fragment key={tool.id}>
{renderToolLink(tool)}
</React.Fragment>
))}
</div>
</section>
......
......@@ -432,3 +432,348 @@ header nav {
background: rgba(99, 102, 241, 0.1);
color: #6366F1;
}
@media screen and (max-width: 768px) {
header {
height: auto;
padding: 0;
}
header nav {
padding: 0.6rem 1rem;
}
.logo {
width: 30px;
height: 30px;
}
.title {
font-size: 1.4rem;
}
.menu-items {
position: fixed;
top: 60px;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
padding: 1rem;
flex-direction: column;
gap: 1rem;
border-bottom: 1px solid rgba(99, 102, 241, 0.1);
display: none;
}
.menu-items.active {
display: flex;
}
.menu-items a {
width: 100%;
text-align: center;
padding: 0.8rem;
}
.right-container {
gap: 0.8rem;
}
.auth-container .login-button {
padding: 6px 12px;
font-size: 0.9rem;
}
}
/* 移动端菜单按钮 */
.mobile-menu-button {
display: none;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
transition: color 0.3s ease;
}
.mobile-menu-button:hover {
color: #6366F1;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
z-index: 1000;
}
header nav {
padding: 0.8rem 1rem;
justify-content: space-between;
align-items: center;
}
.mobile-menu-button {
display: block;
z-index: 1001;
}
.menu-container {
position: fixed;
top: 60px; /* 导航栏高度 */
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
padding: 1rem;
transform: translateX(100%);
transition: transform 0.3s ease;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 1rem;
z-index: 1000;
}
.menu-container.mobile-menu-open {
transform: translateX(0);
}
.menu-items {
display: flex;
flex-direction: column;
margin: 0;
width: 100%;
gap: 0.5rem;
}
.menu-items a {
width: 100%;
padding: 1rem;
text-align: center;
}
.right-container {
margin-top: 1rem;
width: 100%;
}
}
/* 横屏模式优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.menu-container {
top: 50px;
}
.menu-items {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.menu-items a {
width: auto;
}
.right-container {
flex-direction: row;
justify-content: center;
}
}
/* 移动端样式统一处理 */
@media screen and (max-width: 768px) {
header {
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
z-index: 1000;
}
header nav {
padding: 0.8rem 1rem;
justify-content: space-between;
align-items: center;
}
.mobile-menu-button {
display: block;
z-index: 1001;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
transition: color 0.3s ease;
}
.mobile-menu-button:hover {
color: #6366F1;
}
.menu-container {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(10px);
padding: 1rem;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 999;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.menu-container.mobile-menu-open {
transform: translateX(0);
}
.menu-items {
display: flex;
flex-direction: column;
margin: 0;
width: 100%;
gap: 1rem;
}
.menu-items a {
width: 100%;
text-align: center;
padding: 1rem;
font-size: 1.1rem;
}
.right-container {
margin-top: 1rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.auth-container {
width: 100%;
display: flex;
justify-content: center;
}
.login-button {
width: 100%;
justify-content: center;
}
}
/* 横屏模式优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.menu-container {
top: 50px;
}
.menu-items {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.menu-items a {
width: auto;
}
.right-container {
flex-direction: row;
justify-content: center;
}
}
/* 移动端菜单按钮 */
.mobile-menu-button {
display: none;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
transition: color 0.3s ease;
}
/* 移动端菜单容器 */
.menu-container {
display: flex;
align-items: center;
flex: 1;
}
@media screen and (max-width: 768px) {
.mobile-menu-button {
display: block;
}
.menu-container {
position: fixed;
top: 60px;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.98);
padding: 1rem;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 999;
display: flex;
flex-direction: column;
}
.menu-container.mobile-menu-open {
transform: translateX(0);
}
.menu-items {
display: flex !important; /* 覆盖之前的 display: none */
flex-direction: column;
margin: 0;
width: 100%;
gap: 1rem;
}
.menu-items a {
width: 100%;
text-align: center;
padding: 1rem;
}
.right-container {
margin-top: 1rem;
width: 100%;
}
}
/* 横屏模式优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.menu-container {
top: 50px;
}
.menu-items {
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.menu-items a {
width: auto;
}
}
......@@ -255,4 +255,72 @@ footer {
linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px),
linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px);
background-size: 100% 100%, 20px 20px, 20px 20px;
}
/* 移动端适配基础设置 */
@media screen and (max-width: 768px) {
:root {
font-size: 14px; /* 调整基础字体大小 */
}
main {
padding: 1rem;
margin-top: 3.5rem; /* 为固定导航栏留出空间 */
}
.tools-section {
padding: 1rem 0;
}
.tools-section h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
padding: 0 1rem;
}
.tools-grid {
grid-template-columns: 1fr;
gap: 1rem;
padding: 0 1rem;
}
.tool-card {
padding: 1.2rem;
}
.tool-icon {
width: 35px;
height: 35px;
}
.tool-title {
font-size: 1.2rem;
}
.tool-description {
font-size: 0.95rem;
}
}
/* 针对更小屏幕的优化 */
@media screen and (max-width: 480px) {
.tools-section h2 {
font-size: 1.8rem;
}
.tool-card {
padding: 1rem;
}
.tool-icon {
width: 30px;
height: 30px;
}
}
/* 针对横屏模式的优化 */
@media screen and (max-width: 768px) and (orientation: landscape) {
.tools-grid {
grid-template-columns: repeat(2, 1fr);
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment