diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..96ec02c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + pull_request: + branches: ['main'] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: '25' + cache: npm + - name: Restore cache + uses: actions/cache@v5 + with: + path: .next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- + - name: Install dependencies + run: npm ci + - name: Lint + run: npx --no-install eslint src/ + - name: Build + run: npx --no-install next build diff --git a/.github/workflows/nextjs.yml b/.github/workflows/nextjs.yml index c19858e..7bdc4e1 100644 --- a/.github/workflows/nextjs.yml +++ b/.github/workflows/nextjs.yml @@ -67,6 +67,8 @@ jobs: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- - name: Install dependencies run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + - name: Lint + run: ${{ steps.detect-package-manager.outputs.runner }} eslint src/ - name: Build with Next.js run: ${{ steps.detect-package-manager.outputs.runner }} next build - name: Upload artifact diff --git a/package-lock.json b/package-lock.json index ecf65e8..232abde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitremote-website", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bitremote-website", - "version": "1.0.3", + "version": "1.0.4", "dependencies": { "next": "^15.1.0", "react": "^19.0.0", diff --git a/package.json b/package.json index 6f3c9e8..466204a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bitremote-website", "private": true, - "version": "1.0.3", + "version": "1.0.4", "scripts": { "dev": "next dev", "build": "next build", diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..90ef9a8 --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,47 @@ +# BitRemote + +> Remote download manager for iPhone, iPad, and Mac. Control aria2, qBittorrent, Transmission, Synology Download Station, and QNAP Download Station from anywhere. + +## About + +BitRemote is a native Apple platform app that lets you remotely manage download tasks on NAS devices, seedboxes, and home servers. It connects to your existing downloader over LAN, VPN, or any remote access method — downloads stay on your server, not your device. + +- Platforms: iPhone, iPad, Mac (requires iOS / iPadOS / macOS 26.0 or later) +- Pricing: Free with optional BitRemote+ subscription ($1.99/month, $9.99/year, or $49.99 one-time) +- Publisher: Ark Studios + +## Supported Downloaders + +- [aria2](https://bitremote.app/en/downloaders/aria2/) +- [qBittorrent](https://bitremote.app/en/downloaders/qbittorrent/) +- [Transmission](https://bitremote.app/en/downloaders/transmission/) +- [Synology Download Station](https://bitremote.app/en/downloaders/synology-download-station/) +- [QNAP Download Station](https://bitremote.app/en/downloaders/qnap-download-station/) + +## Features + +- Control remote download tasks (pause, resume, remove, add) +- Filter and sort large queues by status, category, and tag +- Monitor real-time transfer speeds and statistics +- Back up and restore downloader connections (JSON export/import) +- Connect to self-hosted environments with self-signed certificates +- Rich accessibility support (VoiceOver, Voice Control, Dynamic Type) + +## Pages + +- [Home](https://bitremote.app/en/) +- [Support](https://bitremote.app/en/support/) +- [Privacy Policy](https://bitremote.app/en/privacy/) +- [Terms / EULA](https://bitremote.app/en/terms/) + +## Full Content + +- [llms-full.txt](https://bitremote.app/llms-full.txt): Complete site content for LLMs + +## Links + +- [App Store](https://apps.apple.com/app/id6477765303) +- [GitHub](https://github.com/BitRemoteApp/BitRemote) +- [Discord](https://discord.gg/x5TP2z6cFj) +- [Telegram](https://t.me/bitremote) +- [Twitter](https://twitter.com/bitremote) diff --git a/public/opengraph.jpg b/public/opengraph.jpg new file mode 100644 index 0000000..ac1a97b Binary files /dev/null and b/public/opengraph.jpg differ diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 0ed4efb..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Allow: / - -Sitemap: https://bitremote.app/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 76865a8..0000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - https://bitremote.app/ - - https://bitremote.app/en/ - https://bitremote.app/en/privacy/ - https://bitremote.app/en/terms/ - https://bitremote.app/en/support/ - - https://bitremote.app/ja/ - https://bitremote.app/ja/privacy/ - https://bitremote.app/ja/terms/ - https://bitremote.app/ja/support/ - - https://bitremote.app/zh-hans/ - https://bitremote.app/zh-hans/privacy/ - https://bitremote.app/zh-hans/terms/ - https://bitremote.app/zh-hans/support/ - - https://bitremote.app/zh-hant/ - https://bitremote.app/zh-hant/privacy/ - https://bitremote.app/zh-hant/terms/ - https://bitremote.app/zh-hant/support/ - diff --git a/src/app/[locale]/downloaders/[slug]/page.tsx b/src/app/[locale]/downloaders/[slug]/page.tsx new file mode 100644 index 0000000..b89721e --- /dev/null +++ b/src/app/[locale]/downloaders/[slug]/page.tsx @@ -0,0 +1,91 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; + +import { DownloaderLandingPage } from '@/components/DownloaderLandingPage'; +import { + downloaderLandingSlugs, + getDownloaderLandingContent, + getDownloaderLandingEntries, + type DownloaderLandingSlug, +} from '@/domain/downloader-landings'; +import { defaultLocale, isLocale, type Locale } from '@/i18n/locales'; +import { getMessages } from '@/i18n/messages'; +import { buildBreadcrumbSchema, serializeJsonLd } from '@/seo/schema'; +import { buildDownloaderLandingMetadata } from '@/seo/downloader-metadata'; + +export function generateStaticParams() { + return getDownloaderLandingEntries().map(({ locale, content }) => ({ + locale, + slug: content.slug, + })); +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string; slug: string }>; +}): Promise { + const { locale: rawLocale, slug: rawSlug } = await params; + const locale: Locale = isLocale(rawLocale) ? rawLocale : defaultLocale; + const slug = downloaderLandingSlugs.find((candidate) => candidate === rawSlug); + + if (!slug) { + return {}; + } + + const content = getDownloaderLandingContent(locale, slug); + if (!content) { + return {}; + } + + return buildDownloaderLandingMetadata({ + locale, + slug, + content, + }); +} + +export default async function DownloaderLandingRoute({ + params, +}: { + params: Promise<{ locale: string; slug: string }>; +}) { + const { locale: rawLocale, slug: rawSlug } = await params; + const locale: Locale = isLocale(rawLocale) ? rawLocale : defaultLocale; + const messages = getMessages(locale); + const slug = downloaderLandingSlugs.find((candidate) => candidate === rawSlug) as + | DownloaderLandingSlug + | undefined; + + if (!slug) { + notFound(); + } + + const content = getDownloaderLandingContent(locale, slug); + if (!content) { + notFound(); + } + + const breadcrumbSchema = buildBreadcrumbSchema({ + locale, + items: [ + { name: messages.nav.home, path: '/' }, + { name: messages.sections.downloaders.title, path: '/' }, + { name: content.downloader, path: `/downloaders/${content.slug}/` }, + ], + }); + + return ( + <> +