diff --git a/index.html b/index.html index bf1dc41..585a390 100644 --- a/index.html +++ b/index.html @@ -4,35 +4,56 @@ - + <!-- + These are no-JS fallback SEO tags. Tags marked data-rh="true" are the ones + react-helmet-async manages, so each route's <Seo> can replace them with + route-specific title/description/canonical/og values. Untagged tags + (site verification, theme-color, og:site_name) are intentionally left for + Helmet to pass over. + --> + <title data-rh="true"> Renderdragon - Minecraft Content Creator Tools & Assets - - + + + + - - + - - + + - + + + + + - - - - + + + + - - + + + + + + + + @@ -40,12 +61,63 @@ + +
+ - \ No newline at end of file + diff --git a/package.json b/package.json index aaa4314..ca4f1df 100644 --- a/package.json +++ b/package.json @@ -142,8 +142,7 @@ "tailwindcss": "^3.4.19", "typescript": "^5.9.3", "typescript-eslint": "^8.54.0", - "vite": "^5.4.21", - "vite-plugin-sitemap": "^0.7.1" + "vite": "^5.4.21" }, "trustedDependencies": [ "@swc/core", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15b8de4..db3d777 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -381,9 +381,6 @@ importers: vite: specifier: ^5.4.21 version: 5.4.21(@types/node@22.19.13) - vite-plugin-sitemap: - specifier: ^0.7.1 - version: 0.7.1 packages: @@ -4900,9 +4897,6 @@ packages: videojs-vtt.js@0.15.5: resolution: {integrity: sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==} - vite-plugin-sitemap@0.7.1: - resolution: {integrity: sha512-4NRTkiWytLuAmcikckrLcLl9iYA20+5v6l8XshcOrzxH1WR8H0O3S6sTQYfjMrE8su/LG6Y0cTodvOdcOIxaLw==} - vite@5.4.21: resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} engines: {node: ^18.0.0 || >=20.0.0} @@ -9808,8 +9802,6 @@ snapshots: dependencies: global: 4.4.0 - vite-plugin-sitemap@0.7.1: {} - vite@5.4.21(@types/node@22.19.13): dependencies: esbuild: 0.21.5 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..6b86890 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..6d6f502 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..7d54f99 Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index 29c4e17..0000000 Binary files a/public/icon.png and /dev/null differ diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 3bc7dee..0000000 --- a/public/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - Renderdragon - Tools & Resources for Minecraft Content Creators - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - diff --git a/public/robots.txt b/public/robots.txt index b23451e..55b7fc5 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,11 @@ User-agent: * Allow: / + +# Private / account-only areas — keep them out of the index. +Disallow: /admin +Disallow: /account +Disallow: /analytics +Disallow: /creator-packs/manage + Sitemap: https://renderdragon.org/sitemap.xml +Host: https://renderdragon.org diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..0eee7c0 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,31 @@ +{ + "name": "Renderdragon", + "short_name": "Renderdragon", + "description": "Free tools and assets for Minecraft YouTube creators.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0b0b12", + "theme_color": "#9333ea", + "lang": "en", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "any" + } + ] +} diff --git a/public/sitemap.xml b/public/sitemap.xml index fa5d65a..7157196 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1 +1 @@ -https://renderdragon.org/https://renderdragon.org/resourceshttps://renderdragon.org/blogshttps://renderdragon.org/guideshttps://renderdragon.org/faqhttps://renderdragon.org/contacthttps://renderdragon.org/showcasehttps://renderdragon.org/communityhttps://renderdragon.org/changelogshttps://renderdragon.org/utilitieshttps://renderdragon.org/generatorshttps://renderdragon.org/background-generatorhttps://renderdragon.org/text-generatorhttps://renderdragon.org/ai-title-helperhttps://renderdragon.org/youtube-downloaderhttps://renderdragon.org/player-rendererhttps://renderdragon.org/renderbothttps://renderdragon.org/native-applicationhttps://renderdragon.org/toshttps://renderdragon.org/privacyhttps://renderdragon.org/guides/scriptwritinghttps://renderdragon.org/guides/AIhttps://renderdragon.org/guides/questionshttps://renderdragon.org/guides/copyrighthttps://renderdragon.org/guides/thingstoaskhttps://renderdragon.org/guides/voice +https://renderdragon.org/daily1.0https://renderdragon.org/resourcesweekly0.9https://renderdragon.org/generatorsweekly0.8https://renderdragon.org/guidesweekly0.8https://renderdragon.org/gappaweekly0.8https://renderdragon.org/background-generatorweekly0.7https://renderdragon.org/text-generatorweekly0.7https://renderdragon.org/player-rendererweekly0.7https://renderdragon.org/youtube-downloaderweekly0.7https://renderdragon.org/ai-title-helperweekly0.7https://renderdragon.org/utilitiesweekly0.6https://renderdragon.org/showcaseweekly0.6https://renderdragon.org/blogsweekly0.6https://renderdragon.org/communityweekly0.6https://renderdragon.org/renderbotweekly0.5https://renderdragon.org/native-applicationweekly0.5https://renderdragon.org/changelogsweekly0.4https://renderdragon.org/contactweekly0.4https://renderdragon.org/faqweekly0.5https://renderdragon.org/privacyweekly0.2https://renderdragon.org/tosweekly0.2https://renderdragon.org/guides/AIweekly0.6https://renderdragon.org/guides/copyrightweekly0.6https://renderdragon.org/guides/questionsweekly0.6https://renderdragon.org/guides/scriptwritingweekly0.6https://renderdragon.org/guides/thingstoaskweekly0.6https://renderdragon.org/guides/voiceweekly0.6 diff --git a/scripts/generate_sitemap.mjs b/scripts/generate_sitemap.mjs index f10da5a..beaba8d 100644 --- a/scripts/generate_sitemap.mjs +++ b/scripts/generate_sitemap.mjs @@ -1,16 +1,59 @@ -import { mkdir, writeFile } from "node:fs/promises"; +import { mkdir, readdir, 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", -]; -const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'"); -const urls = routes.map((route) => `${site}${route}`); +// Static routes with a priority hint. Higher priority = more important to crawlers. +// Guide routes are discovered from public/guides/*.md below. +const staticRoutes = { + "/": 1.0, + "/resources": 0.9, + "/generators": 0.8, + "/guides": 0.8, + "/gappa": 0.8, + "/background-generator": 0.7, + "/text-generator": 0.7, + "/player-renderer": 0.7, + "/youtube-downloader": 0.7, + "/ai-title-helper": 0.7, + "/utilities": 0.6, + "/showcase": 0.6, + "/blogs": 0.6, + "/community": 0.6, + "/renderbot": 0.5, + "/native-application": 0.5, + "/changelogs": 0.4, + "/contact": 0.4, + "/faq": 0.5, + "/privacy": 0.2, + "/tos": 0.2, +}; + +const escapeXml = (value) => + String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + +// lastmod is only emitted when a real modification time is known; static routes +// do not have one, so we omit it rather than stamping build time on every page. +const urlEntry = (loc, priority, changefreq = "weekly", lastmod) => + `${escapeXml(loc)}${lastmod ? `${escapeXml(lastmod)}` : ""}${changefreq}${priority.toFixed(1)}`; + +const urls = Object.entries(staticRoutes).map(([route, priority]) => + urlEntry(`${site}${route}`, priority, route === "/" ? "daily" : "weekly"), +); + +// Guide slugs come straight from the markdown files that GuideView serves. +// Sort so the tracked sitemap is stable across filesystem readdir orderings. +const guideFiles = await readdir("public/guides").catch((error) => { + console.warn("Skipping guide sitemap entries:", error instanceof Error ? error.message : error); + return []; +}); +for (const file of guideFiles.sort()) { + if (file.endsWith(".md")) urls.push(urlEntry(`${site}/guides/${file.slice(0, -3)}`, 0.6)); +} // 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) { @@ -27,7 +70,10 @@ if (process.env.VITE_SUPABASE_URL && process.env.VITE_SUPABASE_PUBLISHABLE_KEY) headers: { ...headers, Range: `${start}-${start + pageSize - 1}`, Prefer: "count=exact" }, signal: controller.signal, }); - if (!response.ok) return rows; + if (!response.ok) { + console.warn(`Skipping sitemap enrichment for ${path}: HTTP ${response.status}`); + return rows; + } const page = await response.json(); if (!Array.isArray(page)) return rows; @@ -44,14 +90,17 @@ if (process.env.VITE_SUPABASE_URL && process.env.VITE_SUPABASE_PUBLISHABLE_KEY) } } }; + // Select only columns readable by the anonymous role: + // - profiles.updated_at is revoked from `anon`, so we only ask for username. + // - creator_packs has no updated_at column, so we use created_at for lastmod. const [profiles, packs, blogs] = await Promise.all([ fetchJson("profiles?select=username&username=not.is.null&order=username.asc"), - fetchJson("creator_packs?select=slug&status=eq.approved&order=slug.asc"), - fetchJson("blogs?select=slug&published=eq.true&order=slug.asc"), + fetchJson("creator_packs?select=slug,created_at&status=eq.approved&order=slug.asc"), + fetchJson("blogs?select=slug,updated_at&published=eq.true&order=slug.asc"), ]); - for (const { username } of profiles) if (username) urls.push(`${site}/u/${escapeXml(username)}`); - for (const { slug } of packs) if (slug) urls.push(`${site}/creator-packs/${escapeXml(slug)}`); - for (const { slug } of blogs) if (slug) urls.push(`${site}/blogs/${escapeXml(slug)}`); + for (const { username } of profiles) if (username) urls.push(urlEntry(`${site}/u/${username}`, 0.4)); + for (const { slug, created_at } of packs) if (slug) urls.push(urlEntry(`${site}/creator-packs/${slug}`, 0.6, "weekly", created_at)); + for (const { slug, updated_at } of blogs) if (slug) urls.push(urlEntry(`${site}/blogs/${slug}`, 0.6, "weekly", updated_at)); } await mkdir("public", { recursive: true }); diff --git a/src/App.tsx b/src/App.tsx index fe18522..d46c3ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,17 +16,28 @@ import { AdBlockDetector } from "@/components/AdBlockDetector"; import Navbar from "@/components/Navbar"; import Seo from "@/components/Seo"; +// Routes that should never be indexed by search engines. +const PRIVATE_ROUTE_PREFIXES = ['/admin', '/account', '/analytics', '/creator-packs/manage']; + +// Subset of private routes that also hide the global Donate button. +const DONATE_HIDDEN_ROUTE_PREFIXES = ['/admin', '/account']; + // Global components wrapper to use hooks like useLocation const GlobalComponents = () => { const location = useLocation(); - const hideDonateButton = location.pathname.startsWith('/admin') || - location.pathname.startsWith('/account'); + const isPrivateRoute = PRIVATE_ROUTE_PREFIXES.some((prefix) => location.pathname.startsWith(prefix)); + const hideDonateButton = DONATE_HIDDEN_ROUTE_PREFIXES.some((prefix) => location.pathname.startsWith(prefix)); const routeName = location.pathname === '/' ? 'Minecraft Creator Tools & Resources' : location.pathname.split('/').filter(Boolean).map((part) => part.split('-').join(' ')).join(' / '); return ( <> - letter.toUpperCase())} | RenderDragon`} description={`Explore ${routeName} on RenderDragon, free tools and resources for Minecraft content creators.`} path={location.pathname} /> + letter.toUpperCase())} | RenderDragon`} + description={`Explore ${routeName} on RenderDragon, free tools and resources for Minecraft content creators.`} + path={location.pathname} + robots={isPrivateRoute ? 'noindex, nofollow' : undefined} + /> {!hideDonateButton && } diff --git a/src/components/Seo.tsx b/src/components/Seo.tsx index 07654cf..7cb4a97 100644 --- a/src/components/Seo.tsx +++ b/src/components/Seo.tsx @@ -1,30 +1,62 @@ import { Helmet } from "react-helmet-async"; +import { DEFAULT_OG_IMAGE, SITE_URL, getOgImageDimensions } from "@/lib/site"; -const SITE_URL = "https://renderdragon.org"; +const DEFAULT_ROBOTS = "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"; + +type JsonLd = Record; interface SeoProps { title: string; description: string; path: string; image?: string; + imageAlt?: string; + type?: "website" | "article"; + robots?: string; + jsonLd?: JsonLd | JsonLd[]; } -export default function Seo({ title, description, path, image = "/ogimg.png" }: SeoProps) { +export default function Seo({ + title, + description, + path, + image = DEFAULT_OG_IMAGE, + imageAlt, + type = "website", + robots = DEFAULT_ROBOTS, + jsonLd, +}: SeoProps) { const normalizedPath = path.replace(/\/+$/, "") || "/"; const canonical = `${SITE_URL}${normalizedPath === "/" ? "" : normalizedPath}`; const imageUrl = image.startsWith("http") ? image : `${SITE_URL}${image}`; - return - {title} - - - - - - - - - - - - ; + const dimensions = getOgImageDimensions(image); + const alt = imageAlt ?? title; + const schemas = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : []; + + return ( + + {title} + + + {robots && } + + + + + + {dimensions && } + {dimensions && } + + + + + + + {schemas.map((schema, index) => ( + + ))} + + ); } diff --git a/src/lib/site.ts b/src/lib/site.ts new file mode 100644 index 0000000..90bea95 --- /dev/null +++ b/src/lib/site.ts @@ -0,0 +1,14 @@ +export const SITE_URL = "https://renderdragon.org"; +export const DEFAULT_OG_IMAGE = "/ogimg.png"; + +const OG_IMAGE_DIMENSIONS: Record = { + "/ogimg.png": { width: 1920, height: 1440 }, + "/renderdragon.png": { width: 900, height: 650 }, +}; + +export function getOgImageDimensions(image: string) { + if (OG_IMAGE_DIMENSIONS[image]) return OG_IMAGE_DIMENSIONS[image]; + // Every per-page social card under /ogimg/ is 1000x525. + if (image.startsWith("/ogimg/")) return { width: 1000, height: 525 }; + return undefined; +} diff --git a/src/lib/structuredData.ts b/src/lib/structuredData.ts new file mode 100644 index 0000000..ee9725a --- /dev/null +++ b/src/lib/structuredData.ts @@ -0,0 +1,47 @@ +import { SITE_URL } from "@/lib/site"; + +interface SoftwareApplicationOptions { + name: string; + description: string; + path: string; + image?: string; + category?: string; +} + +export function softwareApplicationSchema({ + name, + description, + path, + image, + category = "MultimediaApplication", +}: SoftwareApplicationOptions) { + return { + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name, + description, + url: `${SITE_URL}${path}`, + applicationCategory: category, + operatingSystem: "Web browser", + ...(image ? { image: `${SITE_URL}${image}` } : {}), + offers: { + "@type": "Offer", + price: "0", + priceCurrency: "USD", + }, + publisher: { "@id": `${SITE_URL}/#organization` }, + }; +} + +export function breadcrumbSchema(items: { name: string; path: string }[]) { + return { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: items.map((item, index) => ({ + "@type": "ListItem", + position: index + 1, + name: item.name, + item: `${SITE_URL}${item.path}`, + })), + }; +} diff --git a/src/pages/AiTitleHelper.tsx b/src/pages/AiTitleHelper.tsx index 9eecbc7..ce06d23 100644 --- a/src/pages/AiTitleHelper.tsx +++ b/src/pages/AiTitleHelper.tsx @@ -1,6 +1,7 @@ import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; -import { Helmet } from 'react-helmet-async'; +import Seo from '@/components/Seo'; +import { softwareApplicationSchema } from '@/lib/structuredData'; import { Card, CardContent } from '@/components/ui/card'; import { Label } from '@/components/ui/label'; import { Input } from '@/components/ui/input'; @@ -75,10 +76,19 @@ const AiTitleHelper = () => { return (
- - AI Title Helper - Renderdragon - - +
diff --git a/src/pages/BackgroundGenerator.tsx b/src/pages/BackgroundGenerator.tsx index 3399931..2cc6fa1 100644 --- a/src/pages/BackgroundGenerator.tsx +++ b/src/pages/BackgroundGenerator.tsx @@ -25,7 +25,8 @@ import { IconRotateClockwise, } from "@tabler/icons-react"; import { toast } from "sonner"; -import { Helmet } from "react-helmet-async"; +import Seo from "@/components/Seo"; +import { softwareApplicationSchema } from "@/lib/structuredData"; import { fetchFromAssetsApi } from "@/lib/assetsApi"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -623,38 +624,18 @@ const BackgroundGenerator = () => { return (
- - Background Generator - Renderdragon - - - - - - - - - +
diff --git a/src/pages/BlogView.tsx b/src/pages/BlogView.tsx index 2b1f93d..b377014 100644 --- a/src/pages/BlogView.tsx +++ b/src/pages/BlogView.tsx @@ -2,7 +2,9 @@ import { useEffect, useState } from "react"; import { useParams, Link } from "react-router-dom"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; -import { Helmet } from "react-helmet-async"; +import Seo from "@/components/Seo"; +import { breadcrumbSchema } from "@/lib/structuredData"; +import { SITE_URL } from "@/lib/site"; import ReactMarkdown from "react-markdown"; import { IconArrowLeft, IconLoader2, IconCalendar, IconUser } from "@tabler/icons-react"; import { supabase } from "@/integrations/supabase/client"; @@ -101,10 +103,32 @@ export default function BlogView() { return (
- - {blog.title} - Renderdragon Blog - - + diff --git a/src/pages/FAQ.tsx b/src/pages/FAQ.tsx index 08d6408..0e39712 100644 --- a/src/pages/FAQ.tsx +++ b/src/pages/FAQ.tsx @@ -1,119 +1,96 @@ -import { useEffect } from 'react'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; - -import { Helmet } from 'react-helmet-async'; +import Seo from '@/components/Seo'; + +interface FaqEntry { + question: string; + answer: string; +} + +const FAQ_GROUPS: { title: string; entries: FaqEntry[] }[] = [ + { + title: 'General Questions', + entries: [ + { question: "Is everything on Renderdragon really free?", answer: "Yes! All resources, tools, and guides on Renderdragon are 100% free to use. We believe in making content creation accessible to everyone." }, + { question: "Do I need to credit Renderdragon when using resources?", answer: "While crediting is not required, it's always appreciated! A simple mention helps spread the word and supports our mission to help more creators." }, + { question: "Can I use resources for commercial projects?", answer: "Yes, you can use our resources in your commercial projects unless specifically stated otherwise on the resource page." }, + ], + }, + { + title: 'Technical Questions', + entries: [ + { question: "What file formats do you support?", answer: "We provide resources in various formats including PNG, MP3, WAV, PSD, and more. Each resource specifies its available formats." }, + { question: "Are the tools compatible with my device?", answer: "Our tools are web-based and work on any modern browser, regardless of your operating system (Windows, Mac, Linux, etc.)." }, + { question: "What if I encounter technical issues?", answer: "If you experience any technical problems, please reach out through our Discord server or contact page. Our team is here to help!" }, + ], + }, + { + title: 'Resource Usage', + entries: [ + { question: "Can I modify the resources?", answer: "Yes, you're free to modify our resources to suit your needs. We encourage creativity!" }, + { question: "Are there any usage restrictions?", answer: "The only restriction is reselling or redistributing our resources as-is. Please don't claim our resources as your own or share them on other platforms." }, + { question: "What about copyright claims?", answer: "We strive to provide copyright-safe resources, but it's always good practice to check the specific terms for each resource, especially for music and sound effects." }, + ], + }, + { + title: 'Contact & Support', + entries: [ + { question: "How can I get help?", answer: "Join our Discord server for quick support, or use the Contact page for specific inquiries. We typically respond within 48 hours." }, + { question: "Can I suggest new features or resources?", answer: "Absolutely! We love hearing from our community. Share your suggestions on our Discord server or through the Contact page." }, + { question: "How can I support Renderdragon?", answer: "The best ways to support us are spreading the word, giving credit when using our resources, and considering a donation if you'd like to contribute financially." }, + ], + }, +]; + +const faqSchema = { + "@context": "https://schema.org", + "@type": "FAQPage", + mainEntity: FAQ_GROUPS.flatMap((group) => group.entries).map(({ question, answer }) => ({ + "@type": "Question", + name: question, + acceptedAnswer: { "@type": "Answer", text: answer }, + })), +}; const FAQ = () => { return (
- - FAQ - Renderdragon - - - - - - - - - +
-

- Frequently Asked Questions +

+ Frequently Asked Questions

-
-

General Questions

- -
-
-

Is everything on Renderdragon really free?

-

Yes! All resources, tools, and guides on Renderdragon are 100% free to use. We believe in making content creation accessible to everyone.

-
- -
-

Do I need to credit Renderdragon when using resources?

-

While crediting is not required, it's always appreciated! A simple mention helps spread the word and supports our mission to help more creators.

-
- -
-

Can I use resources for commercial projects?

-

Yes, you can use our resources in your commercial projects unless specifically stated otherwise on the resource page.

-
-
-
- -
-

Technical Questions

- -
-
-

What file formats do you support?

-

We provide resources in various formats including PNG, MP3, WAV, PSD, and more. Each resource specifies its available formats.

-
- -
-

Are the tools compatible with my device?

-

Our tools are web-based and work on any modern browser, regardless of your operating system (Windows, Mac, Linux, etc.).

-
- -
-

What if I encounter technical issues?

-

If you experience any technical problems, please reach out through our Discord server or contact page. Our team is here to help!

-
-
-
- -
-

Resource Usage

- -
-
-

Can I modify the resources?

-

Yes, you're free to modify our resources to suit your needs. We encourage creativity!

-
- -
-

Are there any usage restrictions?

-

The only restriction is reselling or redistributing our resources as-is. Please don't claim our resources as your own or share them on other platforms.

-
- -
-

What about copyright claims?

-

We strive to provide copyright-safe resources, but it's always good practice to check the specific terms for each resource, especially for music and sound effects.

-
-
-
- -
-

Contact & Support

- -
-
-

How can I get help?

-

Join our Discord server for quick support, or use the Contact page for specific inquiries. We typically respond within 48 hours.

-
- -
-

Can I suggest new features or resources?

-

Absolutely! We love hearing from our community. Share your suggestions on our Discord server or through the Contact page.

-
- -
-

How can I support Renderdragon?

-

The best ways to support us are spreading the word, giving credit when using our resources, and considering a donation if you'd like to contribute financially.

+ {FAQ_GROUPS.map((group, groupIndex) => ( +
+

+ {group.title} +

+ +
+ {group.entries.map(({ question, answer }) => ( +
+

{question}

+

{answer}

+
+ ))}
-
+ ))}
@@ -126,4 +103,4 @@ const FAQ = () => { ); }; -export default FAQ; \ No newline at end of file +export default FAQ; diff --git a/src/pages/GuideView.tsx b/src/pages/GuideView.tsx index b08ff08..b62455a 100644 --- a/src/pages/GuideView.tsx +++ b/src/pages/GuideView.tsx @@ -3,7 +3,9 @@ import { useParams, Link } from "react-router-dom"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; -import { Helmet } from "react-helmet-async"; +import Seo from "@/components/Seo"; +import { breadcrumbSchema } from "@/lib/structuredData"; +import { SITE_URL } from "@/lib/site"; import ReactMarkdown from "react-markdown"; import { IconArrowLeft, IconList, IconLoader2 } from "@tabler/icons-react"; @@ -69,32 +71,32 @@ export default function GuideView() { return (
- - {title} - Guides - Renderdragon - - - - - - - - - + diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index 1f99144..ba61e95 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -1,5 +1,6 @@ import { motion } from 'framer-motion'; -import { Helmet } from 'react-helmet-async'; +import Seo from '@/components/Seo'; +import { softwareApplicationSchema } from '@/lib/structuredData'; import Navbar from '@/components/Navbar'; import Hero from '@/components/Hero'; import TrustBar from '@/components/TrustBar'; @@ -20,17 +21,18 @@ const fadeInUp = { const Index = () => { return (
- - RenderDragon - Free Minecraft Creator Tools & Resources - - - - - - - - - +
{ @@ -18,7 +19,19 @@ const MusicCopyright = () => { const showRecord = (record: LooneyHistoryRecord) => { if (record.result) navigate(`/gappa/check/${encodeURIComponent(record.jobId)}`); }; return
- Looney Checks - Renderdragon +
diff --git a/src/pages/PlayerRenderer.tsx b/src/pages/PlayerRenderer.tsx index 35ce7ac..649d849 100644 --- a/src/pages/PlayerRenderer.tsx +++ b/src/pages/PlayerRenderer.tsx @@ -2,7 +2,8 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { IconDownload, IconCopy, IconRefresh, IconLoader2 } from '@tabler/icons-react'; import { toast } from 'sonner'; -import { Helmet } from 'react-helmet-async'; +import Seo from '@/components/Seo'; +import { softwareApplicationSchema } from '@/lib/structuredData'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Button } from '@/components/ui/button'; @@ -145,17 +146,18 @@ const PlayerRenderer = () => { return (
- - Player Renderer - Renderdragon - - - - - - - - - + diff --git a/src/pages/TextGenerator.tsx b/src/pages/TextGenerator.tsx index b0daf2b..8c55c44 100644 --- a/src/pages/TextGenerator.tsx +++ b/src/pages/TextGenerator.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from 'react'; -import { Helmet } from 'react-helmet-async'; +import Seo from '@/components/Seo'; +import { softwareApplicationSchema } from '@/lib/structuredData'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Button } from '@/components/ui/button'; @@ -108,10 +109,18 @@ const TextGenerator = () => { return ( <> - - Minecraft Text Generator - - + diff --git a/src/pages/YouTubeDownloader.tsx b/src/pages/YouTubeDownloader.tsx index d6dfd90..7ee787f 100644 --- a/src/pages/YouTubeDownloader.tsx +++ b/src/pages/YouTubeDownloader.tsx @@ -7,7 +7,8 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { toast } from 'sonner'; -import { Helmet } from 'react-helmet-async'; +import Seo from '@/components/Seo'; +import { softwareApplicationSchema } from '@/lib/structuredData'; import VideoInfoSkeleton from '@/components/skeletons/VideoInfoSkeleton'; // API types from mediapye YouTube Video Inspector @@ -259,13 +260,19 @@ const YouTubeDownloader: React.FC = () => { return (
- - Youtube Tools - Renderdragon - - +
diff --git a/supabase/migrations/20261003000000_blogs_public_read_policy.sql b/supabase/migrations/20261003000000_blogs_public_read_policy.sql new file mode 100644 index 0000000..e6a5e98 --- /dev/null +++ b/supabase/migrations/20261003000000_blogs_public_read_policy.sql @@ -0,0 +1,9 @@ +-- Public read access for published blog posts. +-- Mirrors the creator_packs policy so anonymous visitors and search-engine +-- crawlers can read published posts without signing in. +ALTER TABLE blogs ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS "Published blogs are publicly readable" ON blogs; +CREATE POLICY "Published blogs are publicly readable" ON blogs +FOR SELECT +USING (published = true); diff --git a/vite.config.ts b/vite.config.ts index faf783a..1b80964 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,4 @@ import { defineConfig } from "vite"; -import sitemap from 'vite-plugin-sitemap'; import react from "@vitejs/plugin-react-swc"; import path from "path"; @@ -34,11 +33,8 @@ export default defineConfig(() => { }, }, plugins: [ - sitemap({ - hostname: 'https://renderdragon.org', - }), react(), - ].filter(Boolean), + ], resolve: { alias: { "@": path.resolve(__dirname, "./src"),