Skip to content
Merged
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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .github/workflows/nextjs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "bitremote-website",
"private": true,
"version": "1.0.3",
"version": "1.0.4",
"scripts": {
"dev": "next dev",
"build": "next build",
Expand Down
47 changes: 47 additions & 0 deletions public/llms.txt
Original file line number Diff line number Diff line change
@@ -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)
Binary file added public/opengraph.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 0 additions & 4 deletions public/robots.txt

This file was deleted.

24 changes: 0 additions & 24 deletions public/sitemap.xml

This file was deleted.

91 changes: 91 additions & 0 deletions src/app/[locale]/downloaders/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
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 (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={serializeJsonLd(breadcrumbSchema)}
/>
<DownloaderLandingPage
locale={locale}
messages={messages}
content={content}
/>
</>
);
}
33 changes: 1 addition & 32 deletions src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,14 @@
import type { Metadata } from 'next';

import { TextTabsNav } from '@/components/TextTabsNav';
import { defaultLocale, isLocale, localeLang, locales, type Locale } from '@/i18n/locales';
import { getMessages } from '@/i18n/messages';
import { absoluteUrl, localePath } from '@/i18n/urls';
import { localePath } from '@/i18n/urls';

export const dynamicParams = false;

export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const { locale: rawLocale } = await params;
const locale: Locale = isLocale(rawLocale) ? rawLocale : defaultLocale;
const messages = getMessages(locale);

const siteName = messages.site.name;

return {
title: {
default: siteName,
template: `%s | ${siteName}`,
},
description: messages.site.description,
alternates: {
canonical: absoluteUrl(localePath(locale, '/')),
languages: {
en: absoluteUrl(localePath('en', '/')),
ja: absoluteUrl(localePath('ja', '/')),
'zh-Hans': absoluteUrl(localePath('zh-hans', '/')),
'zh-Hant': absoluteUrl(localePath('zh-hant', '/')),
},
},
};
}

export default async function LocaleLayout({
children,
params,
Expand Down
Loading