some fixes
This commit is contained in:
parent
5c3922f230
commit
92366a6781
@ -384,7 +384,7 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.plugin-sidebar {
|
.plugin-sidebar {
|
||||||
order: -1;
|
order: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -126,7 +126,7 @@ const App = () => (
|
|||||||
{/* All routes share MainLayout (navbar + footer) */}
|
{/* All routes share MainLayout (navbar + footer) */}
|
||||||
<Route path="/" element={<MainLayout />}>
|
<Route path="/" element={<MainLayout />}>
|
||||||
<Route index element={<Home />} />
|
<Route index element={<Home />} />
|
||||||
<Route path="plugin/:id" element={<PluginDetail />} />
|
<Route path="plugin/:id-:slug" element={<PluginDetail />} />
|
||||||
<Route path="profile/:username" element={<AuthorProfilePage />} />
|
<Route path="profile/:username" element={<AuthorProfilePage />} />
|
||||||
<Route path="search" element={<SearchResultsPage />} />
|
<Route path="search" element={<SearchResultsPage />} />
|
||||||
<Route path="terms" element={<TermsOfServicePage />} />
|
<Route path="terms" element={<TermsOfServicePage />} />
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useRef, useMemo } from 'react';
|
import { useState, useRef, useMemo } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { getPluginUrl } from '../utils/url';
|
||||||
|
|
||||||
const getAccentColor = (name: string) => {
|
const getAccentColor = (name: string) => {
|
||||||
const colors = [
|
const colors = [
|
||||||
@ -81,7 +82,7 @@ const PluginCard = ({ plugin }: { plugin: any }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/plugin/${plugin.id}`} className="plugin-card-link" onMouseEnter={startHover} onMouseLeave={stopHover}>
|
<Link to={getPluginUrl(plugin)} className="plugin-card-link" onMouseEnter={startHover} onMouseLeave={stopHover}>
|
||||||
<div className="plugin-card-v2">
|
<div className="plugin-card-v2">
|
||||||
<div className="pcv2-preview">
|
<div className="pcv2-preview">
|
||||||
{slideshowImages.length > 0 ? (
|
{slideshowImages.length > 0 ? (
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useParams, Link } from 'react-router-dom';
|
|||||||
import api from '../api';
|
import api from '../api';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import SEO from '../components/SEO';
|
import SEO from '../components/SEO';
|
||||||
|
import { getPluginUrl } from '../utils/url';
|
||||||
|
|
||||||
const AuthorProfilePage = () => {
|
const AuthorProfilePage = () => {
|
||||||
const { username } = useParams();
|
const { username } = useParams();
|
||||||
@ -138,7 +139,7 @@ const AuthorProfilePage = () => {
|
|||||||
const imageUrl = plugin.preview_path;
|
const imageUrl = plugin.preview_path;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={`/plugin/${plugin.id}`} key={plugin.id} className="plugin-card-link">
|
<Link to={getPluginUrl(plugin)} key={plugin.id} className="plugin-card-link">
|
||||||
<div className="plugin-card-v2">
|
<div className="plugin-card-v2">
|
||||||
<div className="pcv2-preview">
|
<div className="pcv2-preview">
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import api from '../api';
|
|||||||
import SEO from '../components/SEO';
|
import SEO from '../components/SEO';
|
||||||
import PluginCard from '../components/PluginCard';
|
import PluginCard from '../components/PluginCard';
|
||||||
|
|
||||||
|
import { getPluginUrl } from '../utils/url';
|
||||||
|
|
||||||
// Global cache to persist data across component unmounts (navigation)
|
// Global cache to persist data across component unmounts (navigation)
|
||||||
let homeCache: {
|
let homeCache: {
|
||||||
popular: any[];
|
popular: any[];
|
||||||
@ -120,7 +122,7 @@ const Home = () => {
|
|||||||
{activeFeatured && (
|
{activeFeatured && (
|
||||||
<section className="featured-showcase-container">
|
<section className="featured-showcase-container">
|
||||||
<div className="featured-showcase-main">
|
<div className="featured-showcase-main">
|
||||||
<Link to={`/plugin/${activeFeatured.id}`} className="featured-showcase-link">
|
<Link to={getPluginUrl(activeFeatured)} className="featured-showcase-link">
|
||||||
{/* Main visual display */}
|
{/* Main visual display */}
|
||||||
<div className="featured-main-visual">
|
<div className="featured-main-visual">
|
||||||
{activeImage ? (
|
{activeImage ? (
|
||||||
|
|||||||
@ -135,7 +135,8 @@ const formatDate = (dateStr: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PluginDetail = () => {
|
const PluginDetail = () => {
|
||||||
const { id } = useParams();
|
const { id: rawId, slug } = useParams();
|
||||||
|
const id = rawId ? rawId.split('-')[0] : rawId;
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -233,6 +234,15 @@ const PluginDetail = () => {
|
|||||||
} else if (res.data.screenshots?.length > 0) {
|
} else if (res.data.screenshots?.length > 0) {
|
||||||
setMainScreenshot(res.data.screenshots[0].path);
|
setMainScreenshot(res.data.screenshots[0].path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Canonical URL check & replace
|
||||||
|
if (res.data.name) {
|
||||||
|
const expectedSlug = res.data.name.toLowerCase().trim().replace(/[\s_\.\-]+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
||||||
|
const targetPath = expectedSlug ? `/plugin/${res.data.id}-${expectedSlug}` : `/plugin/${res.data.id}`;
|
||||||
|
if (location.pathname !== targetPath || slug !== expectedSlug) {
|
||||||
|
window.history.replaceState({}, '', targetPath + location.search);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -485,33 +495,71 @@ const PluginDetail = () => {
|
|||||||
return `Download ${sizeStr}`;
|
return `Download ${sizeStr}`;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const cleanDesc = currentDescription.replace(/[#*`_\[\]]/g, '').slice(0, 160).trim();
|
||||||
|
|
||||||
const productSchema = {
|
const productSchema = {
|
||||||
"@context": "https://schema.org/",
|
"@context": "https://schema.org",
|
||||||
"@type": "SoftwareApplication",
|
"@graph": [
|
||||||
"name": plugin.name,
|
{
|
||||||
"operatingSystem": "Minecraft",
|
"@type": "SoftwareApplication",
|
||||||
"applicationCategory": "GameApplication",
|
"name": plugin.name,
|
||||||
"offers": {
|
"description": cleanDesc,
|
||||||
"@type": "Offer",
|
"operatingSystem": "Minecraft",
|
||||||
"price": plugin.type === 'paid' ? (plugin.price_cents / 100).toFixed(2) : "0.00",
|
"applicationCategory": "GameApplication",
|
||||||
"priceCurrency": "EUR"
|
"softwareVersion": plugin.version || "1.0.0",
|
||||||
},
|
"fileSize": plugin.file_size ? `${plugin.file_size} B` : undefined,
|
||||||
"aggregateRating": stats ? {
|
"image": plugin.preview_path || "https://market.pumpkinmc.org/icon.png",
|
||||||
"@type": "AggregateRating",
|
"offers": {
|
||||||
"ratingValue": stats.avg,
|
"@type": "Offer",
|
||||||
"reviewCount": stats.total
|
"price": plugin.type === 'paid' ? (plugin.price_cents / 100).toFixed(2) : "0.00",
|
||||||
} : undefined,
|
"priceCurrency": "EUR",
|
||||||
"author": {
|
"availability": "https://schema.org/InStock"
|
||||||
"@type": "Person",
|
},
|
||||||
"name": plugin.dev_name
|
"aggregateRating": stats ? {
|
||||||
}
|
"@type": "AggregateRating",
|
||||||
|
"ratingValue": stats.avg,
|
||||||
|
"ratingCount": stats.total,
|
||||||
|
"bestRating": "5",
|
||||||
|
"worstRating": "1"
|
||||||
|
} : undefined,
|
||||||
|
"author": {
|
||||||
|
"@type": "Person",
|
||||||
|
"name": plugin.dev_name,
|
||||||
|
"url": `https://market.pumpkinmc.org/profile/${plugin.dev_name}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Home",
|
||||||
|
"item": "https://market.pumpkinmc.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": plugin.category || "Plugins",
|
||||||
|
"item": `https://market.pumpkinmc.org/search?category=${encodeURIComponent(plugin.category || '')}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 3,
|
||||||
|
"name": plugin.name,
|
||||||
|
"item": `https://market.pumpkinmc.org/plugin/${plugin.id}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<SEO
|
<SEO
|
||||||
title={plugin.name}
|
title={plugin.name}
|
||||||
description={currentDescription.substring(0, 160)}
|
description={cleanDesc || `Download ${plugin.name} by ${plugin.dev_name} on Pumpkin Market.`}
|
||||||
|
keywords={`${plugin.name}, minecraft plugin, ${plugin.category || ''}, ${plugin.keywords || ''}`}
|
||||||
ogType="product"
|
ogType="product"
|
||||||
ogImage={plugin.preview_path || "/icon.png"}
|
ogImage={plugin.preview_path || "/icon.png"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import api from '../api';
|
|||||||
import { useAuth } from '../App';
|
import { useAuth } from '../App';
|
||||||
import { getCodeList } from 'country-list';
|
import { getCodeList } from 'country-list';
|
||||||
import DeveloperOnboardingModal from '../components/DeveloperOnboardingModal';
|
import DeveloperOnboardingModal from '../components/DeveloperOnboardingModal';
|
||||||
|
import { getPluginUrl } from '../utils/url';
|
||||||
import {
|
import {
|
||||||
User, Mail, Globe, Lock, Shield, CreditCard,
|
User, Mail, Globe, Lock, Shield, CreditCard,
|
||||||
BookOpen, AlertTriangle, LogOut, CheckCircle,
|
BookOpen, AlertTriangle, LogOut, CheckCircle,
|
||||||
@ -854,7 +855,7 @@ const ProfilePage = () => {
|
|||||||
{library.map((entry, idx) => (
|
{library.map((entry, idx) => (
|
||||||
<Link
|
<Link
|
||||||
key={entry.plugin_id}
|
key={entry.plugin_id}
|
||||||
to={`/plugin/${entry.plugin_id}`}
|
to={getPluginUrl({ id: entry.plugin_id, name: entry.name })}
|
||||||
className="library-card"
|
className="library-card"
|
||||||
style={{ animationDelay: `${idx * 0.05}s` }}
|
style={{ animationDelay: `${idx * 0.05}s` }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { Settings, Plus, ExternalLink, Sparkles } from 'lucide-react';
|
import { Settings, Plus, ExternalLink, Sparkles } from 'lucide-react';
|
||||||
import { useAuth } from '../../App';
|
import { useAuth } from '../../App';
|
||||||
import DeveloperOnboardingModal from '../../components/DeveloperOnboardingModal';
|
import DeveloperOnboardingModal from '../../components/DeveloperOnboardingModal';
|
||||||
|
import { getPluginUrl } from '../../utils/url';
|
||||||
|
|
||||||
// --- Main Component ---
|
// --- Main Component ---
|
||||||
const DashboardPlugins = () => {
|
const DashboardPlugins = () => {
|
||||||
@ -121,7 +122,7 @@ const DashboardPlugins = () => {
|
|||||||
{/* View on Marketplace */}
|
{/* View on Marketplace */}
|
||||||
<button
|
<button
|
||||||
className="btn btn-icon-only"
|
className="btn btn-icon-only"
|
||||||
onClick={() => navigate(`/plugin/${p.id}`)}
|
onClick={() => navigate(getPluginUrl(p))}
|
||||||
title="View in Store"
|
title="View in Store"
|
||||||
>
|
>
|
||||||
<ExternalLink size={16} />
|
<ExternalLink size={16} />
|
||||||
|
|||||||
17
src/utils/url.ts
Normal file
17
src/utils/url.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export const slugify = (text: string): string => {
|
||||||
|
if (!text) return '';
|
||||||
|
return text
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/[\s_\.\-]+/g, '-') // Replace spaces, underscores, dots, and hyphens with a single -
|
||||||
|
.replace(/[^\w\-]+/g, '') // Remove all non-word chars except hyphens
|
||||||
|
.replace(/\-\-+/g, '-') // Replace multiple hyphens with single hyphen
|
||||||
|
.replace(/^-+/, '') // Trim hyphens from start
|
||||||
|
.replace(/-+$/, ''); // Trim hyphens from end
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPluginUrl = (plugin: { id: number | string; name?: string }): string => {
|
||||||
|
const slug = plugin.name ? slugify(plugin.name) : '';
|
||||||
|
return slug ? `/plugin/${plugin.id}-${slug}` : `/plugin/${plugin.id}`;
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user