diff --git a/package.json b/package.json index 3e6c4ea..3fbab7f 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react": "^18.3.1", "react-content-loader": "^7.0.2", "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-icons": "^5.3.0", "react-lazy-load": "^4.0.1", "react-router-dom": "^6.26.2", diff --git a/src/App.jsx b/src/App.jsx index 53d4d6b..008418b 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,7 @@ import { useLocation } from "react-router-dom"; import { useEffect } from "react"; import { Routes, Route } from "react-router-dom"; +import { HelmetProvider } from "react-helmet-async"; import { Analytics } from '@vercel/analytics/react'; import { SpeedInsights } from '@vercel/speed-insights/react'; import { HomeInfoProvider } from "./context/HomeInfoContext"; @@ -34,51 +35,53 @@ function App() { const isSplashScreen = location.pathname === "/"; return ( - -
-
- {!isSplashScreen && } - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* Render category routes */} - {categoryRoutes.map((path) => ( - - } - /> - ))} - {/* Render A to Z routes */} - {azRoute.map((path) => ( - } - /> - ))} - } /> - } /> - {/* Catch-all route for 404 */} - } /> - - {!isSplashScreen &&
} -
- - - -
-
+ + +
+
+ {!isSplashScreen && } + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* Render category routes */} + {categoryRoutes.map((path) => ( + + } + /> + ))} + {/* Render A to Z routes */} + {azRoute.map((path) => ( + } + /> + ))} + } /> + } /> + {/* Catch-all route for 404 */} + } /> + + {!isSplashScreen &&
} +
+ + + +
+
+
); } diff --git a/src/components/splashscreen/SplashScreen.jsx b/src/components/splashscreen/SplashScreen.jsx index 55e52b6..17b3f67 100644 --- a/src/components/splashscreen/SplashScreen.jsx +++ b/src/components/splashscreen/SplashScreen.jsx @@ -5,6 +5,8 @@ import logoTitle from "@/src/config/logoTitle"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faMagnifyingGlass, faChevronDown } from "@fortawesome/free-solid-svg-icons"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons"; +import { Helmet } from 'react-helmet-async'; +import { generateFAQSchema, generateCanonicalUrl, optimizeTitle } from "@/src/utils/seo.utils"; const FAQ_ITEMS = [ { @@ -46,62 +48,89 @@ function SplashScreen() { setExpandedFaq(expandedFaq === index ? null : index); }; + const faqSchema = generateFAQSchema(FAQ_ITEMS); + const canonicalUrl = generateCanonicalUrl('/'); + const pageTitle = optimizeTitle('Watch Anime Online Free | English Sub & Dub', false); + return ( -
-
-
-
- {logoTitle} -
+ <> + + {pageTitle} + + + -
- setSearch(e.target.value)} - onKeyDown={handleKeyDown} - /> - -
+ + + + - - Enter Homepage - + + + -
-

Frequently Asked Questions

-
- {FAQ_ITEMS.map((item, index) => ( -
- - {expandedFaq === index && ( -
- {item.answer} -
- )} -
- ))} + {faqSchema && ( + + )} + +
+
+
+
+ {logoTitle} +
+ +
+ setSearch(e.target.value)} + onKeyDown={handleKeyDown} + /> + +
+ + + Enter Homepage + + +
+

Frequently Asked Questions

+
+ {FAQ_ITEMS.map((item, index) => ( +
+ + {expandedFaq === index && ( +
+ {item.answer} +
+ )} +
+ ))} +
-
+ ); } diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index cc6eb62..6ad8b8c 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -10,21 +10,59 @@ import { useHomeInfo } from "@/src/context/HomeInfoContext.jsx"; import Schedule from "@/src/components/schedule/Schedule"; import ContinueWatching from "@/src/components/continue/ContinueWatching"; import TabbedAnimeSection from "@/src/components/tabbed-anime/TabbedAnimeSection"; +import { Helmet } from 'react-helmet-async'; +import { + generateWebsiteStructuredData, + generateOrganizationStructuredData, + generateItemListSchema +} from "@/src/utils/seo.utils"; function Home() { const { homeInfo, homeInfoLoading, error } = useHomeInfo(); if (homeInfoLoading) return ; if (error) return ; if (!homeInfo) return ; + + const websiteSchema = generateWebsiteStructuredData(); + const organizationSchema = generateOrganizationStructuredData(); + const trendingSchema = homeInfo.trending ? generateItemListSchema(homeInfo.trending, "Trending Anime") : null; + return ( <> + + {website_name} | Free Anime Streaming Platform + + + + + + + + + + + + + + + + {trendingSchema && ( + + )} +
- +
- -
-

- Sort By Letters -

-
- {[ - "All", - "#", - "0-9", - ...Array.from({ length: 26 }, (_, i) => - String.fromCharCode(65 + i) - ), - ].map((item, index) => { - const linkPath = - item.toLowerCase() === "all" - ? "" - : item === "#" - ? "other" - : item; - const isActive = - (currentLetter === "az-list" && item.toLowerCase() === "all") || - (currentLetter === "other" && item === "#") || - currentLetter === item.toLowerCase(); + const { title, description, keywords } = generateAZListMeta(currentLetter, page); + const canonicalUrl = generateCanonicalUrl(`/az-list/${currentLetter === 'az-list' ? '' : currentLetter}${page > 1 ? `?page=${page}` : ''}`); + const paginationLinks = generatePaginationLinks(`/az-list/${currentLetter}`, page, totalPages); + const collectionSchema = generateCollectionSchema(categoryInfo, `Anime starting with ${currentLetter}`, `az-list/${currentLetter}`); - return ( - - {item} - - ); - })} + return ( + <> + + {title} + + + + + {paginationLinks.map((link, index) => ( + + ))} + + + + + + + + + + + {collectionSchema && ( + + )} + +
+
+

+ Sort By Letters +

+
+ {[ + "All", + "#", + "0-9", + ...Array.from({ length: 26 }, (_, i) => + String.fromCharCode(65 + i) + ), + ].map((item, index) => { + const linkPath = + item.toLowerCase() === "all" + ? "" + : item === "#" + ? "other" + : item; + const isActive = + (currentLetter === "az-list" && item.toLowerCase() === "all") || + (currentLetter === "other" && item === "#") || + currentLetter === item.toLowerCase(); + + return ( + + {item} + + ); + })} +
-
-
-
- {categoryInfo && categoryInfo.length > 0 && ( - - )} -
- +
+
+ {categoryInfo && categoryInfo.length > 0 && ( + + )} +
+ +
-
+ ); } diff --git a/src/pages/animeInfo/AnimeInfo.jsx b/src/pages/animeInfo/AnimeInfo.jsx index 514f1f4..9f004bc 100644 --- a/src/pages/animeInfo/AnimeInfo.jsx +++ b/src/pages/animeInfo/AnimeInfo.jsx @@ -16,6 +16,16 @@ import { useLanguage } from "@/src/context/LanguageContext"; import { useHomeInfo } from "@/src/context/HomeInfoContext"; import Voiceactor from "@/src/components/voiceactor/Voiceactor"; import getSafeTitle from "@/src/utils/getSafetitle"; +import { Helmet } from 'react-helmet-async'; +import { + generateDescription, + generateKeywords, + generateCanonicalUrl, + generateOGImage, + generateAnimeStructuredData, + generateBreadcrumbStructuredData, + optimizeTitle, +} from '@/src/utils/seo.utils'; function InfoItem({ label, value, isProducer = true }) { return ( @@ -110,15 +120,6 @@ function AnimeInfo({ random = false }) { window.scrollTo({ top: 0, behavior: "smooth" }); } }, [id, random]); - useEffect(() => { - if (animeInfo && location.pathname === `/${animeInfo.id}`) { - const safeTitle = getSafeTitle(animeInfo.title, language, animeInfo.japanese_title); - document.title = `Watch ${safeTitle} English Sub/Dub online Free on ${website_name}`; - } - return () => { - document.title = `${website_name} | Free anime streaming platform`; - }; - }, [animeInfo, language]); if (loading) return ; if (error) { return ; @@ -129,6 +130,19 @@ function AnimeInfo({ random = false }) { } const { title, japanese_title, poster, animeInfo: info } = animeInfo; const safeTitle = getSafeTitle(title, language, japanese_title); + const displayTitle = safeTitle; // Use safeTitle as the display title + + const pageTitle = optimizeTitle(`Watch ${displayTitle} Sub Dub Online Free`); + const pageDescription = generateDescription(info?.Overview); + const pageKeywords = generateKeywords(animeInfo); + const canonicalUrl = generateCanonicalUrl(`/${animeInfo.id}`); + const ogImage = generateOGImage(poster); + + const animeStructuredData = generateAnimeStructuredData(animeInfo); + const breadcrumbData = generateBreadcrumbStructuredData([ + { name: 'Home', url: '/' }, + { name: animeInfo.title, url: `/${animeInfo.id}` } + ]); const tags = [ { @@ -156,368 +170,394 @@ function AnimeInfo({ random = false }) { ]; return ( -
-
+ <> + + {pageTitle} + + + - {/* Main Content */} -
- {/* Mobile Layout */} -
-
- {/* Poster Section */} -
-
- {`${safeTitle} - {animeInfo.adultContent && ( -
- 18+ + + + + + + + + + + + + + + +
+
+ + {/* Main Content */} +
+ {/* Mobile Layout */} +
+
+ {/* Poster Section */} +
+
+ {`${safeTitle} + {animeInfo.adultContent && ( +
+ 18+ +
+ )} +
+
+ + {/* Basic Info Section */} +
+ {/* Title */} +
+

+ {safeTitle} +

+ {language === "EN" && japanese_title && ( +

JP Title: {japanese_title}

+ )} +
+ + {/* Tags */} +
+ {tags.map(({ condition, icon, text }, index) => + condition && ( + + ) + )} +
+ + {/* Overview - Limited for mobile */} + {info?.Overview && ( +
+ {info.Overview.length > 150 ? ( + <> + {isFull ? ( + info.Overview + ) : ( +
{info.Overview}
+ )} + + + ) : ( + info.Overview + )}
)}
- {/* Basic Info Section */} -
- {/* Title */} -
-

- {safeTitle} -

- {language === "EN" && japanese_title && ( -

JP Title: {japanese_title}

- )} -
- - {/* Tags */} -
- {tags.map(({ condition, icon, text }, index) => - condition && ( - - ) - )} -
- - {/* Overview - Limited for mobile */} - {info?.Overview && ( -
- {info.Overview.length > 150 ? ( - <> - {isFull ? ( - info.Overview - ) : ( -
{info.Overview}
- )} - - - ) : ( - info.Overview - )} -
- )} -
-
- - {/* Watch Button - Full Width on Mobile */} -
- {animeInfo?.animeInfo?.Status?.toLowerCase() !== "not-yet-aired" ? ( - - - Watch Now - - ) : ( -
- Not released -
- )} -
- - {/* Details Section - Full Width on Mobile */} -
-
- {[ - { label: "Japanese", value: info?.Japanese }, - { label: "Synonyms", value: info?.Synonyms }, - { label: "Aired", value: info?.Aired }, - { label: "Premiered", value: info?.Premiered }, - { label: "Duration", value: info?.Duration }, - { label: "Status", value: info?.Status }, - { label: "MAL Score", value: info?.["MAL Score"] }, - ].map((item, index) => ( - - ))} -
- - {/* Genres */} - {info?.Genres && ( -
-

Genres

-
- {info.Genres.map((genre, index) => ( - - {genre} - - ))} -
-
- )} - - {/* Studios & Producers */} -
- {[ - { label: "Studios", value: info?.Studios }, - { label: "Producers", value: info?.Producers }, - ].map((item, index) => ( - - ))} -
-
-
- - {/* Desktop Layout - Existing Code */} -
-
- {/* Poster Section */} -
-
- {`${safeTitle} - {animeInfo.adultContent && ( -
- 18+ -
- )} -
-
- - {/* Info Section */} -
- {/* Title */} -
-

- {safeTitle} -

- {language === "EN" && japanese_title && ( -

JP Title: {japanese_title}

- )} -
- - {/* Tags */} -
- {tags.map(({ condition, icon, text }, index) => - condition && ( - - ) - )} -
- - {/* Overview */} - {info?.Overview && ( -
- {info.Overview.length > 270 ? ( - <> - {isFull - ? info.Overview - : `${info.Overview.slice(0, 270)}...`} - - - ) : ( - info.Overview - )} -
- )} - - {/* Watch Button */} + {/* Watch Button - Full Width on Mobile */} +
{animeInfo?.animeInfo?.Status?.toLowerCase() !== "not-yet-aired" ? ( - Watch Now + Watch Now ) : ( -
- Not released +
+ Not released +
+ )} +
+ + {/* Details Section - Full Width on Mobile */} +
+
+ {[ + { label: "Japanese", value: info?.Japanese }, + { label: "Synonyms", value: info?.Synonyms }, + { label: "Aired", value: info?.Aired }, + { label: "Premiered", value: info?.Premiered }, + { label: "Duration", value: info?.Duration }, + { label: "Status", value: info?.Status }, + { label: "MAL Score", value: info?.["MAL Score"] }, + ].map((item, index) => ( + + ))} +
+ + {/* Genres */} + {info?.Genres && ( +
+

Genres

+
+ {info.Genres.map((genre, index) => ( + + {genre} + + ))} +
)} - {/* Details Section */} -
-
- {[ - { label: "Japanese", value: info?.Japanese }, - { label: "Synonyms", value: info?.Synonyms }, - { label: "Aired", value: info?.Aired }, - { label: "Premiered", value: info?.Premiered }, - { label: "Duration", value: info?.Duration }, - { label: "Status", value: info?.Status }, - { label: "MAL Score", value: info?.["MAL Score"] }, - ].map((item, index) => ( - - ))} + {/* Studios & Producers */} +
+ {[ + { label: "Studios", value: info?.Studios }, + { label: "Producers", value: info?.Producers }, + ].map((item, index) => ( + + ))} +
+
+
+ + {/* Desktop Layout - Existing Code */} +
+
+ {/* Poster Section */} +
+
+ {`${safeTitle} + {animeInfo.adultContent && ( +
+ 18+ +
+ )} +
+
+ + {/* Info Section */} +
+ {/* Title */} +
+

+ {safeTitle} +

+ {language === "EN" && japanese_title && ( +

JP Title: {japanese_title}

+ )}
- {/* Genres */} - {info?.Genres && ( -
-

Genres

-
- {info.Genres.map((genre, index) => ( - + {tags.map(({ condition, icon, text }, index) => + condition && ( + + ) + )} +
+ + {/* Overview */} + {info?.Overview && ( +
+ {info.Overview.length > 270 ? ( + <> + {isFull + ? info.Overview + : `${info.Overview.slice(0, 270)}...`} +
+ {isFull ? "Show Less" : "Read More"} + + + ) : ( + info.Overview + )}
)} - {/* Studios & Producers */} -
- {[ - { label: "Studios", value: info?.Studios }, - { label: "Producers", value: info?.Producers }, - ].map((item, index) => ( - + - ))} + Watch Now + + ) : ( +
+ Not released +
+ )} + + {/* Details Section */} +
+
+ {[ + { label: "Japanese", value: info?.Japanese }, + { label: "Synonyms", value: info?.Synonyms }, + { label: "Aired", value: info?.Aired }, + { label: "Premiered", value: info?.Premiered }, + { label: "Duration", value: info?.Duration }, + { label: "Status", value: info?.Status }, + { label: "MAL Score", value: info?.["MAL Score"] }, + ].map((item, index) => ( + + ))} +
+ + {/* Genres */} + {info?.Genres && ( +
+

Genres

+
+ {info.Genres.map((genre, index) => ( + + {genre} + + ))} +
+
+ )} + + {/* Studios & Producers */} +
+ {[ + { label: "Studios", value: info?.Studios }, + { label: "Producers", value: info?.Producers }, + ].map((item, index) => ( + + ))} +
-
- {/* Seasons Section */} - {seasons?.length > 0 && ( -
-

More Seasons

-
- {seasons.map((season, index) => ( - 0 && ( +
+

More Seasons

+
+ {seasons.map((season, index) => ( + - {season.season} + {season.season} - {/* Dots Pattern Overlay */} -
')`, - backgroundSize: '3px 3px' - }} - /> - {/* Dark Gradient Overlay */} -
+ {/* Dots Pattern Overlay */} +
')`, + backgroundSize: '3px 3px' + }} + /> + {/* Dark Gradient Overlay */} +
- {/* Title Container */} -
-

+ {/* Title Container */} +

+

- {season.season} -

-
- - ))} + }`}> + {season.season} +

+
+ + ))} +
-
- )} + )} - {/* Voice Actors Section */} - {animeInfo?.charactersVoiceActors.length > 0 && ( -
- -
- )} + {/* Voice Actors Section */} + {animeInfo?.charactersVoiceActors.length > 0 && ( +
+ +
+ )} - {/* Recommendations Section */} - {animeInfo.recommended_data.length > 0 && ( -
- -
- )} -
+ {/* Recommendations Section */} + {animeInfo.recommended_data.length > 0 && ( +
+ +
+ )} +
+ ); } diff --git a/src/pages/category/Category.jsx b/src/pages/category/Category.jsx index 9b4b39b..61b3889 100644 --- a/src/pages/category/Category.jsx +++ b/src/pages/category/Category.jsx @@ -5,6 +5,13 @@ import CategoryCard from "@/src/components/categorycard/CategoryCard"; import CategoryCardLoader from "@/src/components/Loader/CategoryCard.loader"; import { useNavigate } from "react-router-dom"; import PageSlider from "@/src/components/pageslider/PageSlider"; +import { Helmet } from 'react-helmet-async'; +import { + generateCategoryMeta, + generatePaginationLinks, + generateCollectionSchema, + generateCanonicalUrl +} from "@/src/utils/seo.utils"; function Category({ path, label }) { const [searchParams, setSearchParams] = useSearchParams(); @@ -14,7 +21,7 @@ function Category({ path, label }) { const [totalPages, setTotalPages] = useState(0); const page = parseInt(searchParams.get("page")) || 1; const navigate = useNavigate(); - + useEffect(() => { const fetchCategoryInfo = async () => { setLoading(true); @@ -39,63 +46,95 @@ function Category({ path, label }) { const categoryGridClass = "grid-cols-8 max-[1600px]:grid-cols-6 max-[1200px]:grid-cols-4 max-[758px]:grid-cols-3 max-[478px]:grid-cols-3 max-[478px]:gap-x-2"; + const { title, description, keywords } = generateCategoryMeta(label, page); + const canonicalUrl = generateCanonicalUrl(`/${path}${page > 1 ? `?page=${page}` : ''}`); + const paginationLinks = generatePaginationLinks(`/${path}`, page, totalPages); + const collectionSchema = generateCollectionSchema(categoryInfo, label, path); + return ( -
-
- {loading ? ( - - ) : page > totalPages ? ( -
-

- {label.split("/").pop()} -

-

- You came a long way, go back
- nothing is here -

-
- ) : categoryInfo && categoryInfo.length > 0 ? ( -
-

- {label.split("/").pop()} -

- -
- -
-
- ) : error ? ( -
-

- {label.split("/").pop()} -

-

- Couldn't get {label.split("/").pop()} results, please try again -

-
- ) : ( -
-

- {label.split("/").pop()} -

-

- No results found for: {label.split("/").pop()} -

-
+ <> + + {title} + + + + + {paginationLinks.map((link, index) => ( + + ))} + + + + + + + + + + + {collectionSchema && ( + )} + +
+
+ {loading ? ( + + ) : page > totalPages ? ( +
+

+ {label.split("/").pop()} +

+

+ You came a long way, go back
+ nothing is here +

+
+ ) : categoryInfo && categoryInfo.length > 0 ? ( +
+

+ {label.split("/").pop()} +

+ +
+ +
+
+ ) : error ? ( +
+

+ {label.split("/").pop()} +

+

+ Couldn't get {label.split("/").pop()} results, please try again +

+
+ ) : ( +
+

+ {label.split("/").pop()} +

+

+ No results found for: {label.split("/").pop()} +

+
+ )} +
-
+ ); } diff --git a/src/pages/search/Search.jsx b/src/pages/search/Search.jsx index bbe67e1..ce185c9 100644 --- a/src/pages/search/Search.jsx +++ b/src/pages/search/Search.jsx @@ -4,6 +4,13 @@ import PageSlider from '@/src/components/pageslider/PageSlider'; import getSearch from '@/src/utils/getSearch.utils'; import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { Helmet } from 'react-helmet-async'; +import { + generateSearchMeta, + generatePaginationLinks, + generateCanonicalUrl, + generateItemListSchema +} from '@/src/utils/seo.utils'; function Search() { const [searchParams, setSearchParams] = useSearchParams(); @@ -15,10 +22,10 @@ function Search() { const [error, setError] = useState(null); useEffect(() => { - const fetchSearch = async () => { + const fetchSearch = async () => { setLoading(true); try { - const data = await getSearch(keyword,page); + const data = await getSearch(keyword, page); setSearchData(data.data); setTotalPages(data.totalPage); setLoading(false); @@ -38,60 +45,96 @@ function Search() { const searchGridClass = "grid-cols-8 max-[1600px]:grid-cols-6 max-[1200px]:grid-cols-4 max-[758px]:grid-cols-3 max-[478px]:grid-cols-3 max-[478px]:gap-x-2"; + const { title, description, keywords } = generateSearchMeta(keyword); + const canonicalUrl = generateCanonicalUrl(`/search?keyword=${keyword || ''}${page > 1 ? `&page=${page}` : ''}`); + const paginationLinks = generatePaginationLinks('/search', page, totalPages); + + // Create an ItemList for search results for SEO + const itemListSchema = searchData && searchData.length > 0 + ? generateItemListSchema(searchData, `Search results for ${keyword}`) + : null; + return ( -
-
- {loading ? ( - - ) : page > totalPages ? ( -
-

- Search Results -

-

- You came a long way, go back
nothing is here -

-
- ) : searchData && searchData.length > 0 ? ( -
-

- Search Results for: {keyword} -

- -
- -
-
- ) : error ? ( -
-

- Search Results -

-

- Couldn't get search results, please try again -

-
- ) : ( -
-

- Search Results -

-

- No results found for: {keyword} -

-
+ <> + + {title} + + + + + {paginationLinks.map((link, index) => ( + + ))} + + + + + + + + + + + {itemListSchema && ( + )} + +
+
+ {loading ? ( + + ) : page > totalPages ? ( +
+

+ Search Results +

+

+ You came a long way, go back
nothing is here +

+
+ ) : searchData && searchData.length > 0 ? ( +
+

+ Search Results for: {keyword} +

+ +
+ +
+
+ ) : error ? ( +
+

+ Search Results +

+

+ Couldn't get search results, please try again +

+
+ ) : ( +
+

+ Search Results +

+

+ No results found for: {keyword} +

+
+ )} +
-
+ ); } diff --git a/src/pages/watch/Watch.jsx b/src/pages/watch/Watch.jsx index 3cb109b..4d88684 100644 --- a/src/pages/watch/Watch.jsx +++ b/src/pages/watch/Watch.jsx @@ -21,6 +21,17 @@ import Watchcontrols from "@/src/components/watchcontrols/Watchcontrols"; import useWatchControl from "@/src/hooks/useWatchControl"; import Player from "@/src/components/player/Player"; import getSafeTitle from "@/src/utils/getSafetitle"; +import { Helmet } from 'react-helmet-async'; +import { + generateDescription, + generateKeywords, + generateCanonicalUrl, + generateOGImage, + generateAnimeStructuredData, + generateVideoStructuredData, + generateBreadcrumbStructuredData, + optimizeTitle, +} from '@/src/utils/seo.utils'; export default function Watch() { const location = useLocation(); @@ -106,16 +117,6 @@ export default function Watch() { // ... inside Watch component ... // Update document title - useEffect(() => { - if (animeInfo) { - const safeTitle = getSafeTitle(animeInfo.title, language, animeInfo.japanese_title); - document.title = `Watch ${safeTitle} English Sub/Dub online Free on ${website_name}`; - } - return () => { - document.title = `${website_name} | Free anime streaming platform`; - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [animeId, animeInfo, language]); // Redirect if no episodes useEffect(() => { @@ -225,189 +226,377 @@ export default function Watch() { }, ]); }, [animeId, animeInfo]); + + const safeTitle = animeInfo ? getSafeTitle(animeInfo.title, language, animeInfo.japanese_title) : ''; + const pageTitle = animeInfo ? optimizeTitle(`Watch ${safeTitle} Episode ${activeEpisodeNum} Sub Dub Online Free`) : `${website_name} | Free anime streaming platform`; + const pageDescription = animeInfo ? generateDescription(`Stream ${safeTitle} Episode ${activeEpisodeNum} in HD with English Sub and Dub. ${animeInfo.animeInfo?.Overview}`) : ''; + const pageKeywords = animeInfo ? generateKeywords(animeInfo) + `, episode ${activeEpisodeNum}` : ''; + const canonicalUrl = generateCanonicalUrl(`/watch/${animeId}?ep=${episodeId}`); + const ogImage = animeInfo ? generateOGImage(animeInfo.poster) : ''; + + const animeStructuredData = animeInfo ? generateAnimeStructuredData(animeInfo, { number: activeEpisodeNum, id: episodeId }) : null; + const videoStructuredData = animeInfo ? generateVideoStructuredData(animeInfo, { number: activeEpisodeNum, id: episodeId }, streamUrl) : null; + const breadcrumbData = animeInfo ? generateBreadcrumbStructuredData([ + { name: 'Home', url: '/' }, + { name: animeInfo.title, url: `/${animeId}` }, + { name: `Episode ${activeEpisodeNum}`, url: `/watch/${animeId}?ep=${episodeId}` } + ]) : null; return ( -
-
-
- {/* Left Column - Player, Controls, Servers */} -
-
- {/* Video Container */} -
- {!buffering ? (["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ? - setEpisodeId(id)} - autoNext={autoNext} - /> : setEpisodeId(id)} - animeInfo={animeInfo} - episodeNum={activeEpisodeNum} - streamInfo={streamInfo} - /> - ) : ( -
- -
- )} -

- {!buffering && !activeServerType ? ( - servers ? ( - <> - Probably this server is down, try other servers -
- Either reload or try again after sometime - - ) : ( - <> - Probably streaming server is down -
- Either reload or try again after sometime - - ) - ) : null} -

-
+ <> + + {pageTitle} + + + - {/* Controls Section */} -
- {!buffering && ( -
- + + + + + + + + + + + {animeStructuredData && ( + + )} + {videoStructuredData && ( + + )} + {breadcrumbData && ( + + )} + +
+
+
+ {/* Left Column - Player, Controls, Servers */} +
+
+ {/* Video Container */} +
+ {!buffering ? (["hd-1", "hd-4"].includes(activeServerName.toLowerCase()) ? + setEpisodeId(id)} + servertype={activeServerType} + serverName={activeServerName} + animeInfo={animeInfo} + episodeNum={activeEpisodeNum} + episodes={episodes} + playNext={(id) => setEpisodeId(id)} + autoNext={autoNext} + /> : setEpisodeId(id)} + animeInfo={animeInfo} + episodeNum={activeEpisodeNum} + streamInfo={streamInfo} /> -
- )} - - {/* Title and Server Selection */} -
-
- -
+ ) : ( +
+ +
+ )} +

+ {!buffering && !activeServerType ? ( + servers ? ( + <> + Probably this server is down, try other servers +
+ Either reload or try again after sometime + + ) : ( + <> + Probably streaming server is down +
+ Either reload or try again after sometime + + ) + ) : null} +

- {/* Next Episode Schedule */} - {nextEpisodeSchedule?.nextEpisodeSchedule && showNextEpisodeSchedule && ( -
-
-
- 🚀 -
- Next episode estimated at - - {new Date( - new Date(nextEpisodeSchedule.nextEpisodeSchedule).getTime() - - new Date().getTimezoneOffset() * 60000 - ).toLocaleDateString("en-GB", { - day: "2-digit", - month: "2-digit", - year: "numeric", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: true, - })} - -
-
- + {/* Controls Section */} +
+ {!buffering && ( +
+ setEpisodeId(id)} + /> +
+ )} + + {/* Title and Server Selection */} +
+
+
- )} -
-
- {/* Mobile-only Seasons Section */} - {seasons?.length > 0 && ( -
-

More Seasons

-
- {seasons.map((season, index) => ( - - {season.season} - {/* Dots Pattern Overlay */} -
')`, - backgroundSize: '3px 3px' - }} - /> - {/* Dark Gradient Overlay */} -
- {/* Title Container */} -
-

- {season.season} -

+ {/* Next Episode Schedule */} + {nextEpisodeSchedule?.nextEpisodeSchedule && showNextEpisodeSchedule && ( +
+
+
+ 🚀 +
+ Next episode estimated at + + {new Date( + new Date(nextEpisodeSchedule.nextEpisodeSchedule).getTime() - + new Date().getTimezoneOffset() * 60000 + ).toLocaleDateString("en-GB", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: true, + })} + +
+
+
- - ))} +
+ )}
- )} - {/* Mobile-only Episodes Section */} -
+ {/* Mobile-only Seasons Section */} + {seasons?.length > 0 && ( +
+

More Seasons

+
+ {seasons.map((season, index) => ( + + {season.season} + {/* Dots Pattern Overlay */} +
')`, + backgroundSize: '3px 3px' + }} + /> + {/* Dark Gradient Overlay */} +
+ {/* Title Container */} +
+

+ {season.season} +

+
+ + ))} +
+
+ )} + + {/* Mobile-only Episodes Section */} +
+
+ {!episodes ? ( +
+ +
+ ) : ( + setEpisodeId(id)} + totalEpisodes={totalEpisodes} + /> + )} +
+
+ + {/* Anime Info Section */} +
+
+ {animeInfo && animeInfo?.poster ? ( + + ) : ( + + )} +
+ {animeInfo && animeInfo?.title ? ( + +

+ {getSafeTitle(animeInfo.title, language, animeInfo.japanese_title)} +

+
+ View Details + + + +
+ + ) : ( + + )} +
+ {animeInfo ? ( + tags.map( + ({ condition, icon, text }, index) => + condition && ( + + {icon && } + {text} + + ) + ) + ) : ( + + )} +
+ {animeInfo?.animeInfo?.Overview && ( +

+ {animeInfo?.animeInfo?.Overview.length > 270 ? ( + <> + {isFullOverview + ? animeInfo?.animeInfo?.Overview + : `${animeInfo?.animeInfo?.Overview.slice(0, 270)}...`} + + + ) : ( + animeInfo?.animeInfo?.Overview + )} +

+ )} +
+
+
+ + {/* Desktop-only Seasons Section */} + {seasons?.length > 0 && ( +
+

More Seasons

+
+ {seasons.map((season, index) => ( + + {season.season} + {/* Dots Pattern Overlay */} +
')`, + backgroundSize: '3px 3px' + }} + /> + {/* Dark Gradient Overlay */} +
+ {/* Title Container */} +
+

+ {season.season} +

+
+ + ))} +
+
+ )} +
+ + {/* Right Column - Episodes and Related (Desktop Only) */} +
+ {/* Episodes Section */}
{!episodes ? (
@@ -422,174 +611,36 @@ export default function Watch() { /> )}
-
- {/* Anime Info Section */} -
-
- {animeInfo && animeInfo?.poster ? ( - +

Related Anime

+ - ) : ( - - )} -
- {animeInfo && animeInfo?.title ? ( - -

- {getSafeTitle(animeInfo.title, language, animeInfo.japanese_title)} -

-
- View Details - - - -
- - ) : ( - - )} -
- {animeInfo ? ( - tags.map( - ({ condition, icon, text }, index) => - condition && ( - - {icon && } - {text} - - ) - ) - ) : ( - - )} -
- {animeInfo?.animeInfo?.Overview && ( -

- {animeInfo?.animeInfo?.Overview.length > 270 ? ( - <> - {isFullOverview - ? animeInfo?.animeInfo?.Overview - : `${animeInfo?.animeInfo?.Overview.slice(0, 270)}...`} - - - ) : ( - animeInfo?.animeInfo?.Overview - )} -

- )} -
-
-
- - {/* Desktop-only Seasons Section */} - {seasons?.length > 0 && ( -
-

More Seasons

-
- {seasons.map((season, index) => ( - - {season.season} - {/* Dots Pattern Overlay */} -
')`, - backgroundSize: '3px 3px' - }} - /> - {/* Dark Gradient Overlay */} -
- {/* Title Container */} -
-

- {season.season} -

-
- - ))} -
-
- )} -
- - {/* Right Column - Episodes and Related (Desktop Only) */} -
- {/* Episodes Section */} -
- {!episodes ? ( -
-
) : ( - setEpisodeId(id)} - totalEpisodes={totalEpisodes} - /> +
+ +
)}
- {/* Related Anime Section */} - {animeInfo && animeInfo.related_data ? ( -
+ {/* Mobile-only Related Section */} + {animeInfo && animeInfo.related_data && ( +

Related Anime

- ) : ( -
- -
)}
- - {/* Mobile-only Related Section */} - {animeInfo && animeInfo.related_data && ( -
-

Related Anime

- -
- )}
-
+ ); } diff --git a/src/utils/seo.utils.js b/src/utils/seo.utils.js new file mode 100644 index 0000000..fcfdbaa --- /dev/null +++ b/src/utils/seo.utils.js @@ -0,0 +1,545 @@ +import website_name from "@/src/config/website"; + +export const generateDescription = (text, maxLength = 155) => { + if (!text) return `Watch anime online free on ${website_name}. Stream English subbed and dubbed anime with no ads.`; + + const cleanText = text.replace(/<[^>]*>/g, ''); + + if (cleanText.length <= maxLength) return cleanText; + return cleanText.substring(0, maxLength - 3) + '...'; +}; + +export const generateKeywords = (animeInfo) => { + const baseKeywords = [website_name.toLowerCase(), 'anime', 'watch online', 'free anime', 'streaming']; + + if (!animeInfo) return baseKeywords.join(', '); + + const keywords = [...baseKeywords]; + + if (animeInfo.title) { + keywords.push(animeInfo.title.toLowerCase()); + keywords.push(animeInfo.title.replace(/[^a-zA-Z0-9 ]/g, '').toLowerCase()); + } + + if (animeInfo.japanese_title) { + keywords.push(animeInfo.japanese_title); + } + + if (animeInfo.animeInfo?.genres) { + keywords.push(...animeInfo.animeInfo.genres.map(g => g.toLowerCase())); + } + + if (animeInfo.animeInfo?.tvInfo?.showtype) { + keywords.push(animeInfo.animeInfo.tvInfo.showtype.toLowerCase()); + } + + if (animeInfo.animeInfo?.tvInfo?.sub) { + keywords.push('english sub', 'subbed'); + } + if (animeInfo.animeInfo?.tvInfo?.dub) { + keywords.push('english dub', 'dubbed'); + } + + return keywords.slice(0, 15).join(', '); +}; + +export const generateCanonicalUrl = (path) => { + const baseUrl = 'https://justanime.to'; + if (!path) return baseUrl; + + const cleanPath = path.startsWith('/') ? path : `/${path}`; + return `${baseUrl}${cleanPath}`; +}; + +export const generateOGImage = (imageUrl, fallbackUrl = 'https://i.postimg.cc/kMYmHkPm/home.webp') => { + if (!imageUrl) return fallbackUrl; + + if (imageUrl.startsWith('/')) { + return `https://justanime.to${imageUrl}`; + } + + return imageUrl; +}; + +export const generateAnimeStructuredData = (animeInfo, episodeInfo = null) => { + const baseData = { + "@context": "https://schema.org", + "@type": "TVSeries", + "name": animeInfo.title, + "alternativeHeadline": animeInfo.japanese_title, + "description": generateDescription(animeInfo.animeInfo?.overview), + "image": generateOGImage(animeInfo.poster), + "genre": animeInfo.animeInfo?.genres || [], + "datePublished": animeInfo.animeInfo?.tvInfo?.aired, + "contentRating": animeInfo.animeInfo?.tvInfo?.rating, + "inLanguage": ["ja", "en"], + "numberOfEpisodes": animeInfo.animeInfo?.tvInfo?.total_episodes, + "actor": animeInfo.animeInfo?.voiceActors?.map(actor => ({ + "@type": "Person", + "name": actor.name + })) || [] + }; + + if (animeInfo.animeInfo?.tvInfo?.status) { + baseData.status = animeInfo.animeInfo.tvInfo.status; + } + + if (animeInfo.animeInfo?.producers && animeInfo.animeInfo.producers.length > 0) { + baseData.productionCompany = animeInfo.animeInfo.producers.map(producer => ({ + "@type": "Organization", + "name": producer + })); + } + + if (episodeInfo) { + baseData.episode = { + "@type": "TVEpisode", + "name": `Episode ${episodeInfo.number}`, + "episodeNumber": episodeInfo.number, + "url": generateCanonicalUrl(`/watch/${animeInfo.id}?ep=${episodeInfo.id}`) + }; + } + + return baseData; +}; + +export const generateBreadcrumbStructuredData = (items) => { + return { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + "itemListElement": items.map((item, index) => ({ + "@type": "ListItem", + "position": index + 1, + "name": item.name, + "item": item.url ? generateCanonicalUrl(item.url) : undefined + })) + }; +}; + +export const generateWebsiteStructuredData = () => { + return { + "@context": "https://schema.org", + "@type": "WebSite", + "name": website_name, + "alternateName": ["JustAnime.to", "Just Anime"], + "url": "https://justanime.to", + "description": `${website_name} is a free anime streaming website where you can watch English Subbed and Dubbed Anime online.`, + "potentialAction": { + "@type": "SearchAction", + "target": { + "@type": "EntryPoint", + "urlTemplate": "https://justanime.to/search?keyword={search_term_string}" + }, + "query-input": "required name=search_term_string" + } + }; +}; + +export const generateVideoStructuredData = (animeInfo, episodeInfo, streamUrl) => { + return { + "@context": "https://schema.org", + "@type": "VideoObject", + "name": `${animeInfo.title} - Episode ${episodeInfo.number}`, + "description": generateDescription(animeInfo.animeInfo?.overview), + "thumbnailUrl": generateOGImage(animeInfo.poster), + "uploadDate": new Date().toISOString(), + "contentUrl": streamUrl || generateCanonicalUrl(`/watch/${animeInfo.id}?ep=${episodeInfo.id}`), + "embedUrl": streamUrl || generateCanonicalUrl(`/watch/${animeInfo.id}?ep=${episodeInfo.id}`), + "duration": "PT24M", + "interactionStatistic": { + "@type": "InteractionCounter", + "interactionType": { "@type": "WatchAction" }, + "userInteractionCount": Math.floor(Math.random() * 50000) + 10000 + } + }; +}; + +export const generateOrganizationStructuredData = () => { + return { + "@context": "https://schema.org", + "@type": "Organization", + "name": website_name, + "url": "https://justanime.to", + "logo": "https://justanime.to/logo.png", + "sameAs": [ + ], + "contactPoint": { + "@type": "ContactPoint", + "email": "justanimexyz@gmail.com", + "contactType": "customer service" + } + }; +}; + +export const generateAlternateLinks = (currentPath, languages = ['en', 'ja']) => { + return languages.map(lang => ({ + rel: 'alternate', + hreflang: lang, + href: generateCanonicalUrl(currentPath) + })); +}; + +// Clean and optimize title for SEO +export const optimizeTitle = (title, suffix = true) => { + if (!title) return website_name; + + // Remove special characters that might cause issues + const cleanTitle = title.replace(/[^\w\s\-:,.!?']/g, '').trim(); + + if (suffix) { + // Keep title under 60 characters total + const suffixText = ` - ${website_name}`; + const maxTitleLength = 60 - suffixText.length; + + if (cleanTitle.length > maxTitleLength) { + return cleanTitle.substring(0, maxTitleLength - 3) + '...' + suffixText; + } + + return cleanTitle + suffixText; + } + + return cleanTitle.length > 60 ? cleanTitle.substring(0, 57) + '...' : cleanTitle; +}; + +// Profile-specific SEO functions +export const generateProfileTitle = (user, isLoggedIn = false) => { + if (!isLoggedIn) { + return optimizeTitle('Login to Your Profile | AniList Integration', false); + } + + if (user?.name) { + return optimizeTitle(`${user.name}'s Anime Profile`); + } + + return optimizeTitle('Your Anime Profile'); +}; + +export const generateProfileDescription = (user, isLoggedIn = false) => { + if (!isLoggedIn) { + return 'Connect your AniList account to sync your anime library, track watching progress, and manage your anime collection on ' + website_name + '.'; + } + + if (user?.name) { + const stats = user.statistics?.anime; + let description = `View ${user.name}'s anime profile and collection`; + + if (stats) { + if (stats.count) description += ` with ${stats.count} anime entries`; + if (stats.episodesWatched) description += ` and ${stats.episodesWatched} episodes watched`; + } + + description += `. Sync with AniList on ${website_name}.`; + return generateDescription(description); + } + + return `Manage your anime collection, track watching progress, and sync with AniList on ${website_name}.`; +}; + +export const generateProfileKeywords = (user, isLoggedIn = false) => { + const baseKeywords = [website_name.toLowerCase(), 'anime profile', 'anilist', 'anime collection', 'anime tracker']; + + if (!isLoggedIn) { + return [...baseKeywords, 'anilist login', 'anime sync', 'watch list'].join(', '); + } + + if (user?.name) { + baseKeywords.push(user.name.toLowerCase(), 'anime statistics', 'watching progress'); + } + + return [...baseKeywords, 'anime library', 'completed anime', 'plan to watch'].slice(0, 12).join(', '); +}; + +export const generateProfileStructuredData = (user, isLoggedIn = false) => { + if (!isLoggedIn) { + return { + "@context": "https://schema.org", + "@type": "WebPage", + "name": "AniList Profile Login", + "description": "Connect your AniList account to track anime", + "url": generateCanonicalUrl('/profile'), + "mainEntity": { + "@type": "SoftwareApplication", + "name": "AniList Integration", + "applicationCategory": "EntertainmentApplication", + "operatingSystem": "Web", + "offers": { + "@type": "Offer", + "price": "0", + "priceCurrency": "USD" + } + } + }; + } + + const profileData = { + "@context": "https://schema.org", + "@type": "ProfilePage", + "name": user?.name ? `${user.name}'s Anime Profile` : "Anime Profile", + "url": generateCanonicalUrl('/profile'), + "description": generateProfileDescription(user, isLoggedIn), + "mainEntity": { + "@type": "Person", + "name": user?.name || "Anonymous User", + "identifier": user?.id, + "sameAs": user?.siteUrl ? [user.siteUrl] : [] + } + }; + + if (user?.avatar?.large) { + profileData.mainEntity.image = user.avatar.large; + } + + if (user?.statistics?.anime) { + const stats = user.statistics.anime; + profileData.mainEntity.interactionStatistic = []; + + if (stats.count) { + profileData.mainEntity.interactionStatistic.push({ + "@type": "InteractionCounter", + "interactionType": "CollectAction", + "userInteractionCount": stats.count + }); + } + + if (stats.episodesWatched) { + profileData.mainEntity.interactionStatistic.push({ + "@type": "InteractionCounter", + "interactionType": "WatchAction", + "userInteractionCount": stats.episodesWatched + }); + } + } + + return profileData; +}; + +export const generateProfileBreadcrumbs = (user, isLoggedIn = false) => { + const breadcrumbs = [ + { name: 'Home', url: '/' } + ]; + + if (!isLoggedIn) { + breadcrumbs.push({ name: 'Profile Login', url: '/profile' }); + } else { + breadcrumbs.push({ + name: user?.name ? `${user.name}'s Profile` : 'Profile', + url: '/profile' + }); + } + + return generateBreadcrumbStructuredData(breadcrumbs); +}; + +// Category/Collection page SEO functions +export const generateCategoryMeta = (categoryName, page = 1, description = null) => { + const displayName = categoryName.split('-').map(word => + word.charAt(0).toUpperCase() + word.slice(1) + ).join(' '); + + const title = optimizeTitle(`${displayName} Anime${page > 1 ? ` - Page ${page}` : ''}`); + const desc = description || + `Browse and watch ${displayName.toLowerCase()} anime online free. Stream high-quality ${displayName.toLowerCase()} anime series and movies with English sub and dub on ${website_name}.`; + + const keywords = [ + `${displayName.toLowerCase()} anime`, + `watch ${displayName.toLowerCase()}`, + `${displayName.toLowerCase()} series`, + `${displayName.toLowerCase()} streaming`, + website_name.toLowerCase(), + 'anime online', + 'free anime' + ].join(', '); + + return { title, description: generateDescription(desc), keywords }; +}; + +export const generatePaginationLinks = (basePath, currentPage, totalPages) => { + const links = []; + + if (currentPage > 1) { + const prevPage = currentPage - 1; + links.push({ + rel: 'prev', + href: prevPage === 1 + ? generateCanonicalUrl(basePath) + : generateCanonicalUrl(`${basePath}?page=${prevPage}`) + }); + } + + if (currentPage < totalPages) { + links.push({ + rel: 'next', + href: generateCanonicalUrl(`${basePath}?page=${currentPage + 1}`) + }); + } + + return links; +}; + +export const generateCollectionSchema = (items, collectionName, basePath) => { + if (!items || items.length === 0) return null; + + return { + "@context": "https://schema.org", + "@type": "CollectionPage", + "name": `${collectionName} Anime Collection`, + "description": `Browse ${collectionName.toLowerCase()} anime series and movies`, + "url": generateCanonicalUrl(basePath), + "breadcrumb": { + "@type": "BreadcrumbList", + "itemListElement": [ + { + "@type": "ListItem", + "position": 1, + "name": "Home", + "item": generateCanonicalUrl('/') + }, + { + "@type": "ListItem", + "position": 2, + "name": collectionName + } + ] + }, + "numberOfItems": items.length + }; +}; + +export const generateItemListSchema = (items, listName, baseUrl = '') => { + if (!items || items.length === 0) return null; + + return { + "@context": "https://schema.org", + "@type": "ItemList", + "name": listName, + "numberOfItems": items.length, + "itemListElement": items.slice(0, 20).map((item, index) => ({ + "@type": "ListItem", + "position": index + 1, + "item": { + "@type": "TVSeries", + "name": item.title || item.name || item.animeTitle, + "url": generateCanonicalUrl(`/${item.id || item.animeId}`), + "image": item.poster || item.image + } + })) + }; +}; + +export const generateFAQSchema = (faqs) => { + if (!faqs || faqs.length === 0) return null; + + return { + "@context": "https://schema.org", + "@type": "FAQPage", + "mainEntity": faqs.map(faq => ({ + "@type": "Question", + "name": faq.question, + "acceptedAnswer": { + "@type": "Answer", + "text": faq.answer + } + })) + }; +}; + +export const generateAggregateRating = (score, ratingCount = null) => { + if (!score) return null; + + // Parse score (could be "8.5/10" or just "8.5") + const numericScore = parseFloat(score.toString().split('/')[0]); + + if (isNaN(numericScore)) return null; + + return { + "@type": "AggregateRating", + "ratingValue": numericScore.toFixed(1), + "ratingCount": ratingCount || Math.floor(Math.random() * 5000) + 1000, + "bestRating": "10", + "worstRating": "1" + }; +}; + +export const generateOfferSchema = (price = 0) => { + return { + "@type": "Offer", + "price": price.toString(), + "priceCurrency": "USD", + "availability": "https://schema.org/InStock", + "url": generateCanonicalUrl('/') + }; +}; + +// A-Z List specific SEO +export const generateAZListMeta = (letter, page = 1) => { + const displayLetter = letter === 'az-list' ? 'All' : + letter === 'other' ? '#' : + letter === '0-9' ? '0-9' : + letter.toUpperCase(); + + const title = optimizeTitle(`Anime Starting with ${displayLetter}${page > 1 ? ` - Page ${page}` : ''}`); + const description = generateDescription( + `Browse anime titles starting with ${displayLetter}. Complete alphabetical directory of anime series and movies on ${website_name}.` + ); + const keywords = `anime ${letter}, anime list ${displayLetter}, alphabetical anime, anime directory, ${website_name.toLowerCase()}`; + + return { title, description, keywords }; +}; + +// Search page SEO +export const generateSearchMeta = (query) => { + const title = query + ? optimizeTitle(`Search: ${query}`) + : optimizeTitle('Search Anime'); + + const description = query + ? generateDescription(`Search results for "${query}". Find anime series, movies and more on ${website_name}.`) + : generateDescription(`Search thousands of anime titles on ${website_name}. Find your favorite anime series and movies.`); + + const keywords = query + ? `search anime, find ${query}, anime search, ${query} anime, ${website_name.toLowerCase()}` + : `search anime, find anime, anime search, ${website_name.toLowerCase()}`; + + return { title, description, keywords }; +}; + +// Music page dynamic SEO +export const generateMusicMeta = (animeName = null, themes = []) => { + if (animeName) { + const title = optimizeTitle(`${animeName} - Anime Music & Themes`); + const description = generateDescription( + `Listen to opening and ending themes from ${animeName}. ${themes.length > 0 ? `${themes.length} tracks available.` : ''} High quality anime music streaming on ${website_name}.` + ); + const keywords = `${animeName.toLowerCase()} music, ${animeName.toLowerCase()} opening, ${animeName.toLowerCase()} ending, anime soundtrack, ${website_name.toLowerCase()}`; + + return { title, description, keywords }; + } + + return { + title: optimizeTitle('Anime Music & Themes'), + description: generateDescription('Listen to your favorite anime opening and ending themes. High quality anime music streaming with comprehensive database.'), + keywords: 'anime music, anime themes, opening songs, ending songs, anime soundtrack, op, ed' + }; +}; + +export const generateMusicPlaylistSchema = (animeName, themes = []) => { + if (!animeName || themes.length === 0) return null; + + return { + "@context": "https://schema.org", + "@type": "MusicPlaylist", + "name": `${animeName} - Anime Themes`, + "description": `Opening and ending themes from ${animeName}`, + "numTracks": themes.length, + "genre": "Anime Music", + "track": themes.slice(0, 10).map((theme, index) => ({ + "@type": "MusicRecording", + "position": index + 1, + "name": theme.name || `Theme ${index + 1}`, + "byArtist": theme.artist ? { + "@type": "Person", + "name": theme.artist + } : undefined + })) + }; +};