Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion convex/_generated/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/**
* Generated `api` utility.
*
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/dataModel.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/**
* Generated data model types.
*
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/server.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
Expand Down
2 changes: 1 addition & 1 deletion convex/_generated/server.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/**
* Generated utilities for implementing server-side Convex query and mutation functions.
*
Expand Down
2 changes: 1 addition & 1 deletion convex/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const listEvents = query({
},
handler: async (ctx, args) => {
if (args.search && args.search.trim().length > 0) {
let sq = ctx.db.query("events").withSearchIndex("search_name", (q) => {
const sq = ctx.db.query("events").withSearchIndex("search_name", (q) => {
const base = q.search("name", args.search!);
return args.type ? base.eq("type", args.type) : base;
});
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
"private": true,
"version": "0.0.0",
"type": "module",
"engines": { "node": ">=20" },
"scripts": {
"prebuild": "node -e \"const fs=require('fs'),p=require('path');fs.mkdirSync('dist',{recursive:true});const src='changelog.md',dst=p.join('public','changelog.md');if(fs.existsSync(src)){fs.mkdirSync('public',{recursive:true});fs.copyFileSync(src,dst);}\"",
"prebuild": "node scripts/generate_sitemap.mjs && node -e \"const fs=require('fs'),p=require('path');fs.mkdirSync('dist',{recursive:true});const src='changelog.md',dst=p.join('public','changelog.md');if(fs.existsSync(src)){fs.mkdirSync('public',{recursive:true});fs.copyFileSync(src,dst);}\"",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"dev": "concurrently \"pnpm run dev:vite\" \"pnpm run dev:server\"",
"dev:vite": "vite",
"dev:server": "node server.js",
Expand Down
3 changes: 2 additions & 1 deletion public/robots.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
User-agent: *
Allow: /
Allow: /
Sitemap: https://renderdragon.org/sitemap.xml
1 change: 1 addition & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><url><loc>https://renderdragon.org/</loc></url><url><loc>https://renderdragon.org/resources</loc></url><url><loc>https://renderdragon.org/blogs</loc></url><url><loc>https://renderdragon.org/guides</loc></url><url><loc>https://renderdragon.org/faq</loc></url><url><loc>https://renderdragon.org/contact</loc></url><url><loc>https://renderdragon.org/showcase</loc></url><url><loc>https://renderdragon.org/community</loc></url><url><loc>https://renderdragon.org/changelogs</loc></url><url><loc>https://renderdragon.org/utilities</loc></url><url><loc>https://renderdragon.org/generators</loc></url><url><loc>https://renderdragon.org/background-generator</loc></url><url><loc>https://renderdragon.org/text-generator</loc></url><url><loc>https://renderdragon.org/ai-title-helper</loc></url><url><loc>https://renderdragon.org/youtube-downloader</loc></url><url><loc>https://renderdragon.org/player-renderer</loc></url><url><loc>https://renderdragon.org/renderbot</loc></url><url><loc>https://renderdragon.org/native-application</loc></url><url><loc>https://renderdragon.org/tos</loc></url><url><loc>https://renderdragon.org/privacy</loc></url><url><loc>https://renderdragon.org/guides/scriptwriting</loc></url><url><loc>https://renderdragon.org/guides/AI</loc></url><url><loc>https://renderdragon.org/guides/questions</loc></url><url><loc>https://renderdragon.org/guides/copyright</loc></url><url><loc>https://renderdragon.org/guides/thingstoask</loc></url><url><loc>https://renderdragon.org/guides/voice</loc></url></urlset>
4 changes: 2 additions & 2 deletions scripts/export_resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async function exportResources() {
});
return acc;
},
{} as Record<string, any[]>,
{} as Record<string, Array<Record<string, unknown>>>,
);

const mcicons = await fetchMcicons();
Expand Down Expand Up @@ -112,7 +112,7 @@ async function exportResources() {
file: `resources/${file}`,
};
allResources.push(
...items.map((item: any) => ({
...items.map((item: Record<string, unknown>) => ({
...item,
category,
})),
Expand Down
42 changes: 42 additions & 0 deletions scripts/generate_sitemap.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { mkdir, writeFile } from "node:fs/promises";

const site = "https://renderdragon.org";
const routes = [
"/", "/resources", "/blogs", "/guides", "/faq", "/contact", "/showcase",
"/community", "/changelogs", "/utilities", "/generators", "/background-generator",
"/text-generator", "/ai-title-helper", "/youtube-downloader", "/player-renderer",
"/renderbot", "/native-application", "/tos", "/privacy",
"/guides/scriptwriting", "/guides/AI", "/guides/questions", "/guides/copyright", "/guides/thingstoask", "/guides/voice",
];
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const escapeXml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
const urls = routes.map((route) => `<url><loc>${site}${route}</loc></url>`);

// Public profile and creator-pack slugs are added when build credentials are available.
if (process.env.VITE_SUPABASE_URL && process.env.VITE_SUPABASE_PUBLISHABLE_KEY) {
const headers = { apikey: process.env.VITE_SUPABASE_PUBLISHABLE_KEY };
const fetchJson = async (path) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(`${process.env.VITE_SUPABASE_URL}/rest/v1/${path}`, { headers, signal: controller.signal });
return response.ok ? await response.json() : [];
} catch (error) {
console.warn(`Skipping sitemap enrichment for ${path}:`, error instanceof Error ? error.message : error);
return [];
} finally {
clearTimeout(timeout);
}
};
const [profiles, packs, blogs] = await Promise.all([
fetchJson("profiles?select=username&username=not.is.null"),
fetchJson("creator_packs?select=slug&status=eq.approved"),
fetchJson("blogs?select=slug&published=eq.true"),
]);
for (const { username } of profiles) if (username) urls.push(`<url><loc>${site}/u/${escapeXml(username)}</loc></url>`);
for (const { slug } of packs) if (slug) urls.push(`<url><loc>${site}/creator-packs/${escapeXml(slug)}</loc></url>`);
for (const { slug } of blogs) if (slug) urls.push(`<url><loc>${site}/blogs/${escapeXml(slug)}</loc></url>`);
}

await mkdir("public", { recursive: true });
await writeFile("public/sitemap.xml", `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls.join("")}</urlset>\n`);
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { IconLoader2 } from "@tabler/icons-react";
import DonateButton from "@/components/DonateButton";
import { AdBlockDetector } from "@/components/AdBlockDetector";
import Navbar from "@/components/Navbar";
import Seo from "@/components/Seo";

const ExternalRedirect = ({ url }: { url: string }) => {
useEffect(() => { window.location.href = url; }, [url]);
Expand All @@ -25,9 +26,12 @@ const GlobalComponents = () => {
const location = useLocation();
const hideDonateButton = location.pathname.startsWith('/admin') ||
location.pathname.startsWith('/account');
const routeName = location.pathname === '/' ? 'Minecraft Creator Tools & Resources' :
location.pathname.split('/').filter(Boolean).map((part) => part.split('-').join(' ')).join(' / ');

return (
<>
<Seo title={`${routeName.replace(/\b\w/g, (letter) => letter.toUpperCase())} | RenderDragon`} description={`Explore ${routeName} on RenderDragon, free tools and resources for Minecraft content creators.`} path={location.pathname} />
Comment on lines +29 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 \
  'renderToString|renderToPipeableStream|prerender|ssr|createRoot|BrowserRouter|HelmetProvider' \
  package.json vite.config.ts src || true

Repository: creatorcluster/renderdragon.org

Length of output: 2419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package/scripts/types/config and relevant SEO implementation files without running repo code.
printf '--- package scripts and deps ---\n'
python3 - <<'PY'
import json, pathlib
p = pathlib.Path('package.json')
if p.exists():
    data=json.loads(p.read_text())
    for k in ['scripts','devDependencies','dependencies']:
        print(f'[{k}]')
        v=data.get(k,{})
        if isinstance(v, dict):
            for key,val in v.items():
                if any(s in key.lower() or isinstance(val,str) and any(s in val.lower() for s in ['vite','react-router','separate','prerender','render','server','next','remix','vercel','netlify', 'render dragon'])):
                    print(f'{key}: {val}')
                elif k=='scripts':
                    print(f'{key}: {val}')
PY

printf '\n--- config files ---\n'
git ls-files | rg '(^|/)(vite\.config\.(ts|js)|wrangler\.toml|netlify\.toml|vercel\.json|app\.config\.(ts|js)|render\.yaml|cypress|playwright|index\.html|sitemap|robots)' || true

printf '\n--- vite.config relevant ---\n'
if [ -f vite.config.ts ]; then
  nl -ba vite.config.ts | sed -n '1,220p'
fi
if [ -f src/index.html ]; then
  printf '\n--- src/index.html ---\n'
  nl -ba src/index.html | sed -n '1,160p'
elif [ -f index.html ]; then
  printf '\n--- index.html ---\n'
  nl -ba index.html | sed -n '1,160p'
fi

printf '\n--- Seo component references ---\n'
rg -n -C 4 'function Seo|const Seo|export .*Seo|<Seo|title=|description=|path=|canonical|og:|twitter:' src || true

Repository: creatorcluster/renderdragon.org

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect package metadata and build/deploy config without running repo code.
printf '%s\n' '--- package scripts and deps ---'
python3 - <<'PY'
import json, pathlib
p = pathlib.Path('package.json')
if p.exists():
    data=json.loads(p.read_text())
    for k in ['scripts','devDependencies','dependencies']:
        print(f'[{k}]')
        v=data.get(k,{})
        if isinstance(v, dict):
            for key,val in v.items():
                if k=='scripts' or any(s in key.lower() or (isinstance(val,str) and any(s in val.lower() for s in ['vite','react-router','ssr','server','vercel','netlify','render']))):
                    print(f'{key}: {val}')
PY

printf '%s\n' ''
printf '%s\n' '--- config files ---'
git ls-files | rg '(^|/)(vite\.config\.(ts|js)|wrangler\.toml|netlify\.toml|vercel\.json|app\.config\.(ts|js)|render\.yaml|cypress|playwright|index\.html|sitemap|robots)' || true

printf '%s\n' ''
printf '%s\n' '--- vite.config relevant ---'
if [ -f vite.config.ts ]; then
  nl -ba vite.config.ts | sed -n '1,220p'
fi
printf '%s\n' ''
if [ -f src/index.html ]; then
  printf '%s\n' '--- src/index.html ---'
  nl -ba src/index.html | sed -n '1,160p'
elif [ -f index.html ]; then
  printf '%s\n' '--- index.html ---'
  nl -ba index.html | sed -n '1,160p'
fi

printf '%s\n' ''
printf '%s\n' '--- route-name/SEO code references ---'
rg -n -C 4 'function Seo|const Seo|export .*Seo|<Seo|title=|description=|path=|canonical|og:|twitter:|renderToString|renderToString|Prerenderer|renderingRoute|ssr|server:' src || true

Repository: creatorcluster/renderdragon.org

Length of output: 845


Make SEO metadata render before client JavaScript executes.

BrowserRouter and useLocation mean this route metadata is only available after hydration, and main.tsx still renders via ReactDOM.createRoot. Since non-JavaScript/social crawlers rely on initial HTML, make sure the deploy prerenders/SSR each route or switch the metadata path to generate route values at build/server render time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 29 - 34, Update the routing and SEO rendering
around BrowserRouter, useLocation, and the Seo component so route metadata is
available in the initial HTML before client JavaScript executes. Configure
prerendering or SSR for every route, or move route-name/path generation to
build/server render time, while preserving the existing per-route title and
description values during client navigation.

{!hideDonateButton && <DonateButton />}
<AdBlockDetector />
</>
Expand Down
59 changes: 41 additions & 18 deletions src/components/AudioPlayer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import WaveSurfer from 'wavesurfer.js';
import {
IconPlayerPlay,
IconPlayerPause,
Expand All @@ -23,14 +22,22 @@ const AudioPlayer = ({ src, className, isInView = true, allowPlayBeforeReady = f
const [duration, setDuration] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [isReady, setIsReady] = useState(false);
const [loadError, setLoadError] = useState(false);
const [retryKey, setRetryKey] = useState(0);

const containerRef = useRef<HTMLDivElement>(null);
const wavesurfer = useRef<WaveSurfer | null>(null);
const wavesurfer = useRef<import('wavesurfer.js').default | null>(null);

useEffect(() => {
if (!containerRef.current) return;

const ws = WaveSurfer.create({
let ws: import('wavesurfer.js').default | null = null;
let isMounted = true;
setLoadError(false);
setIsReady(false);
Comment on lines +36 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset playback state when the source changes.

Line 36 resets readiness but retains isPlaying, currentTime, and duration. If src changes during playback, the old player is destroyed but the new player can display the old pause icon and time values. Reset these values when initialization starts.

Proposed fix
     setLoadError(false);
     setIsReady(false);
+    setIsPlaying(false);
+    setCurrentTime(0);
+    setDuration(0);
+    setIsLoading(!allowPlayBeforeReady);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setLoadError(false);
setIsReady(false);
setLoadError(false);
setIsReady(false);
setIsPlaying(false);
setCurrentTime(0);
setDuration(0);
setIsLoading(!allowPlayBeforeReady);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AudioPlayer.tsx` around lines 36 - 37, Update the
source-change initialization logic in AudioPlayer to reset isPlaying,
currentTime, and duration alongside setLoadError(false) and setIsReady(false).
Ensure the new source starts with stopped playback and cleared time values
rather than retaining state from the previous player.

import('wavesurfer.js').then(({ default: WaveSurfer }) => {
if (!isMounted || !containerRef.current) return;
ws = WaveSurfer.create({
Comment thread
coderabbitai[bot] marked this conversation as resolved.
container: containerRef.current,
waveColor: 'rgba(139, 92, 246, 0.2)', // Soft cow-purple
progressColor: '#8b5cf6', // Solid cow-purple
Expand All @@ -42,42 +49,52 @@ const AudioPlayer = ({ src, className, isInView = true, allowPlayBeforeReady = f
barGap: 3,
normalize: true,
hideScrollbar: true,
});

let isMounted = true;
wavesurfer.current = ws;
});
wavesurfer.current = ws;

if (allowPlayBeforeReady) {
setIsLoading(false);
} else {
setIsLoading(true);
}
ws.load(src).catch((err) => {
ws.load(src).catch((err) => {
if (err.name === 'AbortError') return;
console.error('WaveSurfer load error:', err);
});
if (isMounted) {
setIsLoading(false);
setLoadError(true);
}
});

ws.on('ready', () => {
ws.on('ready', () => {
if (!isMounted) return;
setDuration(ws.getDuration());
setIsLoading(false);
setIsReady(true);
});
});

ws.on('audioprocess', () => {
ws.on('audioprocess', () => {
if (!isMounted) return;
setCurrentTime(ws.getCurrentTime());
});
});

ws.on('play', () => isMounted && setIsPlaying(true));
ws.on('pause', () => isMounted && setIsPlaying(false));
ws.on('finish', () => isMounted && setIsPlaying(false));
ws.on('play', () => isMounted && setIsPlaying(true));
ws.on('pause', () => isMounted && setIsPlaying(false));
ws.on('finish', () => isMounted && setIsPlaying(false));
}).catch((error: unknown) => {
if (!isMounted) return;
console.error('Failed to load WaveSurfer:', error);
setIsLoading(false);
setLoadError(true);
});

return () => {
isMounted = false;
ws.destroy();
setIsReady(false);
if (wavesurfer.current === ws) wavesurfer.current = null;
ws?.destroy();
};
}, [src]);
}, [src, allowPlayBeforeReady, retryKey]);

// Handle visibility
useEffect(() => {
Expand Down Expand Up @@ -132,6 +149,12 @@ const AudioPlayer = ({ src, className, isInView = true, allowPlayBeforeReady = f
</div>
)}
<div ref={containerRef} className="w-full" />
{loadError && (
<div className="absolute inset-0 flex items-center justify-center gap-2 bg-card/90 text-sm text-muted-foreground">
<span>Audio preview unavailable.</span>
<Button variant="outline" size="sm" onClick={() => setRetryKey((key) => key + 1)}>Retry</Button>
</div>
)}
</div>

{/* Controls and Info */}
Expand Down
21 changes: 10 additions & 11 deletions src/components/FeaturedResources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import ResourceCard from "@/components/resources/ResourceCard";
import ResourceCardSkeleton from "./resources/ResourceCardSkeleton";

const FEATURED_CATEGORIES = ["music", "images", "sfx", "fonts"];
type RawResource = Record<string, unknown>;
const stringValue = (value: unknown): string | undefined => typeof value === "string" && value.trim() ? value : undefined;

const FeaturedResources = () => {
const [featuredResources, setFeaturedResources] = useState<Resource[]>([]);
Expand All @@ -32,18 +34,15 @@ const FeaturedResources = () => {
const rawItems = await catRes.json();
const resources: Resource[] = (Array.isArray(rawItems) ? rawItems : [])
.slice(0, 4)
.map((item: any, idx: number) => ({
id: item.id ?? `${catKeys[0]}-${idx}`,
title: String(item?.title || "").trim() || `Resource ${idx + 1}`,
.map((item: RawResource, idx: number) => ({
id: typeof item.id === "number" || typeof item.id === "string" ? item.id : `${catKeys[0]}-${idx}`,
title: stringValue(item.title) || `Resource ${idx + 1}`,
category: catKeys[0] as Resource["category"],
subcategory: item.subcategory || undefined,
credit: item.credit || undefined,
filetype: item.filetype || item.ext || undefined,
download_url: item.download_url || item.url || undefined,
preview_url: item.preview_url || undefined,
image_url: item.image_url || undefined,
software: item.software || undefined,
description: item.description || undefined,
subcategory: stringValue(item.subcategory), credit: stringValue(item.credit),
filetype: stringValue(item.filetype) || stringValue(item.ext),
download_url: stringValue(item.download_url) || stringValue(item.url),
preview_url: stringValue(item.preview_url), image_url: stringValue(item.image_url),
software: stringValue(item.software), description: stringValue(item.description),
Comment on lines +37 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate each resource entry before reading its fields.

Array.isArray(rawItems) still permits null and primitive entries. item.id then throws, and the catch block discards all featured resources.

Filter object records before slice and map.

Proposed fix
-        const resources: Resource[] = (Array.isArray(rawItems) ? rawItems : [])
+        const resources: Resource[] = (Array.isArray(rawItems) ? rawItems : [])
+          .filter((item): item is RawResource => item !== null && typeof item === "object")
           .slice(0, 4)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/FeaturedResources.tsx` around lines 37 - 45, Filter rawItems
to object records before applying slice and map in the resource-processing flow,
excluding null and primitive entries so field access in the RawResource mapper
is safe. Preserve the existing fallback, normalization, and featured-resource
behavior for valid entries.

}));

setFeaturedResources(resources);
Expand Down
2 changes: 0 additions & 2 deletions src/components/InfiniteMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1097,8 +1097,6 @@ const InfiniteMenu: FC<InfiniteMenuProps> = ({ items = [] }) => {
if (!activeItem?.link) return;
if (activeItem.link.startsWith('http')) {
window.open(activeItem.link, '_blank', 'noopener,noreferrer');
} else {

}
};

Expand Down
30 changes: 30 additions & 0 deletions src/components/Seo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Helmet } from "react-helmet-async";

const SITE_URL = "https://renderdragon.org";

interface SeoProps {
title: string;
description: string;
path: string;
image?: string;
}

export default function Seo({ title, description, path, image = "/ogimg.png" }: SeoProps) {
const normalizedPath = path.replace(/\/+$/, "") || "/";
const canonical = `${SITE_URL}${normalizedPath === "/" ? "" : normalizedPath}`;
const imageUrl = image.startsWith("http") ? image : `${SITE_URL}${image}`;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return <Helmet>
<title>{title}</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonical} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:type" content="website" />
<meta property="og:image" content={imageUrl} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={imageUrl} />
</Helmet>;
}
Loading
Loading