From ec762e46897518ea6d42324a22b611fd6b86cc59 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 20 Feb 2026 23:26:39 +0800 Subject: [PATCH 01/41] fix(ci): stabilize desktop release workflow (#4865) --- .github/workflows/build-desktop.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index a51ec95f8..332341ef8 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -215,7 +215,7 @@ jobs: - name: Upload file (windows-x64-exe unsigned) uses: actions/upload-artifact@v6 id: upload-unsigned-windows-x64-exe - if: runner.os == 'windows' + if: runner.os == 'windows' && github.event.inputs.store != 'true' with: name: windows-x64-exe path: | @@ -225,7 +225,7 @@ jobs: - uses: signpath/github-action-submit-signing-request@v2.0 continue-on-error: true - if: runner.os == 'windows' && env.RELEASE == 'true' + if: runner.os == 'windows' && env.RELEASE == 'true' && github.event.inputs.store != 'true' with: api-token: "${{ secrets.SIGNPATH_API_TOKEN }}" organization-id: "8c651516-fdaf-40a1-9fea-001dffde850e" @@ -236,12 +236,12 @@ jobs: output-artifact-directory: "apps/desktop/out/make/" - name: Update latest.yml - if: runner.os == 'windows' && env.RELEASE == 'true' + if: runner.os == 'windows' && env.RELEASE == 'true' && github.event.inputs.store != 'true' run: npx tsx apps/desktop/scripts/update-windows-yml.ts - name: Upload file (windows-x64-exe signed) uses: actions/upload-artifact@v6 - if: runner.os == 'windows' && env.RELEASE == 'true' + if: runner.os == 'windows' && env.RELEASE == 'true' && github.event.inputs.store != 'true' with: name: windows-x64-exe path: | @@ -286,6 +286,8 @@ jobs: - run: npx changelogithub if: env.RELEASE == 'true' continue-on-error: true + env: + GITHUB_TOKEN: ${{ github.token }} - name: Setup Version if: env.RELEASE == 'true' @@ -297,6 +299,7 @@ jobs: - name: Prepare Release Notes if: env.RELEASE == 'true' id: release_notes + shell: bash run: | version="${{ steps.version.outputs.APP_VERSION }}" changelog_file="apps/desktop/changelog/${version}.md" From 845539a8d4572b16c726a7d4a6672831fd194edb Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sat, 21 Feb 2026 10:59:07 +0800 Subject: [PATCH 02/41] fix(ci): improve release workflow orchestration (#4866) --- .claude/skills/desktop-release/SKILL.md | 36 ++++++++++++++++++------- .claude/skills/mobile-release/SKILL.md | 26 +++++++++++++++--- .github/workflows/build-android.yml | 2 +- .github/workflows/build-desktop.yml | 4 +-- .github/workflows/build-ios.yml | 2 +- .github/workflows/sync.yaml | 4 +-- .github/workflows/tag.yml | 22 +++++++++++++-- 7 files changed, 75 insertions(+), 21 deletions(-) diff --git a/.claude/skills/desktop-release/SKILL.md b/.claude/skills/desktop-release/SKILL.md index 4c657ebf8..2ccd56207 100644 --- a/.claude/skills/desktop-release/SKILL.md +++ b/.claude/skills/desktop-release/SKILL.md @@ -60,7 +60,21 @@ Perform a regular desktop release. This skill handles the full release workflow 5. Keep `NEXT_VERSION` as the placeholder - it will be replaced by `apply-changelog.ts` during bump. -## Step 3: Evaluate mainHash +## Step 3: Commit changelog updates before bump + +`nbump` requires a clean working tree. Commit changelog edits before running bump. + +1. Stage the changelog update: + ```bash + git add apps/desktop/changelog/next.md + ``` +2. Commit it on `dev`: + ```bash + git commit -m "docs(desktop): prepare release changelog" + ``` +3. If there are no changes to commit, continue without creating an extra commit. + +## Step 4: Evaluate mainHash This is critical for determining whether users need a full app update or can use the lightweight renderer hot update. @@ -86,14 +100,18 @@ Present your analysis to the user with: - Your recommendation (update or skip mainHash) - Ask for explicit confirmation -## Step 4: Save old mainHash and execute bump +## Step 5: Save old mainHash and execute bump 1. Save the current mainHash from `apps/desktop/package.json` for later comparison. -2. Change directory to `apps/desktop/` and run the bump: +2. Verify working tree is clean before bump: + ```bash + git status --short + ``` +3. Change directory to `apps/desktop/` and run the bump: ```bash cd apps/desktop && pnpm bump ``` -3. This command will: +4. This command will: - Pull latest changes - Apply changelog (rename next.md to {version}.md, create new next.md) - Recalculate mainHash and write to package.json @@ -103,11 +121,11 @@ Present your analysis to the user with: - Create branch `release/desktop/{NEW_VERSION}` - Push branch and create PR to `main` -## Step 5: Restore mainHash if skipping update +## Step 6: Restore mainHash if skipping update -If Step 3 decided mainHash should NOT be updated, restore the old value now. The bump has already committed, pushed, and created the PR on a new release branch, so we amend the commit and force push. This is safe because the release branch was just created. +If Step 4 decided mainHash should NOT be updated, restore the old value now. The bump has already committed, pushed, and created the PR on a new release branch, so we amend the commit and force push. This is safe because the release branch was just created. -1. Change back to the repo root first (Step 4 left the working directory at `apps/desktop/`): +1. Change back to the repo root first (Step 5 left the working directory at `apps/desktop/`): ```bash cd ../.. ``` @@ -122,9 +140,9 @@ If Step 3 decided mainHash should NOT be updated, restore the old value now. The git push --force origin release/desktop/{NEW_VERSION} ``` -If Step 3 decided mainHash SHOULD be updated, skip this step entirely — the bump already wrote the correct new value. +If Step 4 decided mainHash SHOULD be updated, skip this step entirely — the bump already wrote the correct new value. -## Step 6: Verify +## Step 7: Verify 1. Confirm the PR was created successfully by checking the output. 2. Report the new version number and PR URL to the user. diff --git a/.claude/skills/mobile-release/SKILL.md b/.claude/skills/mobile-release/SKILL.md index fac03c949..308abb965 100644 --- a/.claude/skills/mobile-release/SKILL.md +++ b/.claude/skills/mobile-release/SKILL.md @@ -64,13 +64,31 @@ Perform a regular mobile release. This skill handles the full release workflow f 5. Keep `NEXT_VERSION` as the placeholder - it will be replaced by `apply-changelog.ts` during bump. -## Step 3: Execute bump +## Step 3: Commit changelog updates before bump -1. Change directory to `apps/mobile/` and run the bump: +`nbump` requires a clean working tree. Commit changelog edits before running bump. + +1. Stage the changelog update: + ```bash + git add apps/mobile/changelog/next.md + ``` +2. Commit it on `dev`: + ```bash + git commit -m "docs(mobile): prepare release changelog" + ``` +3. If there are no changes to commit, continue without creating an extra commit. + +## Step 4: Execute bump + +1. Verify working tree is clean before bump: + ```bash + git status --short + ``` +2. Change directory to `apps/mobile/` and run the bump: ```bash cd apps/mobile && pnpm bump ``` -2. This is an interactive `nbump` command that prompts for version selection. It will: +3. This is an interactive `nbump` command that prompts for version selection. It will: - Pull latest changes - Apply changelog (rename next.md to {version}.md, create new next.md from template) - Format package.json with eslint + prettier @@ -82,7 +100,7 @@ Perform a regular mobile release. This skill handles the full release workflow f - Create branch `release/mobile/{NEW_VERSION}` - Push branch and create PR to `mobile-main` -## Step 4: Verify +## Step 5: Verify 1. Confirm the PR was created successfully by checking the output. 2. Report the new version number and PR URL to the user. diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 6633e5cb5..5c1ecbcae 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -28,7 +28,7 @@ concurrency: jobs: build: name: Build Android apk for device - if: github.secret_source != 'None' && (github.event_name != 'push' || !startsWith(github.event.head_commit.message || '', 'release(mobile):')) + if: github.secret_source != 'None' && (github.event_name != 'push' || !contains(github.event.head_commit.message || '', 'release(mobile):')) runs-on: ubuntu-latest steps: diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 332341ef8..dadf03010 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -23,7 +23,7 @@ on: # https://docs.github.com/en/enterprise-cloud@latest/actions/writing-workflows/choosing-what-your-workflow-does/control-the-concurrency-of-workflows-and-jobs concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-build + group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.tag_version == 'true' && 'tag-version' || github.event.inputs.store == 'true' && 'store' || 'manual') || 'build' }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }} env: VITE_WEB_URL: ${{ vars.VITE_WEB_URL }} @@ -37,7 +37,7 @@ env: jobs: release: - if: github.secret_source != 'None' && (github.event_name != 'push' || !startsWith(github.event.head_commit.message || '', 'release(desktop):')) + if: github.secret_source != 'None' && (github.event_name != 'push' || !contains(github.event.head_commit.message || '', 'release(desktop):')) runs-on: ${{ matrix.os }} env: PROD: ${{ github.event.inputs.tag_version == 'true' || github.ref_type == 'tag' || github.event.inputs.store == 'true' }} diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 06a86fd8a..d1e8a7461 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -23,7 +23,7 @@ concurrency: jobs: check-runner: - if: github.secret_source != 'None' && (github.event_name != 'push' || !startsWith(github.event.head_commit.message || '', 'release(mobile):')) + if: github.secret_source != 'None' && (github.event_name != 'push' || !contains(github.event.head_commit.message || '', 'release(mobile):')) runs-on: ubuntu-latest outputs: runner-label: ${{ steps.set-runner.outputs.runner-label }} diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml index dd0847a30..d22bff603 100644 --- a/.github/workflows/sync.yaml +++ b/.github/workflows/sync.yaml @@ -13,8 +13,8 @@ jobs: sync-to-dev: runs-on: ubuntu-latest if: | - (github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'release(desktop):')) || - (github.ref == 'refs/heads/mobile-main' && startsWith(github.event.head_commit.message, 'release(mobile):')) + (github.ref == 'refs/heads/main' && contains(github.event.head_commit.message || '', 'release(desktop):')) || + (github.ref == 'refs/heads/mobile-main' && contains(github.event.head_commit.message || '', 'release(mobile):')) steps: - name: Checkout repository uses: actions/checkout@v6 diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index def2232d0..9bd2eb768 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -110,7 +110,7 @@ jobs: echo "build_version=${next_build}" >> "$GITHUB_OUTPUT" echo "Desktop build version: ${next_build}" - - name: Trigger Desktop Build + - name: Trigger Desktop Tag Version Build if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main' uses: actions/github-script@v8 with: @@ -123,11 +123,29 @@ jobs: ref: 'main', inputs: { tag_version: 'true', + store: 'false' + } + }); + console.log('Desktop Tag Version build triggered successfully'); + + - name: Trigger Desktop Store Build + if: needs.create_tag.outputs.platform == 'desktop' && needs.create_tag.outputs.ref_name == 'main' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const response = await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'build-desktop.yml', + ref: 'main', + inputs: { + tag_version: 'false', store: 'true', build_version: '${{ steps.desktop_build_version.outputs.build_version }}' } }); - console.log('Desktop build triggered successfully'); + console.log('Desktop store build triggered successfully'); - name: Trigger Mobile Preview Release Build if: needs.create_tag.outputs.platform == 'mobile' && needs.create_tag.outputs.ref_name == 'mobile-main' From 0bba08f10356d33b897fa191e8c770c81df6fc2c Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sun, 22 Feb 2026 15:15:13 +0800 Subject: [PATCH 03/41] chore(firebase): switch to diygod Firebase config (#4868) --- apps/mobile/build/GoogleService-Info.plist | 10 +++--- apps/mobile/build/google-services.json | 24 +++++++++----- apps/mobile/ios/Folo/GoogleService-Info.plist | 31 +------------------ packages/internal/shared/src/env.common.ts | 11 +++++++ packages/internal/shared/src/env.desktop.ts | 2 +- packages/internal/shared/src/env.ssr.ts | 2 +- 6 files changed, 36 insertions(+), 44 deletions(-) mode change 100644 => 120000 apps/mobile/ios/Folo/GoogleService-Info.plist diff --git a/apps/mobile/build/GoogleService-Info.plist b/apps/mobile/build/GoogleService-Info.plist index 2a5206366..ff64bf7c3 100644 --- a/apps/mobile/build/GoogleService-Info.plist +++ b/apps/mobile/build/GoogleService-Info.plist @@ -3,17 +3,17 @@ API_KEY - AIzaSyBD8lePDcfuPeJpJkvY1Egc_5sLX5lrtpg + AIzaSyBl6-TXvw5F0CX1r-935w0x_xPbVr0SSbQ GCM_SENDER_ID - 194977404578 + 992336953943 PLIST_VERSION 1 BUNDLE_ID is.follow PROJECT_ID - follow-428106 + diygod-folo STORAGE_BUCKET - follow-428106.firebasestorage.app + diygod-folo.firebasestorage.app IS_ADS_ENABLED IS_ANALYTICS_ENABLED @@ -25,6 +25,6 @@ IS_SIGNIN_ENABLED GOOGLE_APP_ID - 1:194977404578:ios:f26373d7487896293669fb + 1:992336953943:ios:75a436f716af190dc11912 \ No newline at end of file diff --git a/apps/mobile/build/google-services.json b/apps/mobile/build/google-services.json index f7531d888..5882f8aca 100644 --- a/apps/mobile/build/google-services.json +++ b/apps/mobile/build/google-services.json @@ -1,26 +1,36 @@ { "project_info": { - "project_number": "194977404578", - "project_id": "follow-428106", - "storage_bucket": "follow-428106.firebasestorage.app" + "project_number": "992336953943", + "project_id": "diygod-folo", + "storage_bucket": "diygod-folo.firebasestorage.app" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:194977404578:android:b160305b94681dcb3669fb", + "mobilesdk_app_id": "1:992336953943:android:7e45e76b1a8986e1c11912", "android_client_info": { "package_name": "is.follow" } }, - "oauth_client": [], + "oauth_client": [ + { + "client_id": "992336953943-3rja1g44ep4tmsmv84mkaa6itv9ejrsi.apps.googleusercontent.com", + "client_type": 3 + } + ], "api_key": [ { - "current_key": "AIzaSyA8knDQi334H3YBPflxfrYuod7AGZFQt64" + "current_key": "AIzaSyC7X-OdrxXWogeQwVDIFNkZzy81TINa7Fs" } ], "services": { "appinvite_service": { - "other_platform_oauth_client": [] + "other_platform_oauth_client": [ + { + "client_id": "992336953943-3rja1g44ep4tmsmv84mkaa6itv9ejrsi.apps.googleusercontent.com", + "client_type": 3 + } + ] } } } diff --git a/apps/mobile/ios/Folo/GoogleService-Info.plist b/apps/mobile/ios/Folo/GoogleService-Info.plist deleted file mode 100644 index 2a5206366..000000000 --- a/apps/mobile/ios/Folo/GoogleService-Info.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - API_KEY - AIzaSyBD8lePDcfuPeJpJkvY1Egc_5sLX5lrtpg - GCM_SENDER_ID - 194977404578 - PLIST_VERSION - 1 - BUNDLE_ID - is.follow - PROJECT_ID - follow-428106 - STORAGE_BUCKET - follow-428106.firebasestorage.app - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:194977404578:ios:f26373d7487896293669fb - - \ No newline at end of file diff --git a/apps/mobile/ios/Folo/GoogleService-Info.plist b/apps/mobile/ios/Folo/GoogleService-Info.plist new file mode 120000 index 000000000..5064a11d9 --- /dev/null +++ b/apps/mobile/ios/Folo/GoogleService-Info.plist @@ -0,0 +1 @@ +../../build/GoogleService-Info.plist \ No newline at end of file diff --git a/packages/internal/shared/src/env.common.ts b/packages/internal/shared/src/env.common.ts index c9128fafb..f81745086 100644 --- a/packages/internal/shared/src/env.common.ts +++ b/packages/internal/shared/src/env.common.ts @@ -1,8 +1,19 @@ +const FIREBASE_CONFIG_DEFAULT = JSON.stringify({ + apiKey: "AIzaSyBpGB2C2Vz-9ktivqVkW7uTtVopNh3ELvo", + authDomain: "diygod-folo.firebaseapp.com", + projectId: "diygod-folo", + storageBucket: "diygod-folo.firebasestorage.app", + messagingSenderId: "992336953943", + appId: "1:992336953943:web:998aae576c8bc77dc11912", + measurementId: "G-HS4SF4GHWG", +}) + export const DEFAULT_VALUES = { PROD: { API_URL: "https://api.folo.is", WEB_URL: "https://app.folo.is", INBOXES_EMAIL: "@follow.re", + FIREBASE_CONFIG: FIREBASE_CONFIG_DEFAULT, OPENPANEL_CLIENT_ID: "4382168f-b8d2-40c1-9a26-133a312d072b", OPENPANEL_API_URL: "https://openpanel.follow.is/api", RECAPTCHA_V3_SITE_KEY: "6LeNhgwsAAAAAHO1dhunH0a-FI1YvOc9Ny98Oast", diff --git a/packages/internal/shared/src/env.desktop.ts b/packages/internal/shared/src/env.desktop.ts index e09210985..e2f08936f 100644 --- a/packages/internal/shared/src/env.desktop.ts +++ b/packages/internal/shared/src/env.desktop.ts @@ -14,7 +14,7 @@ export const env = createEnv({ VITE_DEV_PROXY: z.string().optional(), VITE_SENTRY_DSN: z.string().optional(), VITE_INBOXES_EMAIL: z.string().default(DEFAULT_VALUES.PROD.INBOXES_EMAIL), - VITE_FIREBASE_CONFIG: z.string().optional(), + VITE_FIREBASE_CONFIG: z.string().default(DEFAULT_VALUES.PROD.FIREBASE_CONFIG), VITE_OPENPANEL_CLIENT_ID: z.string().optional(), VITE_OPENPANEL_API_URL: z.string().url().optional(), diff --git a/packages/internal/shared/src/env.ssr.ts b/packages/internal/shared/src/env.ssr.ts index cc5b336fa..1e8cebbde 100644 --- a/packages/internal/shared/src/env.ssr.ts +++ b/packages/internal/shared/src/env.ssr.ts @@ -14,7 +14,7 @@ export const env = createEnv({ VITE_DEV_PROXY: z.string().optional(), VITE_SENTRY_DSN: z.string().optional(), VITE_INBOXES_EMAIL: z.string().default(DEFAULT_VALUES.PROD.INBOXES_EMAIL), - VITE_FIREBASE_CONFIG: z.string().optional(), + VITE_FIREBASE_CONFIG: z.string().default(DEFAULT_VALUES.PROD.FIREBASE_CONFIG), VITE_OPENPANEL_CLIENT_ID: z.string().optional(), VITE_OPENPANEL_API_URL: z.string().url().optional(), From 81c5b9f2e005f76bfb030fbb3a42e7cad6ecb924 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sun, 22 Feb 2026 20:58:17 +0800 Subject: [PATCH 04/41] chore(infra): migrate desktop web to Cloudflare (#4869) * chore(infra): migrate desktop web from Vercel to Cloudflare Workers Replaces separate Vercel deployments (follow SPA + follow-external-ssr) with unified Cloudflare Workers + Assets deployment. Implements meta tag injection, OG image generation, and environment variable management in Hono-based Worker. Adds GitHub Actions CI/CD for automatic deployment on push to dev/main branches. - Replace Fastify with Hono for Cloudflare Workers compatibility - Create Worker entry point with SSR routes and SPA fallback - Add AsyncLocalStorage-based request context shim - Implement WASM-based OG image rendering with R2 font storage - Split SPA and SSR routing: /share/* and auth routes use SSR, others fallback to SPA - Add Cloudflare wrangler configuration with dev/prod environments - Create GitHub Actions workflow for automated deployments - Add build scripts for font data and WASM patching Co-Authored-By: Claude Opus 4.6 * fix(ssr): exclude worker files from typecheck and fix tsdown config Worker-specific files (*.worker.ts) use Cloudflare Workers types and generated modules that aren't available during the main tsc typecheck. Exclude them from tsconfig since they're only used via tsdown aliases. Also fix broken path.resolve reference in tsdown.worker.config.ts. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/workflows/deploy-cloudflare.yml | 70 +++ .gitignore | 2 + apps/ssr/package.json | 8 +- apps/ssr/scripts/check-fonts.ts | 20 + apps/ssr/scripts/generate-font-data.ts | 46 ++ apps/ssr/scripts/patch-worker-build.ts | 24 + apps/ssr/scripts/upload-fonts-to-r2.ts | 36 ++ apps/ssr/src/lib/load-env.worker.ts | 2 + apps/ssr/src/lib/og/fonts.worker.ts | 66 ++ apps/ssr/src/lib/og/render-to-image.worker.ts | 33 + apps/ssr/src/lib/og/resvg-wasm-shim.ts | 22 + apps/ssr/src/lib/worker-request-context.ts | 43 ++ apps/ssr/tsconfig.json | 2 +- apps/ssr/tsdown.worker.config.ts | 28 + apps/ssr/worker-app.ts | 71 +++ apps/ssr/worker-entry.ts | 334 +++++++++++ apps/ssr/wrangler.jsonc | 74 +++ pnpm-lock.yaml | 562 +++++++++++++++++- 18 files changed, 1411 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/deploy-cloudflare.yml create mode 100644 apps/ssr/scripts/check-fonts.ts create mode 100644 apps/ssr/scripts/generate-font-data.ts create mode 100644 apps/ssr/scripts/patch-worker-build.ts create mode 100644 apps/ssr/scripts/upload-fonts-to-r2.ts create mode 100644 apps/ssr/src/lib/load-env.worker.ts create mode 100644 apps/ssr/src/lib/og/fonts.worker.ts create mode 100644 apps/ssr/src/lib/og/render-to-image.worker.ts create mode 100644 apps/ssr/src/lib/og/resvg-wasm-shim.ts create mode 100644 apps/ssr/src/lib/worker-request-context.ts create mode 100644 apps/ssr/tsdown.worker.config.ts create mode 100644 apps/ssr/worker-app.ts create mode 100644 apps/ssr/worker-entry.ts create mode 100644 apps/ssr/wrangler.jsonc diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml new file mode 100644 index 000000000..aca9c7e24 --- /dev/null +++ b/.github/workflows/deploy-cloudflare.yml @@ -0,0 +1,70 @@ +on: + push: + branches: [main, dev] + +name: ☁️ Deploy to Cloudflare Workers +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + deploy: + name: Build & Deploy + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [lts/*] + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + lfs: true + + - name: Checkout LFS objects + run: git lfs checkout + + - name: Cache turbo build setup + uses: actions/cache@v5 + with: + path: .turbo + key: ${{ runner.os }}-turbo-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-turbo- + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Build desktop web (SPA) + run: pnpm exec turbo run Folo#build:web + + - name: Build SSR Worker + working-directory: apps/ssr + run: pnpm run build:worker + + - name: Copy WASM file + run: cp node_modules/@resvg/resvg-wasm/index_bg.wasm apps/ssr/dist/worker/resvg.wasm + + - name: Deploy to Cloudflare (dev) + if: github.ref == 'refs/heads/dev' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: apps/ssr + command: deploy --env dev + + - name: Deploy to Cloudflare (prod) + if: github.ref == 'refs/heads/main' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: apps/ssr + command: deploy diff --git a/.gitignore b/.gitignore index 6bec08d32..da495911b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ apps/desktop/build/appxmanifest.xml .claude/settings.local.json .serena + +.wrangler diff --git a/apps/ssr/package.json b/apps/ssr/package.json index 6875cc608..0dbf62f58 100644 --- a/apps/ssr/package.json +++ b/apps/ssr/package.json @@ -4,6 +4,9 @@ "private": true, "scripts": { "build": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsdown && tsx scripts/cleanup-vercel-build.ts", + "build:worker": "cross-env NODE_ENV=production vite build && tsx scripts/prepare-vercel-build.ts && tsx scripts/generate-font-data.ts && tsdown --config tsdown.worker.config.ts && tsx scripts/patch-worker-build.ts && cp -r dist/dist-external ../desktop/out/web/dist-external", + "deploy:dev": "wrangler deploy --env dev", + "deploy:prod": "wrangler deploy", "dev": "cross-env NODE_ENV=development tsx watch --include \"src/**/*.ts\" --exclude \"./*.ts\" --exclude \"./*.mjs\" index.ts", "meta": "tsx helper/meta-map.ts --watch", "start": "tsx index.ts", @@ -54,6 +57,7 @@ "@follow/shared": "workspace:*", "@follow/types": "workspace:*", "@follow/utils": "workspace:*", + "@resvg/resvg-wasm": "2.6.2", "@types/html-minifier-terser": "7.0.2", "chokidar": "4.0.3", "code-inspector-plugin": "1.4.2", @@ -62,6 +66,7 @@ "es-toolkit": "1.44.0", "fast-glob": "3.3.3", "foxact": "0.2.52", + "hono": "4.12.1", "html-minifier-terser": "7.2.0", "lightningcss": "1.31.1", "masonic": "4.1.0", @@ -72,6 +77,7 @@ "tsx": "4.21.0", "typescript": "catalog:", "vite": "7.3.1", - "vite-plugin-route-builder": "0.4.1" + "vite-plugin-route-builder": "0.4.1", + "wrangler": "4.67.0" } } diff --git a/apps/ssr/scripts/check-fonts.ts b/apps/ssr/scripts/check-fonts.ts new file mode 100644 index 000000000..9c01c40fe --- /dev/null +++ b/apps/ssr/scripts/check-fonts.ts @@ -0,0 +1,20 @@ +import fs from "node:fs" +import { createRequire } from "node:module" + +import path, { resolve } from "pathe" + +const require = createRequire(import.meta.url) + +const snPath = require.resolve("@fontsource/sn-pro") +const filesDir = resolve(snPath, "../files") +const files = fs.readdirSync(filesDir).filter((f) => !f.endsWith(".woff2") && !f.includes("italic")) +files.forEach((f) => { + const stat = fs.statSync(path.join(filesDir, f)) + console.info(f, `${(stat.size / 1024).toFixed(1)}KB`) +}) +const kosePath = require.resolve("kose-font") +const koseSize = fs.statSync(kosePath).size +console.info(`kose-font: ${(koseSize / 1024).toFixed(1)}KB`) +let total = files.reduce((sum, f) => sum + fs.statSync(path.join(filesDir, f)).size, 0) +total += koseSize +console.info(`Total: ${(total / 1024 / 1024).toFixed(1)}MB`) diff --git a/apps/ssr/scripts/generate-font-data.ts b/apps/ssr/scripts/generate-font-data.ts new file mode 100644 index 000000000..fd6937899 --- /dev/null +++ b/apps/ssr/scripts/generate-font-data.ts @@ -0,0 +1,46 @@ +import fs from "node:fs" +import { createRequire } from "node:module" +import { fileURLToPath } from "node:url" + +import path, { dirname, resolve } from "pathe" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const require = createRequire(import.meta.url) + +const weights = [ + { name: "Thin", weight: 100 }, + { name: "ExtraLight", weight: 200 }, + { name: "Light", weight: 300 }, + { name: "Regular", weight: 400 }, + { name: "Italic", weight: 400 }, + { name: "Medium", weight: 500 }, + { name: "SemiBold", weight: 600 }, + { name: "Bold", weight: 700 }, + { name: "ExtraBold", weight: 800 }, + { name: "Black", weight: 900 }, +] as const + +const snFontDepsPath = require.resolve("@fontsource/sn-pro") +const snFontsDirPath = resolve(snFontDepsPath, "../files") +const snFontsDir = fs + .readdirSync(snFontsDirPath) + .filter((name) => !name.endsWith(".woff2") && !name.includes("italic")) + +const fontsData: Record = {} + +for (const file of snFontsDir) { + const weight = weights.find((w) => file.includes(w.weight.toString())) + if (!weight) continue + const data = fs.readFileSync(path.join(snFontsDirPath, file)) + fontsData[`sn-pro-${weight.weight}`] = data.toString("base64") +} + +// kose-font is too large (~24MB) to bundle, loaded from R2 at runtime instead + +const outDir = path.join(__dirname, "../.generated") +fs.mkdirSync(outDir, { recursive: true }) +fs.writeFileSync( + path.join(outDir, "fonts-data.ts"), + `export default ${JSON.stringify(fontsData)} as Record`, +) +console.info("Generated fonts-data.ts with", Object.keys(fontsData).length, "fonts") diff --git a/apps/ssr/scripts/patch-worker-build.ts b/apps/ssr/scripts/patch-worker-build.ts new file mode 100644 index 000000000..a9624088b --- /dev/null +++ b/apps/ssr/scripts/patch-worker-build.ts @@ -0,0 +1,24 @@ +import fs from "node:fs" + +import { dirname, join } from "pathe" + +const distDir = join(dirname(import.meta.url.replace("file://", "")), "../dist/worker") + +const files = fs.readdirSync(distDir).filter((f) => f.endsWith(".mjs")) + +for (const file of files) { + const filePath = join(distDir, file) + const code = fs.readFileSync(filePath, "utf-8") + + // Fix createRequire(import.meta.url) - import.meta.url is undefined in Cloudflare Workers + // Provide a fallback URL so createRequire can initialize properly + const patched = code.replaceAll( + "createRequire(import.meta.url)", + 'createRequire(import.meta.url || "file:///worker.mjs")', + ) + + if (patched !== code) { + fs.writeFileSync(filePath, patched) + console.info(`Patched createRequire in ${file}`) + } +} diff --git a/apps/ssr/scripts/upload-fonts-to-r2.ts b/apps/ssr/scripts/upload-fonts-to-r2.ts new file mode 100644 index 000000000..1bb145357 --- /dev/null +++ b/apps/ssr/scripts/upload-fonts-to-r2.ts @@ -0,0 +1,36 @@ +import { execSync } from "node:child_process" +import fs from "node:fs" +import { createRequire } from "node:module" + +import path, { resolve } from "pathe" + +const require = createRequire(import.meta.url) + +const BUCKET = "follow" +const PREFIX = "ssr-fonts" +const ACCOUNT_ID = "1f1d1678a2413a54c944b3081bab5c84" + +const snFontDepsPath = require.resolve("@fontsource/sn-pro") +const snFontsDirPath = resolve(snFontDepsPath, "../files") +const snFontsDir = fs + .readdirSync(snFontsDirPath) + .filter((name) => !name.endsWith(".woff2") && !name.includes("italic")) + +for (const file of snFontsDir) { + const filePath = path.join(snFontsDirPath, file) + const key = `${PREFIX}/${file}` + console.info(`Uploading ${file}...`) + execSync( + `CLOUDFLARE_ACCOUNT_ID=${ACCOUNT_ID} npx wrangler r2 object put ${BUCKET}/${key} --file "${filePath}" --remote`, + { stdio: "inherit" }, + ) +} + +const koseFontPath = require.resolve("kose-font") +console.info("Uploading kose-font.ttf...") +execSync( + `CLOUDFLARE_ACCOUNT_ID=${ACCOUNT_ID} npx wrangler r2 object put ${BUCKET}/${PREFIX}/kose-font.ttf --file "${koseFontPath}" --remote`, + { stdio: "inherit" }, +) + +console.info("All fonts uploaded successfully!") diff --git a/apps/ssr/src/lib/load-env.worker.ts b/apps/ssr/src/lib/load-env.worker.ts new file mode 100644 index 000000000..daa7f8835 --- /dev/null +++ b/apps/ssr/src/lib/load-env.worker.ts @@ -0,0 +1,2 @@ +// No-op for Cloudflare Workers - environment variables are provided via wrangler config +export {} diff --git a/apps/ssr/src/lib/og/fonts.worker.ts b/apps/ssr/src/lib/og/fonts.worker.ts new file mode 100644 index 000000000..5b9fb6267 --- /dev/null +++ b/apps/ssr/src/lib/og/fonts.worker.ts @@ -0,0 +1,66 @@ +import fontsBase64Data from "../../../.generated/fonts-data" + +let cachedFonts: any[] | null = null +let koseFont: any | null = null + +// Global reference to R2 bucket, set by worker-entry.ts +let _fontsBucket: R2Bucket | null = null +export function setFontsBucket(bucket: R2Bucket) { + _fontsBucket = bucket +} + +function decodeSNProFonts(): any[] { + const fontsData: any[] = [] + for (const [key, base64] of Object.entries(fontsBase64Data)) { + if (key === "kose-400") continue + const weightStr = key.split("-").pop()! + const weight = Number.parseInt(weightStr) + const buf = Uint8Array.from(atob(base64), (c) => c.codePointAt(0)!) + fontsData.push({ + name: "SN Pro", + data: buf.buffer, + weight, + style: "normal" as const, + }) + } + return fontsData +} + +async function loadKoseFont(): Promise { + if (koseFont) return koseFont + if (!_fontsBucket) { + console.warn("R2 fonts bucket not configured, skipping kose-font") + return null + } + try { + const obj = await _fontsBucket.get("ssr-fonts/kose-font.ttf") + if (!obj) { + console.warn("Kose font not found in R2") + return null + } + const data = await obj.arrayBuffer() + koseFont = { + name: "Kose", + data, + weight: 400, + style: "normal" as const, + } + return koseFont + } catch (e) { + console.error("Failed to load kose font from R2:", e) + return null + } +} + +export async function getFonts(): Promise { + if (cachedFonts) return cachedFonts + + const snProFonts = decodeSNProFonts() + const kose = await loadKoseFont() + + cachedFonts = kose ? [...snProFonts, kose] : snProFonts + return cachedFonts +} + +// Default export for compatibility with original fonts module API +export default [] as any[] diff --git a/apps/ssr/src/lib/og/render-to-image.worker.ts b/apps/ssr/src/lib/og/render-to-image.worker.ts new file mode 100644 index 000000000..2048f7bfa --- /dev/null +++ b/apps/ssr/src/lib/og/render-to-image.worker.ts @@ -0,0 +1,33 @@ +import type { ReactElement } from "react" +import type { SatoriOptions } from "satori" +import satori from "satori" + +import { getFonts } from "./fonts.worker" +import { ensureInitialized, Resvg } from "./resvg-wasm-shim" + +export async function renderToImage( + node: ReactElement, + options: { + width?: number + height: number + debug?: boolean + fonts?: SatoriOptions["fonts"] + }, +) { + await ensureInitialized() + + const fonts = options.fonts || (await getFonts()) + + const svg = await satori(node, { + ...options, + fonts, + }) + + const w = new Resvg(svg) + const image = w.render().asPng() + + return { + image, + contentType: "image/png", + } +} diff --git a/apps/ssr/src/lib/og/resvg-wasm-shim.ts b/apps/ssr/src/lib/og/resvg-wasm-shim.ts new file mode 100644 index 000000000..65017bb0b --- /dev/null +++ b/apps/ssr/src/lib/og/resvg-wasm-shim.ts @@ -0,0 +1,22 @@ +import { initWasm } from "@resvg/resvg-wasm" + +let initialized = false + +let _wasmModule: WebAssembly.Module | null = null + +export function setWasmModule(mod: WebAssembly.Module) { + _wasmModule = mod +} + +async function ensureInitialized() { + if (!initialized) { + if (_wasmModule) { + await initWasm(_wasmModule) + } + initialized = true + } +} + +export { ensureInitialized } + +export { Resvg } from "@resvg/resvg-wasm" diff --git a/apps/ssr/src/lib/worker-request-context.ts b/apps/ssr/src/lib/worker-request-context.ts new file mode 100644 index 000000000..8c7bd16fd --- /dev/null +++ b/apps/ssr/src/lib/worker-request-context.ts @@ -0,0 +1,43 @@ +import { AsyncLocalStorage } from "node:async_hooks" + +// Shim for @fastify/request-context that works in Workers +// Uses AsyncLocalStorage to provide per-request context + +const storage = new AsyncLocalStorage>() + +export const requestContext = { + get(key: string) { + const store = storage.getStore() + return store?.get(key) + }, + set(key: string, value: any) { + const store = storage.getStore() + store?.set(key, value) + }, +} + +export function runWithRequestContext(fn: () => T | Promise): T | Promise { + const store = new Map() + return storage.run(store, fn) +} + +// Provide a req-like object with requestContext for compatibility +export function createRequestProxy( + url: string, + headers: Record, + params: Record = {}, +) { + return { + originalUrl: url, + headers, + params, + requestContext: { + get(key: string) { + return requestContext.get(key) + }, + set(key: string, value: any) { + requestContext.set(key, value) + }, + }, + } +} diff --git a/apps/ssr/tsconfig.json b/apps/ssr/tsconfig.json index ee71e60f4..5ee15d7bc 100644 --- a/apps/ssr/tsconfig.json +++ b/apps/ssr/tsconfig.json @@ -40,5 +40,5 @@ "./tailwind.config.ts", "./helper/**/*.ts" ], - "exclude": ["node_modules"] + "exclude": ["node_modules", "./src/**/*.worker.ts"] } diff --git a/apps/ssr/tsdown.worker.config.ts b/apps/ssr/tsdown.worker.config.ts new file mode 100644 index 000000000..c6f6b1dc7 --- /dev/null +++ b/apps/ssr/tsdown.worker.config.ts @@ -0,0 +1,28 @@ +import { dirname, resolve } from "pathe" +import { defineConfig } from "tsdown" + +const __dirname = dirname(import.meta.url.replace("file://", "")) + +export default defineConfig({ + entry: ["./worker-entry.ts"], + outDir: "dist/worker", + + clean: true, + format: ["esm"], + external: ["node:*", /\.wasm$/], + noExternal: ["**"], + treeshake: true, + splitting: false, + + alias: { + "./src/lib/og/render-to-image": "./src/lib/og/render-to-image.worker", + "./src/lib/og/fonts": "./src/lib/og/fonts.worker", + "../../lib/og/render-to-image": "../../lib/og/render-to-image.worker", + "./src/lib/load-env": "./src/lib/load-env.worker", + "@fastify/request-context": resolve(__dirname, "src/lib/worker-request-context.ts"), + }, + + define: { + __DEV__: JSON.stringify(false), + }, +}) diff --git a/apps/ssr/worker-app.ts b/apps/ssr/worker-app.ts new file mode 100644 index 000000000..93295f5c1 --- /dev/null +++ b/apps/ssr/worker-app.ts @@ -0,0 +1,71 @@ +import { fastifyRequestContext } from "@fastify/request-context" +import { env } from "@follow/shared/env.ssr" +import type { FastifyRequest } from "fastify" +import Fastify from "fastify" +import { nanoid } from "nanoid" +import { FetchError } from "ofetch" + +import { MetaError } from "./src/meta-handler" +import { globalRoute } from "./src/router/global" +import { ogRoute } from "./src/router/og" + +declare module "@fastify/request-context" { + interface RequestContextData { + req: FastifyRequest + upstreamEnv: "prod" | "dev" + upstreamOrigin: string + } +} + +export const createApp = () => { + const app = Fastify({}) + + // Test: minimal route to verify Fastify works in Workers + app.get("/healthz", async () => ({ ok: true })) + + app.register(fastifyRequestContext) + + app.after(() => { + app.setErrorHandler(function (err, req, reply) { + this.log.error(err) + + const traceId = nanoid(8) + + if (err instanceof FetchError) { + reply + .status((err as FetchError).response?.status || 500) + .send({ ok: false, traceId, message: err.message }) + } else if (err instanceof MetaError) { + reply.status(err.status).send({ ok: false, traceId, message: err.metaMessage }) + } else { + const message = (err as any).message || "Internal Server Error" + const status = Number.parseInt((err as any).code as string) || 500 + reply.status(status).send({ ok: false, message, traceId }) + } + }) + + app.addHook("onRequest", (req, reply, done) => { + req.requestContext.set("req", req) + + const { host } = req.headers + + const forwardedHost = req.headers["x-forwarded-host"] + const finalHost = forwardedHost || host + + const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod" + req.requestContext.set("upstreamEnv", upstreamEnv) + if (upstreamEnv === "prod") { + req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) + } else { + req.requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) + } + reply.header("x-handled-host", finalHost) + done() + }) + + ogRoute(app) + globalRoute(app) + }) + + return app +} diff --git a/apps/ssr/worker-entry.ts b/apps/ssr/worker-entry.ts new file mode 100644 index 000000000..f29f62b09 --- /dev/null +++ b/apps/ssr/worker-entry.ts @@ -0,0 +1,334 @@ +import "./global" + +// Global route dependencies +import { env } from "@follow/shared/env.ssr" +import { Hono } from "hono" +import { minify } from "html-minifier-terser" +import { parseHTML } from "linkedom" +import { FetchError } from "ofetch" +import xss from "xss" + +// @ts-expect-error - WASM import handled by Wrangler +import resvgWasm from "./resvg.wasm" +// OG image rendering +import { createFollowClient } from "./src/lib/api-client" +import { NotFoundError } from "./src/lib/not-found" +import { setFontsBucket } from "./src/lib/og/fonts.worker" +import { setWasmModule } from "./src/lib/og/resvg-wasm-shim" +import { buildSeoMetaTags } from "./src/lib/seo" +import { + createRequestProxy, + requestContext, + runWithRequestContext, +} from "./src/lib/worker-request-context" +import { injectMetaHandler, MetaError } from "./src/meta-handler" +import { renderFeedOG } from "./src/router/og/feed" +import { renderListOG } from "./src/router/og/list" +import { renderUserOG } from "./src/router/og/user" + +Object.assign(globalThis, { + __DEV__: false, +}) + +// Initialize WASM module +setWasmModule(resvgWasm) + +interface Env { + FONTS_BUCKET: R2Bucket + ASSETS: Fetcher + VITE_API_URL: string + VITE_WEB_URL: string + VITE_EXTERNAL_DEV_API_URL: string + VITE_EXTERNAL_PROD_API_URL: string + VITE_WEB_DEV_URL: string + VITE_SENTRY_DSN: string +} + +const app = new Hono<{ Bindings: Env }>() + +let envInitialized = false + +// Redirects (migrated from vercel.json) +app.get("/feed/:id", (c) => { + return c.redirect(`/share/feeds/${c.req.param("id")}`, 301) +}) +app.get("/list/:id", (c) => { + return c.redirect(`/share/lists/${c.req.param("id")}`, 301) +}) +app.get("/profile/:path{.*}", (c) => { + return c.redirect(`/share/users/${c.req.param("path")}`, 301) +}) + +// Middleware: set up env vars and request context +app.use("*", async (c, next) => { + if (!envInitialized) { + const bindings = c.env + for (const [key, value] of Object.entries(bindings)) { + if (typeof value === "string") { + process.env[key] = value + } + } + if (bindings.FONTS_BUCKET) { + setFontsBucket(bindings.FONTS_BUCKET) + } + envInitialized = true + } + + return runWithRequestContext(async () => { + // Determine upstream env from host + const host = c.req.header("host") || "" + const forwardedHost = c.req.header("x-forwarded-host") + const finalHost = forwardedHost || host + const upstreamEnv = + finalHost === "dev.folo.is" || finalHost?.includes("folo-ssr-dev") ? "dev" : "prod" + + // Create a req-like proxy for compatibility with existing modules + const headers: Record = {} + c.req.raw.headers.forEach((value, key) => { + headers[key] = value + }) + + const reqProxy = createRequestProxy( + c.req.path + (c.req.raw.url.includes("?") ? `?${c.req.raw.url.split("?")[1]}` : ""), + headers, + ) + + // Set request context values + requestContext.set("req", reqProxy) + requestContext.set("upstreamEnv", upstreamEnv) + if (upstreamEnv === "prod") { + requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) + } else { + requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) + } + + await next() + + c.header("x-handled-host", finalHost) + }) +}) + +// OG image route +app.get("/og/:type/:id", async (c) => { + const type = c.req.param("type") + const id = c.req.param("id") + + const apiClient = createFollowClient() + let imageRes: { image: Buffer; contentType: string } | null = null + + try { + switch (type) { + case "feed": { + imageRes = await renderFeedOG(apiClient, id) + break + } + case "user": { + imageRes = await renderUserOG(apiClient, id) + break + } + case "list": { + imageRes = await renderListOG(apiClient, id) + break + } + default: { + return c.text("Not found", 404) + } + } + } catch (e: any) { + if (typeof e === "number") { + return c.text(e === 404 ? "Not found" : "Internal server error", e) + } + console.error("OG render error:", e) + return c.text(e?.message || "Internal server error", 500) + } + + if (!imageRes) { + return c.text("Not found", 404) + } + + return new Response(imageRes.image, { + headers: { + "Content-Type": imageRes.contentType, + "Cache-Control": "max-age=3600, s-maxage=3600, stale-while-revalidate=600", + "Cloudflare-CDN-Cache-Control": "max-age=3600, s-maxage=3600, stale-while-revalidate=600", + "CDN-Cache-Control": "max-age=3600, s-maxage=3600, stale-while-revalidate=600", + }, + }) +}) + +// SSR routes - use SSR template with meta injection +// These routes correspond to the vercel.json rewrites to follow-external-ssr +const ssrHandler = async (c: any) => { + // @ts-ignore - dynamic import of generated template + const template = await import("./.generated/index.template").then((m) => m.default) + const { document } = parseHTML(template) + + // Inject meta tags + try { + await injectMetaToTemplate(document, c) + } catch (e) { + console.error("inject meta error", e) + + if (e instanceof NotFoundError) { + c.status(404) + document.documentElement.dataset.notFound = "true" + } else if (e instanceof FetchError && e.response?.status) { + c.status(e.response.status as any) + } else if (e instanceof MetaError) { + return c.json({ ok: false, message: e.metaMessage }, e.status as any) + } + } + + injectEnvToDocument(document) + + const html = await minify(document.toString(), { + removeComments: true, + html5: true, + minifyJS: true, + minifyCSS: true, + removeTagWhitespace: true, + collapseWhitespace: true, + collapseBooleanAttributes: true, + collapseInlineTagWhitespace: true, + }) + + return c.html(html) +} + +app.get("/share/*", ssrHandler) +app.get("/login", ssrHandler) +app.get("/register", ssrHandler) +app.get("/forget-password", ssrHandler) +app.get("/reset-password", ssrHandler) + +// SPA catch-all - fetch index.html from Assets and inject env vars +app.get("*", async (c) => { + const assetResponse = await c.env.ASSETS.fetch(new Request("http://fakehost/index.html")) + const spaHtml = await assetResponse.text() + const { document } = parseHTML(spaHtml) + + injectEnvToDocument(document) + + const html = document.toString() + return c.html(html) +}) + +// Error handling +app.onError((err, c) => { + console.error(err) + + if (err instanceof FetchError) { + return c.json({ ok: false, message: err.message }, (err.response?.status as any) || 500) + } + if (err instanceof MetaError) { + return c.json({ ok: false, message: err.metaMessage }, err.status as any) + } + + return c.json({ ok: false, message: err.message || "Internal Server Error" }, 500) +}) + +export default app + +// Helper: inject env vars into HTML document +function injectEnvToDocument(document: any) { + const upstreamEnv = requestContext.get("upstreamEnv") as string + const upstreamOrigin = requestContext.get("upstreamOrigin") as string + + if (upstreamEnv) { + document.head.prepend(document.createComment(`upstreamEnv: ${upstreamEnv}`)) + + const injectScript = (apiUrl: string) => { + const scriptContent = `function injectEnv(env2) { + for (const key in env2) { + if (env2[key] === void 0) continue; + globalThis["__followEnv"] ??= {}; + globalThis["__followEnv"][key] = env2[key]; + } + } +injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_WEB_URL":"${upstreamOrigin}"})` + const $script = document.createElement("script") + $script.innerHTML = scriptContent + document.head.prepend($script) + } + if (upstreamEnv === "dev" && env.VITE_EXTERNAL_DEV_API_URL) { + injectScript(env.VITE_EXTERNAL_DEV_API_URL) + } + if (upstreamEnv === "prod" && env.VITE_EXTERNAL_PROD_API_URL) { + injectScript(env.VITE_EXTERNAL_PROD_API_URL) + } + } +} + +// Helper: inject meta tags into HTML document +async function injectMetaToTemplate(document: Document, c: any) { + // Create a req/res proxy compatible with injectMetaHandler + const reqProxy = requestContext.get("req") + const resProxy = { + status(code: number) { + c.status(code) + }, + raw: { statusMessage: "" }, + } + + const injectMetadata = await injectMetaHandler(reqProxy as any, resProxy as any) + + if (!injectMetadata) return document + + for (const meta of injectMetadata) { + switch (meta.type) { + case "openGraph": { + const $metaArray = buildSeoMetaTags(document, { openGraph: meta }) + for (const $meta of $metaArray) { + document.head.append($meta) + } + break + } + case "meta": { + const $oldMeta = document.querySelector(`meta[name="${meta.property}"]`) + if ($oldMeta) { + $oldMeta.setAttribute("content", xss(meta.content)) + } else { + const $meta = document.createElement("meta") + $meta.setAttribute("name", meta.property) + $meta.setAttribute("content", xss(meta.content)) + document.head.append($meta) + } + break + } + case "title": { + if (meta.title) { + const $title = document.querySelector("title") + if ($title) { + $title.textContent = `${xss(meta.title)} | Folo` + } else { + const $head = document.querySelector("head") + if ($head) { + const $title = document.createElement("title") + $title.textContent = `${xss(meta.title)} | Folo` + $head.append($title) + } + } + } + break + } + case "description": { + const $meta = document.createElement("meta") + $meta.setAttribute("name", "description") + $meta.setAttribute("content", xss(meta.description)) + document.head.append($meta) + break + } + case "hydrate": { + const script = document.createElement("script") + script.innerHTML = ` + window.__HYDRATE__ = window.__HYDRATE__ || {} + window.__HYDRATE__[${JSON.stringify(meta.key)}] = JSON.parse(${JSON.stringify(JSON.stringify(meta.data))}) + ` + document.head.append(script) + break + } + } + } + + return document +} diff --git a/apps/ssr/wrangler.jsonc b/apps/ssr/wrangler.jsonc new file mode 100644 index 000000000..11092ff3e --- /dev/null +++ b/apps/ssr/wrangler.jsonc @@ -0,0 +1,74 @@ +{ + "$schema": "./node_modules/wrangler/config-schema.json", + "name": "folo-ssr", + "main": "dist/worker/worker-entry.mjs", + "compatibility_date": "2026-02-01", + "compatibility_flags": ["nodejs_compat"], + "account_id": "1f1d1678a2413a54c944b3081bab5c84", + "placement": { + "mode": "smart", + }, + "limits": { + "cpu_ms": 30000, + }, + "rules": [ + { + "type": "CompiledWasm", + "globs": ["**/*.wasm"], + "fallthrough": true, + }, + ], + "assets": { + "directory": "../desktop/out/web", + "not_found_handling": "none", + "html_handling": "none", + "binding": "ASSETS", + }, + "workers_dev": true, + "routes": [ + { + "pattern": "app.folo.is/*", + "zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63", + }, + ], + "r2_buckets": [ + { + "binding": "FONTS_BUCKET", + "bucket_name": "follow", + }, + ], + "vars": { + "VITE_API_URL": "https://api.folo.is", + "VITE_WEB_URL": "https://app.folo.is", + "VITE_EXTERNAL_DEV_API_URL": "https://api.dev.folo.is", + "VITE_EXTERNAL_PROD_API_URL": "https://api.folo.is", + "VITE_WEB_DEV_URL": "https://dev.folo.is", + "VITE_SENTRY_DSN": "https://e5bccf7428aa4e881ed5cb713fdff181@o4507542488023040.ingest.us.sentry.io/4507570439979008", + }, + "env": { + "dev": { + "name": "folo-ssr-dev", + "workers_dev": true, + "routes": [ + { + "pattern": "dev.folo.is/*", + "zone_id": "115ea8e6a7865dbfc1cf4530d5f87f63", + }, + ], + "r2_buckets": [ + { + "binding": "FONTS_BUCKET", + "bucket_name": "follow", + }, + ], + "vars": { + "VITE_API_URL": "https://api.dev.folo.is", + "VITE_WEB_URL": "https://dev.folo.is", + "VITE_EXTERNAL_DEV_API_URL": "https://api.dev.folo.is", + "VITE_EXTERNAL_PROD_API_URL": "https://api.folo.is", + "VITE_WEB_DEV_URL": "https://dev.folo.is", + "VITE_SENTRY_DSN": "https://e5bccf7428aa4e881ed5cb713fdff181@o4507542488023040.ingest.us.sentry.io/4507570439979008", + }, + }, + }, +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4d8098a3..6196896bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -335,7 +335,7 @@ importers: version: 4.3.0 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow-app/readability': specifier: workspace:* version: link:../../../../packages/readability @@ -447,7 +447,7 @@ importers: version: 3.0.2(electron@38.3.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/database': specifier: workspace:* version: link:../../../../packages/internal/database @@ -769,7 +769,7 @@ importers: version: link:../../../../packages/internal/utils '@folo-services/ai-tools': specifier: 'catalog:' - version: 0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3) + version: 0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) '@types/node': specifier: 25.2.3 version: 25.2.3 @@ -802,7 +802,7 @@ importers: version: 4.1.1(@types/react@19.1.17)(react@19.1.0) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/components': specifier: workspace:* version: link:../../packages/internal/components @@ -1183,7 +1183,7 @@ importers: version: 6.2.1 '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/tracker': specifier: workspace:* version: link:../../packages/internal/tracker @@ -1302,6 +1302,9 @@ importers: '@follow/utils': specifier: workspace:* version: link:../../packages/internal/utils + '@resvg/resvg-wasm': + specifier: 2.6.2 + version: 2.6.2 '@types/html-minifier-terser': specifier: 7.0.2 version: 7.0.2 @@ -1326,6 +1329,9 @@ importers: foxact: specifier: 0.2.52 version: 0.2.52(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + hono: + specifier: 4.12.1 + version: 4.12.1 html-minifier-terser: specifier: 7.2.0 version: 7.2.0 @@ -1359,6 +1365,9 @@ importers: vite-plugin-route-builder: specifier: 0.4.1 version: 0.4.1 + wrangler: + specifier: 4.67.0 + version: 4.67.0(bufferutil@4.1.0) packages/configs: dependencies: @@ -1678,7 +1687,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1690,7 +1699,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1762,7 +1771,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/constants': specifier: workspace:* version: link:../constants @@ -1793,10 +1802,10 @@ importers: version: 2.0.0(@types/node@25.2.3) '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@folo-services/drizzle': specifier: 0.1.44 - version: 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) + version: 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) '@t3-oss/env-core': specifier: 0.13.10 version: 0.13.10(typescript@5.9.3)(zod@3.25.76) @@ -1823,7 +1832,7 @@ importers: dependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -1872,7 +1881,7 @@ importers: devDependencies: '@follow-app/client-sdk': specifier: 'catalog:' - version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + version: 0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@follow/configs': specifier: workspace:* version: link:../../configs @@ -2820,6 +2829,49 @@ packages: '@clack/prompts@0.8.2': resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.14.0': + resolution: {integrity: sha512-XKAkWhi1nBdNsSEoNG9nkcbyvfUrSjSf+VYVPfOto3gLTZVc3F4g6RASCMh6IixBKCG2yDgZKQIHGKtjcnLnKg==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: ^1.20260218.0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260219.0': + resolution: {integrity: sha512-k+xM+swQBQnkrvwobjRPxyeYwjLSludJusR0PqeHe+h6X9QIRGgw3s1AO38lXQsqzMSgG5709oOXSF19NKVVaQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260219.0': + resolution: {integrity: sha512-EyfQdsG1KcIVAf4qndT00LZly7sLFm1VxMWHBvOFB/EVYF2sE5HZ0dPbe+yrax5p3eS0oLZthR8ynhz4UulMUQ==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260219.0': + resolution: {integrity: sha512-N0UHXILYYa6htFO/uC92uAqusvynbSbOcHcrVXMKqP9Jy7eqXGMovyKIrNgzYnKIszNB+0lfUYdGI3Wci07LuA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260219.0': + resolution: {integrity: sha512-835pjQ9uuAtwPBOAkPf+oxH3mNE5mqWuE3H7hJsul7WZsRD2FDcariyoT2AW6xyOePILrn4uMnmG1KGc9m/8Pg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260219.0': + resolution: {integrity: sha512-i7qcuOsuAxqqn1n5Ar3Rh1dHUL9vNmpF9FcdMTT84jIrdm5UNrPZz5grJthPmpB9LTcreT9iiP6qKbzGjnCwPA==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + '@code-inspector/core@1.4.2': resolution: {integrity: sha512-7OPkFtkfYaXhuTlwub2jT++rW7VggMMEeqsPIZGvHdXykwKAtzB8nnrj3N3uBT/mRoFfP627ShrVyRzCqyfr2w==} @@ -4601,111 +4653,248 @@ packages: '@ide/backoff@1.0.0': resolution: {integrity: sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@innei/react-resizable-layout@0.7.3-fork.1': resolution: {integrity: sha512-xBgY0+prk58+Hq235dmdi9AI60xYxqGEYxP44t2+3IqZoa/MUNC3FbGz1ICFGhaQurY/hUPQGOawZNVmcw8tNw==} peerDependencies: @@ -5228,6 +5417,15 @@ packages: resolution: {integrity: sha512-ONJU5cUeoeJSy50qOYsMZQHTA/9QKmGgh1ATfEpCLgtbdwqUiwD9MxHNeXUYYI/pocBCz6r1ZCFqiQvO+8SUKA==} engines: {node: '>=16.14'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@posthog/core@1.22.0': resolution: {integrity: sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==} @@ -6135,6 +6333,10 @@ packages: resolution: {integrity: sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==} engines: {node: '>= 10'} + '@resvg/resvg-wasm@2.6.2': + resolution: {integrity: sha512-FqALmHI8D4o6lk/LRWDnhw95z5eO+eAa6ORjVg09YRR7BkcM6oPHU9uyC0gtQG5vpFLvgpeU4+zEAz2H8APHNw==} + engines: {node: '>= 10'} + '@rolldown/binding-android-arm64@1.0.0-rc.3': resolution: {integrity: sha512-0T1k9FinuBZ/t7rZ8jN6OpUKPnUjNdYHoj/cESWrQ3ZraAJ4OMm6z7QjSfCxqj8mOp9kTKc1zHK3kGz5vMu+nQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6495,12 +6697,19 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + '@splinetool/react-spline@4.1.0': resolution: {integrity: sha512-Y379gm17gw+1nxT/YXTCJnVIWuu7tsUH1tp/YxsYb0pZnc9Gljk7Om4Kpq7WPq0bZ4zidVCxf6xn6jgDcbHifQ==} peerDependencies: @@ -7816,6 +8025,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bluebird-lst@1.0.9: resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} @@ -9247,6 +9459,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} @@ -10663,8 +10878,8 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hono@4.11.10: - resolution: {integrity: sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg==} + hono@4.12.1: + resolution: {integrity: sha512-hi9afu8g0lfJVLolxElAZGANCTTl6bewIdsRNhaywfP9K8BPf++F2z6OLrYGIinUwpRKzbZHMhPwvc0ZEpAwGw==} engines: {node: '>=16.9.0'} hookable@6.0.1: @@ -12110,6 +12325,11 @@ packages: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} + miniflare@4.20260219.0: + resolution: {integrity: sha512-EIb5wXbWUnnC60XU2aiFOPNd4fgTXzECkwRSOXZ1vdcY9WZaEE9rVf+h+Apw+WkOHRkp3Dr9/ZhQ5y1R+9iZ4Q==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -13236,6 +13456,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -14383,6 +14604,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -14803,6 +15028,10 @@ packages: resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} engines: {node: '>=14.0.0'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -15374,6 +15603,13 @@ packages: resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==} engines: {node: '>=18.17'} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -15932,10 +16168,25 @@ packages: workbox-window@7.4.0: resolution: {integrity: sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==} + workerd@1.20260219.0: + resolution: {integrity: sha512-l4U4iT5H8jNV6+EK23ExnUV2z6JvqQtQPrT8XCm4G8RpwC9EPpYTOO9s/ImMPJKe1WSbQUQoJ4k8Nd83fz8skQ==} + engines: {node: '>=16'} + hasBin: true + workspace-root@3.3.1: resolution: {integrity: sha512-t/KqdJ8+Fa6ovo2o0tV2+WKfd+5gg2SP59MicW8dnwiN6A2huw73yQrnFyrFokJLPJ+ItwJHUjjhNvabUvl/gg==} hasBin: true + wrangler@4.67.0: + resolution: {integrity: sha512-58OoVth7bqm0nqsRgcI67gHbpp0IfR1JIBqDY0XR1FzRu9Qkjn6v2iJAdFf82QcVBFhaMBYQi88WqYGswq5wlQ==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260219.0 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -15985,6 +16236,18 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -16105,6 +16368,12 @@ packages: yoga-layout@3.2.1: resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zeego@3.0.6: resolution: {integrity: sha512-vg0GCMPYg6or/J91bwRnUpIYwz7QnhkyeKOdd3FjvICg+Gzq2D5QhD8k5RUSv1B+048LpNmNYdLm8qJVIbBONw==} peerDependencies: @@ -17254,6 +17523,29 @@ snapshots: picocolors: 1.1.1 sisteransi: 1.0.5 + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260219.0)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260219.0 + + '@cloudflare/workerd-darwin-64@1.20260219.0': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260219.0': + optional: true + + '@cloudflare/workerd-linux-64@1.20260219.0': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260219.0': + optional: true + + '@cloudflare/workerd-windows-64@1.20260219.0': + optional: true + '@code-inspector/core@1.4.2': dependencies: '@vue/compiler-dom': 3.5.28 @@ -19562,12 +19854,12 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@follow-app/client-sdk@0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@follow-app/client-sdk@0.3.92(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@folo-services/constants': 0.1.53 - '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) + '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) '@folo-services/exceptions': 0.1.28 - '@folo-services/shared': 0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3) + '@folo-services/shared': 0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3) better-auth: 1.3.28(better-sqlite3@12.6.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) zod: 4.1.12 transitivePeerDependencies: @@ -19611,10 +19903,10 @@ snapshots: - svelte - vue - '@folo-services/ai-tools@0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)': + '@folo-services/ai-tools@0.2.65(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3)': dependencies: '@ai-sdk/openai': 2.0.63(zod@4.1.12) - '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) + '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) ai: 5.0.88(zod@4.1.12) drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) zod: 4.1.12 @@ -19655,10 +19947,10 @@ snapshots: dependencies: zod: 4.1.12 - '@folo-services/drizzle@0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)': + '@folo-services/drizzle@0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)': dependencies: '@folo-services/exceptions': 0.1.28 - '@hono/zod-openapi': 1.1.4(hono@4.11.10)(zod@4.1.12) + '@hono/zod-openapi': 1.1.4(hono@4.12.1)(zod@4.1.12) drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3))(zod@4.1.12) nanoid: 5.1.6 @@ -19698,11 +19990,11 @@ snapshots: '@folo-services/exceptions@0.1.28': {} - '@folo-services/shared@0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11)(pg@8.16.3)': + '@folo-services/shared@0.0.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11)(pg@8.16.3)': dependencies: '@folo-services/constants': 0.1.53 - '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.11.10)(kysely@0.28.11) - '@hono/zod-openapi': 1.1.4(hono@4.11.10)(zod@4.1.12) + '@folo-services/drizzle': 0.1.44(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(hono@4.12.1)(kysely@0.28.11) + '@hono/zod-openapi': 1.1.4(hono@4.12.1)(zod@4.1.12) drizzle-orm: 0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3) drizzle-zod: 0.8.3(drizzle-orm@0.45.1(@opentelemetry/api@1.9.0)(better-sqlite3@12.6.2)(expo-sqlite@16.0.10(expo@54.0.33)(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(kysely@0.28.11)(pg@8.16.3))(zod@4.1.12) zod: 4.1.12 @@ -19781,17 +20073,17 @@ snapshots: '@hexagon/base64@1.1.28': {} - '@hono/zod-openapi@1.1.4(hono@4.11.10)(zod@4.1.12)': + '@hono/zod-openapi@1.1.4(hono@4.12.1)(zod@4.1.12)': dependencies: '@asteasolutions/zod-to-openapi': 8.4.1(zod@4.1.12) - '@hono/zod-validator': 0.7.6(hono@4.11.10)(zod@4.1.12) - hono: 4.11.10 + '@hono/zod-validator': 0.7.6(hono@4.12.1)(zod@4.1.12) + hono: 4.12.1 openapi3-ts: 4.5.0 zod: 4.1.12 - '@hono/zod-validator@0.7.6(hono@4.11.10)(zod@4.1.12)': + '@hono/zod-validator@0.7.6(hono@4.12.1)(zod@4.1.12)': dependencies: - hono: 4.11.10 + hono: 4.12.1 zod: 4.1.12 '@hookform/resolvers@5.2.2(react-hook-form@7.71.1(react@19.1.0))': @@ -19875,81 +20167,177 @@ snapshots: '@ide/backoff@1.0.0': {} + '@img/colour@1.0.0': {} + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.8.1 optional: true + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.5': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.5': + optional: true + '@innei/react-resizable-layout@0.7.3-fork.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 @@ -20809,6 +21197,18 @@ snapshots: dependencies: '@pnpm/constants': 7.1.1 + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@posthog/core@1.22.0': dependencies: cross-spawn: 7.0.6 @@ -21776,6 +22176,8 @@ snapshots: '@resvg/resvg-js-win32-ia32-msvc': 2.6.2 '@resvg/resvg-js-win32-x64-msvc': 2.6.2 + '@resvg/resvg-wasm@2.6.2': {} + '@rolldown/binding-android-arm64@1.0.0-rc.3': optional: true @@ -22083,6 +22485,8 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@sindresorhus/is@7.2.0': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -22091,6 +22495,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@speed-highlight/core@1.2.14': {} + '@splinetool/react-spline@4.1.0(@splinetool/runtime@1.12.58)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@splinetool/runtime': 1.12.58 @@ -23711,6 +24117,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + bluebird-lst@1.0.9: dependencies: bluebird: 3.7.2 @@ -25285,6 +25693,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -27326,7 +27736,7 @@ snapshots: dependencies: react-is: 16.13.1 - hono@4.11.10: {} + hono@4.12.1: {} hookable@6.0.1: {} @@ -29102,6 +29512,18 @@ snapshots: mimic-response@3.1.0: {} + miniflare@4.20260219.0(bufferutil@4.1.0): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260219.0 + ws: 8.18.0(bufferutil@4.1.0) + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.1 @@ -31454,6 +31876,37 @@ snapshots: '@img/sharp-win32-ia32': 0.33.5 '@img/sharp-win32-x64': 0.33.5 + sharp@0.34.5: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -31903,6 +32356,8 @@ snapshots: superstruct@2.0.2: {} + supports-color@10.2.2: {} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 @@ -32479,6 +32934,12 @@ snapshots: undici@6.23.0: {} + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -33177,12 +33638,36 @@ snapshots: '@types/trusted-types': 2.0.7 workbox-core: 7.4.0 + workerd@1.20260219.0: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260219.0 + '@cloudflare/workerd-darwin-arm64': 1.20260219.0 + '@cloudflare/workerd-linux-64': 1.20260219.0 + '@cloudflare/workerd-linux-arm64': 1.20260219.0 + '@cloudflare/workerd-windows-64': 1.20260219.0 + workspace-root@3.3.1: dependencies: '@node-kit/lerna-workspace-root': 3.3.1 '@node-kit/pnpm-workspace-root': 3.3.1 '@node-kit/yarn-workspace-root': 3.3.1 + wrangler@4.67.0(bufferutil@4.1.0): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.14.0(unenv@2.0.0-rc.24)(workerd@1.20260219.0) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260219.0(bufferutil@4.1.0) + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260219.0 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -33230,6 +33715,10 @@ snapshots: optionalDependencies: bufferutil: 4.1.0 + ws@8.18.0(bufferutil@4.1.0): + optionalDependencies: + bufferutil: 4.1.0 + ws@8.19.0(bufferutil@4.1.0): optionalDependencies: bufferutil: 4.1.0 @@ -33336,6 +33825,19 @@ snapshots: yoga-layout@3.2.1: {} + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 + zeego@3.0.6(@react-native-menu/menu@2.0.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react-native-ios-context-menu@3.2.1(react-native-ios-utilities@5.2.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(react-native-ios-utilities@5.2.0(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@types/react@19.1.17)(bufferutil@4.1.0)(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/react-context-menu': 2.2.16(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) From a491a82b583623ec48d7393c21a9f4da577c4e8e Mon Sep 17 00:00:00 2001 From: DIYgod Date: Sun, 22 Feb 2026 21:04:33 +0800 Subject: [PATCH 05/41] fix(ssr): disable tsdown inlineOnly check for worker build (#4870) The worker build intentionally bundles all dependencies (noExternal: ["**"]) for Cloudflare Workers. Set inlineOnly: false to suppress the error that causes the build to exit with code 1. Co-authored-by: Claude Opus 4.6 --- apps/ssr/tsdown.worker.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ssr/tsdown.worker.config.ts b/apps/ssr/tsdown.worker.config.ts index c6f6b1dc7..a3ce6d0b0 100644 --- a/apps/ssr/tsdown.worker.config.ts +++ b/apps/ssr/tsdown.worker.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ format: ["esm"], external: ["node:*", /\.wasm$/], noExternal: ["**"], + inlineOnly: false, treeshake: true, splitting: false, From 330ee9cd1a70379ce37890501cce4d620282117b Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 24 Feb 2026 12:24:20 +0800 Subject: [PATCH 06/41] fix: harden setting sync auth lifecycle (#4872) * fix: harden setting sync auth lifecycle * fix: preserve legacy setting sync queue * fix: keep setting queue on auth flaps * fix: persist queue reset on auth errors * fix: initialize setting sync queue earlier * fix: await mobile sync queue load --- .../layer/renderer/src/initialize/index.ts | 25 ++- .../src/modules/settings/helper/sync-queue.ts | 139 ++++++++++++++- apps/mobile/src/initialize/index.ts | 46 +++-- .../mobile/src/modules/settings/sync-queue.ts | 160 +++++++++++++++++- 4 files changed, 341 insertions(+), 29 deletions(-) diff --git a/apps/desktop/layer/renderer/src/initialize/index.ts b/apps/desktop/layer/renderer/src/initialize/index.ts index ff9028aa7..c5333468d 100644 --- a/apps/desktop/layer/renderer/src/initialize/index.ts +++ b/apps/desktop/layer/renderer/src/initialize/index.ts @@ -2,6 +2,8 @@ import { initializeDayjs } from "@follow/components/dayjs" import { registerGlobalContext } from "@follow/shared/bridge" import { DEV, ELECTRON_BUILD, IN_ELECTRON } from "@follow/shared/constants" import { hydrateDatabaseToStore } from "@follow/store/hydrate" +import { whoami } from "@follow/store/user/getters" +import { userSyncService } from "@follow/store/user/store" import { tracker } from "@follow/tracker" import { repository } from "@pkg" import { enableMapSet } from "immer" @@ -82,14 +84,25 @@ export const initializeApp = async () => { apm("initializeSettings", initializeSettings) await apm("i18n", initI18n) - - apm("setting sync", () => { - settingSyncQueue.init() - settingSyncQueue.syncLocal() - }) - await apm("initAnalytics", initAnalytics) + void apm("setting sync", async () => { + await settingSyncQueue.init() + + await userSyncService.whoami().catch(() => null) + + if (!whoami()) { + return + } + await settingSyncQueue.syncLocal() + }).catch((error) => { + appLog("setting sync failed", error) + void tracker.manager.captureException(error, { + module: "setting_sync", + stage: "bootstrap", + }) + }) + const loadingTime = Date.now() - now appLog(`Initialize ${APP_NAME} done,`, `${loadingTime}ms`) diff --git a/apps/desktop/layer/renderer/src/modules/settings/helper/sync-queue.ts b/apps/desktop/layer/renderer/src/modules/settings/helper/sync-queue.ts index 9dc838a66..414ca2816 100644 --- a/apps/desktop/layer/renderer/src/modules/settings/helper/sync-queue.ts +++ b/apps/desktop/layer/renderer/src/modules/settings/helper/sync-queue.ts @@ -1,8 +1,11 @@ import type { AISettings, GeneralSettings, UISettings } from "@follow/shared/settings/interface" +import { whoami } from "@follow/store/user/getters" +import { tracker } from "@follow/tracker" import { EventBus } from "@follow/utils/event-bus" import { getStorageNS } from "@follow/utils/ns" import { isEmptyObject, sleep } from "@follow/utils/utils" import type { SettingsTab } from "@follow-app/client-sdk" +import { FollowAPIError } from "@follow-app/client-sdk" import { omit } from "es-toolkit/compat" import type { PrimitiveAtom } from "jotai" @@ -56,14 +59,56 @@ const bizSettingKeyToTabMapping = { ai: "ai", } +const isUnauthorizedError = (error: unknown) => { + if (error instanceof FollowAPIError) { + return error.status === 401 + } + + if (error && typeof error === "object" && "status" in error) { + return Number((error as { status?: unknown }).status) === 401 + } + + return false +} + export type SettingSyncTab = keyof SettingMapping export interface SettingSyncQueueItem { tab: T payload: Partial date: number } + +interface PersistedSettingSyncQueue { + ownerUserId: string | null + queue: SettingSyncQueueItem[] +} + class SettingSyncQueue { queue: SettingSyncQueueItem[] = [] + private ownerUserId: string | null = null + + private getCurrentUserId() { + return whoami()?.id ?? null + } + + private bindQueueOwner(currentUserId: string) { + if (this.ownerUserId === null) { + this.ownerUserId = currentUserId + return + } + + if (this.ownerUserId !== currentUserId) { + this.ownerUserId = currentUserId + this.queue = [] + } + } + + private reportSyncError(stage: "flush" | "syncLocal", error: unknown) { + void tracker.manager.captureException(error, { + module: "setting_sync", + stage, + }) + } private disposers: (() => void)[] = [] async init() { @@ -72,6 +117,11 @@ class SettingSyncQueue { this.load() const d1 = EventBus.subscribe("SETTING_CHANGE_EVENT", (data) => { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) return + + this.bindQueueOwner(currentUserId) + const tab = bizSettingKeyToTabMapping[data.key] if (!tab) return @@ -97,14 +147,21 @@ class SettingSyncQueue { disposer() } this.queue = [] + this.ownerUserId = null } private readonly storageKey = getStorageNS("setting_sync_queue") private persist() { if (this.queue.length === 0) { + localStorage.removeItem(this.storageKey) return } - localStorage.setItem(this.storageKey, JSON.stringify(this.queue)) + + const payload: PersistedSettingSyncQueue = { + ownerUserId: this.ownerUserId, + queue: this.queue, + } + localStorage.setItem(this.storageKey, JSON.stringify(payload)) } private load() { @@ -114,11 +171,37 @@ class SettingSyncQueue { return } + const currentUserId = this.getCurrentUserId() + try { - this.queue = JSON.parse(queue) + const parsed = JSON.parse(queue) as unknown + if (Array.isArray(parsed)) { + // Backward compatibility: legacy versions persisted the queue array directly. + this.queue = parsed + this.ownerUserId = currentUserId + } else if (!parsed || typeof parsed !== "object") { + this.queue = [] + this.ownerUserId = null + return + } else { + const payload = parsed as Partial + this.queue = Array.isArray(payload.queue) ? payload.queue : [] + if (typeof payload.ownerUserId === "string" || payload.ownerUserId === null) { + this.ownerUserId = payload.ownerUserId + } else { + // Backward compatibility for payloads without owner information. + this.ownerUserId = currentUserId + } + } } catch { /* empty */ } + + if (!currentUserId) { + return + } + + this.bindQueueOwner(currentUserId) } private chain = Promise.resolve() @@ -127,6 +210,13 @@ class SettingSyncQueue { private enqueueTime = Date.now() async enqueue(tab: T, payload: Partial) { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) { + return + } + + this.bindQueueOwner(currentUserId) + const now = Date.now() if (isEmptyObject(payload)) { return @@ -144,6 +234,13 @@ class SettingSyncQueue { } private async flush() { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) { + return + } + + this.bindQueueOwner(currentUserId) + if (navigator.onLine === false) { return } @@ -191,10 +288,27 @@ class SettingSyncQueue { promises.push(promise) } - await Promise.all(promises) + try { + await Promise.all(promises) + } catch (error) { + if (isUnauthorizedError(error)) { + this.queue = [] + this.ownerUserId = currentUserId + return + } + + this.reportSyncError("flush", error) + } } replaceRemote(tab?: SettingSyncTab) { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) { + return this.chain + } + + this.bindQueueOwner(currentUserId) + if (!tab) { const promises = [] as Promise[] for (const tab in localSettingGetterMap) { @@ -225,7 +339,24 @@ class SettingSyncQueue { } async syncLocal() { - const remoteSettings = await settings.get().prefetch() + const currentUserId = this.getCurrentUserId() + if (!currentUserId) return + + this.bindQueueOwner(currentUserId) + + const remoteSettings = await settings + .get() + .prefetch() + .catch((error) => { + if (isUnauthorizedError(error)) { + this.queue = [] + this.ownerUserId = currentUserId + return null + } + + this.reportSyncError("syncLocal", error) + return null + }) if (!remoteSettings) return diff --git a/apps/mobile/src/initialize/index.ts b/apps/mobile/src/initialize/index.ts index e9c957fa4..92e9b959b 100644 --- a/apps/mobile/src/initialize/index.ts +++ b/apps/mobile/src/initialize/index.ts @@ -1,5 +1,7 @@ import { initializeDB } from "@follow/database/db" import { hydrateDatabaseToStore } from "@follow/store/hydrate" +import { whoami } from "@follow/store/user/getters" +import { userSyncService } from "@follow/store/user/store" import { tracker } from "@follow/tracker" import { nativeApplicationVersion } from "expo-application" @@ -14,6 +16,20 @@ import { hydrateQueryClient, hydrateSettings } from "./hydrate" import { migrateDatabase } from "./migration" import { initializePlayer } from "./player" +type RequestIdleCallback = (callback: () => void, options?: { timeout?: number }) => number + +const runWhenIdle = (callback: () => void) => { + const requestIdle = (globalThis as { requestIdleCallback?: RequestIdleCallback }) + .requestIdleCallback + + if (requestIdle) { + requestIdle(callback, { timeout: 5000 }) + return + } + + setTimeout(callback, 0) +} + /* eslint-disable no-console */ export const initializeApp = async () => { console.log(`Initialize...`) @@ -38,19 +54,27 @@ export const initializeApp = async () => { dataHydratedTime = Date.now() - dataHydratedTime await apm("hydrateQueryClient", hydrateQueryClient) await apm("initializeAppCheck", initializeAppCheck) - requestIdleCallback( - () => { - apm("initializePlayer", initializePlayer) - }, - { timeout: 5000 }, // Max delay of 5 seconds - ) - - apm("setting sync", () => { - settingSyncQueue.init() - settingSyncQueue.syncLocal() + runWhenIdle(() => { + apm("initializePlayer", initializePlayer) }) - await initAnalytics() + + void apm("setting sync", async () => { + await settingSyncQueue.init() + + await userSyncService.whoami().catch(() => null) + + if (!whoami()) { + return + } + await settingSyncQueue.syncLocal() + }).catch((error) => { + console.error("setting sync failed", error) + void tracker.manager.captureException(error, { + module: "setting_sync", + stage: "bootstrap", + }) + }) const loadingTime = Date.now() - now tracker.appInit({ rn: true, diff --git a/apps/mobile/src/modules/settings/sync-queue.ts b/apps/mobile/src/modules/settings/sync-queue.ts index a980733d5..4911c5993 100644 --- a/apps/mobile/src/modules/settings/sync-queue.ts +++ b/apps/mobile/src/modules/settings/sync-queue.ts @@ -1,7 +1,10 @@ import type { GeneralSettings, UISettings } from "@follow/shared/settings/interface" +import { whoami } from "@follow/store/user/getters" +import { tracker } from "@follow/tracker" import { isEmptyObject, jotaiStore, sleep } from "@follow/utils" import { EventBus } from "@follow/utils/event-bus" import type { SettingsTab } from "@follow-app/client-sdk" +import { FollowAPIError } from "@follow-app/client-sdk" import { omit } from "es-toolkit/compat" import type { PrimitiveAtom } from "jotai" @@ -47,6 +50,18 @@ const bizSettingKeyToTabMapping = { general: "general", } +const isUnauthorizedError = (error: unknown) => { + if (error instanceof FollowAPIError) { + return error.status === 401 + } + + if (error && typeof error === "object" && "status" in error) { + return Number((error as { status?: unknown }).status) === 401 + } + + return false +} + export type SettingSyncTab = keyof SettingMapping export interface SettingSyncQueueItem { tab: T @@ -54,6 +69,11 @@ export interface SettingSyncQueueItem date: number } +interface PersistedSettingSyncQueue { + ownerUserId: string | null + queue: SettingSyncQueueItem[] +} + declare module "@follow/utils/event-bus" { interface CustomEvent { SETTING_CHANGE_EVENT: { @@ -65,14 +85,49 @@ declare module "@follow/utils/event-bus" { class SettingSyncQueue { queue: SettingSyncQueueItem[] = [] + private ownerUserId: string | null = null + + private getCurrentUserId() { + return whoami()?.id ?? null + } + + private bindQueueOwner(currentUserId: string) { + if (this.ownerUserId === null) { + this.ownerUserId = currentUserId + return + } + + if (this.ownerUserId !== currentUserId) { + this.ownerUserId = currentUserId + this.queue = [] + } + } + + private reportSyncError(stage: "flush" | "syncLocal", error: unknown) { + void tracker.manager.captureException(error, { + module: "setting_sync", + stage, + }) + } + + private async clearQueueAndPersist(ownerUserId: string | null) { + this.queue = [] + this.ownerUserId = ownerUserId + await kv.delete(this.storageKey) + } private disposers: (() => void)[] = [] async init() { this.teardown() - this.load() + const loadPromise = this.load() const d1 = EventBus.subscribe("SETTING_CHANGE_EVENT", (data) => { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) return + + this.bindQueueOwner(currentUserId) + const tab = bizSettingKeyToTabMapping[data.key] as SettingSyncTab if (!tab) return @@ -80,10 +135,12 @@ class SettingSyncQueue { if (isEmptyObject(nextPayload)) return this.enqueue(tab, nextPayload) - this.persist() + void this.persist() }) this.disposers.push(d1) + + await loadPromise } teardown() { @@ -91,28 +148,69 @@ class SettingSyncQueue { disposer() } this.queue = [] + this.ownerUserId = null } private readonly storageKey = "setting_sync_queue" private async persist() { if (this.queue.length === 0) { + kv.delete(this.storageKey) return } - kv.set(this.storageKey, JSON.stringify(this.queue)) + + const payload: PersistedSettingSyncQueue = { + ownerUserId: this.ownerUserId, + queue: this.queue, + } + kv.set(this.storageKey, JSON.stringify(payload)) } private async load() { const queue = await kv.get(this.storageKey) - kv.delete(this.storageKey) + await kv.delete(this.storageKey) if (!queue) { return } + const currentUserId = this.getCurrentUserId() + let nextQueue: SettingSyncQueueItem[] = [] + let nextOwnerUserId: string | null = null + try { - this.queue = JSON.parse(queue) + const parsed = JSON.parse(queue) as unknown + if (Array.isArray(parsed)) { + // Backward compatibility: legacy versions persisted the queue array directly. + nextQueue = parsed + nextOwnerUserId = currentUserId + } else if (!parsed || typeof parsed !== "object") { + return + } else { + const payload = parsed as Partial + nextQueue = Array.isArray(payload.queue) ? payload.queue : [] + if (typeof payload.ownerUserId === "string" || payload.ownerUserId === null) { + nextOwnerUserId = payload.ownerUserId + } else { + // Backward compatibility for payloads without owner information. + nextOwnerUserId = currentUserId + } + } } catch { - /* empty */ + return } + + // If queue state has already changed after init starts, keep the newer in-memory state. + if (this.queue.length > 0 || this.ownerUserId !== null) { + return + } + + this.queue = nextQueue + this.ownerUserId = nextOwnerUserId + + if (!currentUserId) { + return + } + + this.bindQueueOwner(currentUserId) } private chain = Promise.resolve() @@ -121,6 +219,13 @@ class SettingSyncQueue { private enqueueTime = Date.now() async enqueue(tab: T, payload: Partial) { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) { + return + } + + this.bindQueueOwner(currentUserId) + const now = Date.now() if (isEmptyObject(payload)) { return @@ -138,6 +243,13 @@ class SettingSyncQueue { } private async flush() { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) { + return + } + + this.bindQueueOwner(currentUserId) + if (navigator.onLine === false) { return } @@ -189,10 +301,26 @@ class SettingSyncQueue { promises.push(promise) } - await Promise.all(promises) + try { + await Promise.all(promises) + } catch (error) { + if (isUnauthorizedError(error)) { + await this.clearQueueAndPersist(currentUserId) + return + } + + this.reportSyncError("flush", error) + } } replaceRemote(tab?: SettingSyncTab) { + const currentUserId = this.getCurrentUserId() + if (!currentUserId) { + return this.chain + } + + this.bindQueueOwner(currentUserId) + if (!tab) { const promises = [] as Promise[] for (const tab in localSettingGetterMap) { @@ -238,7 +366,23 @@ class SettingSyncQueue { return promise } async syncLocal() { - const remoteSettings = await this.fetchSettingRemote() + const currentUserId = this.getCurrentUserId() + if (!currentUserId) return + + this.bindQueueOwner(currentUserId) + + let remoteSettings: Awaited> | null = null + try { + remoteSettings = await this.fetchSettingRemote() + } catch (error) { + if (isUnauthorizedError(error)) { + await this.clearQueueAndPersist(currentUserId) + return + } + + this.reportSyncError("syncLocal", error) + return + } if (!remoteSettings) return if (__DEV__) { From afddc8e84670b1f9c05e6b6066de78e7075681b8 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 24 Feb 2026 14:20:27 +0800 Subject: [PATCH 07/41] fix(ssr): use VITE_API_URL and VITE_WEB_URL only (#4874) --- apps/ssr/client/lib/api-fetch.ts | 2 +- apps/ssr/index.ts | 11 -------- apps/ssr/src/lib/api-client.ts | 14 ++-------- apps/ssr/src/meta-handler.ts | 7 +++-- apps/ssr/src/router/global.ts | 33 ++++------------------ apps/ssr/worker-app.ts | 10 ------- apps/ssr/worker-entry.ts | 37 ++++--------------------- apps/ssr/wrangler.jsonc | 6 ---- packages/internal/shared/src/env.ssr.ts | 7 ----- 9 files changed, 18 insertions(+), 109 deletions(-) diff --git a/apps/ssr/client/lib/api-fetch.ts b/apps/ssr/client/lib/api-fetch.ts index 1e5443a5c..08154f89f 100644 --- a/apps/ssr/client/lib/api-fetch.ts +++ b/apps/ssr/client/lib/api-fetch.ts @@ -9,7 +9,7 @@ import PKG from "../../../desktop/package.json" export const followClient = new FollowClient({ credentials: "include", timeout: 30000, - baseURL: env.VITE_EXTERNAL_API_URL || env.VITE_API_URL, + baseURL: env.VITE_API_URL, fetch: async (input: any, options = {}) => fetch(input.toString(), { ...options, diff --git a/apps/ssr/index.ts b/apps/ssr/index.ts index 19bf82b7a..638b8fde0 100644 --- a/apps/ssr/index.ts +++ b/apps/ssr/index.ts @@ -5,7 +5,6 @@ import os from "node:os" import middie from "@fastify/middie" import { fastifyRequestContext } from "@fastify/request-context" -import { env } from "@follow/shared/env.ssr" import type { FastifyRequest } from "fastify" import Fastify from "fastify" import { nanoid } from "nanoid" @@ -20,9 +19,6 @@ const isVercel = process.env.VERCEL === "1" declare module "@fastify/request-context" { interface RequestContextData { req: FastifyRequest - - upstreamEnv: "prod" | "dev" - upstreamOrigin: string } } @@ -60,13 +56,6 @@ export const createApp = async () => { const forwardedHost = req.headers["x-forwarded-host"] const finalHost = forwardedHost || host - const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod" - if (!__DEV__) req.requestContext.set("upstreamEnv", upstreamEnv) - if (upstreamEnv === "prod") { - req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) - } else { - req.requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) - } reply.header("x-handled-host", finalHost) done() }) diff --git a/apps/ssr/src/lib/api-client.ts b/apps/ssr/src/lib/api-client.ts index e6502665e..cb896e80a 100644 --- a/apps/ssr/src/lib/api-client.ts +++ b/apps/ssr/src/lib/api-client.ts @@ -11,18 +11,10 @@ import PKG from "../../../desktop/package.json" const getBaseURL = () => { const req = requestContext.get("req")! const { host } = req.headers - let baseURL = env.VITE_EXTERNAL_API_URL || env.VITE_API_URL + let baseURL = env.VITE_API_URL - if (env.VITE_EXTERNAL_API_URL?.startsWith("/")) { - baseURL = `http://${host}${env.VITE_EXTERNAL_API_URL}` - } - - const upstreamEnv = req.requestContext.get("upstreamEnv") - if (upstreamEnv === "dev" && env.VITE_EXTERNAL_DEV_API_URL) { - baseURL = env.VITE_EXTERNAL_DEV_API_URL - } - if (upstreamEnv === "prod" && env.VITE_EXTERNAL_PROD_API_URL) { - baseURL = env.VITE_EXTERNAL_PROD_API_URL + if (env.VITE_API_URL.startsWith("/")) { + baseURL = `http://${host}${env.VITE_API_URL}` } return baseURL } diff --git a/apps/ssr/src/meta-handler.ts b/apps/ssr/src/meta-handler.ts index 668d4ec1c..3e12e018b 100644 --- a/apps/ssr/src/meta-handler.ts +++ b/apps/ssr/src/meta-handler.ts @@ -1,3 +1,4 @@ +import { env } from "@follow/shared/env.ssr" import type { FollowClient } from "@follow-app/client-sdk" import type { FastifyReply, FastifyRequest } from "fastify" import { match } from "path-to-regexp" @@ -41,12 +42,12 @@ export async function injectMetaHandler( res: FastifyReply, ): Promise { const apiClient = createFollowClient() - const upstreamOrigin = req.requestContext.get("upstreamOrigin") + const webOrigin = env.VITE_WEB_URL const url = req.originalUrl for (const [pattern, handler] of Object.entries(importer)) { const matchFn = match(pattern, { decode: decodeURIComponent }) - const parsedUrl = new URL(url, upstreamOrigin) + const parsedUrl = new URL(url, webOrigin) const result = matchFn(parsedUrl.pathname) if (result) { @@ -56,7 +57,7 @@ export async function injectMetaHandler( url: parsedUrl, req, apiClient, - origin: upstreamOrigin || "", + origin: webOrigin, setStatus(status) { res.status(status) }, diff --git a/apps/ssr/src/router/global.ts b/apps/ssr/src/router/global.ts index 16a1d9358..32fb3fd48 100644 --- a/apps/ssr/src/router/global.ts +++ b/apps/ssr/src/router/global.ts @@ -42,40 +42,17 @@ const prodHandler = (app: FastifyInstance) => { const { document } = parseHTML(template) await safeInjectMetaToTemplate(document, req, reply) - const isInVercelReverseProxy = req.headers["x-middleware-subrequest"] - - const upstreamEnv = req.requestContext.get("upstreamEnv") - if (isInVercelReverseProxy || upstreamEnv === "prod") { - const upstreamEnv = req.requestContext.get("upstreamEnv") - - if (upstreamEnv) { - document.head.prepend(document.createComment(`upstreamEnv: ${upstreamEnv}`)) - - // override client side api url - - const upstreamOrigin = req.requestContext.get("upstreamOrigin") - - const injectScript = (apiUrl: string) => { - const template = `function injectEnv(env2) { + const scriptContent = `function injectEnv(env2) { for (const key in env2) { if (env2[key] === void 0) continue; globalThis["__followEnv"] ??= {}; globalThis["__followEnv"][key] = env2[key]; } } -injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_WEB_URL":"${upstreamOrigin}"})` - const $script = document.createElement("script") - $script.innerHTML = template - document.head.prepend($script) - } - if (upstreamEnv === "dev" && env.VITE_EXTERNAL_DEV_API_URL) { - injectScript(env.VITE_EXTERNAL_DEV_API_URL) - } - if (upstreamEnv === "prod" && env.VITE_EXTERNAL_PROD_API_URL) { - injectScript(env.VITE_EXTERNAL_PROD_API_URL) - } - } - } +injectEnv({"VITE_API_URL":"${env.VITE_API_URL}","VITE_WEB_URL":"${env.VITE_WEB_URL}"})` + const $script = document.createElement("script") + $script.innerHTML = scriptContent + document.head.prepend($script) reply.type("text/html") reply.send( diff --git a/apps/ssr/worker-app.ts b/apps/ssr/worker-app.ts index 93295f5c1..22be11646 100644 --- a/apps/ssr/worker-app.ts +++ b/apps/ssr/worker-app.ts @@ -1,5 +1,4 @@ import { fastifyRequestContext } from "@fastify/request-context" -import { env } from "@follow/shared/env.ssr" import type { FastifyRequest } from "fastify" import Fastify from "fastify" import { nanoid } from "nanoid" @@ -12,8 +11,6 @@ import { ogRoute } from "./src/router/og" declare module "@fastify/request-context" { interface RequestContextData { req: FastifyRequest - upstreamEnv: "prod" | "dev" - upstreamOrigin: string } } @@ -52,13 +49,6 @@ export const createApp = () => { const forwardedHost = req.headers["x-forwarded-host"] const finalHost = forwardedHost || host - const upstreamEnv = finalHost?.includes("dev") ? "dev" : "prod" - req.requestContext.set("upstreamEnv", upstreamEnv) - if (upstreamEnv === "prod") { - req.requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) - } else { - req.requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) - } reply.header("x-handled-host", finalHost) done() }) diff --git a/apps/ssr/worker-entry.ts b/apps/ssr/worker-entry.ts index f29f62b09..dd7dbd50d 100644 --- a/apps/ssr/worker-entry.ts +++ b/apps/ssr/worker-entry.ts @@ -38,9 +38,6 @@ interface Env { ASSETS: Fetcher VITE_API_URL: string VITE_WEB_URL: string - VITE_EXTERNAL_DEV_API_URL: string - VITE_EXTERNAL_PROD_API_URL: string - VITE_WEB_DEV_URL: string VITE_SENTRY_DSN: string } @@ -75,12 +72,9 @@ app.use("*", async (c, next) => { } return runWithRequestContext(async () => { - // Determine upstream env from host const host = c.req.header("host") || "" const forwardedHost = c.req.header("x-forwarded-host") const finalHost = forwardedHost || host - const upstreamEnv = - finalHost === "dev.folo.is" || finalHost?.includes("folo-ssr-dev") ? "dev" : "prod" // Create a req-like proxy for compatibility with existing modules const headers: Record = {} @@ -95,12 +89,6 @@ app.use("*", async (c, next) => { // Set request context values requestContext.set("req", reqProxy) - requestContext.set("upstreamEnv", upstreamEnv) - if (upstreamEnv === "prod") { - requestContext.set("upstreamOrigin", env.VITE_WEB_PROD_URL || env.VITE_WEB_URL) - } else { - requestContext.set("upstreamOrigin", env.VITE_WEB_DEV_URL || env.VITE_WEB_URL) - } await next() @@ -231,32 +219,17 @@ export default app // Helper: inject env vars into HTML document function injectEnvToDocument(document: any) { - const upstreamEnv = requestContext.get("upstreamEnv") as string - const upstreamOrigin = requestContext.get("upstreamOrigin") as string - - if (upstreamEnv) { - document.head.prepend(document.createComment(`upstreamEnv: ${upstreamEnv}`)) - - const injectScript = (apiUrl: string) => { - const scriptContent = `function injectEnv(env2) { + const scriptContent = `function injectEnv(env2) { for (const key in env2) { if (env2[key] === void 0) continue; globalThis["__followEnv"] ??= {}; globalThis["__followEnv"][key] = env2[key]; } } -injectEnv({"VITE_API_URL":"${apiUrl}","VITE_EXTERNAL_API_URL":"${apiUrl}","VITE_WEB_URL":"${upstreamOrigin}"})` - const $script = document.createElement("script") - $script.innerHTML = scriptContent - document.head.prepend($script) - } - if (upstreamEnv === "dev" && env.VITE_EXTERNAL_DEV_API_URL) { - injectScript(env.VITE_EXTERNAL_DEV_API_URL) - } - if (upstreamEnv === "prod" && env.VITE_EXTERNAL_PROD_API_URL) { - injectScript(env.VITE_EXTERNAL_PROD_API_URL) - } - } +injectEnv({"VITE_API_URL":"${env.VITE_API_URL}","VITE_WEB_URL":"${env.VITE_WEB_URL}"})` + const $script = document.createElement("script") + $script.innerHTML = scriptContent + document.head.prepend($script) } // Helper: inject meta tags into HTML document diff --git a/apps/ssr/wrangler.jsonc b/apps/ssr/wrangler.jsonc index 11092ff3e..513147edf 100644 --- a/apps/ssr/wrangler.jsonc +++ b/apps/ssr/wrangler.jsonc @@ -40,9 +40,6 @@ "vars": { "VITE_API_URL": "https://api.folo.is", "VITE_WEB_URL": "https://app.folo.is", - "VITE_EXTERNAL_DEV_API_URL": "https://api.dev.folo.is", - "VITE_EXTERNAL_PROD_API_URL": "https://api.folo.is", - "VITE_WEB_DEV_URL": "https://dev.folo.is", "VITE_SENTRY_DSN": "https://e5bccf7428aa4e881ed5cb713fdff181@o4507542488023040.ingest.us.sentry.io/4507570439979008", }, "env": { @@ -64,9 +61,6 @@ "vars": { "VITE_API_URL": "https://api.dev.folo.is", "VITE_WEB_URL": "https://dev.folo.is", - "VITE_EXTERNAL_DEV_API_URL": "https://api.dev.folo.is", - "VITE_EXTERNAL_PROD_API_URL": "https://api.folo.is", - "VITE_WEB_DEV_URL": "https://dev.folo.is", "VITE_SENTRY_DSN": "https://e5bccf7428aa4e881ed5cb713fdff181@o4507542488023040.ingest.us.sentry.io/4507570439979008", }, }, diff --git a/packages/internal/shared/src/env.ssr.ts b/packages/internal/shared/src/env.ssr.ts index 1e8cebbde..df3a41ba2 100644 --- a/packages/internal/shared/src/env.ssr.ts +++ b/packages/internal/shared/src/env.ssr.ts @@ -22,13 +22,6 @@ export const env = createEnv({ VITE_POSTHOG_KEY: z.string().optional().default(DEFAULT_VALUES.PROD.POSTHOG_KEY), VITE_POSTHOG_HOST: z.string().url().optional().default(DEFAULT_VALUES.PROD.POSTHOG_HOST), - // For external, use api_url if you don't want to fill it in. - VITE_EXTERNAL_PROD_API_URL: z.string().optional(), - VITE_EXTERNAL_DEV_API_URL: z.string().optional(), - VITE_EXTERNAL_API_URL: z.string().optional(), - VITE_WEB_PROD_URL: z.string().optional(), - VITE_WEB_DEV_URL: z.string().optional(), - VITE_RECAPTCHA_V3_SITE_KEY: z.string().default(DEFAULT_VALUES.PROD.RECAPTCHA_V3_SITE_KEY), }, From adca1960a6932b6efcd7d4107905f2bcce168ac4 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 24 Feb 2026 15:47:22 +0800 Subject: [PATCH 08/41] fix(desktop): switch AI spline asset domain (#4875) --- .../src/modules/ai-chat/components/3d-models/AISplineLoader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/layer/renderer/src/modules/ai-chat/components/3d-models/AISplineLoader.tsx b/apps/desktop/layer/renderer/src/modules/ai-chat/components/3d-models/AISplineLoader.tsx index 235ee3b14..c92941874 100644 --- a/apps/desktop/layer/renderer/src/modules/ai-chat/components/3d-models/AISplineLoader.tsx +++ b/apps/desktop/layer/renderer/src/modules/ai-chat/components/3d-models/AISplineLoader.tsx @@ -2,7 +2,7 @@ import { clamp, cn } from "@follow/utils" import Spline from "@splinetool/react-spline" import { useCallback, useRef } from "react" -const resolvedAIIconUrl = "https://cdn.follow.is/ai2.splinecode" +const resolvedAIIconUrl = "https://assets.folo.is/ai2.splinecode" export const AISplineLoader = ({ className }: { className?: string }) => { const containerRef = useRef(null) From 3d812446c6178b775fc02a6715bb09f593d3718c Mon Sep 17 00:00:00 2001 From: DIYgod Date: Tue, 24 Feb 2026 18:01:33 +0800 Subject: [PATCH 09/41] feat(mobile): add French locale support (#4876) --- apps/mobile/app.config.ts | 2 +- apps/mobile/ios/Folo/Info.plist | 1 + apps/mobile/src/@types/constants.ts | 3 ++- apps/mobile/src/@types/default-resource.ts | 12 ++++++++++++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 8413bbe26..3b02f97a6 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -57,7 +57,7 @@ export default ({ config }: ConfigContext): ExpoConfig => { LSApplicationQueriesSchemes: ["bilibili", "youtube"], CFBundleAllowMixedLocalizations: true, // apps/mobile/src/@types/constants.ts currentSupportedLanguages - CFBundleLocalizations: ["en", "ja", "zh-CN", "zh-TW"], + CFBundleLocalizations: ["en", "ja", "zh-CN", "zh-TW", "fr-FR"], CFBundleDevelopmentRegion: "en", }, googleServicesFile: "./build/GoogleService-Info.plist", diff --git a/apps/mobile/ios/Folo/Info.plist b/apps/mobile/ios/Folo/Info.plist index 402f88e04..f1d5335f6 100644 --- a/apps/mobile/ios/Folo/Info.plist +++ b/apps/mobile/ios/Folo/Info.plist @@ -26,6 +26,7 @@ ja zh-CN zh-TW + fr-FR CFBundleName $(PRODUCT_NAME) diff --git a/apps/mobile/src/@types/constants.ts b/apps/mobile/src/@types/constants.ts index 6ab13e658..eb091b9af 100644 --- a/apps/mobile/src/@types/constants.ts +++ b/apps/mobile/src/@types/constants.ts @@ -1,4 +1,4 @@ -const langs = ["en", "ja", "zh-CN", "zh-TW"] as const +const langs = ["en", "ja", "zh-CN", "zh-TW", "fr-FR"] as const export const currentSupportedLanguages = langs as readonly string[] export type MobileSupportedLanguages = (typeof langs)[number] @@ -10,4 +10,5 @@ export const dayjsLocaleImportMap = { ["zh-CN"]: ["zh-cn", () => import("dayjs/locale/zh-cn")], ["ja"]: ["ja", () => import("dayjs/locale/ja")], ["zh-TW"]: ["zh-tw", () => import("dayjs/locale/zh-tw")], + ["fr-FR"]: ["fr", () => import("dayjs/locale/fr")], } as const diff --git a/apps/mobile/src/@types/default-resource.ts b/apps/mobile/src/@types/default-resource.ts index 2f69b4930..d42c0d04d 100644 --- a/apps/mobile/src/@types/default-resource.ts +++ b/apps/mobile/src/@types/default-resource.ts @@ -1,20 +1,25 @@ import common_en from "@locales/common/en.json" +import common_frFR from "@locales/common/fr-FR.json" import common_ja from "@locales/common/ja.json" import common_zhCN from "@locales/common/zh-CN.json" import common_zhTW from "@locales/common/zh-TW.json" import errors_en from "@locales/errors/en.json" +import errors_frFR from "@locales/errors/fr-FR.json" import errors_ja from "@locales/errors/ja.json" import errors_zhCN from "@locales/errors/zh-CN.json" import errors_zhTW from "@locales/errors/zh-TW.json" import lang_en from "@locales/lang/en.json" +import lang_frFR from "@locales/lang/fr-FR.json" import lang_ja from "@locales/lang/ja.json" import lang_zhCN from "@locales/lang/zh-CN.json" import lang_zhTW from "@locales/lang/zh-TW.json" import en from "@locales/mobile/default/en.json" +import frFR from "@locales/mobile/default/fr-FR.json" import ja from "@locales/mobile/default/ja.json" import zhCN from "@locales/mobile/default/zh-CN.json" import zhTW from "@locales/mobile/default/zh-TW.json" import settings_en from "@locales/settings/en.json" +import settings_frFR from "@locales/settings/fr-FR.json" import settings_ja from "@locales/settings/ja.json" import settings_zhCN from "@locales/settings/zh-CN.json" import settings_zhTW from "@locales/settings/zh-TW.json" @@ -23,6 +28,13 @@ import type { MobileSupportedLanguages, ns } from "./constants" // @keep-sorted export const defaultResources = { + "fr-FR": { + common: common_frFR, + default: frFR, + errors: errors_frFR, + lang: lang_frFR, + settings: settings_frFR, + }, // @keep-sorted "zh-CN": { common: common_zhCN, From f0a691da30fa4b4f02c1b9c5769e82eeb83abf2c Mon Sep 17 00:00:00 2001 From: DIYgod Date: Wed, 25 Feb 2026 21:21:01 +0800 Subject: [PATCH 10/41] chore(env): update default recaptcha v3 site key (#4879) --- packages/internal/shared/src/env.common.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/internal/shared/src/env.common.ts b/packages/internal/shared/src/env.common.ts index f81745086..860b689f1 100644 --- a/packages/internal/shared/src/env.common.ts +++ b/packages/internal/shared/src/env.common.ts @@ -16,7 +16,7 @@ export const DEFAULT_VALUES = { FIREBASE_CONFIG: FIREBASE_CONFIG_DEFAULT, OPENPANEL_CLIENT_ID: "4382168f-b8d2-40c1-9a26-133a312d072b", OPENPANEL_API_URL: "https://openpanel.follow.is/api", - RECAPTCHA_V3_SITE_KEY: "6LeNhgwsAAAAAHO1dhunH0a-FI1YvOc9Ny98Oast", + RECAPTCHA_V3_SITE_KEY: "6LeGa3csAAAAALi_WqhlWoaGaqd_kke4HRGvNE0C", POSTHOG_KEY: "phc_EZGEvBt830JgBHTiwpHqJAEbWnbv63m5UpreojwEWNL", POSTHOG_HOST: "https://us.posthog.com", From b89290af8dd53ac42829b611a5eff538866dfd2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:22:28 +0800 Subject: [PATCH 11/41] build(deps): bump actions/attest-build-provenance from 3 to 4 (#4880) Bumps [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) from 3 to 4. - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/attest-build-provenance dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-desktop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index dadf03010..bfa24d3a4 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -272,7 +272,7 @@ jobs: - name: Generate artifact attestation if: env.RELEASE == 'true' continue-on-error: true - uses: actions/attest-build-provenance@v3 + uses: actions/attest-build-provenance@v4 with: subject-path: | apps/desktop/out/make/**/Folo-*.dmg From 5e26d1d33dcb06dee9e4473e9469675c8ce904ca Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 26 Feb 2026 11:33:16 +0800 Subject: [PATCH 12/41] feat(landing): merge landing app into monorepo (#4881) --- .github/workflows/deploy-cloudflare.yml | 21 + .gitignore | 3 + .prettierignore | 1 + apps/desktop/package.json | 1 + apps/landing/.prettierrc.mjs | 5 + apps/landing/components.json | 25 + apps/landing/eslint.config.mjs | 80 + apps/landing/global.d.ts | 42 + apps/landing/next.config.ts | 63 + apps/landing/open-next.config.ts | 3 + apps/landing/package.json | 92 + .../landing/plugins/eslint-recursive-sort.mjs | 60 + apps/landing/postcss.config.mjs | 5 + apps/landing/public/favicon.ico | Bin 0 -> 15406 bytes apps/landing/public/icon-192x192.png | Bin 0 -> 7589 bytes apps/landing/public/icon-512x512.png | Bin 0 -> 30437 bytes apps/landing/public/icon.svg | 1 + apps/landing/public/manifest.json | 16 + apps/landing/public/og.png | Bin 0 -> 124060 bytes apps/landing/src/actions/github-star.ts | 23 + apps/landing/src/app/ClientInit.tsx | 6 + apps/landing/src/app/InitInClient.ts | 9 + .../src/app/[locale]/download/page.tsx | 32 + apps/landing/src/app/[locale]/error.tsx | 26 + apps/landing/src/app/[locale]/layout.tsx | 146 + apps/landing/src/app/[locale]/page.tsx | 19 + .../src/app/[locale]/privacy-policy/page.tsx | 36 + .../app/[locale]/terms-of-service/page.tsx | 34 + .../app/apple-app-site-association/route.ts | 9 + apps/landing/src/app/globals.css | 1 + apps/landing/src/app/init.ts | 7 + apps/landing/src/app/layout.tsx | 9 + apps/landing/src/app/robots.ts | 11 + apps/landing/src/atoms/css-media.ts | 6 + apps/landing/src/atoms/index.ts | 2 + apps/landing/src/atoms/is-interactive.ts | 10 + apps/landing/src/atoms/viewport.ts | 54 + apps/landing/src/components/brand/Folo.tsx | 20 + apps/landing/src/components/brand/Logo.tsx | 29 + .../src/components/common/ClientOnly.tsx | 9 + .../src/components/common/ErrorBoundary.tsx | 37 + .../src/components/common/GithubTrending.tsx | 81 + .../common/HydrationEndDetector.tsx | 23 + .../src/components/common/Lazyload.tsx | 35 + .../src/components/common/LightRays.tsx | 581 ++ .../components/common/ProviderComposer.tsx | 12 + .../src/components/common/QueryHydrate.tsx | 8 + .../src/components/common/ScrollTop.tsx | 18 + .../src/components/hoc/with-no-ssr.tsx | 13 + .../components/layout/container/Normal.tsx | 17 + .../src/components/layout/content/Content.tsx | 7 + .../src/components/layout/content/index.ts | 1 + .../src/components/layout/footer/Footer.tsx | 184 + .../src/components/layout/root/Root.tsx | 13 + .../src/components/ui/3d-models/AISpline.ts | 23 + .../ui/3d-models/AISplineLoader.tsx | 129 + .../src/components/ui/accordion/Accordion.tsx | 251 + .../landing/src/components/ui/border-beam.tsx | 106 + .../src/components/ui/button/Button.tsx | 154 + .../src/components/ui/button/MotionButton.tsx | 27 + .../landing/src/components/ui/button/index.ts | 2 + .../src/components/ui/checkbox/Checkbox.tsx | 174 + .../src/components/ui/checkbox/index.ts | 1 + .../components/ui/collapse/CollapseCss.tsx | 188 + .../src/components/ui/collapse/hooks.tsx | 15 + .../src/components/ui/collapse/index.ts | 1 + .../src/components/ui/dialog/Dialog.tsx | 267 + .../landing/src/components/ui/dialog/index.ts | 12 + .../src/components/ui/divider/Divider.tsx | 36 + .../src/components/ui/divider/index.ts | 1 + .../ui/dropdown-menu/DropdownMenu.tsx | 258 + .../src/components/ui/effects/GridGuides.tsx | 51 + .../components/ui/effects/ParticlesAura.tsx | 64 + .../src/components/ui/effects/TiltCard.tsx | 106 + .../landing/src/components/ui/glass/index.tsx | 390 ++ .../landing/src/components/ui/highlighter.tsx | 101 + .../components/ui/hover-card/HoverCard.tsx | 77 + .../src/components/ui/hover-card/index.ts | 9 + .../landing/src/components/ui/input/Input.tsx | 253 + .../src/components/ui/input/TextArea.tsx | 53 + apps/landing/src/components/ui/input/index.ts | 1 + .../components/ui/json-highlighter/index.tsx | 422 ++ .../landing/src/components/ui/label/Label.tsx | 48 + apps/landing/src/components/ui/light-rays.tsx | 149 + .../src/components/ui/loading/index.tsx | 34 + apps/landing/src/components/ui/magic-card.tsx | 103 + .../src/components/ui/markdown/index.tsx | 51 + .../components/ui/modal/ModalContainer.tsx | 161 + .../src/components/ui/modal/ModalManager.ts | 72 + apps/landing/src/components/ui/modal/hooks.ts | 9 + apps/landing/src/components/ui/modal/index.ts | 4 + apps/landing/src/components/ui/modal/types.ts | 26 + .../src/components/ui/panel/PanelSplitter.tsx | 47 + .../src/components/ui/portal/index.tsx | 20 + .../src/components/ui/portal/provider.tsx | 19 + .../src/components/ui/prompts/BasePrompt.tsx | 100 + .../src/components/ui/prompts/InputPrompt.tsx | 125 + .../src/components/ui/prompts/Prompt.ts | 26 + .../src/components/ui/prompts/index.ts | 3 + .../landing/src/components/ui/radio/index.tsx | 137 + .../ui/relative-time/RelativeTime.tsx | 39 + .../src/components/ui/relative-time/index.ts | 1 + .../components/ui/scroll-areas/ScrollArea.tsx | 178 + .../src/components/ui/scroll-areas/ctx.ts | 5 + .../src/components/ui/scroll-areas/hooks.ts | 9 + .../components/ui/segment-tab/SegmentTab.tsx | 164 + .../src/components/ui/segment-tab/index.ts | 2 + .../components/ui/select/ComboboxSelect.tsx | 158 + .../src/components/ui/select/MultiSelect.tsx | 207 + .../src/components/ui/select/Select.tsx | 220 + .../landing/src/components/ui/select/index.ts | 3 + .../landing/src/components/ui/sheet/Sheet.tsx | 88 + apps/landing/src/components/ui/sheet/index.ts | 1 + .../src/components/ui/skeleton/Skeleton.tsx | 13 + .../src/components/ui/skeleton/index.ts | 1 + .../src/components/ui/switch/index.tsx | 120 + .../ui/theme-switcher/ThemeSwitcher.tsx | 158 + .../src/components/ui/theme-switcher/index.ts | 1 + .../src/components/ui/tooltip/index.tsx | 75 + .../src/components/ui/tooltip/styles.ts | 11 + .../BottomToUpSoftScaleTransitionView.tsx | 15 + .../transition/BottomToUpTransitionView.tsx | 17 + .../ui/transition/FadeInOutTransitionView.tsx | 17 + .../ui/transition/IconTransiton.tsx | 44 + .../transition/LeftToRightTransitionView.tsx | 14 + .../transition/RightToLeftTransitionView.tsx | 14 + .../ui/transition/ScaleTransitionView.tsx | 14 + .../ui/transition/TextUpTransitionView.tsx | 57 + .../src/components/ui/transition/factor.tsx | 86 + .../src/components/ui/transition/typings.ts | 20 + .../components/ui/viewport/OnlyDesktop.tsx | 20 + .../src/components/ui/viewport/OnlyMobile.tsx | 16 + .../src/components/ui/viewport/index.ts | 1 + .../widgets/download/DownloadHero.tsx | 58 + .../widgets/download/PlatformDownloads.tsx | 216 + .../components/widgets/landing/Audience.tsx | 294 + .../components/widgets/landing/BuiltOpen.tsx | 137 + .../components/widgets/landing/Features.tsx | 122 + .../src/components/widgets/landing/Header.tsx | 192 + .../src/components/widgets/landing/Hero.tsx | 182 + .../components/widgets/landing/PromptDemo.tsx | 64 + .../components/widgets/landing/RepoStats.tsx | 85 + .../widgets/landing/SocialProof.tsx | 157 + .../widgets/landing/ViewsShowcase.tsx | 107 + .../widgets/landing/WindowChrome.tsx | 41 + .../widgets/simulators/EntryChatPanel.tsx | 25 + .../widgets/simulators/EntryPage.tsx | 83 + .../widgets/simulators/ListDemo.tsx | 166 + .../widgets/simulators/TimelineChatDemo.tsx | 18 + .../components/EntryPageOverlay.tsx | 5 + .../components/SimulatorReaders.tsx | 27 + .../components/ai/AIChainOfThought.tsx | 245 + .../components/ai/AIMarkdownMessage.tsx | 37 + .../components/ai/AIReasoningPart.tsx | 68 + .../components/ai/ToolInvocationComponent.tsx | 87 + .../ai/animated/AnimatedMarkdown.tsx | 182 + .../components/ai/animated/TokenizedText.tsx | 129 + .../components/ai/animated/constants.ts | 4 + .../widgets/simulators/components/ai/mocks.ts | 3 + .../ai/parse-incomplete-markdown.ts | 730 ++ .../components/ai/reasoning-mock.json | 22 + .../components/ai/shiny-text/ShinyText.tsx | 38 + .../components/ai/shiny-text/index.module.css | 37 + .../components/chat/AiMessageContextBar.tsx | 26 + .../components/chat/AiMockMessage.tsx | 148 + .../components/chat/AiUserMessage.tsx | 31 + .../components/chat/ListChatPlayer.tsx | 439 ++ .../components/chat/MarkdownMessage.tsx | 19 + .../simulators/components/chat/stream.ts | 77 + .../components/widgets/simulators/mocks.tsx | 274 + apps/landing/src/constants/download.ts | 23 + apps/landing/src/constants/env.ts | 12 + apps/landing/src/constants/site.ts | 85 + apps/landing/src/constants/spring.ts | 32 + apps/landing/src/hooks/biz/use-github-star.ts | 10 + .../src/hooks/common/use-before-mounted.ts | 10 + .../src/hooks/common/use-click-away.ts | 59 + .../src/hooks/common/use-debounce-value.ts | 17 + .../src/hooks/common/use-event-callback.ts | 8 + .../src/hooks/common/use-input-composition.ts | 98 + .../landing/src/hooks/common/use-is-active.ts | 14 + .../landing/src/hooks/common/use-is-client.ts | 23 + apps/landing/src/hooks/common/use-is-dark.ts | 6 + .../src/hooks/common/use-is-mounted.ts | 10 + .../src/hooks/common/use-is-unmounted.ts | 12 + apps/landing/src/hooks/common/use-previous.ts | 9 + .../landing/src/hooks/common/use-ref-value.ts | 12 + .../src/hooks/common/use-safe-setState.ts | 13 + .../landing/src/hooks/common/use-state-ref.ts | 9 + .../src/hooks/common/use-sync-effect.ts | 15 + apps/landing/src/hooks/common/useMeasure.ts | 236 + .../src/hooks/shared/use-mask-scrollarea.ts | 72 + apps/landing/src/i18n/request.ts | 10 + apps/landing/src/i18n/routing.ts | 13 + apps/landing/src/legal/privacy.md | 115 + apps/landing/src/legal/tos.md | 131 + .../src/lib/apple-app-site-association.ts | 11 + apps/landing/src/lib/cn.ts | 45 + apps/landing/src/lib/color.ts | 182 + apps/landing/src/lib/cookie.ts | 22 + apps/landing/src/lib/datetime.ts | 83 + apps/landing/src/lib/dom.ts | 18 + apps/landing/src/lib/env.ts | 4 + apps/landing/src/lib/fonts.ts | 10 + apps/landing/src/lib/helper.ts | 19 + apps/landing/src/lib/jotai.ts | 41 + apps/landing/src/lib/noop.ts | 5 + apps/landing/src/lib/platform.ts | 13 + apps/landing/src/lib/query-client.server.ts | 11 + apps/landing/src/lib/scroller.ts | 58 + apps/landing/src/lib/sleep.ts | 2 + apps/landing/src/lib/spring.ts | 75 + apps/landing/src/lib/store.ts | 3 + apps/landing/src/messages/en.json | 92 + apps/landing/src/messages/jp.json | 92 + apps/landing/src/messages/zh.json | 92 + apps/landing/src/middleware.ts | 9 + .../src/providers/root/debug-provider.tsx | 20 + .../src/providers/root/event-provider.tsx | 60 + .../src/providers/root/framer-lazy-feature.ts | 1 + apps/landing/src/providers/root/index.tsx | 40 + .../src/providers/root/jotai-provider.tsx | 7 + .../root/page-scroll-info-provider.tsx | 88 + .../providers/root/react-query-provider.tsx | 68 + apps/landing/src/providers/root/sonner.tsx | 41 + .../shared/LayoutRightSideProvider.tsx | 40 + .../shared/WrappedElementProvider.tsx | 136 + apps/landing/src/styles/globals.css | 515 ++ .../landing/src/styles/pastel-theme-oklch.css | 601 ++ apps/landing/tsconfig.json | 35 + apps/landing/wrangler.jsonc | 29 + apps/ssr/package.json | 1 + plugins/eslint/eslint-package-json.js | 5 + pnpm-lock.yaml | 5875 +++++++++++++++-- pnpm-workspace.yaml | 13 + turbo.json | 3 + 236 files changed, 21510 insertions(+), 625 deletions(-) create mode 100644 apps/landing/.prettierrc.mjs create mode 100644 apps/landing/components.json create mode 100644 apps/landing/eslint.config.mjs create mode 100644 apps/landing/global.d.ts create mode 100644 apps/landing/next.config.ts create mode 100644 apps/landing/open-next.config.ts create mode 100644 apps/landing/package.json create mode 100644 apps/landing/plugins/eslint-recursive-sort.mjs create mode 100644 apps/landing/postcss.config.mjs create mode 100644 apps/landing/public/favicon.ico create mode 100644 apps/landing/public/icon-192x192.png create mode 100644 apps/landing/public/icon-512x512.png create mode 100644 apps/landing/public/icon.svg create mode 100644 apps/landing/public/manifest.json create mode 100644 apps/landing/public/og.png create mode 100644 apps/landing/src/actions/github-star.ts create mode 100644 apps/landing/src/app/ClientInit.tsx create mode 100644 apps/landing/src/app/InitInClient.ts create mode 100644 apps/landing/src/app/[locale]/download/page.tsx create mode 100644 apps/landing/src/app/[locale]/error.tsx create mode 100644 apps/landing/src/app/[locale]/layout.tsx create mode 100644 apps/landing/src/app/[locale]/page.tsx create mode 100644 apps/landing/src/app/[locale]/privacy-policy/page.tsx create mode 100644 apps/landing/src/app/[locale]/terms-of-service/page.tsx create mode 100644 apps/landing/src/app/apple-app-site-association/route.ts create mode 100644 apps/landing/src/app/globals.css create mode 100644 apps/landing/src/app/init.ts create mode 100644 apps/landing/src/app/layout.tsx create mode 100644 apps/landing/src/app/robots.ts create mode 100644 apps/landing/src/atoms/css-media.ts create mode 100644 apps/landing/src/atoms/index.ts create mode 100644 apps/landing/src/atoms/is-interactive.ts create mode 100644 apps/landing/src/atoms/viewport.ts create mode 100644 apps/landing/src/components/brand/Folo.tsx create mode 100644 apps/landing/src/components/brand/Logo.tsx create mode 100644 apps/landing/src/components/common/ClientOnly.tsx create mode 100644 apps/landing/src/components/common/ErrorBoundary.tsx create mode 100644 apps/landing/src/components/common/GithubTrending.tsx create mode 100644 apps/landing/src/components/common/HydrationEndDetector.tsx create mode 100644 apps/landing/src/components/common/Lazyload.tsx create mode 100644 apps/landing/src/components/common/LightRays.tsx create mode 100644 apps/landing/src/components/common/ProviderComposer.tsx create mode 100644 apps/landing/src/components/common/QueryHydrate.tsx create mode 100644 apps/landing/src/components/common/ScrollTop.tsx create mode 100644 apps/landing/src/components/hoc/with-no-ssr.tsx create mode 100644 apps/landing/src/components/layout/container/Normal.tsx create mode 100644 apps/landing/src/components/layout/content/Content.tsx create mode 100644 apps/landing/src/components/layout/content/index.ts create mode 100644 apps/landing/src/components/layout/footer/Footer.tsx create mode 100644 apps/landing/src/components/layout/root/Root.tsx create mode 100644 apps/landing/src/components/ui/3d-models/AISpline.ts create mode 100644 apps/landing/src/components/ui/3d-models/AISplineLoader.tsx create mode 100644 apps/landing/src/components/ui/accordion/Accordion.tsx create mode 100644 apps/landing/src/components/ui/border-beam.tsx create mode 100644 apps/landing/src/components/ui/button/Button.tsx create mode 100644 apps/landing/src/components/ui/button/MotionButton.tsx create mode 100644 apps/landing/src/components/ui/button/index.ts create mode 100644 apps/landing/src/components/ui/checkbox/Checkbox.tsx create mode 100644 apps/landing/src/components/ui/checkbox/index.ts create mode 100644 apps/landing/src/components/ui/collapse/CollapseCss.tsx create mode 100644 apps/landing/src/components/ui/collapse/hooks.tsx create mode 100644 apps/landing/src/components/ui/collapse/index.ts create mode 100644 apps/landing/src/components/ui/dialog/Dialog.tsx create mode 100644 apps/landing/src/components/ui/dialog/index.ts create mode 100644 apps/landing/src/components/ui/divider/Divider.tsx create mode 100644 apps/landing/src/components/ui/divider/index.ts create mode 100644 apps/landing/src/components/ui/dropdown-menu/DropdownMenu.tsx create mode 100644 apps/landing/src/components/ui/effects/GridGuides.tsx create mode 100644 apps/landing/src/components/ui/effects/ParticlesAura.tsx create mode 100644 apps/landing/src/components/ui/effects/TiltCard.tsx create mode 100644 apps/landing/src/components/ui/glass/index.tsx create mode 100644 apps/landing/src/components/ui/highlighter.tsx create mode 100644 apps/landing/src/components/ui/hover-card/HoverCard.tsx create mode 100644 apps/landing/src/components/ui/hover-card/index.ts create mode 100644 apps/landing/src/components/ui/input/Input.tsx create mode 100644 apps/landing/src/components/ui/input/TextArea.tsx create mode 100644 apps/landing/src/components/ui/input/index.ts create mode 100644 apps/landing/src/components/ui/json-highlighter/index.tsx create mode 100644 apps/landing/src/components/ui/label/Label.tsx create mode 100644 apps/landing/src/components/ui/light-rays.tsx create mode 100644 apps/landing/src/components/ui/loading/index.tsx create mode 100644 apps/landing/src/components/ui/magic-card.tsx create mode 100644 apps/landing/src/components/ui/markdown/index.tsx create mode 100644 apps/landing/src/components/ui/modal/ModalContainer.tsx create mode 100644 apps/landing/src/components/ui/modal/ModalManager.ts create mode 100644 apps/landing/src/components/ui/modal/hooks.ts create mode 100644 apps/landing/src/components/ui/modal/index.ts create mode 100644 apps/landing/src/components/ui/modal/types.ts create mode 100644 apps/landing/src/components/ui/panel/PanelSplitter.tsx create mode 100644 apps/landing/src/components/ui/portal/index.tsx create mode 100644 apps/landing/src/components/ui/portal/provider.tsx create mode 100644 apps/landing/src/components/ui/prompts/BasePrompt.tsx create mode 100644 apps/landing/src/components/ui/prompts/InputPrompt.tsx create mode 100644 apps/landing/src/components/ui/prompts/Prompt.ts create mode 100644 apps/landing/src/components/ui/prompts/index.ts create mode 100644 apps/landing/src/components/ui/radio/index.tsx create mode 100644 apps/landing/src/components/ui/relative-time/RelativeTime.tsx create mode 100644 apps/landing/src/components/ui/relative-time/index.ts create mode 100644 apps/landing/src/components/ui/scroll-areas/ScrollArea.tsx create mode 100644 apps/landing/src/components/ui/scroll-areas/ctx.ts create mode 100644 apps/landing/src/components/ui/scroll-areas/hooks.ts create mode 100644 apps/landing/src/components/ui/segment-tab/SegmentTab.tsx create mode 100644 apps/landing/src/components/ui/segment-tab/index.ts create mode 100644 apps/landing/src/components/ui/select/ComboboxSelect.tsx create mode 100644 apps/landing/src/components/ui/select/MultiSelect.tsx create mode 100644 apps/landing/src/components/ui/select/Select.tsx create mode 100644 apps/landing/src/components/ui/select/index.ts create mode 100644 apps/landing/src/components/ui/sheet/Sheet.tsx create mode 100644 apps/landing/src/components/ui/sheet/index.ts create mode 100644 apps/landing/src/components/ui/skeleton/Skeleton.tsx create mode 100644 apps/landing/src/components/ui/skeleton/index.ts create mode 100644 apps/landing/src/components/ui/switch/index.tsx create mode 100644 apps/landing/src/components/ui/theme-switcher/ThemeSwitcher.tsx create mode 100644 apps/landing/src/components/ui/theme-switcher/index.ts create mode 100644 apps/landing/src/components/ui/tooltip/index.tsx create mode 100644 apps/landing/src/components/ui/tooltip/styles.ts create mode 100644 apps/landing/src/components/ui/transition/BottomToUpSoftScaleTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/BottomToUpTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/FadeInOutTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/IconTransiton.tsx create mode 100644 apps/landing/src/components/ui/transition/LeftToRightTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/RightToLeftTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/ScaleTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/TextUpTransitionView.tsx create mode 100644 apps/landing/src/components/ui/transition/factor.tsx create mode 100644 apps/landing/src/components/ui/transition/typings.ts create mode 100644 apps/landing/src/components/ui/viewport/OnlyDesktop.tsx create mode 100644 apps/landing/src/components/ui/viewport/OnlyMobile.tsx create mode 100644 apps/landing/src/components/ui/viewport/index.ts create mode 100644 apps/landing/src/components/widgets/download/DownloadHero.tsx create mode 100644 apps/landing/src/components/widgets/download/PlatformDownloads.tsx create mode 100644 apps/landing/src/components/widgets/landing/Audience.tsx create mode 100644 apps/landing/src/components/widgets/landing/BuiltOpen.tsx create mode 100644 apps/landing/src/components/widgets/landing/Features.tsx create mode 100644 apps/landing/src/components/widgets/landing/Header.tsx create mode 100644 apps/landing/src/components/widgets/landing/Hero.tsx create mode 100644 apps/landing/src/components/widgets/landing/PromptDemo.tsx create mode 100644 apps/landing/src/components/widgets/landing/RepoStats.tsx create mode 100644 apps/landing/src/components/widgets/landing/SocialProof.tsx create mode 100644 apps/landing/src/components/widgets/landing/ViewsShowcase.tsx create mode 100644 apps/landing/src/components/widgets/landing/WindowChrome.tsx create mode 100644 apps/landing/src/components/widgets/simulators/EntryChatPanel.tsx create mode 100644 apps/landing/src/components/widgets/simulators/EntryPage.tsx create mode 100644 apps/landing/src/components/widgets/simulators/ListDemo.tsx create mode 100644 apps/landing/src/components/widgets/simulators/TimelineChatDemo.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/EntryPageOverlay.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/SimulatorReaders.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/AIChainOfThought.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/AIMarkdownMessage.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/AIReasoningPart.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/ToolInvocationComponent.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/animated/AnimatedMarkdown.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/animated/TokenizedText.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/animated/constants.ts create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/mocks.ts create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/parse-incomplete-markdown.ts create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/reasoning-mock.json create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/shiny-text/ShinyText.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/ai/shiny-text/index.module.css create mode 100644 apps/landing/src/components/widgets/simulators/components/chat/AiMessageContextBar.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/chat/AiMockMessage.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/chat/AiUserMessage.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/chat/ListChatPlayer.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/chat/MarkdownMessage.tsx create mode 100644 apps/landing/src/components/widgets/simulators/components/chat/stream.ts create mode 100644 apps/landing/src/components/widgets/simulators/mocks.tsx create mode 100644 apps/landing/src/constants/download.ts create mode 100644 apps/landing/src/constants/env.ts create mode 100644 apps/landing/src/constants/site.ts create mode 100644 apps/landing/src/constants/spring.ts create mode 100644 apps/landing/src/hooks/biz/use-github-star.ts create mode 100644 apps/landing/src/hooks/common/use-before-mounted.ts create mode 100644 apps/landing/src/hooks/common/use-click-away.ts create mode 100644 apps/landing/src/hooks/common/use-debounce-value.ts create mode 100644 apps/landing/src/hooks/common/use-event-callback.ts create mode 100644 apps/landing/src/hooks/common/use-input-composition.ts create mode 100644 apps/landing/src/hooks/common/use-is-active.ts create mode 100644 apps/landing/src/hooks/common/use-is-client.ts create mode 100644 apps/landing/src/hooks/common/use-is-dark.ts create mode 100644 apps/landing/src/hooks/common/use-is-mounted.ts create mode 100644 apps/landing/src/hooks/common/use-is-unmounted.ts create mode 100644 apps/landing/src/hooks/common/use-previous.ts create mode 100644 apps/landing/src/hooks/common/use-ref-value.ts create mode 100644 apps/landing/src/hooks/common/use-safe-setState.ts create mode 100644 apps/landing/src/hooks/common/use-state-ref.ts create mode 100644 apps/landing/src/hooks/common/use-sync-effect.ts create mode 100644 apps/landing/src/hooks/common/useMeasure.ts create mode 100644 apps/landing/src/hooks/shared/use-mask-scrollarea.ts create mode 100644 apps/landing/src/i18n/request.ts create mode 100644 apps/landing/src/i18n/routing.ts create mode 100644 apps/landing/src/legal/privacy.md create mode 100644 apps/landing/src/legal/tos.md create mode 100644 apps/landing/src/lib/apple-app-site-association.ts create mode 100644 apps/landing/src/lib/cn.ts create mode 100644 apps/landing/src/lib/color.ts create mode 100644 apps/landing/src/lib/cookie.ts create mode 100644 apps/landing/src/lib/datetime.ts create mode 100644 apps/landing/src/lib/dom.ts create mode 100644 apps/landing/src/lib/env.ts create mode 100644 apps/landing/src/lib/fonts.ts create mode 100644 apps/landing/src/lib/helper.ts create mode 100644 apps/landing/src/lib/jotai.ts create mode 100644 apps/landing/src/lib/noop.ts create mode 100644 apps/landing/src/lib/platform.ts create mode 100644 apps/landing/src/lib/query-client.server.ts create mode 100644 apps/landing/src/lib/scroller.ts create mode 100644 apps/landing/src/lib/sleep.ts create mode 100644 apps/landing/src/lib/spring.ts create mode 100644 apps/landing/src/lib/store.ts create mode 100644 apps/landing/src/messages/en.json create mode 100644 apps/landing/src/messages/jp.json create mode 100644 apps/landing/src/messages/zh.json create mode 100644 apps/landing/src/middleware.ts create mode 100644 apps/landing/src/providers/root/debug-provider.tsx create mode 100644 apps/landing/src/providers/root/event-provider.tsx create mode 100644 apps/landing/src/providers/root/framer-lazy-feature.ts create mode 100644 apps/landing/src/providers/root/index.tsx create mode 100644 apps/landing/src/providers/root/jotai-provider.tsx create mode 100644 apps/landing/src/providers/root/page-scroll-info-provider.tsx create mode 100644 apps/landing/src/providers/root/react-query-provider.tsx create mode 100644 apps/landing/src/providers/root/sonner.tsx create mode 100644 apps/landing/src/providers/shared/LayoutRightSideProvider.tsx create mode 100644 apps/landing/src/providers/shared/WrappedElementProvider.tsx create mode 100644 apps/landing/src/styles/globals.css create mode 100644 apps/landing/src/styles/pastel-theme-oklch.css create mode 100644 apps/landing/tsconfig.json create mode 100644 apps/landing/wrangler.jsonc diff --git a/.github/workflows/deploy-cloudflare.yml b/.github/workflows/deploy-cloudflare.yml index aca9c7e24..11f0b6e5b 100644 --- a/.github/workflows/deploy-cloudflare.yml +++ b/.github/workflows/deploy-cloudflare.yml @@ -51,6 +51,9 @@ jobs: - name: Copy WASM file run: cp node_modules/@resvg/resvg-wasm/index_bg.wasm apps/ssr/dist/worker/resvg.wasm + - name: Build Landing Worker + run: pnpm exec turbo run @follow/landing#cf:build + - name: Deploy to Cloudflare (dev) if: github.ref == 'refs/heads/dev' uses: cloudflare/wrangler-action@v3 @@ -60,6 +63,15 @@ jobs: workingDirectory: apps/ssr command: deploy --env dev + - name: Deploy Landing to Cloudflare (dev) + if: github.ref == 'refs/heads/dev' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: apps/landing + command: deploy --env dev + - name: Deploy to Cloudflare (prod) if: github.ref == 'refs/heads/main' uses: cloudflare/wrangler-action@v3 @@ -68,3 +80,12 @@ jobs: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} workingDirectory: apps/ssr command: deploy + + - name: Deploy Landing to Cloudflare (prod) + if: github.ref == 'refs/heads/main' + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + workingDirectory: apps/landing + command: deploy diff --git a/.gitignore b/.gitignore index da495911b..22fb1f8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules dist out +.next +.open-next +next-env.d.ts .DS_Store *.log* .env diff --git a/.prettierignore b/.prettierignore index 165674e58..c68d0c0fa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,7 @@ pnpm-lock.yaml CHANGELOG.md +.context apps/external/postcss.config.cjs diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 1abc329f6..734deb27f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -75,6 +75,7 @@ "pathe": "2.0.3", "react": "19.0.0", "react-dom": "19.0.0", + "tailwindcss": "3.4.17", "tailwindcss-content-visibility": "1.0.2", "tailwindcss-multi": "0.4.6", "tar": "7.5.7", diff --git a/apps/landing/.prettierrc.mjs b/apps/landing/.prettierrc.mjs new file mode 100644 index 000000000..0f454b1f2 --- /dev/null +++ b/apps/landing/.prettierrc.mjs @@ -0,0 +1,5 @@ +import { factory } from '@innei/prettier' + +export default factory({ + importSort: false, +}) diff --git a/apps/landing/components.json b/apps/landing/components.json new file mode 100644 index 000000000..e8253ed9c --- /dev/null +++ b/apps/landing/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/tailwind.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "~/components", + "utils": "~/lib/utils", + "ui": "~/components/ui", + "lib": "~/lib", + "hooks": "~/hooks" + }, + "registries": { + "@animate-ui": "https://animate-ui.com/r/{name}.json", + "@magicui": "https://magicui.design/r/{name}.json" + } +} diff --git a/apps/landing/eslint.config.mjs b/apps/landing/eslint.config.mjs new file mode 100644 index 000000000..7b431334b --- /dev/null +++ b/apps/landing/eslint.config.mjs @@ -0,0 +1,80 @@ +// @ts-check +import { defineConfig } from 'eslint-config-hyoban' + +import recursiveSort from './plugins/eslint-recursive-sort.mjs' + +export default defineConfig( + { + formatting: false, + lessOpinionated: true, + ignores: ['dist/**'], + preferESM: false, + }, + { + settings: { + tailwindcss: { + whitelist: ['center'], + }, + }, + rules: { + 'tailwindcss/classnames-order': 'off', + 'tailwindcss/no-custom-classname': 'off', + 'unicorn/prefer-math-trunc': 'off', + 'unicorn/expiring-todo-comments': 0, + '@eslint-react/no-clone-element': 0, + '@eslint-react/hooks-extra/no-direct-set-state-in-use-effect': 0, + // NOTE: Disable this temporarily + 'react-compiler/react-compiler': 0, + 'no-restricted-syntax': 0, + 'package-json/valid-name': 0, + + // disable react compiler rules for now + 'react-hooks/no-unused-directives': 'off', + 'react-hooks/static-components': 'off', + 'react-hooks/use-memo': 'off', + 'react-hooks/component-hook-factories': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-hooks/immutability': 'off', + 'react-hooks/globals': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/error-boundaries': 'off', + 'react-hooks/purity': 'off', + 'react-hooks/set-state-in-render': 'off', + 'react-hooks/unsupported-syntax': 'off', + 'react-hooks/config': 'off', + 'react-hooks/gating': 'off', + + 'no-restricted-globals': [ + 'error', + { + name: 'location', + message: + "Since you don't use the same router instance in electron and browser, you can't use the global location to get the route info. \n\n" + + 'You can use `useLocaltion` or `getReadonlyRoute` to get the route info.', + }, + ], + }, + }, + { + files: ['**/*.tsx'], + rules: { + '@stylistic/jsx-self-closing-comp': 'error', + }, + }, + { + files: ['locales/**/*.json'], + plugins: { + 'recursive-sort': recursiveSort, + }, + rules: { + 'recursive-sort/recursive-sort': 'error', + }, + }, + { + files: ['package.json'], + rules: { + 'package-json/valid-name': 0, + }, + }, +) diff --git a/apps/landing/global.d.ts b/apps/landing/global.d.ts new file mode 100644 index 000000000..bc4a25e68 --- /dev/null +++ b/apps/landing/global.d.ts @@ -0,0 +1,42 @@ +/* eslint-disable @typescript-eslint/no-empty-object-type */ +/* eslint-disable @typescript-eslint/method-signature-style */ +import type { FC, PropsWithChildren } from 'react' + +declare global { + export type NextErrorProps = { + reset(): void + error: Error + } + export type NextPageParams

= PropsWithChildren< + { + params: P + } & Props + > + + export type Component

= FC + + export type ComponentType

= { + className?: string + } & PropsWithChildren & + P + + // TODO should remove in next TypeScript version + interface Document { + startViewTransition(callback?: () => void | Promise): ViewTransition + } + + interface ViewTransition { + finished: Promise + ready: Promise + updateCallbackDone: () => void + skipTransition(): void + } +} + +declare module 'react' { + export interface AriaAttributes { + 'data-hide-print'?: boolean + 'data-event'?: string + 'data-testid'?: string + } +} diff --git a/apps/landing/next.config.ts b/apps/landing/next.config.ts new file mode 100644 index 000000000..a4e5c5bff --- /dev/null +++ b/apps/landing/next.config.ts @@ -0,0 +1,63 @@ +import NextBundleAnalyzer from '@next/bundle-analyzer' +import { codeInspectorPlugin } from 'code-inspector-plugin' +import { config } from 'dotenv' +import type { NextConfig } from 'next' +import createNextIntlPlugin from 'next-intl/plugin' + +process.title = 'Folo Landing (NextJS)' + +const env = config().parsed || {} +const isProd = process.env.NODE_ENV === 'production' + +const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts') + +let nextConfig: NextConfig = { + reactStrictMode: false, + productionBrowserSourceMaps: true, + output: 'standalone', + + reactCompiler: true, + assetPrefix: isProd ? env.ASSETPREFIX || undefined : undefined, + compiler: { + // reactRemoveProperties: { properties: ['^data-id$', '^data-(\\w+)-id$'] }, + }, + + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '**', + }, + ], + dangerouslyAllowSVG: true, + contentSecurityPolicy: + "default-src 'self'; script-src 'none'; sandbox; style-src 'unsafe-inline';", + }, + + async rewrites() { + return { + beforeFiles: [ + { source: '/atom.xml', destination: '/feed' }, + { source: '/sitemap.xml', destination: '/sitemap' }, + { + source: '/.well-known/apple-app-site-association', + destination: '/apple-app-site-association', + }, + ], + } + }, + turbopack: { + rules: codeInspectorPlugin({ + bundler: 'turbopack', + hotKeys: ['altKey'], + }), + }, +} + +if (process.env.ANALYZE === 'true') { + nextConfig = NextBundleAnalyzer({ + enabled: true, + })(nextConfig) +} + +export default withNextIntl(nextConfig) diff --git a/apps/landing/open-next.config.ts b/apps/landing/open-next.config.ts new file mode 100644 index 000000000..9269c261c --- /dev/null +++ b/apps/landing/open-next.config.ts @@ -0,0 +1,3 @@ +import { defineCloudflareConfig } from '@opennextjs/cloudflare' + +export default defineCloudflareConfig() diff --git a/apps/landing/package.json b/apps/landing/package.json new file mode 100644 index 000000000..bde6bd3c9 --- /dev/null +++ b/apps/landing/package.json @@ -0,0 +1,92 @@ +{ + "name": "@follow/landing", + "type": "module", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "cross-env NODE_ENV=production next build", + "cf:build": "pnpm exec opennextjs-cloudflare build", + "cf:deploy": "pnpm exec wrangler deploy", + "cf:deploy:dev": "pnpm exec wrangler deploy --env dev", + "dev": "cross-env NODE_ENV=development next dev -p 4399", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@base-ui-components/react": "1.0.0-beta.4", + "@floating-ui/react-dom": "2.1.6", + "@radix-ui/react-accordion": "1.2.12", + "@splinetool/react-spline": "4.1.0", + "@tanstack/query-async-storage-persister": "5.90.7", + "@tanstack/react-query": "5.90.5", + "@tanstack/react-query-persist-client": "5.90.7", + "@types/js-cookie": "3.0.6", + "ai": "5.0.68", + "axios": "1.13.0", + "clsx": "2.1.1", + "dayjs": "1.11.18", + "es-toolkit": "1.41.0", + "foxact": "0.2.49", + "idb-keyval": "6.2.2", + "immer": "10.2.0", + "jojoo": "0.3.0", + "jotai": "2.15.0", + "js-cookie": "3.0.5", + "motion": "12.23.24", + "next": "16.0.11", + "next-intl": "4.4.0", + "next-themes": "0.4.6", + "ogl": "1.0.11", + "progressive-blur": "1.0.0", + "radix-ui": "1.4.3", + "re-resizable": "6.11.2", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-error-boundary": "6.0.0", + "react-intersection-observer": "9.16.0", + "react-markdown": "10.1.0", + "react-resizable-layout": "0.7.3", + "rehype-stringify": "10.0.1", + "remark-emoji": "5.0.2", + "remark-parse": "11.0.0", + "remark-rehype": "11.1.2", + "rough-notation": "0.5.1", + "sonner": "2.0.7", + "tailwind-merge": "3.3.1", + "unified": "11.0.5", + "usehooks-ts": "3.1.1", + "vaul": "1.1.2" + }, + "devDependencies": { + "@egoist/tailwindcss-icons": "1.9.0", + "@iconify-json/lucide": "1.2.71", + "@iconify-json/mingcute": "1.2.5", + "@iconify-json/simple-icons": "1.2.56", + "@iconify/tailwind": "1.2.0", + "@innei/prettier": "1.0.0", + "@next/bundle-analyzer": "16.0.11", + "@opennextjs/cloudflare": "1.16.5", + "@tailwindcss/postcss": "4.1.16", + "@tailwindcss/typography": "0.5.19", + "@tanstack/react-query-devtools": "5.90.2", + "@types/node": "24.9.1", + "@types/react": "19.1.17", + "@types/react-dom": "19.2.3", + "autoprefixer": "10.4.21", + "babel-plugin-react-compiler": "1.0.0", + "code-inspector-plugin": "1.2.10", + "cross-env": "10.1.0", + "dotenv": "17.2.3", + "eslint": "9.38.0", + "eslint-config-hyoban": "4.0.10", + "postcss": "8.5.6", + "prettier": "3.6.2", + "rimraf": "6.0.1", + "tailwind-scrollbar": "4.0.2", + "tailwind-variants": "3.1.1", + "tailwindcss": "4.1.16", + "tailwindcss-animate": "1.0.7", + "tailwindcss-safe-area": "1.1.0", + "typescript": "catalog:", + "wrangler": "4.67.0" + } +} diff --git a/apps/landing/plugins/eslint-recursive-sort.mjs b/apps/landing/plugins/eslint-recursive-sort.mjs new file mode 100644 index 000000000..d451554bd --- /dev/null +++ b/apps/landing/plugins/eslint-recursive-sort.mjs @@ -0,0 +1,60 @@ +const sortObjectKeys = (obj) => { + if (typeof obj !== 'object' || obj === null) { + return obj + } + + if (Array.isArray(obj)) { + return obj.map((element) => sortObjectKeys(element)) + } + + return Object.keys(obj) + .sort() + .reduce((acc, key) => { + acc[key] = sortObjectKeys(obj[key]) + return acc + }, {}) +} +/** + * @type {import("eslint").ESLint.Plugin} + */ +export default { + rules: { + 'recursive-sort': { + meta: { + type: 'layout', + fixable: 'code', + }, + create(context) { + return { + Program(node) { + if (context.getFilename().endsWith('.json')) { + const sourceCode = context.getSourceCode() + const text = sourceCode.getText() + + try { + const json = JSON.parse(text) + const sortedJson = sortObjectKeys(json) + const sortedText = JSON.stringify(sortedJson, null, 2) + + if (text.trim() !== sortedText.trim()) { + context.report({ + node, + message: 'JSON keys are not sorted recursively', + fix(fixer) { + return fixer.replaceText(node, sortedText) + }, + }) + } + } catch (error) { + context.report({ + node, + message: `Invalid JSON: ${error.message}`, + }) + } + } + }, + } + }, + }, + }, +} diff --git a/apps/landing/postcss.config.mjs b/apps/landing/postcss.config.mjs new file mode 100644 index 000000000..a7f73a2d1 --- /dev/null +++ b/apps/landing/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/apps/landing/public/favicon.ico b/apps/landing/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..89db74809d3321e3b97ff5c103e1678f37c84926 GIT binary patch literal 15406 zcmeHOdvH|M89(>lO(Gx&G&JL5+KeyI>I^Nk0-{YzBjpiL;7a}A`DTIfL(8w4m z)0PpCwjwePK5?um2r=c?76ckc6l)qpF@Z^J=un1nK3iyG>=%r&{QU6qs9}t~hI@_x_HPj(b-1bDw1;Dck4Fh~zxnv*u&Js70o`gunB1y}pdbn6$~?R)g}<3KJ5Zr?E;{elNz z(aQiY;rF2MFLCx0*>_TJoUFvTh9LXD;L$(-g}(2Q&ku0xqj6x60~Z+0YobDr{tG<% zH~*_oKc(oq%EL!>1jT>RME_Sja(?y%Kx;=B!|7Upt4mX)U-5J4CyX@rC|CzDU>?72 zOZ&+;C_9?+mjOKW7Qn{$^=Z!Y=vVBM(R}*PCjQsaPxf7GjBIbcrKFP`*_0>|BTgd+W%$a@#p480K8rN2xyHr;VFIndi@V23s&EsJ5~X#eiL8~+GR!+ zRRFYJQ`;^g#_td5ithjPEGXU85p{LO!lcXPzzI^Nr(aY8Zze6@0<$0ocj@V-JW27p z(ybrYZiuWN`y{wU%Swfx_lQi9oGDsH54DCt9&cTy4{JJsQ2IkzoeWX;lZ>lu? zSgAPU^a}C*Gpof}XZ|2QP*V{iHLb?le9F_3T1N3qEF)PcQ<`zSOq#rZT6V!pqw?;1 zdQh^s8LN_V#%>-dPCve$Ia-c0x9?jfcefyUne1=JxFdDsN0}(w?Q6ijt`p*n6C1O~ zZMjW*ZW5P0_Nr`Y%Ev{_(a5!TgWqNSd1*5Fj+W1)DWBXQ>Z5UiXM*jwpH61Zwo`~J zm=JHZOS zV zjQ8+tc=$piT%7rGuw8U`7a)G!J%%2;ge=)xmMH!azXLc|-wVAraTNC{uV2E6)3B`( zzsvh@I`NbL$=A_g_aT6x3%*DE*j9=3AL6-fWw_6)Hq>1Xp9S&n!yDPj>3;?Ln36bt zvVnX}cR9+o16-GMzftW;J$@1Q;0NO#INg&lqnr{ILozWh{!UB(Q(u^kcS{bO12|KM zbjh5m&jUQU3qV496YZCwKEd<;D)^3@^dA*_lUMEG{PEVk$$ZOr{cnzc;C$ozI#!mP zZvYtiSfXu^8vNAH|8c+yseXAap4ZVmuf+OLYVgxMVEGP!PM*>W-|-#$0dB`L%fzq~UuyiX>ZdXBhH`-X<<|70rQvZ_dE-esKNi0-fW@Jv3LK>cs~ zKCAmaT=mLjmaHzd)B#zk>u(uLi`EupQH~zp6R;VXBh(TDadkq$0CN>G~zIM zN6p$JcqvP6A7_Psn7#2D#NPk*4;83pZs5ABD=OWO^*8^EGE|`N&Gvr!n#TUs0Okp% z|IQ1JUz~nAiT4%aJ5Q2*tj!FT!{+-l192OCBi4I^~$Z6ys$P6wV8mYk6ek56db=ISw3S?qv@F a{v%EQE?PqHGfWu|WIT}ZK*j?x9{4}tV#~Jx literal 0 HcmV?d00001 diff --git a/apps/landing/public/icon-192x192.png b/apps/landing/public/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..9eb8f5d5eef9300cb8f8518448eca2d582307587 GIT binary patch literal 7589 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 t01~vQX#ajMkRc5J|L^}tFolo \ No newline at end of file diff --git a/apps/landing/public/manifest.json b/apps/landing/public/manifest.json new file mode 100644 index 000000000..243c0d77c --- /dev/null +++ b/apps/landing/public/manifest.json @@ -0,0 +1,16 @@ +{ + "theme_color": "#ff5c00", + "name": "Folo", + "icons": [ + { + "src": "/icon-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/apps/landing/public/og.png b/apps/landing/public/og.png new file mode 100644 index 0000000000000000000000000000000000000000..0dce97465df2c4de305d5464eff8da460c66d7c1 GIT binary patch literal 124060 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%0ALdF%X;3^^#jrD>`6FP)3c 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�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 z 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 z zsnS=w1+?FIv3gMW7GI|gywdlU9mjIpFZ;cY7zd`A?hEUP@y9V~Q@vI^q|hvQNeN(O z*Ivx^>BZ5oa`2XO)CTQO#a4HQ=sIar~6F_~-6dE_G>b?<_ikFHNV zL0|cw{}<|k6&p=BJ%9D9U#E}$(I@G%pZyYj^|`OpM?U&-^}ez;-vpsvuIxRh_V>M< zcU6@K;3-HbJ{k84u=L~w0O2X+i;Y4neTqJF9x~PG-gv7<9#{!};ZN#WA-CCSHUKsAx<7eWlTXQ=W;IJ?Q#aN&gIozj1N;rU~vj(B1%KtB$NqkOW1xP;M4|Q zi9vB^Gu-an6Y%QZ_O}69hYJnWxVYR_y*K1a5sP3$n3Kf0|Rhbkp;G{dObXRax(DPkaGTpmjUDZ{3T%6RHyFN9`Q?cE5yuJ$ML%b9; zKNuU>g?^WRR?7T508_drfM-rYC`!j^HGs;LWPj%{9>#2F!7wE%^`xPJg2NS{k}^2Y zL!q4OYS6#k7)ygV>n^MLL?PsB$PkPT05sK;A&ztLnDR$72eG)NfvJj`Z>>#EyWZ;9 zWd>e7_>CPZO<63OC#TWlPqlIAmeYYl*|b%;<{A`sBpe$M+dUA?fFT%vR-3No_p&39 z=dA#s)V=;WJcWxg%Y+RTmf~TBPy>60P3p*d)f4mzFwN;3NnVMnf}Qu#^z5owQ(8!r z3ii&PpbC^#T9|%wD$y7>?PpHKwD~a*Zj0kEMhflu^O=h`6$QkT?;9#C&F?^TqrNmE z2YvHfFVNREQ0ilU^l1%~k+7J<UH#n*FQy%J@)7ZaJ`Y9{U85F^wfX* z|D(f~ziJU09!7ZB0En0~@t#Gg&Q}eI*YbtCTM-KWn}pQH&8a2Ul2G8cYPCRvOD zEClA+@Ka-G1H2BD%KW{1@glX4qj18N^Z3#9?c0`i^FWK9%VWu-@UHd7GUlL@i4t%; z`GRL9EV-+aKP26kR&WKnTCs;`3cUOAPg@h!K(x=(23~#fum9)t!sT81qYr(IzW$AG z(&xVL75ePwzeLY{?HlycOAksr+m5f_Ghd?8-)rq}c4xemV-Ab1bQHNW%xgtGIprNzt55P`nfH~^ zy+j&+R6LH0#SS`G!)&Wp)cn&NJ ze_SKf86H%Y>xpMG$6n1$>M7(u(($Uzc?g)kP;PHZ9@(?Ygy(dUphLapfR4ckzX?Qa3!s zA=5!C3Vb!+5m4^#ehy)~zIZxT$P@D>CtrZpHi75)PJ)X^K7^0(FuhF{eWiZYb)_Cc z=x_6UmF_v|o>ZW1<;UIAcr;7)Pq9~y(}^E%!Jl1X^jug?APe#^2f1Ow1fMKKQ%lH& zb2*pqemMhJ=kls7sbCgmrto9zY|@HCZaSW=WN7$>2*u5Mxe&1G-d(!40aq8B0e4Tn zt2fpon!x??j(|jWw|iFwy#!NQG$Ef&T;$)ZLk_EwbbJgjW?j=&L9_0#x4xoN@P8hO00ku z6N_vWu!j$4lVPuM9D!iCy0p1kFxOtcj02TGd>ZYJZF07&ucSn=v z{25e0CoZ5QMhTwW!$3f-C+^;*_iTLSXHNH{oBI6+boltE==yDMu^Cb5)%uQd)_9xC#%0mJ z2~L@}1i%5zbzB$yGtGZ4=gPJI5c@jfJ{+ubo%6zY#_Y{T)72q&s8USS6f|+8fZX#@ zH}2z@t>vO!eVNP3c_?_D9|RvsucLe~qcuD^8KUNttk?LB(MHZUi*MVB%P)M1mJN`y zNl0qYeQ0g;w@#1HXZ{a=i|#*oKsUOf&AoFQFaLI4SbI;Zy~ou4p4&hz9N*vbusSKO z8_sVN1h1ope(at*P|35o=agI{;7Hxc7e;j%<&amc719KV!fsGw5rZ<*;+lXM3}t^z z82M-?QKr!XxJI=V2y-4uN5dXA&6yi>(GG?#z$earSuqEF?hFTR2W5C5W}wekE`ocW z;|cr%fTDOW`;L%7UOD&?fGM1Zt`MG5gJM@@rrbFY%?Pim)?%0A0Kfb^F#xgjoTC^g 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 literal 0 HcmV?d00001 diff --git a/apps/landing/src/actions/github-star.ts b/apps/landing/src/actions/github-star.ts new file mode 100644 index 000000000..cdf8452b0 --- /dev/null +++ b/apps/landing/src/actions/github-star.ts @@ -0,0 +1,23 @@ +'use server' + +export async function getGithubStar() { + return await fetch(`https://api.github.com/repos/RSSNext/Folo`, { + headers: { + Authorization: process.env.GITHUB_TOKEN + ? `Bearer ${process.env.GITHUB_TOKEN}` + : '', + }, + }) + .then((res) => res.json()) + .then((data) => { + if (data.message) { + throw new Error(data.message) + } + + return data.stargazers_count as number + }) + .catch((e) => { + console.error(e) + return -1 + }) +} diff --git a/apps/landing/src/app/ClientInit.tsx b/apps/landing/src/app/ClientInit.tsx new file mode 100644 index 000000000..181b32c8f --- /dev/null +++ b/apps/landing/src/app/ClientInit.tsx @@ -0,0 +1,6 @@ +'use client' + +import { init } from './init' + +init() +export const ClientInit = () => null diff --git a/apps/landing/src/app/InitInClient.ts b/apps/landing/src/app/InitInClient.ts new file mode 100644 index 000000000..b0f775614 --- /dev/null +++ b/apps/landing/src/app/InitInClient.ts @@ -0,0 +1,9 @@ +'use client' + +import { init } from './init' + +init() + +export const InitInClient = () => { + return null +} diff --git a/apps/landing/src/app/[locale]/download/page.tsx b/apps/landing/src/app/[locale]/download/page.tsx new file mode 100644 index 000000000..0b8c5847b --- /dev/null +++ b/apps/landing/src/app/[locale]/download/page.tsx @@ -0,0 +1,32 @@ +import type { Metadata } from 'next' +import { headers } from 'next/headers' +import { getTranslations } from 'next-intl/server' + +import { DownloadHero } from '~/components/widgets/download/DownloadHero' +import { PlatformDownloads } from '~/components/widgets/download/PlatformDownloads' +import { detectPlatform } from '~/lib/platform' + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }> +}): Promise { + const { locale } = await params + const t = await getTranslations({ locale, namespace: 'download.metadata' }) + + return { + title: t('title'), + description: t('description'), + } +} + +export default async function DownloadPage() { + const ua = (await headers()).get('user-agent')?.toLowerCase() + + return ( + <> + + + + ) +} diff --git a/apps/landing/src/app/[locale]/error.tsx b/apps/landing/src/app/[locale]/error.tsx new file mode 100644 index 000000000..e46f81069 --- /dev/null +++ b/apps/landing/src/app/[locale]/error.tsx @@ -0,0 +1,26 @@ +'use client' + +import { useTranslations } from 'next-intl' +import { useEffect } from 'react' + +import { NormalContainer } from '~/components/layout/container/Normal' +import { Button } from '~/components/ui/button' + +export default ({ error, reset }: any) => { + const errorT = useTranslations('common.error') + useEffect(() => { + console.error(error) + // captureException(error) + }, [error]) + + return ( + +

+ + ) +} diff --git a/apps/landing/src/app/[locale]/layout.tsx b/apps/landing/src/app/[locale]/layout.tsx new file mode 100644 index 000000000..415da425d --- /dev/null +++ b/apps/landing/src/app/[locale]/layout.tsx @@ -0,0 +1,146 @@ +import type { Metadata, Viewport } from 'next' +import { NextIntlClientProvider } from 'next-intl' +import { getMessages, getTranslations } from 'next-intl/server' + +import { HydrationEndDetector } from '~/components/common/HydrationEndDetector' +import { ScrollTop } from '~/components/common/ScrollTop' +import { Root } from '~/components/layout/root/Root' +import { LightRays } from '~/components/ui/light-rays' +import { LandingHeader } from '~/components/widgets/landing/Header' +import { siteInfo } from '~/constants/site' +import { locales } from '~/i18n/routing' +import { sansFont } from '~/lib/fonts' + +import { Providers } from '../../providers/root' +import { ClientInit } from '../ClientInit' +import { init } from '../init' +import { InitInClient } from '../InitInClient' + +init() + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })) +} + +export async function generateMetadata( + params: Promise<{ locale: string }>, +): Promise { + const { locale } = await params + const t = await getTranslations({ locale, namespace: 'metadata' }) + + const title = t('title', { defaultValue: siteInfo.title }) + const description = t('description', { + defaultValue: siteInfo.description, + }) + + const keywordsString = t('keywords', { + defaultValue: siteInfo.seo.keywords.join(', '), + }) + const keywords = keywordsString.split(',').map((keyword) => keyword.trim()) + + return { + metadataBase: new URL(siteInfo.webUrl), + title: { + template: `%s · ${title}`, + default: `${title} — ${description}`, + }, + description, + keywords, + icons: [ + { + rel: 'icon', + url: '/favicon.ico', + }, + ], + alternates: { + canonical: '/', + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + 'max-video-preview': -1, + 'max-image-preview': 'large', + 'max-snippet': -1, + }, + }, + openGraph: { + title: { + default: `${title} — ${description}`, + template: `%s · ${title}`, + }, + description, + siteName: title, + locale: locale === 'en' ? 'en_US' : locale, + type: 'website', + url: siteInfo.webUrl, + images: [{ url: '/og.png' }], + }, + twitter: { + card: 'summary_large_image', + title: `${title} — ${description}`, + description, + images: ['/og.png'], + }, + } +} + +export function generateViewport(): Viewport { + return { + themeColor: [ + { media: '(prefers-color-scheme: dark)', color: '#000212' }, + { media: '(prefers-color-scheme: light)', color: '#fafafa' }, + ], + initialScale: 1, + viewportFit: 'cover', + width: 'device-width', + maximumScale: 1, + minimumScale: 1, + userScalable: false, + } +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode + params: Promise<{ locale: string }> +}) { + const { locale } = await params + const messages = await getMessages() + + return ( + <> + + + + + + + + +
+ + + + + {children} + +
+
+
+ + + + + + + ) +} diff --git a/apps/landing/src/app/[locale]/page.tsx b/apps/landing/src/app/[locale]/page.tsx new file mode 100644 index 000000000..20ab44830 --- /dev/null +++ b/apps/landing/src/app/[locale]/page.tsx @@ -0,0 +1,19 @@ +import * as React from 'react' + +import { BuiltOpen } from '~/components/widgets/landing/BuiltOpen' +import { Features } from '~/components/widgets/landing/Features' +import { LandingHero } from '~/components/widgets/landing/Hero' +import { SocialProof } from '~/components/widgets/landing/SocialProof' + +export default async function Home() { + return ( + <> + + + {/* */} + {/* */} + + + + ) +} diff --git a/apps/landing/src/app/[locale]/privacy-policy/page.tsx b/apps/landing/src/app/[locale]/privacy-policy/page.tsx new file mode 100644 index 000000000..a9e60ec30 --- /dev/null +++ b/apps/landing/src/app/[locale]/privacy-policy/page.tsx @@ -0,0 +1,36 @@ +import type { Metadata } from 'next/types' + +import { getMarkdownContent, MarkdownContent } from '~/components/ui/markdown' +import { siteInfo } from '~/constants/site' + +export const metadata: Metadata = { + title: 'Privacy Policy', + description: + "Read Folo's privacy policy to understand how we collect, use, and protect your personal information when using our next-generation information browser.", + robots: { + index: true, + follow: true, + }, + openGraph: { + ...siteInfo.seo.openGraph, + title: 'Privacy Policy - Folo', + description: + "Read Folo's privacy policy to understand how we protect your data and privacy.", + url: `${siteInfo.webUrl}/privacy-policy`, + }, + twitter: { + ...siteInfo.seo.twitter, + title: 'Privacy Policy - Folo', + description: + "Read Folo's privacy policy to understand how we protect your data and privacy.", + }, + alternates: { + canonical: '/privacy-policy', + }, +} + +export default async function PrivacyPolicyPage() { + const { content } = await getMarkdownContent('legal/privacy.md') + + return +} diff --git a/apps/landing/src/app/[locale]/terms-of-service/page.tsx b/apps/landing/src/app/[locale]/terms-of-service/page.tsx new file mode 100644 index 000000000..347469b27 --- /dev/null +++ b/apps/landing/src/app/[locale]/terms-of-service/page.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from 'next/types' + +import { getMarkdownContent, MarkdownContent } from '~/components/ui/markdown' +import { siteInfo } from '~/constants/site' + +export const metadata: Metadata = { + title: 'Terms of Service', + description: + "Read Folo's terms of service to understand the rules and guidelines for using our next-generation information browser platform.", + robots: { + index: true, + follow: true, + }, + openGraph: { + ...siteInfo.seo.openGraph, + title: 'Terms of Service - Folo', + description: "Read Folo's terms of service and usage guidelines.", + url: `${siteInfo.webUrl}/terms-of-service`, + }, + twitter: { + ...siteInfo.seo.twitter, + title: 'Terms of Service - Folo', + description: "Read Folo's terms of service and usage guidelines.", + }, + alternates: { + canonical: '/terms-of-service', + }, +} + +export default async function TermsOfServicePage() { + const { content } = await getMarkdownContent('legal/tos.md') + + return +} diff --git a/apps/landing/src/app/apple-app-site-association/route.ts b/apps/landing/src/app/apple-app-site-association/route.ts new file mode 100644 index 000000000..b9ab0fadf --- /dev/null +++ b/apps/landing/src/app/apple-app-site-association/route.ts @@ -0,0 +1,9 @@ +import { APPLE_APP_SITE_ASSOCIATION } from '~/lib/apple-app-site-association' + +export function GET() { + return Response.json(APPLE_APP_SITE_ASSOCIATION, { + headers: { + 'Cache-Control': 'public, max-age=300', + }, + }) +} diff --git a/apps/landing/src/app/globals.css b/apps/landing/src/app/globals.css new file mode 100644 index 000000000..040662136 --- /dev/null +++ b/apps/landing/src/app/globals.css @@ -0,0 +1 @@ +@import '../styles/globals.css'; diff --git a/apps/landing/src/app/init.ts b/apps/landing/src/app/init.ts new file mode 100644 index 000000000..d7e667c5a --- /dev/null +++ b/apps/landing/src/app/init.ts @@ -0,0 +1,7 @@ +import 'dayjs/locale/zh-cn' + +import dayjs from 'dayjs' + +export const init = () => { + dayjs.locale('zh-cn') +} diff --git a/apps/landing/src/app/layout.tsx b/apps/landing/src/app/layout.tsx new file mode 100644 index 000000000..dc8682a93 --- /dev/null +++ b/apps/landing/src/app/layout.tsx @@ -0,0 +1,9 @@ +import './globals.css' + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return children +} diff --git a/apps/landing/src/app/robots.ts b/apps/landing/src/app/robots.ts new file mode 100644 index 000000000..a014655d0 --- /dev/null +++ b/apps/landing/src/app/robots.ts @@ -0,0 +1,11 @@ +import type { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + disallow: ['/login/'], + }, + } +} diff --git a/apps/landing/src/atoms/css-media.ts b/apps/landing/src/atoms/css-media.ts new file mode 100644 index 000000000..ccef7ea00 --- /dev/null +++ b/apps/landing/src/atoms/css-media.ts @@ -0,0 +1,6 @@ +import { createAtomHooks } from 'jojoo/react' +import { atom } from 'jotai' + +export const [, , useIsPrintMode, , , setIsPrintMode] = createAtomHooks( + atom(false), +) diff --git a/apps/landing/src/atoms/index.ts b/apps/landing/src/atoms/index.ts new file mode 100644 index 000000000..ef4f382e8 --- /dev/null +++ b/apps/landing/src/atoms/index.ts @@ -0,0 +1,2 @@ +export * from './css-media' +export * from './viewport' diff --git a/apps/landing/src/atoms/is-interactive.ts b/apps/landing/src/atoms/is-interactive.ts new file mode 100644 index 000000000..ca35e8464 --- /dev/null +++ b/apps/landing/src/atoms/is-interactive.ts @@ -0,0 +1,10 @@ +import { atom, useAtomValue } from 'jotai' + +import { jotaiStore } from '~/lib/store' + +const isInteractiveAtom = atom(false) +export const useIsInteractive = () => useAtomValue(isInteractiveAtom) + +export const getIsInteractive = () => jotaiStore.get(isInteractiveAtom) +export const setIsInteractive = (value: boolean) => + jotaiStore.set(isInteractiveAtom, value) diff --git a/apps/landing/src/atoms/viewport.ts b/apps/landing/src/atoms/viewport.ts new file mode 100644 index 000000000..7efe1880c --- /dev/null +++ b/apps/landing/src/atoms/viewport.ts @@ -0,0 +1,54 @@ +import type { ExtractAtomValue } from 'jotai' +import { atom, useAtomValue } from 'jotai' +import { selectAtom } from 'jotai/utils' +import { useCallback } from 'react' + +export const viewportAtom = atom({ + /** + * 640px + */ + sm: false, + + /** + * 768px + */ + md: false, + + /** + * 1024px + */ + lg: false, + + /** + * 1280px + */ + xl: false, + + /** + * 1536px + */ + '2xl': false, + + h: 0, + w: 0, +}) + +export const useViewport = ( + selector: (value: ExtractAtomValue) => T, +): T => + useAtomValue( + // @ts-ignore + selectAtom( + viewportAtom, + useCallback((atomValue) => selector(atomValue), []), + ), + ) + +export const useIsMobile = () => + useViewport( + useCallback( + (v: ExtractAtomValue) => + (v.sm || v.md || !v.sm) && !v.lg, + [], + ), + ) diff --git a/apps/landing/src/components/brand/Folo.tsx b/apps/landing/src/components/brand/Folo.tsx new file mode 100644 index 000000000..ac348d580 --- /dev/null +++ b/apps/landing/src/components/brand/Folo.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' + +export const Folo = ({ + ref, + ...props +}: React.SVGProps & { + ref?: React.Ref +}) => ( + + + +) diff --git a/apps/landing/src/components/brand/Logo.tsx b/apps/landing/src/components/brand/Logo.tsx new file mode 100644 index 000000000..cbdcd821e --- /dev/null +++ b/apps/landing/src/components/brand/Logo.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' + +export const Logo = ({ + ref, + ...props +}: React.SVGProps & { + ref?: React.Ref + accentColor?: string +}) => { + const { accentColor, ...rest } = props + return ( + + Folo + + + + ) +} diff --git a/apps/landing/src/components/common/ClientOnly.tsx b/apps/landing/src/components/common/ClientOnly.tsx new file mode 100644 index 000000000..f9c070cd5 --- /dev/null +++ b/apps/landing/src/components/common/ClientOnly.tsx @@ -0,0 +1,9 @@ +'use client' + +import { useIsClient } from '~/hooks/common/use-is-client' + +export const ClientOnly: Component = (props) => { + const isClient = useIsClient() + if (!isClient) return null + return <>{props.children} +} diff --git a/apps/landing/src/components/common/ErrorBoundary.tsx b/apps/landing/src/components/common/ErrorBoundary.tsx new file mode 100644 index 000000000..3704e295c --- /dev/null +++ b/apps/landing/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,37 @@ +'use client' + +import type { FC, PropsWithChildren } from 'react' +import { ErrorBoundary as ErrorBoundaryLib } from 'react-error-boundary' + +import { Button } from '../ui/button' + +const FallbackComponent = () => { + return ( +
+ Something went wrong. + +
+ ) +} +export const ErrorBoundary: FC = ({ children }) => { + return ( + { + console.error(e) + + // TODO sentry + + // captureException(e) + }} + > + {children} + + ) +} diff --git a/apps/landing/src/components/common/GithubTrending.tsx b/apps/landing/src/components/common/GithubTrending.tsx new file mode 100644 index 000000000..52ec0cfee --- /dev/null +++ b/apps/landing/src/components/common/GithubTrending.tsx @@ -0,0 +1,81 @@ +export default function GithubTrending() { + return ( + + + + + + + +
1
+
+ + +
GITHUB TRENDING
+
+ + +
#1 Repository Of The Month
+
+
+ ) +} diff --git a/apps/landing/src/components/common/HydrationEndDetector.tsx b/apps/landing/src/components/common/HydrationEndDetector.tsx new file mode 100644 index 000000000..fb6458cf6 --- /dev/null +++ b/apps/landing/src/components/common/HydrationEndDetector.tsx @@ -0,0 +1,23 @@ +'use client' + +import { atom } from 'jotai' +import { useEffect } from 'react' + +import { jotaiStore } from '~/lib/store' + +const hydrateEndAtom = atom(false) + +/** + * To skip page transition when first load, improve LCP + */ +export const HydrationEndDetector = () => { + useEffect(() => { + // waiting for hydration end and animation end + setTimeout(() => { + jotaiStore.set(hydrateEndAtom, true) + }, 2000) + }, []) + return null +} + +export const isHydrationEnded = () => jotaiStore.get(hydrateEndAtom) diff --git a/apps/landing/src/components/common/Lazyload.tsx b/apps/landing/src/components/common/Lazyload.tsx new file mode 100644 index 000000000..132e6ac47 --- /dev/null +++ b/apps/landing/src/components/common/Lazyload.tsx @@ -0,0 +1,35 @@ +'use client' + +import type { FC, PropsWithChildren } from 'react' +import * as React from 'react' +import { useEffect } from 'react' +import type { IntersectionOptions } from 'react-intersection-observer' +import { useInView } from 'react-intersection-observer' + +export type LazyLoadProps = { + offset?: number + placeholder?: React.ReactNode +} & IntersectionOptions +export const LazyLoad: FC = (props) => { + const { placeholder = null, offset = 0, ...rest } = props + const { ref, inView } = useInView({ + triggerOnce: true, + rootMargin: `${offset || 0}px`, + ...rest, + }) + const [isLoaded, setIsLoaded] = React.useState(false) + useEffect(() => { + if (inView) { + setIsLoaded(true) + } + }, [inView]) + + return ( + <> + {!isLoaded && ( + + )} + {!inView ? placeholder : props.children} + + ) +} diff --git a/apps/landing/src/components/common/LightRays.tsx b/apps/landing/src/components/common/LightRays.tsx new file mode 100644 index 000000000..01030fea5 --- /dev/null +++ b/apps/landing/src/components/common/LightRays.tsx @@ -0,0 +1,581 @@ +// https://reactbits.dev/backgrounds/light-rays +'use client' + +import { Mesh, Program, Renderer, Triangle } from 'ogl' +import { useEffect, useRef, useState } from 'react' + +import { useIsDark } from '~/hooks/common/use-is-dark' +import { clsxm } from '~/lib/cn' + +export type RaysOrigin = + | 'top-center' + | 'top-left' + | 'top-right' + | 'right' + | 'left' + | 'bottom-center' + | 'bottom-right' + | 'bottom-left' + +interface LightRaysProps { + raysOrigin?: RaysOrigin + raysColor?: string + raysSpeed?: number + lightSpread?: number + rayLength?: number + pulsating?: boolean + fadeDistance?: number + saturation?: number + followMouse?: boolean + mouseInfluence?: number + noiseAmount?: number + distortion?: number + // Edge feather amounts in CSS pixels + edgeFadeLeft?: number + edgeFadeRight?: number + edgeFadeTop?: number + edgeFadeBottom?: number + className?: string +} + +const DEFAULT_COLOR = '#ffffff' + +const hexToRgb = (hex: string): [number, number, number] => { + const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return m + ? [ + Number.parseInt(m[1], 16) / 255, + Number.parseInt(m[2], 16) / 255, + Number.parseInt(m[3], 16) / 255, + ] + : [1, 1, 1] +} + +const getAnchorAndDir = ( + origin: RaysOrigin, + w: number, + h: number, +): { anchor: [number, number]; dir: [number, number] } => { + const outside = 0.2 + switch (origin) { + case 'top-left': { + return { anchor: [0, -outside * h], dir: [0, 1] } + } + case 'top-right': { + return { anchor: [w, -outside * h], dir: [0, 1] } + } + case 'left': { + return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] } + } + case 'right': { + return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] } + } + case 'bottom-left': { + return { anchor: [0, (1 + outside) * h], dir: [0, -1] } + } + case 'bottom-center': { + return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] } + } + case 'bottom-right': { + return { anchor: [w, (1 + outside) * h], dir: [0, -1] } + } + default: { + // "top-center" + return { anchor: [0.5 * w, -outside * h], dir: [0, 1] } + } + } +} + +export const LightRays: React.FC = ({ + raysOrigin = 'top-center', + raysColor = DEFAULT_COLOR, + raysSpeed = 1, + lightSpread = 1, + rayLength = 2, + pulsating = false, + fadeDistance = 1, + saturation = 1, + followMouse = true, + mouseInfluence = 0.1, + noiseAmount = 0, + distortion = 0, + edgeFadeLeft = 40, + edgeFadeRight = 40, + edgeFadeTop = 20, + edgeFadeBottom = 160, + className = '', +}) => { + const containerRef = useRef(null) + const uniformsRef = useRef(null) + const rendererRef = useRef(null) + const mouseRef = useRef({ x: 0.5, y: 0.5 }) + const smoothMouseRef = useRef({ x: 0.5, y: 0.5 }) + const animationIdRef = useRef(null) + const meshRef = useRef(null) + const cleanupFunctionRef = useRef<(() => void) | null>(null) + const [isVisible, setIsVisible] = useState(false) + const observerRef = useRef(null) + + const isDark = useIsDark() + const effectiveRaysColor = + !isDark && raysColor === DEFAULT_COLOR ? '#ff5c00' : raysColor + + useEffect(() => { + if (!containerRef.current) return + + observerRef.current = new IntersectionObserver( + (entries) => { + const entry = entries[0] + setIsVisible(entry.isIntersecting) + }, + { threshold: 0.1 }, + ) + + observerRef.current.observe(containerRef.current) + + return () => { + if (observerRef.current) { + observerRef.current.disconnect() + observerRef.current = null + } + } + }, []) + + useEffect(() => { + if (!isVisible || !containerRef.current) return + + if (cleanupFunctionRef.current) { + cleanupFunctionRef.current() + cleanupFunctionRef.current = null + } + + const initializeWebGL = async () => { + if (!containerRef.current) return + + // Ensure layout is ready without lingering timers + await Promise.resolve() + + if (!containerRef.current) return + + const renderer = new Renderer({ + dpr: Math.min(window.devicePixelRatio, 2), + alpha: true, + }) + rendererRef.current = renderer + + const { gl } = renderer + gl.canvas.style.width = '100%' + gl.canvas.style.height = '100%' + gl.canvas.style.backgroundColor = 'transparent' + gl.canvas.style.mixBlendMode = isDark ? 'normal' : 'plus-lighter' + + while (containerRef.current.firstChild) { + containerRef.current.firstChild.remove() + } + containerRef.current.append(gl.canvas) + + const vert = ` +attribute vec2 position; +varying vec2 vUv; +void main() { + vUv = position * 0.5 + 0.5; + gl_Position = vec4(position, 0.0, 1.0); +}` + + const frag = `precision highp float; + +uniform float iTime; +uniform vec2 iResolution; + +uniform vec2 rayPos; +uniform vec2 rayDir; +uniform vec3 raysColor; +uniform float raysSpeed; +uniform float lightSpread; +uniform float rayLength; +uniform float pulsating; +uniform float fadeDistance; +uniform float saturation; +uniform vec2 mousePos; +uniform float mouseInfluence; +uniform float noiseAmount; +uniform float distortion; +uniform float globalOpacity; +uniform float alphaCutoff; + +// Per-edge feathering (in device pixels) +uniform float edgeFadeLeft; +uniform float edgeFadeRight; +uniform float edgeFadeTop; +uniform float edgeFadeBottom; + +varying vec2 vUv; + +float noise(vec2 st) { + return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123); +} + +float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord, + float seedA, float seedB, float speed) { + vec2 sourceToCoord = coord - raySource; + vec2 dirNorm = normalize(sourceToCoord); + float cosAngle = dot(dirNorm, rayRefDirection); + + float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2; + + float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001)); + + float distance = length(sourceToCoord); + float maxDistance = iResolution.x * rayLength; + float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0); + + // Allow rays to fully fade to 0 to avoid gray/dark wash in light themes + float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.0, 1.0); + float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0; + + float baseStrength = clamp( + (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) + + (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)), + 0.0, 1.0 + ); + + return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse; +} + +void mainImage(out vec4 fragColor, in vec2 fragCoord) { + vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y); + + vec2 finalRayDir = rayDir; + if (mouseInfluence > 0.0) { + vec2 mouseScreenPos = mousePos * iResolution.xy; + vec2 mouseDirection = normalize(mouseScreenPos - rayPos); + finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence)); + } + + vec4 rays1 = vec4(1.0) * + rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349, + 1.5 * raysSpeed); + vec4 rays2 = vec4(1.0) * + rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234, + 1.1 * raysSpeed); + + fragColor = vec4(1.0); + float intensity = (rays1.a * 0.5 + rays2.a * 0.4); + fragColor.rgb = vec3(intensity); + fragColor.a = intensity; + + if (noiseAmount > 0.0) { + float n = noise(coord * 0.01 + iTime * 0.1); + fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n); + } + + // Subtle vertical brightening without color channel darkening + float brightness = 1.0 - (coord.y / iResolution.y); + float brightnessScale = mix(0.7, 1.0, brightness); + fragColor.rgb *= brightnessScale; + + // Remove grayscale mixing which could introduce unintended dark hues + + // Apply rays color as a tint while preserving brightness + fragColor.rgb = mix(fragColor.rgb, fragColor.rgb * raysColor, 0.8); + + // Thin out low-intensity areas to avoid gray wash; then apply global opacity + float a = fragColor.a; + a = smoothstep(alphaCutoff, alphaCutoff + 0.2, a); + fragColor.a = a * globalOpacity; + + // Per-edge feather to avoid harsh clipping at container boundaries + float fadeL = smoothstep(0.0, edgeFadeLeft, fragCoord.x); + float fadeR = smoothstep(0.0, edgeFadeRight, iResolution.x - fragCoord.x); + float fadeT = smoothstep(0.0, edgeFadeTop, fragCoord.y); + float fadeB = smoothstep(0.0, edgeFadeBottom, iResolution.y - fragCoord.y); + fragColor.a *= fadeL * fadeR * fadeT * fadeB; + + // Keep additive look; no extra premultiplying which can dim highlights under plus-lighter +} + +void main() { + vec4 color; + mainImage(color, gl_FragCoord.xy); + gl_FragColor = color; +}` + + const baseOpacity = isDark ? 0.55 : 0.22 + const baseCutoff = isDark ? 0.08 : 0.18 + + const uniforms = { + iTime: { value: 0 }, + iResolution: { value: [1, 1] }, + + rayPos: { value: [0, 0] }, + rayDir: { value: [0, 1] }, + + raysColor: { value: hexToRgb(effectiveRaysColor) }, + raysSpeed: { value: raysSpeed }, + lightSpread: { value: lightSpread }, + rayLength: { value: rayLength }, + pulsating: { value: pulsating ? 1 : 0 }, + fadeDistance: { value: fadeDistance }, + saturation: { value: saturation }, + mousePos: { value: [0.5, 0.5] }, + mouseInfluence: { value: mouseInfluence }, + noiseAmount: { value: noiseAmount }, + distortion: { value: distortion }, + globalOpacity: { value: baseOpacity }, + alphaCutoff: { value: baseCutoff }, + edgeFadeLeft: { value: 40 }, + edgeFadeRight: { value: 40 }, + edgeFadeTop: { value: 20 }, + edgeFadeBottom: { value: 160 }, + } + uniformsRef.current = uniforms + + const geometry = new Triangle(gl) + const program = new Program(gl, { + vertex: vert, + fragment: frag, + uniforms, + }) + const mesh = new Mesh(gl, { geometry, program }) + meshRef.current = mesh + + const updatePlacement = () => { + if (!containerRef.current || !renderer) return + + renderer.dpr = Math.min(window.devicePixelRatio, 2) + + const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current + renderer.setSize(wCSS, hCSS) + + const { dpr } = renderer + const w = wCSS * dpr + const h = hCSS * dpr + + uniforms.iResolution.value = [w, h] + + // Scale edge feather from CSS pixels to device pixels + uniforms.edgeFadeLeft.value = edgeFadeLeft * dpr + uniforms.edgeFadeRight.value = edgeFadeRight * dpr + uniforms.edgeFadeTop.value = edgeFadeTop * dpr + uniforms.edgeFadeBottom.value = edgeFadeBottom * dpr + + const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h) + uniforms.rayPos.value = anchor + uniforms.rayDir.value = dir + } + + const loop = (t: number) => { + if (!rendererRef.current || !uniformsRef.current || !meshRef.current) { + return + } + + uniforms.iTime.value = t * 0.001 + + if (followMouse && mouseInfluence > 0) { + const smoothing = 0.92 + + smoothMouseRef.current.x = + smoothMouseRef.current.x * smoothing + + mouseRef.current.x * (1 - smoothing) + smoothMouseRef.current.y = + smoothMouseRef.current.y * smoothing + + mouseRef.current.y * (1 - smoothing) + + uniforms.mousePos.value = [ + smoothMouseRef.current.x, + smoothMouseRef.current.y, + ] + } + + try { + renderer.render({ scene: mesh }) + animationIdRef.current = requestAnimationFrame(loop) + } catch (error) { + console.warn('WebGL rendering error:', error) + return + } + } + + updatePlacement() + animationIdRef.current = requestAnimationFrame(loop) + + cleanupFunctionRef.current = () => { + if (animationIdRef.current) { + cancelAnimationFrame(animationIdRef.current) + animationIdRef.current = null + } + + if (renderer) { + try { + const { canvas } = renderer.gl + const loseContextExt = + renderer.gl.getExtension('WEBGL_lose_context') + if (loseContextExt) { + loseContextExt.loseContext() + } + + if (canvas && canvas.parentNode) { + canvas.remove() + } + } catch (error) { + console.warn('Error during WebGL cleanup:', error) + } + } + + rendererRef.current = null + uniformsRef.current = null + meshRef.current = null + } + } + + initializeWebGL() + + return () => { + if (cleanupFunctionRef.current) { + cleanupFunctionRef.current() + cleanupFunctionRef.current = null + } + } + }, [ + isVisible, + raysOrigin, + raysColor, + effectiveRaysColor, + isDark, + raysSpeed, + lightSpread, + rayLength, + pulsating, + fadeDistance, + saturation, + followMouse, + mouseInfluence, + noiseAmount, + distortion, + edgeFadeLeft, + edgeFadeRight, + edgeFadeTop, + edgeFadeBottom, + ]) + + useEffect(() => { + const renderer = rendererRef.current + const uniforms = uniformsRef.current + if (!isVisible || !renderer || !containerRef.current || !uniforms) return + + const handleResize = () => { + if (!containerRef.current) return + renderer.dpr = Math.min(window.devicePixelRatio, 2) + const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current + renderer.setSize(wCSS, hCSS) + + const { dpr } = renderer + const w = wCSS * dpr + const h = hCSS * dpr + + uniforms.iResolution.value = [w, h] + + // Update edge feather values on resize (scale with DPR) + uniforms.edgeFadeLeft.value = edgeFadeLeft * dpr + uniforms.edgeFadeRight.value = edgeFadeRight * dpr + uniforms.edgeFadeTop.value = edgeFadeTop * dpr + uniforms.edgeFadeBottom.value = edgeFadeBottom * dpr + + const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h) + uniforms.rayPos.value = anchor + uniforms.rayDir.value = dir + } + + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [ + isVisible, + raysOrigin, + edgeFadeLeft, + edgeFadeRight, + edgeFadeTop, + edgeFadeBottom, + ]) + + useEffect(() => { + if (!uniformsRef.current || !containerRef.current || !rendererRef.current) + return + + const u = uniformsRef.current + const renderer = rendererRef.current + + u.raysColor.value = hexToRgb(effectiveRaysColor) + u.raysSpeed.value = raysSpeed + u.lightSpread.value = lightSpread + u.rayLength.value = rayLength + u.pulsating.value = pulsating ? 1 : 0 + u.fadeDistance.value = fadeDistance + u.saturation.value = saturation + u.mouseInfluence.value = mouseInfluence + u.noiseAmount.value = noiseAmount + u.distortion.value = distortion + u.globalOpacity.value = isDark ? 0.55 : 0.22 + u.alphaCutoff.value = isDark ? 0.08 : 0.18 + + // Edge feather uniforms (scale with DPR) + u.edgeFadeLeft.value = edgeFadeLeft * renderer.dpr + u.edgeFadeRight.value = edgeFadeRight * renderer.dpr + u.edgeFadeTop.value = edgeFadeTop * renderer.dpr + u.edgeFadeBottom.value = edgeFadeBottom * renderer.dpr + + const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current + const { dpr } = renderer + const { anchor, dir } = getAnchorAndDir(raysOrigin, wCSS * dpr, hCSS * dpr) + u.rayPos.value = anchor + u.rayDir.value = dir + }, [ + raysColor, + isDark, + effectiveRaysColor, + raysSpeed, + lightSpread, + raysOrigin, + rayLength, + pulsating, + fadeDistance, + saturation, + mouseInfluence, + noiseAmount, + distortion, + edgeFadeLeft, + edgeFadeRight, + edgeFadeTop, + edgeFadeBottom, + ]) + + useEffect(() => { + if (!rendererRef.current) return + const { gl } = rendererRef.current + gl.canvas.style.mixBlendMode = isDark ? 'normal' : 'plus-lighter' + }, [isDark]) + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!containerRef.current || !rendererRef.current) return + const rect = containerRef.current.getBoundingClientRect() + const x = (e.clientX - rect.left) / rect.width + const y = (e.clientY - rect.top) / rect.height + mouseRef.current = { x, y } + } + + if (followMouse) { + window.addEventListener('mousemove', handleMouseMove) + return () => window.removeEventListener('mousemove', handleMouseMove) + } + }, [followMouse]) + + return ( +
+ ) +} diff --git a/apps/landing/src/components/common/ProviderComposer.tsx b/apps/landing/src/components/common/ProviderComposer.tsx new file mode 100644 index 000000000..0cf41a862 --- /dev/null +++ b/apps/landing/src/components/common/ProviderComposer.tsx @@ -0,0 +1,12 @@ +'use client' + +import type { JSX } from 'react' +import * as React from 'react' + +export const ProviderComposer: Component<{ + contexts: JSX.Element[] +}> = ({ contexts, children }) => { + return contexts.reduceRight((kids: any, parent: any) => { + return React.cloneElement(parent, { children: kids }) + }, children) +} diff --git a/apps/landing/src/components/common/QueryHydrate.tsx b/apps/landing/src/components/common/QueryHydrate.tsx new file mode 100644 index 000000000..083814812 --- /dev/null +++ b/apps/landing/src/components/common/QueryHydrate.tsx @@ -0,0 +1,8 @@ +'use client' + +import type { HydrationBoundaryProps } from '@tanstack/react-query' +import { HydrationBoundary as RQHydrate } from '@tanstack/react-query' + +export function QueryHydrate(props: HydrationBoundaryProps) { + return +} diff --git a/apps/landing/src/components/common/ScrollTop.tsx b/apps/landing/src/components/common/ScrollTop.tsx new file mode 100644 index 000000000..8b07192b2 --- /dev/null +++ b/apps/landing/src/components/common/ScrollTop.tsx @@ -0,0 +1,18 @@ +'use client' + +import { usePathname } from 'next/navigation' +import { memo, useEffect } from 'react' + +import { isDev } from '~/lib/env' +import { springScrollToTop } from '~/lib/scroller' + +export const ScrollTop = memo(() => { + const pathname = usePathname() + useEffect(() => { + if (isDev) return + springScrollToTop() + }, [pathname]) + return null +}) + +ScrollTop.displayName = 'ScrollTop' diff --git a/apps/landing/src/components/hoc/with-no-ssr.tsx b/apps/landing/src/components/hoc/with-no-ssr.tsx new file mode 100644 index 000000000..c73fca9f1 --- /dev/null +++ b/apps/landing/src/components/hoc/with-no-ssr.tsx @@ -0,0 +1,13 @@ +import type { FC, PropsWithChildren } from 'react' + +import { useIsClientTransition } from '~/hooks/common/use-is-client' + +export const withNoSSR = ( + Component: FC>, +): FC> => { + return function NoSSRWrapper(props: PropsWithChildren

) { + const isClient = useIsClientTransition() + if (!isClient) return null + return + } +} diff --git a/apps/landing/src/components/layout/container/Normal.tsx b/apps/landing/src/components/layout/container/Normal.tsx new file mode 100644 index 000000000..2465b07af --- /dev/null +++ b/apps/landing/src/components/layout/container/Normal.tsx @@ -0,0 +1,17 @@ +import { clsxm } from '~/lib/helper' + +export const NormalContainer: Component = (props) => { + const { children, className } = props + + return ( +

+ {children} +
+ ) +} diff --git a/apps/landing/src/components/layout/content/Content.tsx b/apps/landing/src/components/layout/content/Content.tsx new file mode 100644 index 000000000..917245420 --- /dev/null +++ b/apps/landing/src/components/layout/content/Content.tsx @@ -0,0 +1,7 @@ +export const Content: Component = ({ children }) => { + return ( +
+ {children} +
+ ) +} diff --git a/apps/landing/src/components/layout/content/index.ts b/apps/landing/src/components/layout/content/index.ts new file mode 100644 index 000000000..aa90efe31 --- /dev/null +++ b/apps/landing/src/components/layout/content/index.ts @@ -0,0 +1 @@ +export * from './Content' diff --git a/apps/landing/src/components/layout/footer/Footer.tsx b/apps/landing/src/components/layout/footer/Footer.tsx new file mode 100644 index 000000000..93b316b65 --- /dev/null +++ b/apps/landing/src/components/layout/footer/Footer.tsx @@ -0,0 +1,184 @@ +'use client' + +import Link from 'next/link' + +import { Logo } from '~/components/brand/Logo' +import { cx, focusRing } from '~/lib/cn' + +type LinkItem = { label: string; href: string; external?: boolean } + +const productLinks: LinkItem[] = [ + { label: 'Web App', href: 'https://app.folo.is', external: true }, + { label: 'Download', href: '/download', external: false }, +] + +const communityLinks: LinkItem[] = [ + { label: 'Discord', href: 'https://discord.gg/followapp', external: true }, + { + label: 'GitHub', + href: 'https://github.com/RSSNext/Folo', + external: true, + }, + { label: 'Twitter', href: 'https://x.com/folo_is', external: true }, +] + +const resourceLinks: LinkItem[] = [ + { label: 'For Researchers', href: '#researchers' }, + { label: 'For Builders', href: '#builders' }, + { label: 'For Creators', href: '#creators' }, + { label: 'For Investors', href: '#investors' }, + // { label: 'API', href: '#api' }, +] + +const legalLinks: LinkItem[] = [ + { label: 'Privacy Policy', href: 'privacy-policy' }, + { label: 'Terms of Service', href: 'terms-of-service' }, + // { label: 'Security', href: '#security' }, + // { label: 'Cookie', href: '#cookie' }, +] + +const BrandBlock = () => ( +
+ + + Folo + +

+ The next-generation information browser powered by AI. Transform how you + discover, consume, and interact with content across the web. +

+

+ Deep reading, contextual AI, and noise-free content discovery — all in one + beautiful interface. +

+
+) + +const Social = () => ( +
+ + + + + + + + + + + +
+) + +function LinkColumn({ title, links }: { title: string; links: LinkItem[] }) { + return ( +
+

{title}

+
    + {links.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ ) +} + +/** Props for Footer component */ +export interface FooterProps { + className?: string +} + +export const Footer: Component = ({ className }) => { + const year = new Date().getFullYear() + + return ( +
+
+ {/* Main footer content */} +
+ {/* Brand section with stats */} +
+ + +
+ + {/* Navigation columns */} +
+ + + +
+
+ + {/* Bottom bar */} +
+

+ © {year} Folo. All rights reserved. +

+ +
+ {legalLinks.map((link) => ( + + {link.label} + + ))} +
+
+
+
+ ) +} + +Footer.displayName = 'Footer' diff --git a/apps/landing/src/components/layout/root/Root.tsx b/apps/landing/src/components/layout/root/Root.tsx new file mode 100644 index 000000000..cab8147b7 --- /dev/null +++ b/apps/landing/src/components/layout/root/Root.tsx @@ -0,0 +1,13 @@ +import { Footer } from '~/components/layout/footer/Footer' + +import { Content } from '../content/Content' + +export const Root: Component = ({ children }) => { + return ( + <> +
+ {children} +