misc
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user