diff --git a/apps/web/package.json b/apps/web/package.json index c409136c..1d16576e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -38,6 +38,7 @@ "dotenv": "^16.5.0", "drizzle-orm": "^0.44.2", "embla-carousel-react": "^8.5.1", + "feed": "^5.1.0", "framer-motion": "^11.13.1", "input-otp": "^1.4.1", "lucide-react": "^0.468.0", diff --git a/apps/web/src/app/metadata.ts b/apps/web/src/app/metadata.ts index 6f9e6023..de8cf6f0 100644 --- a/apps/web/src/app/metadata.ts +++ b/apps/web/src/app/metadata.ts @@ -1,25 +1,29 @@ import { Metadata } from "next"; -const title = "OpenCut"; -const description = - "A simple but powerful video editor that gets the job done. In your browser."; -const openGraphImageUrl = "https://opencut.app/open-graph/default.jpg"; -const twitterImageUrl = "/open-graph/default.jpg"; +export const SITE_INFO = { + title: "OpenCut", + description: + "A simple but powerful video editor that gets the job done. In your browser.", + url: "https://opencut.app", + openGraphImage: "https://opencut.app/open-graph/default.jpg", + twitterImage: "/open-graph/default.jpg", + favicon: "/favicon.ico", +}; export const baseMetaData: Metadata = { metadataBase: new URL("https://opencut.app"), - title: title, - description: description, + title: SITE_INFO.title, + description: SITE_INFO.description, openGraph: { - title: title, - description: description, + title: SITE_INFO.title, + description: SITE_INFO.description, url: "https://opencut.app", siteName: "OpenCut", locale: "en_US", type: "website", images: [ { - url: openGraphImageUrl, + url: SITE_INFO.openGraphImage, width: 1200, height: 630, alt: "OpenCut", @@ -28,10 +32,10 @@ export const baseMetaData: Metadata = { }, twitter: { card: "summary_large_image", - title: title, - description: description, + title: SITE_INFO.title, + description: SITE_INFO.description, creator: "@opencutapp", - images: [twitterImageUrl], + images: [SITE_INFO.twitterImage], }, pinterest: { richPin: false, @@ -82,7 +86,7 @@ export const baseMetaData: Metadata = { }, appleWebApp: { capable: true, - title: title, + title: SITE_INFO.title, }, manifest: "/manifest.json", other: { diff --git a/apps/web/src/app/rss.xml/route.ts b/apps/web/src/app/rss.xml/route.ts new file mode 100644 index 00000000..b16bd856 --- /dev/null +++ b/apps/web/src/app/rss.xml/route.ts @@ -0,0 +1,46 @@ +import { Feed } from 'feed'; +import { getPosts } from '@/lib/blog-query'; +import { SITE_INFO } from '../metadata'; + +export async function GET() { + try { + const { posts } = await getPosts(); + + const feed = new Feed({ + title: 'OpenCut Blog', + description: SITE_INFO.description, + id: `${SITE_INFO.url}`, + link: `${SITE_INFO.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_INFO.url}/blog/${post.slug}`, + link: `${SITE_INFO.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 }); + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index 52cfff88..e986b2aa 100644 --- a/bun.lock +++ b/bun.lock @@ -39,6 +39,7 @@ "dotenv": "^16.5.0", "drizzle-orm": "^0.44.2", "embla-carousel-react": "^8.5.1", + "feed": "^5.1.0", "framer-motion": "^11.13.1", "input-otp": "^1.4.1", "lucide-react": "^0.468.0", @@ -675,6 +676,8 @@ "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + "feed": ["feed@5.1.0", "", { "dependencies": { "xml-js": "^1.6.11" } }, "sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -1019,6 +1022,8 @@ "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], @@ -1151,6 +1156,8 @@ "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="], @@ -1163,8 +1170,6 @@ "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - "@types/bun/bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], - "better-auth/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],