This commit is contained in:
2025-12-02 10:05:43 -05:00
parent 6660b9ffd0
commit c3f0197115
11 changed files with 666 additions and 125 deletions

View File

@@ -4,6 +4,8 @@ interface Props {
description?: string;
image?: string;
isWhitelabel?: boolean;
whitelabel?: any;
subsite?: any;
}
import { metaData } from "../config";
@@ -12,24 +14,87 @@ import { JoyUIRootIsland } from "./Components"
import { useHtmlThemeAttr } from "../hooks/useHtmlThemeAttr";
import { usePrimeReactThemeSwitcher } from "../hooks/usePrimeReactThemeSwitcher";
const { title, description, image, isWhitelabel } = Astro.props;
const { title, description, image, isWhitelabel, whitelabel, subsite } = Astro.props;
const { url } = Astro;
const trimmedTitle = title?.trim();
const seoTitle = trimmedTitle || metaData.title;
const shareTitle = isWhitelabel ? (trimmedTitle || (metaData.shareTitle ?? metaData.title)) : (trimmedTitle ? `${trimmedTitle} | ${metaData.title}` : (metaData.shareTitle ?? metaData.title));
// If a whitelabel/subsite override exists, prefer its shareTitle/shareDescription/ogImage/favicon
const subsiteMeta = whitelabel ?? {};
const shareTitle = isWhitelabel
? (trimmedTitle || (subsiteMeta.shareTitle ?? metaData.shareTitle ?? metaData.title))
: (trimmedTitle ? `${trimmedTitle} | ${metaData.title}` : (metaData.shareTitle ?? metaData.title));
const seoTitleTemplate = isWhitelabel ? "%s" : (trimmedTitle ? `%s | ${metaData.title}` : "%s");
const shareDescription = isWhitelabel ? trimmedTitle : (description ?? metaData.shareDescription ?? metaData.description);
const canonicalUrl = url?.href ?? metaData.baseUrl;
const shareImage = new URL(image ?? metaData.ogImage, metaData.baseUrl).toString();
const shareImageAlt = metaData.shareImageAlt ?? metaData.shareTitle ?? metaData.title;
const shareDescription = isWhitelabel
? (trimmedTitle || (subsiteMeta.shareDescription ?? metaData.shareDescription ?? metaData.description))
: (description ?? metaData.shareDescription ?? metaData.description);
// Compute canonical URL with these priorities:
// 1. If the whitelabel/subsite provides a baseUrl, use that as the host for the canonical URL
// 2. If a subsite was detected and the request host matches the subsite host, canonical uses that host
// 3. If the request is for a path-based subsite (host is main site), prefer metaData.baseUrl so canonical remains on main site
// 4. Fallback to the request URL or metaData.baseUrl
const currentHost = (Astro.request?.headers?.get('host') || '').split(':')[0];
let canonicalBase: string | null = null;
if (subsiteMeta?.baseUrl) {
canonicalBase = subsiteMeta.baseUrl;
} else if (subsite?.host) {
// normalize hosts for comparison
const requestedHost = (currentHost || '').toLowerCase();
const subsiteHost = String(subsite.host || '').toLowerCase();
if (requestedHost && requestedHost === subsiteHost) {
canonicalBase = `https://${subsite.host}`;
} else {
// keep canonical on the main configured base (path-based subsites should remain under metaData.baseUrl)
canonicalBase = metaData.baseUrl;
}
}
// Decide whether canonical should be the site-root (e.g. https://req.boatson.boats/)
// or include a path. Rules:
// - If canonicalBase comes from a whitelabel/baseUrl or request host matches subsite host -> prefer site-root.
// - Otherwise (path-based subsites), include the pathname/search so canonical remains under the main site path.
const isHostMatchedSubsite = Boolean(subsite?.host && currentHost && currentHost.toLowerCase() === String(subsite.host).toLowerCase());
const isSubsiteRootCanonical = Boolean(subsiteMeta?.baseUrl || isHostMatchedSubsite);
let canonicalUrl: string;
if (canonicalBase) {
if (isSubsiteRootCanonical) {
// ensure canonicalBase ends with a single '/'
canonicalUrl = canonicalBase.endsWith('/') ? canonicalBase : `${canonicalBase}/`;
} else {
canonicalUrl = new URL((url?.pathname ?? '') + (url?.search ?? ''), canonicalBase).toString();
}
} else {
canonicalUrl = url?.href ?? metaData.baseUrl;
}
// Prefer the whitelabel/subsite ogImage when this page is for a whitelabel site.
// Otherwise fall back to an explicit image prop or the global ogImage.
const resolvedOgImage = (isWhitelabel && subsiteMeta.ogImage) ? subsiteMeta.ogImage : (image ?? metaData.ogImage);
// Keep relative/site-root paths as-is (e.g. '/images/req.png') and don't force
// an absolute URL using metaData.baseUrl. Only keep absolute (http/https)
// URLs if the value is already absolute.
function keepRelativeOrAbsolute(val) {
if (!val) return val;
if (/^https?:\/\//i.test(val)) return val; // already absolute
// Normalize to site-root-relative so local testing always resolves under '/'
return val.startsWith('/') ? val : `/${val}`;
}
const shareImage = keepRelativeOrAbsolute(resolvedOgImage);
const shareImageAlt = subsiteMeta.shareImageAlt ?? metaData.shareImageAlt ?? metaData.shareTitle ?? metaData.title;
// Build icon links: prefer subsite icons when present. If a subsite provides a single
// `favicon` but no `icons` array, do NOT append the global icons to avoid duplicates.
const primaryIconHref = keepRelativeOrAbsolute(subsiteMeta.favicon ?? metaData.favicon);
// Ensure extraIcons are used only when subsite doesn't provide a single favicon (prevents duplicates)
const extraIcons = subsiteMeta.icons ? subsiteMeta.icons : (subsiteMeta.favicon ? [] : (metaData.icons ?? []));
---
<SEO
title={seoTitle}
titleTemplate={seoTitleTemplate}
titleDefault={metaData.title}
canonical={canonicalUrl}
description={shareDescription}
charset="UTF-8"
openGraph={{
@@ -41,14 +106,16 @@ const shareImageAlt = metaData.shareImageAlt ?? metaData.shareTitle ?? metaData.
},
optional: {
description: shareDescription,
siteName: metaData.name,
siteName: subsiteMeta.siteName ?? metaData.name,
locale: "en_US",
},
}}
extend={{
extend={{
link: [
{ rel: "icon", href: "https://codey.lol/images/favicon.png" },
{ rel: "canonical", href: canonicalUrl },
// choose subsite favicon if provided, else global config. Allow absolute or relative paths
{ rel: 'icon', href: primaryIconHref },
// additional icon links from config if present
...extraIcons,
],
meta: [
{ property: "og:image:alt", content: shareImageAlt },

View File

@@ -17,15 +17,17 @@ export default function ReqForm() {
const [suggestions, setSuggestions] = useState([]);
useEffect(() => {
if (title !== selectedTitle && selectedOverview) {
setSelectedOverview("");
setSelectedTitle("");
if (title !== selectedTitle) {
if (selectedOverview) setSelectedOverview("");
if (selectedItem) setSelectedItem(null);
if (type) setType("");
if (selectedTitle) setSelectedTitle("");
}
}, [title, selectedTitle, selectedOverview]);
}, [title, selectedTitle, selectedOverview, selectedItem, type]);
const searchTitles = async (event) => {
const query = event.query;
if (query.length < 3) {
if (query.length < 2) {
setSuggestions([]);
return;
}
@@ -103,6 +105,15 @@ export default function ReqForm() {
}, 0);
};
const formatMediaType = (mediaTypeValue) => {
if (!mediaTypeValue) return "";
if (mediaTypeValue === "tv") return "TV Series";
if (mediaTypeValue === "movie") return "Movie";
return mediaTypeValue.charAt(0).toUpperCase() + mediaTypeValue.slice(1);
};
const selectedTypeLabel = formatMediaType(selectedItem?.mediaType || type);
return (
<div className="flex items-center justify-center min-h-[60vh] p-4">
<div className="w-full max-w-lg p-8 bg-white dark:bg-[#1E1E1E] rounded-3xl shadow-2xl border border-gray-200 dark:border-gray-700">
@@ -124,6 +135,7 @@ export default function ReqForm() {
value={title}
suggestions={suggestions}
completeMethod={searchTitles}
minLength={2}
onChange={(e) => setTitle(typeof e.value === 'string' ? e.value : e.value.label)}
onSelect={(e) => {
setType(e.value.mediaType === 'tv' ? 'tv' : 'movie');
@@ -148,6 +160,11 @@ export default function ReqForm() {
</div>
)}
/>
{selectedItem && selectedTypeLabel && (
<div className="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
Selected type: {selectedTypeLabel}
</div>
)}
</div>
{selectedOverview && (