some fixes
This commit is contained in:
parent
5c3922f230
commit
92366a6781
@ -384,7 +384,7 @@ h3 {
|
||||
}
|
||||
|
||||
.plugin-sidebar {
|
||||
order: -1;
|
||||
order: 1;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -126,7 +126,7 @@ const App = () => (
|
||||
{/* All routes share MainLayout (navbar + footer) */}
|
||||
<Route path="/" element={<MainLayout />}>
|
||||
<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="search" element={<SearchResultsPage />} />
|
||||
<Route path="terms" element={<TermsOfServicePage />} />
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getPluginUrl } from '../utils/url';
|
||||
|
||||
const getAccentColor = (name: string) => {
|
||||
const colors = [
|
||||
@ -81,7 +82,7 @@ const PluginCard = ({ plugin }: { plugin: any }) => {
|
||||
};
|
||||
|
||||
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="pcv2-preview">
|
||||
{slideshowImages.length > 0 ? (
|
||||
|
||||
@ -3,6 +3,7 @@ import { useParams, Link } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SEO from '../components/SEO';
|
||||
import { getPluginUrl } from '../utils/url';
|
||||
|
||||
const AuthorProfilePage = () => {
|
||||
const { username } = useParams();
|
||||
@ -138,7 +139,7 @@ const AuthorProfilePage = () => {
|
||||
const imageUrl = plugin.preview_path;
|
||||
|
||||
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="pcv2-preview">
|
||||
{imageUrl ? (
|
||||
|
||||
@ -5,6 +5,8 @@ import api from '../api';
|
||||
import SEO from '../components/SEO';
|
||||
import PluginCard from '../components/PluginCard';
|
||||
|
||||
import { getPluginUrl } from '../utils/url';
|
||||
|
||||
// Global cache to persist data across component unmounts (navigation)
|
||||
let homeCache: {
|
||||
popular: any[];
|
||||
@ -120,7 +122,7 @@ const Home = () => {
|
||||
{activeFeatured && (
|
||||
<section className="featured-showcase-container">
|
||||
<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 */}
|
||||
<div className="featured-main-visual">
|
||||
{activeImage ? (
|
||||
|
||||
@ -135,7 +135,8 @@ const formatDate = (dateStr: string) => {
|
||||
};
|
||||
|
||||
const PluginDetail = () => {
|
||||
const { id } = useParams();
|
||||
const { id: rawId, slug } = useParams();
|
||||
const id = rawId ? rawId.split('-')[0] : rawId;
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
@ -233,6 +234,15 @@ const PluginDetail = () => {
|
||||
} else if (res.data.screenshots?.length > 0) {
|
||||
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}`;
|
||||
})();
|
||||
|
||||
const cleanDesc = currentDescription.replace(/[#*`_\[\]]/g, '').slice(0, 160).trim();
|
||||
|
||||
const productSchema = {
|
||||
"@context": "https://schema.org/",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": plugin.name,
|
||||
"operatingSystem": "Minecraft",
|
||||
"applicationCategory": "GameApplication",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": plugin.type === 'paid' ? (plugin.price_cents / 100).toFixed(2) : "0.00",
|
||||
"priceCurrency": "EUR"
|
||||
},
|
||||
"aggregateRating": stats ? {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": stats.avg,
|
||||
"reviewCount": stats.total
|
||||
} : undefined,
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": plugin.dev_name
|
||||
}
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "SoftwareApplication",
|
||||
"name": plugin.name,
|
||||
"description": cleanDesc,
|
||||
"operatingSystem": "Minecraft",
|
||||
"applicationCategory": "GameApplication",
|
||||
"softwareVersion": plugin.version || "1.0.0",
|
||||
"fileSize": plugin.file_size ? `${plugin.file_size} B` : undefined,
|
||||
"image": plugin.preview_path || "https://market.pumpkinmc.org/icon.png",
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": plugin.type === 'paid' ? (plugin.price_cents / 100).toFixed(2) : "0.00",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock"
|
||||
},
|
||||
"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 (
|
||||
<div className="container">
|
||||
<SEO
|
||||
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"
|
||||
ogImage={plugin.preview_path || "/icon.png"}
|
||||
/>
|
||||
|
||||
@ -5,6 +5,7 @@ import api from '../api';
|
||||
import { useAuth } from '../App';
|
||||
import { getCodeList } from 'country-list';
|
||||
import DeveloperOnboardingModal from '../components/DeveloperOnboardingModal';
|
||||
import { getPluginUrl } from '../utils/url';
|
||||
import {
|
||||
User, Mail, Globe, Lock, Shield, CreditCard,
|
||||
BookOpen, AlertTriangle, LogOut, CheckCircle,
|
||||
@ -854,7 +855,7 @@ const ProfilePage = () => {
|
||||
{library.map((entry, idx) => (
|
||||
<Link
|
||||
key={entry.plugin_id}
|
||||
to={`/plugin/${entry.plugin_id}`}
|
||||
to={getPluginUrl({ id: entry.plugin_id, name: entry.name })}
|
||||
className="library-card"
|
||||
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 { useAuth } from '../../App';
|
||||
import DeveloperOnboardingModal from '../../components/DeveloperOnboardingModal';
|
||||
import { getPluginUrl } from '../../utils/url';
|
||||
|
||||
// --- Main Component ---
|
||||
const DashboardPlugins = () => {
|
||||
@ -121,7 +122,7 @@ const DashboardPlugins = () => {
|
||||
{/* View on Marketplace */}
|
||||
<button
|
||||
className="btn btn-icon-only"
|
||||
onClick={() => navigate(`/plugin/${p.id}`)}
|
||||
onClick={() => navigate(getPluginUrl(p))}
|
||||
title="View in Store"
|
||||
>
|
||||
<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