├── .dockerignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── docker-compose.yml ├── docker-down.sh ├── docker-up.sh ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── ad-free.png ├── algorithm-free.png ├── app-free.png ├── by1.png ├── by2.png ├── carousel_1.png ├── carousel_2.png ├── download-black.png ├── download-white.png ├── download.png ├── next.svg ├── share-black.png ├── share-white.png ├── share.png └── vercel.svg ├── src ├── app │ ├── [...catchAll] │ │ ├── loading.tsx │ │ └── page.tsx │ ├── api │ │ └── post │ │ │ └── [id] │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.module.scss │ ├── page.tsx │ ├── post │ │ └── [id] │ │ │ ├── page.module.scss │ │ │ └── page.tsx │ └── sd │ │ └── [subdomain] │ │ └── [...path] │ │ ├── loading.tsx │ │ └── page.tsx └── components │ ├── Carousel │ ├── index.tsx │ └── style.module.scss │ ├── Feed │ ├── index.tsx │ └── style.module.scss │ ├── HomePage │ ├── index.tsx │ └── style.module.scss │ ├── Loading.tsx │ ├── Logo.tsx │ ├── PostOptions │ ├── index.tsx │ └── style.module.scss │ └── VideoPlayer │ ├── index.tsx │ └── style.module.scss ├── tailwind.config.ts ├── tsconfig.json └── utils ├── strings ├── check-url.ts └── generate-token.ts └── types └── posts.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile 4 | .dockerignore 5 | .env -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "es5", 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Node.js image as the base image 2 | FROM node:18-alpine 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json (or yarn.lock) to the working directory 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the rest of the application code to the working directory 14 | COPY . . 15 | 16 | # Set environment variables 17 | ENV NEXT_INTERNAL_API_URL=http://localhost:3000 18 | ENV NEXT_PUBLIC_API_URL=http://localhost:2000 19 | ENV GA_TRACING_ID=G-Y2K66L945V 20 | 21 | # Build the Next.js application 22 | RUN npm run build 23 | 24 | # Expose the port the app runs on 25 | EXPOSE 3000 26 | 27 | # Start the Next.js application 28 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Offtiktok | The Open TikTok Client 2 | 3 | Offtiktok allows users to share tiktoks with anyone, regardless of whether they have the app or not, by adding "off" before "tiktok" in the url (e.g: [https://vm.offtiktok.com/ZGe7XpCwV/](https://vm.offtiktok.com/ZGe7XpCwV/) ) 4 | 5 | It also includes a minimalistic TikTok feed that allows watching videos recommended by the platform, no ads, no app, no geo-restrictions. 6 | 7 | ### This repository includes the frontend server. [If you're looking for the backend, it's here](https://github.com/MarsHeer/offtiktokapi) 8 | 9 | ## Deploy it yourself 10 | 11 | ### 1. Install Node & npm (or your package manager of preference) 12 | 13 | ### Using Docker 14 | 15 | - Initialize as a docker container running `./docker-up.sh` 16 | - If script fails due to permissions settings, run `chmod +x ./docker-up.sh` then run the script again. 17 | 18 | ### On your own machine 19 | 20 | ##### macOS 21 | 22 | 1. **Using Homebrew**: 23 | 24 | - Install Homebrew if you haven't already: 25 | `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` 26 | 27 | - Install Node.js and npm: 28 | `brew install node` 29 | brew install node 30 | 31 | 2. **Using Node Version Manager (nvm)**: 32 | 33 | - Install `nvm`: 34 | 35 | `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash` 36 | 37 | - Load `nvm`: 38 | 39 | ``` 40 | export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s 41 | "${XDG_CONFIG_HOME}/nvm")" 42 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 43 | ``` 44 | 45 | - Install Node.js: 46 | 47 | `nvm install node` 48 | 49 | ##### Linux 50 | 51 | 1. **Using NodeSource Binaries**: 52 | 53 | - Install Node.js and npm: 54 | 55 | ``` 56 | curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - 57 | sudo apt-get install -y nodejs 58 | ``` 59 | 60 | 2. **Using Node Version Manager (nvm)**: 61 | 62 | - Install `nvm`: 63 | 64 | `curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash` 65 | 66 | - Load `nvm`: 67 | 68 | ``` 69 | export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s 70 | "${XDG_CONFIG_HOME}/nvm")" 71 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 72 | ``` 73 | 74 | - Install Node.js: 75 | 76 | `nvm install node` 77 | 78 | ##### Windows 79 | 80 | 1. **Using Node.js Installer**: 81 | 82 | - Download the Windows installer from the Node.js website. 83 | - Run the installer and follow the prompts. 84 | - 85 | 86 | 2. **Using Node Version Manager for Windows (nvm-windows)**: 87 | 88 | - Download and install `nvm-windows` from the nvm-windows releases. 89 | - Install Node.js: 90 | ``` 91 | nvm install latest 92 | nvm use latest 93 | ``` 94 | 95 | After installation, verify that Node.js and npm are installed correctly by running: 96 | 97 | ``` 98 | node -v 99 | npm -v 100 | ``` 101 | 102 | #### 2. Install dependencies 103 | 104 | To instal project dependencies, run: `npm install` 105 | 106 | #### 3. Configure your .env 107 | 108 | Create a `.env` file and copy the contets of the `.env.template` file included in the repository. 109 | 110 | The INTERNAL API URL should just be the frontend's URL 111 | 112 | The EXTERNAL API URL should be your backend's URL 113 | 114 | #### 4. Ready for dev! 115 | 116 | Run `npm run dev` to get your development server running in port `2000` 117 | 118 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 119 | 120 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 121 | 122 | ## Learn More 123 | 124 | To learn more about Next.js, take a look at the following resources: 125 | 126 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 127 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 128 | 129 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 130 | 131 | ## Deploy on Vercel 132 | 133 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 134 | 135 | Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 136 | 137 | # Collaborating 138 | 139 | Collaboration is welcome! Please feel free to support the project by opening issues or pull requests 140 | 141 | # License 142 | 143 | This project is licensed under the MIT License 144 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: . 6 | ports: 7 | - '3000:3000' 8 | environment: 9 | NEXT_INTERNAL_API_URL: http://localhost:3000 10 | NEXT_PUBLIC_API_URL: http://localhost:2000 11 | GA_TRACING_ID: G-Y2K66L945V 12 | -------------------------------------------------------------------------------- /docker-down.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Stop and remove the Docker containers 4 | docker-compose down 5 | 6 | # Optionally, remove all stopped containers and unused images 7 | docker system prune -f -------------------------------------------------------------------------------- /docker-up.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build and start the Docker containers 4 | docker-compose up --build -d 5 | 6 | # Print the status of the running containers 7 | docker-compose ps -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | staticPageGenerationTimeout: 1000 * 60 * 5, // 5 minutes 4 | images: { 5 | remotePatterns: [ 6 | { 7 | hostname: 'localhost', 8 | }, 9 | { 10 | hostname: 'smolvideo.com', 11 | }, 12 | ], 13 | }, 14 | redirects: async () => { 15 | return [ 16 | // General redirect rule 17 | { 18 | source: '/:path*', 19 | has: [ 20 | { 21 | type: 'host', 22 | value: '(?[^.]+)\\.(?.+)', 23 | }, 24 | ], 25 | destination: `${ 26 | process.env.NODE_ENV === 'production' 27 | ? 'https://www.offtiktok.com' 28 | : 'http://localhost:3000' 29 | }/sd/:subdomain/:path*`, 30 | permanent: false, 31 | // Add a condition to exclude the destination domain 32 | missing: [ 33 | { 34 | type: 'query', 35 | key: 'id', 36 | }, 37 | { 38 | type: 'host', 39 | value: 'offtiktok.com', 40 | }, 41 | { 42 | type: 'host', 43 | value: 'www.offtiktok.com', 44 | }, 45 | ], 46 | }, 47 | ]; 48 | }, 49 | }; 50 | 51 | export default nextConfig; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "offtiktok", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "clsx": "^2.1.1", 13 | "cookie": "^0.6.0", 14 | "gsap": "^3.12.5", 15 | "hls.js": "^1.5.13", 16 | "next": "14.2.5", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "sass": "^1.77.8", 20 | "zod": "^3.23.8" 21 | }, 22 | "devDependencies": { 23 | "@types/cookie": "^0.6.0", 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "eslint": "^8", 28 | "eslint-config-next": "14.2.5", 29 | "postcss": "^8", 30 | "tailwindcss": "^3.4.1", 31 | "tailwindcss-animated": "^1.1.2", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/ad-free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/ad-free.png -------------------------------------------------------------------------------- /public/algorithm-free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/algorithm-free.png -------------------------------------------------------------------------------- /public/app-free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/app-free.png -------------------------------------------------------------------------------- /public/by1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/by1.png -------------------------------------------------------------------------------- /public/by2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/by2.png -------------------------------------------------------------------------------- /public/carousel_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/carousel_1.png -------------------------------------------------------------------------------- /public/carousel_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/carousel_2.png -------------------------------------------------------------------------------- /public/download-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/download-black.png -------------------------------------------------------------------------------- /public/download-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/download-white.png -------------------------------------------------------------------------------- /public/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/download.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/share-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/share-black.png -------------------------------------------------------------------------------- /public/share-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/share-white.png -------------------------------------------------------------------------------- /public/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/public/share.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[...catchAll]/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |

7 | Processing your video, please be patient 8 |

9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/app/[...catchAll]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | const proccess = async (catchAll: string[]) => { 4 | try { 5 | const buildUrl = `https://tiktok.com/${catchAll.join('/')}`; 6 | 7 | const getData = await fetch( 8 | `${process.env.NEXT_PUBLIC_API_URL}/by_url/${encodeURIComponent( 9 | buildUrl 10 | )}` 11 | ); 12 | 13 | const res = await getData.json(); 14 | console.log(res); 15 | return { 16 | redirectTarget: res.id ? `/post/${res.id}` : '/404', 17 | }; 18 | } catch (err) { 19 | return { 20 | redirectTarget: '/404', 21 | }; 22 | } 23 | }; 24 | 25 | export default async function CatchAll({ 26 | params: { catchAll }, 27 | }: { 28 | params: { 29 | catchAll: string[]; 30 | }; 31 | }) { 32 | const { redirectTarget } = await proccess(catchAll); 33 | 34 | redirect(redirectTarget); 35 | return <>; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/api/post/[id]/route.ts: -------------------------------------------------------------------------------- 1 | // pages/api/post/[id].ts 2 | import { NextApiRequest, NextApiResponse } from 'next'; 3 | import { parse, serialize } from 'cookie'; 4 | import crypto from 'crypto'; 5 | import { postSchema } from '../../../../../utils/types/posts'; 6 | import { NextRequest, NextResponse } from 'next/server'; 7 | 8 | function generateSessionToken(length = 32) { 9 | return crypto.randomBytes(length).toString('hex'); 10 | } 11 | 12 | export async function GET( 13 | req: NextRequest, 14 | { params: { id } }: { params: { id: string } } 15 | ) { 16 | const headersList = new Headers(); 17 | const cookies = req.headers.get('cookie') 18 | ? parse(req.headers.get('cookie') || '') 19 | : {}; 20 | let sessionToken = cookies['SessionToken']; 21 | if (!sessionToken) { 22 | sessionToken = generateSessionToken(); 23 | headersList.set( 24 | 'Set-Cookie', 25 | serialize('SessionToken', sessionToken, { 26 | httpOnly: true, 27 | secure: process.env.NODE_ENV === 'production', 28 | maxAge: 60 * 60 * 24 * 7, // 1 week 29 | path: '/', 30 | }) 31 | ); 32 | } 33 | console.log({ 34 | fetching: `${process.env.NEXT_PUBLIC_API_URL}/by_id/${id}`, 35 | }); 36 | const fetchData = await fetch( 37 | `${process.env.NEXT_PUBLIC_API_URL}/by_id/${id}` 38 | ); 39 | 40 | const jsonData = await fetchData.json(); 41 | console.log({ 42 | jsonData, 43 | }); 44 | const parsedData = postSchema.safeParse(jsonData); 45 | 46 | if (!parsedData.success) { 47 | return Response.json(parsedData.error, { 48 | status: 500, 49 | }); 50 | } 51 | 52 | return Response.json(parsedData.data, { 53 | status: 200, 54 | headers: headersList, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarsHeer/offtiktok/010ee59fea8c694f5ce741c1bd18cb32617b3397/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html { 6 | overscroll-behavior-y: none; 7 | } 8 | 9 | body { 10 | background-color: #fafafa; 11 | } 12 | 13 | .seasons { 14 | font-family: 'the-seasons', sans-serif; 15 | } 16 | 17 | .henriette { 18 | font-family: henriette, sans-serif; 19 | } 20 | 21 | .articulat { 22 | font-family: 'articulat-cf', sans-serif; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | import Script from 'next/script'; 5 | 6 | const inter = Inter({ subsets: ['latin'] }); 7 | 8 | export const metadata: Metadata = { 9 | title: 10 | "OffTikTok | Share TikToks with anyone, even if they don't have the app.", 11 | description: 12 | "OffTikTok lets you share TikToks with anyone, even if they don't have the app. Just paste the link and share it with your friends. Watch Tiktoks without ads, apps or geo-restrictions", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 27 |