summaryrefslogtreecommitdiff
path: root/apps/web/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/app')
-rw-r--r--apps/web/src/app/api/auth/[...all]/route.ts4
-rw-r--r--apps/web/src/app/api/health/route.ts3
-rw-r--r--apps/web/src/app/api/sounds/search/route.ts280
-rw-r--r--apps/web/src/app/blog/[slug]/page.tsx135
-rw-r--r--apps/web/src/app/blog/page.tsx52
-rw-r--r--apps/web/src/app/changelog/[version]/page.tsx2
-rw-r--r--apps/web/src/app/editor/page.tsx (renamed from apps/web/src/app/editor/[project_id]/page.tsx)16
-rw-r--r--apps/web/src/app/layout.tsx12
-rw-r--r--apps/web/src/app/projects/page.tsx10
-rw-r--r--apps/web/src/app/robots.ts2
-rw-r--r--apps/web/src/app/rss.xml/route.ts46
-rw-r--r--apps/web/src/app/sitemap.ts100
12 files changed, 61 insertions, 601 deletions
diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts
deleted file mode 100644
index edf0d0c2..00000000
--- a/apps/web/src/app/api/auth/[...all]/route.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { auth } from "@/auth/server";
-import { toNextJsHandler } from "better-auth/next-js";
-
-export const { POST, GET } = toNextJsHandler(auth);
diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts
deleted file mode 100644
index dbe6fdb1..00000000
--- a/apps/web/src/app/api/health/route.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export async function GET() {
- return new Response("OK", { status: 200 });
-}
diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts
deleted file mode 100644
index aa7a1b60..00000000
--- a/apps/web/src/app/api/sounds/search/route.ts
+++ /dev/null
@@ -1,280 +0,0 @@
-import { webEnv } from "@/env/web";
-import { type NextRequest, NextResponse } from "next/server";
-import { z } from "zod";
-import { checkRateLimit } from "@/auth/rate-limit";
-
-const searchParamsSchema = z.object({
- q: z.string().max(500, "Query too long").optional(),
- type: z.enum(["songs", "effects"]).optional(),
- page: z.coerce.number().int().min(1).max(1000).default(1),
- page_size: z.coerce.number().int().min(1).max(150).default(20),
- sort: z
- .enum(["downloads", "rating", "created", "score"])
- .default("downloads"),
- min_rating: z.coerce.number().min(0).max(5).default(3),
- commercial_only: z.coerce.boolean().default(true),
-});
-
-const freesoundResultSchema = z.object({
- id: z.number(),
- name: z.string(),
- description: z.string(),
- url: z.string().url(),
- previews: z
- .object({
- "preview-hq-mp3": z.string().url(),
- "preview-lq-mp3": z.string().url(),
- "preview-hq-ogg": z.string().url(),
- "preview-lq-ogg": z.string().url(),
- })
- .optional(),
- download: z.string().url().optional(),
- duration: z.number(),
- filesize: z.number(),
- type: z.string(),
- channels: z.number(),
- bitrate: z.number(),
- bitdepth: z.number(),
- samplerate: z.number(),
- username: z.string(),
- tags: z.array(z.string()),
- license: z.string(),
- created: z.string(),
- num_downloads: z.number().optional(),
- avg_rating: z.number().optional(),
- num_ratings: z.number().optional(),
-});
-
-const freesoundResponseSchema = z.object({
- count: z.number(),
- next: z.string().url().nullable(),
- previous: z.string().url().nullable(),
- results: z.array(freesoundResultSchema),
-});
-
-const transformedResultSchema = z.object({
- id: z.number(),
- name: z.string(),
- description: z.string(),
- url: z.string(),
- previewUrl: z.string().optional(),
- downloadUrl: z.string().optional(),
- duration: z.number(),
- filesize: z.number(),
- type: z.string(),
- channels: z.number(),
- bitrate: z.number(),
- bitdepth: z.number(),
- samplerate: z.number(),
- username: z.string(),
- tags: z.array(z.string()),
- license: z.string(),
- created: z.string(),
- downloads: z.number().optional(),
- rating: z.number().optional(),
- ratingCount: z.number().optional(),
-});
-
-const apiResponseSchema = z.object({
- count: z.number(),
- next: z.string().nullable(),
- previous: z.string().nullable(),
- results: z.array(transformedResultSchema),
- query: z.string().optional(),
- type: z.string(),
- page: z.number(),
- pageSize: z.number(),
- sort: z.string(),
- minRating: z.number().optional(),
-});
-
-function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
- if (!query) return `${sort}_desc`;
- return sort === "score" ? "score" : `${sort}_desc`;
-}
-
-function applyEffectsFilters({
- params,
- min_rating,
- commercial_only,
-}: {
- params: URLSearchParams;
- min_rating: number;
- commercial_only: boolean;
-}) {
- params.append("filter", "duration:[* TO 30.0]");
- params.append("filter", `avg_rating:[${min_rating} TO *]`);
-
- if (commercial_only) {
- params.append(
- "filter",
- 'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")',
- );
- }
-
- params.append(
- "filter",
- "tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion",
- );
-}
-
-function transformFreesoundResult(
- result: z.infer<typeof freesoundResultSchema>,
-) {
- return {
- id: result.id,
- name: result.name,
- description: result.description,
- url: result.url,
- previewUrl:
- result.previews?.["preview-hq-mp3"] ||
- result.previews?.["preview-lq-mp3"],
- downloadUrl: result.download,
- duration: result.duration,
- filesize: result.filesize,
- type: result.type,
- channels: result.channels,
- bitrate: result.bitrate,
- bitdepth: result.bitdepth,
- samplerate: result.samplerate,
- username: result.username,
- tags: result.tags,
- license: result.license,
- created: result.created,
- downloads: result.num_downloads || 0,
- rating: result.avg_rating || 0,
- ratingCount: result.num_ratings || 0,
- };
-}
-
-export async function GET(request: NextRequest) {
- try {
- const { limited } = await checkRateLimit({ request });
- if (limited) {
- return NextResponse.json({ error: "Too many requests" }, { status: 429 });
- }
-
- const { searchParams } = new URL(request.url);
-
- const validationResult = searchParamsSchema.safeParse({
- q: searchParams.get("q") || undefined,
- type: searchParams.get("type") || undefined,
- page: searchParams.get("page") || undefined,
- page_size: searchParams.get("page_size") || undefined,
- sort: searchParams.get("sort") || undefined,
- min_rating: searchParams.get("min_rating") || undefined,
- });
-
- if (!validationResult.success) {
- return NextResponse.json(
- {
- error: "Invalid parameters",
- details: validationResult.error.flatten().fieldErrors,
- },
- { status: 400 },
- );
- }
-
- const {
- q: query,
- type,
- page,
- page_size: pageSize,
- sort,
- min_rating,
- commercial_only,
- } = validationResult.data;
-
- if (type === "songs") {
- return NextResponse.json(
- {
- error: "Songs are not available yet",
- message:
- "Song search functionality is coming soon. Try searching for sound effects instead.",
- },
- { status: 501 },
- );
- }
-
- const baseUrl = "https://freesound.org/apiv2/search/text/";
-
- const sortParam = buildSortParameter({ query, sort });
-
- const params = new URLSearchParams({
- query: query || "",
- token: webEnv.FREESOUND_API_KEY,
- page: page.toString(),
- page_size: pageSize.toString(),
- sort: sortParam,
- fields:
- "id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
- });
-
- const isEffectsSearch = type === "effects" || !type;
- if (isEffectsSearch) {
- applyEffectsFilters({ params, min_rating, commercial_only });
- }
-
- const response = await fetch(`${baseUrl}?${params.toString()}`);
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error("Freesound API error:", response.status, errorText);
- return NextResponse.json(
- { error: "Failed to search sounds" },
- { status: response.status },
- );
- }
-
- const rawData = await response.json();
-
- const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
- if (!freesoundValidation.success) {
- console.error(
- "Invalid Freesound API response:",
- freesoundValidation.error,
- );
- return NextResponse.json(
- { error: "Invalid response from Freesound API" },
- { status: 502 },
- );
- }
-
- const data = freesoundValidation.data;
-
- const transformedResults = data.results.map(transformFreesoundResult);
-
- const responseData = {
- count: data.count,
- next: data.next,
- previous: data.previous,
- results: transformedResults,
- query: query || "",
- type: type || "effects",
- page,
- pageSize,
- sort,
- minRating: min_rating,
- };
-
- const responseValidation = apiResponseSchema.safeParse(responseData);
- if (!responseValidation.success) {
- console.error(
- "Invalid API response structure:",
- responseValidation.error,
- );
- return NextResponse.json(
- { error: "Internal response formatting error" },
- { status: 500 },
- );
- }
-
- return NextResponse.json(responseValidation.data);
- } catch (error) {
- console.error("Error searching sounds:", error);
- return NextResponse.json(
- { error: "Internal server error" },
- { status: 500 },
- );
- }
-}
diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx
deleted file mode 100644
index dba9b504..00000000
--- a/apps/web/src/app/blog/[slug]/page.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import type { Metadata } from "next";
-import Image from "next/image";
-import { notFound } from "next/navigation";
-import { BasePage } from "@/app/base-page";
-import Prose from "@/components/ui/prose";
-import { Separator } from "@/components/ui/separator";
-import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query";
-import type { Author, Post } from "@/blog/types";
-
-type PageProps = {
- params: Promise<{ slug: string }>;
- searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
-};
-
-export async function generateMetadata({
- params,
-}: PageProps): Promise<Metadata> {
- const slug = (await params).slug;
-
- const data = await getSinglePost({ slug });
-
- if (!data || !data.post) return {};
-
- return {
- title: data.post.title,
- description: data.post.description,
- twitter: {
- title: `${data.post.title}`,
- description: `${data.post.description}`,
- card: "summary_large_image",
- images: [
- {
- url: data.post.coverImage,
- width: "1200",
- height: "630",
- alt: data.post.title,
- },
- ],
- },
- openGraph: {
- type: "article",
- images: [
- {
- url: data.post.coverImage,
- width: "1200",
- height: "630",
- alt: data.post.title,
- },
- ],
- title: data.post.title,
- description: data.post.description,
- publishedTime: new Date(data.post.publishedAt).toISOString(),
- authors: data.post.authors.map((author: Author) => author.name),
- },
- };
-}
-
-export async function generateStaticParams() {
- const data = await getPosts();
- if (!data || !data.posts.length) return [];
-
- return data.posts.map((post) => ({
- slug: post.slug,
- }));
-}
-
-export default async function BlogPostPage({ params }: PageProps) {
- const slug = (await params).slug;
- const data = await getSinglePost({ slug });
- if (!data || !data.post) return notFound();
-
- const html = await processHtmlContent({ html: data.post.content });
-
- return (
- <BasePage>
- <PostHeader post={data.post} />
- <Separator />
- <PostContent html={html} />
- </BasePage>
- );
-}
-
-function PostHeader({ post }: { post: Post }) {
- const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", {
- day: "numeric",
- month: "long",
- year: "numeric",
- });
-
- return (
- <div className="flex flex-col items-center justify-center gap-8">
- <PostMeta date={formattedDate} publishedAt={post.publishedAt} />
- <PostTitle title={post.title} />
- {post.coverImage && <PostCoverImage post={post} />}
- </div>
- );
-}
-
-function PostCoverImage({ post }: { post: Post }) {
- return (
- <div className="relative aspect-video overflow-hidden rounded-lg w-full mt-4">
- <Image
- src={post.coverImage}
- alt={post.title}
- loading="eager"
- fill
- className="rounded-lg object-cover"
- />
- </div>
- );
-}
-
-function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) {
- return (
- <div className="flex items-center justify-center">
- <time dateTime={publishedAt.toString()}>{date}</time>
- </div>
- );
-}
-
-function PostTitle({ title }: { title: string }) {
- return (
- <h1 className="text-5xl font-bold tracking-tight md:text-4xl text-center">
- {title}
- </h1>
- );
-}
-
-function PostContent({ html }: { html: string }) {
- return (
- <section className="">
- <Prose html={html} />
- </section>
- );
-}
diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx
deleted file mode 100644
index b40bc2fc..00000000
--- a/apps/web/src/app/blog/page.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { Metadata } from "next";
-import Link from "next/link";
-import { BasePage } from "@/app/base-page";
-import { Separator } from "@/components/ui/separator";
-import { getPosts } from "@/blog/query";
-import type { Post } from "@/blog/types";
-
-export const metadata: Metadata = {
- title: "Blog - OpenCut",
- description:
- "Read the latest news and updates about OpenCut, the free and open-source video editor.",
- openGraph: {
- title: "Blog - OpenCut",
- description:
- "Read the latest news and updates about OpenCut, the free and open-source video editor.",
- type: "website",
- },
-};
-
-export default async function BlogPage() {
- const data = await getPosts().catch(() => null);
- if (!data || !data.posts) return <div>No posts yet</div>;
-
- return (
- <BasePage
- title="Blog"
- description="Read the latest news and updates about OpenCut, the free and open-source video editor."
- >
- <div className="flex flex-col">
- {data.posts.map((post) => (
- <div key={post.id} className="flex flex-col">
- <BlogPostItem post={post} />
- <Separator />
- </div>
- ))}
- </div>
- </BasePage>
- );
-}
-
-function BlogPostItem({ post }: { post: Post }) {
- return (
- <Link href={`/blog/${post.slug}`}>
- <div className="flex h-auto w-full items-center justify-between py-6 opacity-100 hover:opacity-75">
- <div className="flex flex-col gap-2">
- <h2 className="text-xl font-semibold">{post.title}</h2>
- <p className="text-muted-foreground">{post.description}</p>
- </div>
- </div>
- </Link>
- );
-}
diff --git a/apps/web/src/app/changelog/[version]/page.tsx b/apps/web/src/app/changelog/[version]/page.tsx
index 6c529ce1..b6623cf3 100644
--- a/apps/web/src/app/changelog/[version]/page.tsx
+++ b/apps/web/src/app/changelog/[version]/page.tsx
@@ -15,6 +15,8 @@ import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button"
type Props = { params: Promise<{ version: string }> };
+export const dynamicParams = false;
+
export async function generateStaticParams() {
return getSortedReleases().map((release) => ({ version: release.version }));
}
diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/page.tsx
index b759b9c2..8229774a 100644
--- a/apps/web/src/app/editor/[project_id]/page.tsx
+++ b/apps/web/src/app/editor/page.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useParams } from "next/navigation";
+import { useSearchParams } from "next/navigation";
import {
ResizablePanelGroup,
ResizablePanel,
@@ -17,7 +17,7 @@ import { MigrationDialog } from "@/project/components/migration-dialog";
import { usePanelStore } from "@/editor/panel-store";
import { usePasteMedia } from "@/media/use-paste-media";
import { MobileGate } from "@/components/editor/mobile-gate";
-import { useMemo, useState } from "react";
+import { Suspense, useMemo, useState } from "react";
import { useEditor } from "@/editor/use-editor";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@@ -36,8 +36,16 @@ import {
} from "@/timeline/bookmarks/index";
export default function Editor() {
- const params = useParams<{ project_id: string }>();
- const projectId = params.project_id;
+ return (
+ <Suspense>
+ <EditorView />
+ </Suspense>
+ );
+}
+
+function EditorView() {
+ const searchParams = useSearchParams();
+ const projectId = searchParams.get("project") ?? "";
return (
<MobileGate>
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 558ae538..9713d9d5 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -5,21 +5,12 @@ import { Toaster } from "../components/ui/sonner";
import { ChangelogNotification } from "@/changelog/components/changelog-notification";
import { TooltipProvider } from "../components/ui/tooltip";
import { baseMetaData } from "./metadata";
-import { BotIdClient } from "botid/client";
-import { webEnv } from "@/env/web";
import { Inter } from "next/font/google";
const siteFont = Inter({ subsets: ["latin"] });
export const metadata = baseMetaData;
-const protectedRoutes = [
- {
- path: "/none",
- method: "GET",
- },
-];
-
export default function RootLayout({
children,
}: Readonly<{
@@ -28,7 +19,6 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<head>
- <BotIdClient protect={protectedRoutes} />
{process.env.NODE_ENV === "development" && (
<>
<Script
@@ -52,7 +42,7 @@ export default function RootLayout({
strategy="afterInteractive"
async
data-client-id="UP-Wcoy5arxFeK7oyjMMZ"
- data-disabled={webEnv.NODE_ENV === "development"}
+ data-disabled={process.env.NODE_ENV === "development"}
data-track-attributes={false}
data-track-errors={true}
data-track-outgoing-links={false}
diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index d238af9e..7cf50c93 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -515,7 +515,7 @@ function NewProjectButton() {
const projectId = await editor.project.createNewProject({
name: "New project",
});
- router.push(`/editor/${projectId}`);
+ router.push(`/editor?project=${projectId}`);
};
return (
@@ -553,7 +553,7 @@ function ImportProjectButton() {
try {
const projectId = await editor.project.importArchive({ file });
if (projectId) {
- router.push(`/editor/${projectId}`);
+ router.push(`/editor?project=${projectId}`);
}
} finally {
setIsImporting(false);
@@ -727,7 +727,7 @@ function ProjectItem({
className="size-5 shrink-0"
/>
- <Link href={`/editor/${project.id}`} className="flex-1 min-w-0">
+ <Link href={`/editor?project=${project.id}`} className="flex-1 min-w-0">
{listRowContent}
</Link>
@@ -753,7 +753,7 @@ function ProjectItem({
<div className="group relative">
{isGridView ? (
<>
- <Link href={`/editor/${project.id}`} className="block">
+ <Link href={`/editor?project=${project.id}`} className="block">
{gridContent}
</Link>
@@ -1043,7 +1043,7 @@ function EmptyState() {
const projectId = await editor.project.createNewProject({
name: "New project",
});
- router.push(`/editor/${projectId}`);
+ router.push(`/editor?project=${projectId}`);
} catch (error) {
toast.error("Failed to create project", {
description:
diff --git a/apps/web/src/app/robots.ts b/apps/web/src/app/robots.ts
index 1b1ca865..6f316d98 100644
--- a/apps/web/src/app/robots.ts
+++ b/apps/web/src/app/robots.ts
@@ -1,6 +1,8 @@
import type { MetadataRoute } from "next";
import { SITE_URL } from "@/site/brand";
+export const dynamic = "force-static";
+
export default function robots(): MetadataRoute.Robots {
return {
rules: {
diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts
deleted file mode 100644
index d4e9570a..00000000
--- a/apps/web/src/app/rss.xml/route.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Feed } from "feed";
-import { getPosts } from "@/blog/query";
-import { SITE_INFO, SITE_URL } from "@/site/brand";
-
-export async function GET() {
- try {
- const { posts } = await getPosts();
-
- const feed = new Feed({
- title: `${SITE_INFO.title} Blog`,
- description: SITE_INFO.description,
- id: `${SITE_URL}`,
- link: `${SITE_URL}/blog/`,
- language: "en",
- image: `${SITE_INFO.openGraphImage}`,
- favicon: `${SITE_INFO.favicon}`,
- copyright: `All rights reserved ${new Date().getFullYear()}, ${
- SITE_INFO.title
- }`,
- });
-
- for (const post of posts) {
- feed.addItem({
- title: post.title,
- id: `${SITE_URL}/blog/${post.slug}`,
- link: `${SITE_URL}/blog/${post.slug}`,
- description: post.description,
- author: post.authors.map((author) => ({
- name: author.name,
- })),
- date: new Date(post.publishedAt),
- image: post.coverImage || SITE_INFO.openGraphImage,
- });
- }
-
- return new Response(feed.rss2(), {
- headers: {
- "Content-Type": "text/xml",
- "Cache-Control": "public, max-age=86400, stale-while-revalidate",
- },
- });
- } catch (error) {
- console.error("Error generating RSS feed", error);
- return new Response("Internal Server Error", { status: 500 });
- }
-}
diff --git a/apps/web/src/app/sitemap.ts b/apps/web/src/app/sitemap.ts
index 73b4180f..a473d2d9 100644
--- a/apps/web/src/app/sitemap.ts
+++ b/apps/web/src/app/sitemap.ts
@@ -1,61 +1,39 @@
-import { SITE_URL } from "@/site/brand";
-import { getPosts } from "@/blog/query";
-import type { MetadataRoute } from "next";
-
-export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
- const data = await getPosts();
-
- const postPages: MetadataRoute.Sitemap =
- data?.posts?.map((post) => ({
- url: `${SITE_URL}/blog/${post.slug}`,
- lastModified: new Date(post.publishedAt),
- changeFrequency: "weekly",
- priority: 0.8,
- })) ?? [];
-
- return [
- {
- url: SITE_URL,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/contributors`,
- lastModified: new Date(),
- changeFrequency: "daily",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/roadmap`,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/privacy`,
- lastModified: new Date(),
- changeFrequency: "monthly",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/terms`,
- lastModified: new Date(),
- changeFrequency: "monthly",
- priority: 0.5,
- },
- {
- url: `${SITE_URL}/why-not-capcut`,
- lastModified: new Date(),
- changeFrequency: "yearly",
- priority: 1,
- },
- {
- url: `${SITE_URL}/blog`,
- lastModified: new Date(),
- changeFrequency: "weekly",
- priority: 1,
- },
- ...postPages,
- ];
-}
+import { SITE_URL } from "@/site/brand";
+import type { MetadataRoute } from "next";
+
+export const dynamic = "force-static";
+
+export default function sitemap(): MetadataRoute.Sitemap {
+ return [
+ {
+ url: SITE_URL,
+ lastModified: new Date(),
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ {
+ url: `${SITE_URL}/contributors`,
+ lastModified: new Date(),
+ changeFrequency: "daily",
+ priority: 0.5,
+ },
+ {
+ url: `${SITE_URL}/roadmap`,
+ lastModified: new Date(),
+ changeFrequency: "weekly",
+ priority: 1,
+ },
+ {
+ url: `${SITE_URL}/privacy`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.5,
+ },
+ {
+ url: `${SITE_URL}/terms`,
+ lastModified: new Date(),
+ changeFrequency: "monthly",
+ priority: 0.5,
+ },
+ ];
+}