Compare commits
51 Commits
feat/websi
...
codex/part
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a582f62e2 | ||
|
|
a0b3c1b436 | ||
|
|
397559b080 | ||
|
|
aa41a160fe | ||
|
|
3e2a02f181 | ||
|
|
cb10d58525 | ||
|
|
6d862f60c8 | ||
|
|
b8ea3851cb | ||
|
|
e6c04359b5 | ||
|
|
27859cb41c | ||
|
|
3be998aaf4 | ||
|
|
99674df73b | ||
|
|
cfaf89edea | ||
|
|
a154e6a311 | ||
|
|
46fec1d47d | ||
|
|
1052658a02 | ||
|
|
f524683f3c | ||
|
|
d1d55585f9 | ||
|
|
98c654df20 | ||
|
|
5bce9c4874 | ||
|
|
6ac9b653bb | ||
|
|
86bccf8d4c | ||
|
|
b52f6ce764 | ||
|
|
5da3e16f33 | ||
|
|
6d0bbd7d7c | ||
|
|
4341972be3 | ||
|
|
98700cfcc7 | ||
|
|
5bf41a41bd | ||
|
|
a7f14a0b3f | ||
|
|
e6d1a9d4a2 | ||
|
|
0b4a960467 | ||
|
|
060957d66c | ||
|
|
1a98362984 | ||
|
|
ab33746b3e | ||
|
|
55c4e807a1 | ||
|
|
74147d7ee2 | ||
|
|
16fdcab4be | ||
|
|
96c6dbcb69 | ||
|
|
ffb4ae34c6 | ||
|
|
59341a4b71 | ||
|
|
97fffb5394 | ||
|
|
4972e4d42f | ||
|
|
8d8ae7dfc0 | ||
|
|
38c034e1d6 | ||
|
|
b40fad0e75 | ||
|
|
01cbfa6a23 | ||
|
|
945a143626 | ||
|
|
193bbaba81 | ||
|
|
1eacb224a1 | ||
|
|
4ed2fe70f3 | ||
|
|
5da5ee5031 |
9
.fallowrc.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
|
||||
"entry": ["src/main.ts", "index.html"],
|
||||
"duplicates": {
|
||||
"minOccurrences": 3,
|
||||
"ignore": ["**/*.generated.*", "**/generatedManagerTypes.ts"]
|
||||
},
|
||||
"rules": {}
|
||||
}
|
||||
8
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -73,8 +73,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shardTotal: [8]
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
|
||||
shardTotal: [16]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
# Run sharded tests (browsers pre-installed in container)
|
||||
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
id: playwright
|
||||
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
|
||||
COLLECT_COVERAGE: 'true'
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
# Run tests (browsers pre-installed in container)
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
id: playwright
|
||||
run: pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob
|
||||
run: pnpm exec playwright test --project=${{ matrix.browser }}
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
|
||||
|
||||
|
||||
4
.github/workflows/cla.yml
vendored
@@ -38,9 +38,11 @@ jobs:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login || github.event.issue.user.login }}
|
||||
BASE_ALLOWLIST: action@github.com,actions-user,ampagent,claude,comfy-pr-bot,GitHub Action,github-actions,github-actions[bot],Glary Bot,Glary-Bot,*[bot]
|
||||
# For each commit emit the GitHub login when the author/committer email resolves to a GitHub account
|
||||
# otherwise fall back to the raw git name.
|
||||
run: |
|
||||
others=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits" --paginate \
|
||||
--jq '.[] | (.author.login // empty), (.committer.login // empty)' \
|
||||
--jq '.[] | (.author.login // .commit.author.name // empty), (.committer.login // .commit.committer.name // empty)' \
|
||||
| sort -u | grep -vix "${PR_AUTHOR}" | paste -sd, -)
|
||||
if [ -n "$others" ]; then
|
||||
echo "allowlist=${BASE_ALLOWLIST},${others}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
75
.github/workflows/pr-backport.yaml
vendored
@@ -278,32 +278,49 @@ jobs:
|
||||
continue
|
||||
fi
|
||||
|
||||
# Create backport branch
|
||||
git checkout -b "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"
|
||||
# Create backport branch. A failure here (e.g. dirty state left
|
||||
# by a prior target) must not abort the loop and skip remaining
|
||||
# targets, so fall back to a clean checkout and record the error.
|
||||
if ! git checkout -B "${BACKPORT_BRANCH}" "origin/${TARGET_BRANCH}"; then
|
||||
echo "::error::Failed to create branch ${BACKPORT_BRANCH} for ${TARGET_BRANCH}"
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:branch-create-failed "
|
||||
git checkout main || git checkout -f main
|
||||
echo "::endgroup::"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Try cherry-pick
|
||||
if git cherry-pick "${MERGE_COMMIT}"; then
|
||||
if [ "$REMOTE_BACKPORT_EXISTS" = true ]; then
|
||||
git push --force-with-lease origin "${BACKPORT_BRANCH}"
|
||||
PUSH_CMD=(git push --force-with-lease origin "${BACKPORT_BRANCH}")
|
||||
else
|
||||
git push origin "${BACKPORT_BRANCH}"
|
||||
PUSH_CMD=(git push origin "${BACKPORT_BRANCH}")
|
||||
fi
|
||||
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
|
||||
# A push failure for one target must not abort the loop and
|
||||
# prevent remaining targets from being attempted.
|
||||
if "${PUSH_CMD[@]}"; then
|
||||
echo "${BACKPORT_BRANCH}" >> "$CREATED_BRANCHES_FILE"
|
||||
SUCCESS="${SUCCESS}${TARGET_BRANCH}:${BACKPORT_BRANCH} "
|
||||
echo "Successfully created backport branch: ${BACKPORT_BRANCH}"
|
||||
else
|
||||
echo "::error::Failed to push ${BACKPORT_BRANCH} for ${TARGET_BRANCH}"
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:push-failed "
|
||||
fi
|
||||
|
||||
# Return to main (keep the branch, we need it for PR)
|
||||
git checkout main
|
||||
git checkout main || git checkout -f main
|
||||
else
|
||||
# Get conflict info
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U | tr '\n' ',')
|
||||
git cherry-pick --abort
|
||||
git cherry-pick --abort || true
|
||||
|
||||
echo "::error::Cherry-pick failed due to conflicts"
|
||||
FAILED="${FAILED}${TARGET_BRANCH}:conflicts:${CONFLICTS} "
|
||||
|
||||
# Clean up the failed branch
|
||||
git checkout main
|
||||
git branch -D "${BACKPORT_BRANCH}"
|
||||
git checkout main || git checkout -f main
|
||||
git branch -D "${BACKPORT_BRANCH}" || true
|
||||
fi
|
||||
|
||||
echo "::endgroup::"
|
||||
@@ -384,6 +401,10 @@ jobs:
|
||||
|
||||
**Reason:** Merge conflicts detected during cherry-pick of `${MERGE_COMMIT_SHORT}`
|
||||
|
||||
The auto-backport could not be completed automatically. Please backport
|
||||
manually onto branch `${BACKPORT_BRANCH}` (from `origin/${target}`) and
|
||||
open a PR to `${target}`.
|
||||
|
||||
<details>
|
||||
<summary>📄 Conflicting files</summary>
|
||||
|
||||
@@ -416,19 +437,37 @@ jobs:
|
||||
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
|
||||
fi
|
||||
|
||||
# Post a comment without letting a single failed `gh pr comment` (e.g.
|
||||
# a locked issue, as happened for PR #13359, or a transient API error)
|
||||
# abort the step under `set -e` and swallow the remaining failures.
|
||||
post_comment() {
|
||||
local body="$1"
|
||||
local context="$2"
|
||||
if ! gh pr comment "${PR_NUMBER}" --body "${body}"; then
|
||||
echo "::warning::Could not comment on PR #${PR_NUMBER} about ${context}. Manual backport required."
|
||||
fi
|
||||
}
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r target reason conflicts <<< "${failure}"
|
||||
|
||||
SAFE_TARGET=$(echo "$target" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
|
||||
if [ "${reason}" = "branch-missing" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist"
|
||||
post_comment "@${PR_AUTHOR} Backport failed: Branch \`${target}\` does not exist" "missing branch ${target}"
|
||||
|
||||
elif [ "${reason}" = "already-exists" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
|
||||
post_comment "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed." "already-backported ${target}"
|
||||
|
||||
elif [ "${reason}" = "branch-create-failed" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` failed: could not create the backport branch. Please retry or backport manually."
|
||||
|
||||
elif [ "${reason}" = "push-failed" ]; then
|
||||
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Backport to \`${target}\` cherry-picked cleanly but the push failed. Please retry or push the backport branch manually."
|
||||
|
||||
elif [ "${reason}" = "conflicts" ]; then
|
||||
CONFLICTS_INLINE=$(echo "${conflicts}" | tr ',' ' ')
|
||||
SAFE_TARGET=$(echo "$target" | tr '/' '-')
|
||||
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
|
||||
PR_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}"
|
||||
|
||||
export PR_NUMBER PR_URL MERGE_COMMIT target BACKPORT_BRANCH CONFLICTS_INLINE
|
||||
@@ -444,10 +483,10 @@ jobs:
|
||||
CONFLICTS_BLOCK=$(echo "${conflicts}" | tr ',' '\n')
|
||||
MERGE_COMMIT_SHORT="${MERGE_COMMIT:0:7}"
|
||||
|
||||
export target MERGE_COMMIT_SHORT CONFLICTS_BLOCK AGENT_PROMPT PR_AUTHOR
|
||||
COMMENT_BODY=$(envsubst '${target} ${MERGE_COMMIT_SHORT} ${CONFLICTS_BLOCK} ${AGENT_PROMPT} ${PR_AUTHOR}' <<<"$COMMENT_BODY_TEMPLATE")
|
||||
export target MERGE_COMMIT_SHORT BACKPORT_BRANCH CONFLICTS_BLOCK AGENT_PROMPT PR_AUTHOR
|
||||
COMMENT_BODY=$(envsubst '${target} ${MERGE_COMMIT_SHORT} ${BACKPORT_BRANCH} ${CONFLICTS_BLOCK} ${AGENT_PROMPT} ${PR_AUTHOR}' <<<"$COMMENT_BODY_TEMPLATE")
|
||||
|
||||
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
|
||||
post_comment "${COMMENT_BODY}" "cherry-pick conflict on ${target} (backport manually onto ${BACKPORT_BRANCH})"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ on:
|
||||
required: false
|
||||
default: 'Comfy-Org/ComfyUI'
|
||||
type: string
|
||||
target_branch:
|
||||
description: 'Optional: force a specific release branch, e.g. core/1.47 or core/2.0. Overrides the pin-derived target — use to skip a dead minor or do an out-of-cadence / major release.'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
check-release-week:
|
||||
@@ -49,12 +53,15 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
env:
|
||||
TARGET_BRANCH_OVERRIDE: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
echo "## Release Check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Target branch override: ${TARGET_BRANCH_OVERRIDE:-(none — pin-derived)}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
resolve-version:
|
||||
needs: check-release-week
|
||||
@@ -103,6 +110,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
env:
|
||||
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}
|
||||
TARGET_BRANCH: ${{ inputs.target_branch }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
1
.gitignore
vendored
@@ -16,6 +16,7 @@ yarn.lock
|
||||
.eslintcache
|
||||
.prettiercache
|
||||
.stylelintcache
|
||||
.fallow/
|
||||
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
@@ -88,6 +88,11 @@ const config: StorybookConfig = {
|
||||
replacement:
|
||||
process.cwd() + '/src/storybook/mocks/useFeatureFlags.ts'
|
||||
},
|
||||
{
|
||||
find: '@/platform/workspace/composables/useWorkspaceUI',
|
||||
replacement:
|
||||
process.cwd() + '/src/storybook/mocks/useWorkspaceUI.ts'
|
||||
},
|
||||
{
|
||||
find: '@/platform/workspace/stores/teamWorkspaceStore',
|
||||
replacement:
|
||||
|
||||
@@ -10,11 +10,13 @@ const PAYMENT_STATUSES = ['success', 'failed'] as const
|
||||
const LOCALE_PREFIXES = LOCALES.map((locale) =>
|
||||
locale === DEFAULT_LOCALE ? '' : `/${locale}`
|
||||
)
|
||||
const SITEMAP_EXCLUDED_PATHNAMES = new Set(
|
||||
LOCALE_PREFIXES.flatMap((prefix) =>
|
||||
const SITEMAP_EXCLUDED_PATHNAMES = new Set([
|
||||
...LOCALE_PREFIXES.flatMap((prefix) =>
|
||||
PAYMENT_STATUSES.map((status) => `${prefix}/payment/${status}`)
|
||||
)
|
||||
)
|
||||
),
|
||||
...LOCALE_PREFIXES.map((prefix) => `${prefix}/individual-submission`),
|
||||
...LOCALE_PREFIXES.map((prefix) => `${prefix}/booking-confirmation`)
|
||||
])
|
||||
|
||||
function isExcludedFromSitemap(page: string): boolean {
|
||||
const pathname = new URL(page).pathname.replace(/\/$/, '')
|
||||
|
||||
51
apps/website/e2e/brand.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { BRAND_ASSETS_ZIP, BRAND_GUIDELINES_PDF } from '../src/data/brandAssets'
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
test.describe('Brand portal @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/brand')
|
||||
})
|
||||
|
||||
test('renders each brand guideline section', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { level: 1, name: 'Create with ComfyUI' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'One mark, many dimensions.' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Every color earns its place.' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Precise, never cute.' })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: 'Trademark guidelines.' })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('shows all four marks', async ({ page }) => {
|
||||
const logos = page.locator('#logos')
|
||||
for (const name of [
|
||||
'Core Logo',
|
||||
'Logomark',
|
||||
'Icon',
|
||||
'Amplified Logomark'
|
||||
]) {
|
||||
await expect(logos.getByText(name, { exact: true })).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('the hero ctas open the gated guidelines and the logo bundle', async ({
|
||||
page
|
||||
}) => {
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'View brand guidelines' })
|
||||
).toHaveAttribute('href', BRAND_GUIDELINES_PDF)
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Download logos' })
|
||||
).toHaveAttribute('href', BRAND_ASSETS_ZIP)
|
||||
})
|
||||
})
|
||||
@@ -8,7 +8,7 @@ test.describe('Careers page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Careers — Comfy')
|
||||
await expect(page).toHaveTitle('Careers - Comfy')
|
||||
})
|
||||
|
||||
test('Roles section heading is visible', async ({ page }) => {
|
||||
@@ -72,7 +72,7 @@ test.describe('Careers page role links', () => {
|
||||
test.describe('Careers page (zh-CN) @smoke', () => {
|
||||
test('renders localized heading and roles', async ({ page }) => {
|
||||
await page.goto('/zh-CN/careers')
|
||||
await expect(page).toHaveTitle('招聘 — Comfy')
|
||||
await expect(page).toHaveTitle('招聘 - Comfy')
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '职位', level: 2 })
|
||||
).toBeVisible()
|
||||
|
||||
@@ -9,7 +9,7 @@ test.describe('Cloud nodes page @smoke', () => {
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(
|
||||
'Custom-node packs on Comfy Cloud — supported by default'
|
||||
'Custom-node packs on Comfy Cloud - supported by default'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ test.describe('Cloud page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Comfy Cloud — AI in the Cloud')
|
||||
await expect(page).toHaveTitle('Comfy Cloud - AI in the Cloud')
|
||||
})
|
||||
|
||||
test('HeroSection heading and subtitle are visible', async ({ page }) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ test.describe('Download page @smoke', () => {
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle(
|
||||
'Download Comfy Desktop — Run AI on Your Hardware'
|
||||
'Download Comfy Desktop - Run AI on Your Hardware'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ test.describe('Homepage @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Comfy — Professional Control of Visual AI')
|
||||
await expect(page).toHaveTitle('Comfy - Professional Control of Visual AI')
|
||||
})
|
||||
|
||||
test('HeroSection heading is visible', async ({ page }) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ test.describe('Learning page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Learning — Comfy')
|
||||
await expect(page).toHaveTitle('Learning - Comfy')
|
||||
})
|
||||
|
||||
test('hero headline references ComfyUI', async ({ page }) => {
|
||||
@@ -137,7 +137,7 @@ test.describe('Learning page (zh-CN) @smoke', () => {
|
||||
test('renders localized title, headings, and tutorials', async ({ page }) => {
|
||||
await page.goto('/zh-CN/learning')
|
||||
|
||||
await expect(page).toHaveTitle('学习 — Comfy')
|
||||
await expect(page).toHaveTitle('学习 - Comfy')
|
||||
await expect(page.getByRole('heading', { level: 1 })).toContainText(
|
||||
/[一-鿿]/
|
||||
)
|
||||
|
||||
153
apps/website/e2e/mcp.spec.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const MCP_ENDPOINT = 'https://cloud.comfy.org/mcp'
|
||||
|
||||
// The setup island hydrates on visibility; clicks before hydration are
|
||||
// no-ops, so retry until the tab actually activates.
|
||||
async function selectClientTab(setup: Locator, name: string) {
|
||||
const tab = setup.getByRole('tab', { name })
|
||||
await expect(async () => {
|
||||
await tab.click()
|
||||
await expect(tab).toHaveAttribute('data-state', 'active', { timeout: 500 })
|
||||
}).toPass()
|
||||
}
|
||||
|
||||
test.describe('MCP page @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/mcp')
|
||||
})
|
||||
|
||||
test('hero and how-it-works INSTALL MCP CTAs anchor to setup', async ({
|
||||
page
|
||||
}) => {
|
||||
const installLinks = page.getByRole('link', { name: 'INSTALL MCP' })
|
||||
await expect(installLinks).toHaveCount(2)
|
||||
for (const link of await installLinks.all()) {
|
||||
await expect(link).toHaveAttribute('href', '#setup')
|
||||
}
|
||||
})
|
||||
|
||||
test('Claude Desktop is the default tab and shows only the connector card', async ({
|
||||
page
|
||||
}) => {
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
await expect(
|
||||
setup.getByRole('tab', { name: 'Claude Desktop' })
|
||||
).toHaveAttribute('data-state', 'active')
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: 'Add Custom Connector' })
|
||||
).toBeVisible()
|
||||
await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible()
|
||||
await expect(
|
||||
setup.getByRole('heading', {
|
||||
name: 'Ask your agent to install Comfy MCP'
|
||||
})
|
||||
).toHaveCount(0)
|
||||
await expect(setup.locator('video')).toBeVisible()
|
||||
})
|
||||
|
||||
test('client tabs swap install instructions and agent-card visibility', async ({
|
||||
page
|
||||
}) => {
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
const activePanel = setup.locator('[role="tabpanel"][data-state="active"]')
|
||||
const agentHeading = setup.getByRole('heading', {
|
||||
name: 'Ask your agent to install Comfy MCP'
|
||||
})
|
||||
|
||||
await expect(activePanel).toContainText('Add custom connector')
|
||||
|
||||
// First interaction retries until the island hydrates; later switches
|
||||
// assert synchronously so steady-state click regressions fail.
|
||||
await selectClientTab(setup, 'Claude Code Terminal')
|
||||
await expect(activePanel).toContainText(
|
||||
`claude mcp add --transport http comfy-cloud ${MCP_ENDPOINT}`
|
||||
)
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: 'Install manually' })
|
||||
).toBeVisible()
|
||||
await expect(agentHeading).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Codex' }).click()
|
||||
await expect(activePanel).toContainText(
|
||||
`codex mcp add comfy-cloud --url ${MCP_ENDPOINT}`
|
||||
)
|
||||
await expect(agentHeading).toHaveCount(0)
|
||||
await expect(setup.locator('video')).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Cursor' }).click()
|
||||
await expect(activePanel).toContainText('X-API-Key')
|
||||
await expect(
|
||||
activePanel.getByRole('link', { name: 'platform.comfy.org' })
|
||||
).toHaveAttribute('href', 'https://platform.comfy.org/profile/api-keys')
|
||||
await expect(agentHeading).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'OpenClaw' }).click()
|
||||
await expect(activePanel).toContainText(
|
||||
'openclaw skills install @comfy-org/comfy'
|
||||
)
|
||||
await expect(agentHeading).toBeVisible()
|
||||
|
||||
await setup.getByRole('tab', { name: 'Others' }).click()
|
||||
await expect(activePanel).toContainText('remote MCP server')
|
||||
await expect(agentHeading).toBeVisible()
|
||||
})
|
||||
|
||||
test('skills plugin link lives in the agent option card', async ({
|
||||
page
|
||||
}) => {
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
await selectClientTab(setup, 'Claude Code Terminal')
|
||||
await expect(
|
||||
setup.getByRole('link', { name: 'View on GitHub' })
|
||||
).toHaveAttribute('href', 'https://github.com/Comfy-Org/comfy-skills')
|
||||
})
|
||||
|
||||
test('capabilities section shows all six tool cards', async ({ page }) => {
|
||||
for (const title of [
|
||||
'Generate anything',
|
||||
'Search the ecosystem',
|
||||
'Run real workflows',
|
||||
'Direct any model',
|
||||
'Generate in batches',
|
||||
'Ship it as an app'
|
||||
]) {
|
||||
await expect(
|
||||
page.getByRole('heading', { name: title, exact: true })
|
||||
).toBeVisible()
|
||||
}
|
||||
})
|
||||
|
||||
test('FAQ lists nine questions and autolinks the server URL', async ({
|
||||
page
|
||||
}) => {
|
||||
const triggers = page.locator('[id^="faq-trigger-"]')
|
||||
await triggers.first().scrollIntoViewIfNeeded()
|
||||
await expect(triggers).toHaveCount(9)
|
||||
|
||||
await page.getByRole('button', { name: "What's the server URL?" }).click()
|
||||
await expect(
|
||||
page.getByRole('link', { name: MCP_ENDPOINT, exact: true })
|
||||
).toHaveAttribute('href', MCP_ENDPOINT)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('MCP page zh-CN @smoke', () => {
|
||||
test('setup section renders localized options', async ({ page }) => {
|
||||
await page.goto('/zh-CN/mcp')
|
||||
const setup = page.locator('#setup')
|
||||
await setup.scrollIntoViewIfNeeded()
|
||||
await expect(
|
||||
setup.getByRole('heading', { name: '添加自定义连接器' })
|
||||
).toBeVisible()
|
||||
await selectClientTab(setup, 'Claude Code Terminal')
|
||||
await expect(setup.getByRole('heading', { name: '手动安装' })).toBeVisible()
|
||||
await expect(setup.getByText(MCP_ENDPOINT, { exact: true })).toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -31,6 +31,26 @@ test.describe('Desktop navigation @smoke', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('NEW badge shows on Products and Community only', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopLinks = nav.getByTestId('desktop-nav-links')
|
||||
|
||||
for (const label of ['Products', 'Community']) {
|
||||
await expect(
|
||||
desktopLinks
|
||||
.getByRole('button', { name: label })
|
||||
.getByText('NEW', { exact: true })
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
await expect(
|
||||
desktopLinks.getByRole('button', { name: 'Company' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
await expect(
|
||||
desktopLinks.getByRole('link', { name: 'Pricing' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('CTA buttons are visible', async ({ page }) => {
|
||||
const nav = page.getByRole('navigation', { name: 'Main navigation' })
|
||||
const desktopCTA = nav.getByTestId('desktop-nav-cta')
|
||||
@@ -117,6 +137,27 @@ test.describe('Mobile menu @mobile', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('NEW badge shows on Products and Community only', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Toggle menu' }).click()
|
||||
|
||||
const menu = page.getByRole('dialog')
|
||||
|
||||
for (const label of ['Products', 'Community']) {
|
||||
await expect(
|
||||
menu.getByRole('button', { name: label }).getByText('NEW', {
|
||||
exact: true
|
||||
})
|
||||
).toBeVisible()
|
||||
}
|
||||
|
||||
await expect(
|
||||
menu.getByRole('button', { name: 'Company' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
await expect(
|
||||
menu.getByRole('link', { name: 'Pricing' }).getByText('NEW')
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('clicking section with subitems drills down and back works', async ({
|
||||
page
|
||||
}) => {
|
||||
|
||||
@@ -22,7 +22,7 @@ test.describe('Payment success page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title and is noindex', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Payment Successful — Comfy')
|
||||
await expect(page).toHaveTitle('Payment Successful - Comfy')
|
||||
await expectNoIndex(page)
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ test.describe('Payment failed page @smoke', () => {
|
||||
})
|
||||
|
||||
test('has correct title and is noindex', async ({ page }) => {
|
||||
await expect(page).toHaveTitle('Payment Failed — Comfy')
|
||||
await expect(page).toHaveTitle('Payment Failed - Comfy')
|
||||
await expectNoIndex(page)
|
||||
})
|
||||
|
||||
@@ -84,7 +84,7 @@ test.describe('Payment failed page @smoke', () => {
|
||||
test.describe('Payment pages zh-CN @smoke', () => {
|
||||
test('zh-CN success page renders and links correctly', async ({ page }) => {
|
||||
await page.goto('/zh-CN/payment/success')
|
||||
await expect(page).toHaveTitle('支付成功 — Comfy')
|
||||
await expect(page).toHaveTitle('支付成功 - Comfy')
|
||||
await expectNoIndex(page)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '支付成功', level: 1 })
|
||||
@@ -99,7 +99,7 @@ test.describe('Payment pages zh-CN @smoke', () => {
|
||||
|
||||
test('zh-CN failed page renders and links correctly', async ({ page }) => {
|
||||
await page.goto('/zh-CN/payment/failed')
|
||||
await expect(page).toHaveTitle('支付失败 — Comfy')
|
||||
await expect(page).toHaveTitle('支付失败 - Comfy')
|
||||
await expectNoIndex(page)
|
||||
await expect(
|
||||
page.getByRole('heading', { name: '无法完成支付', level: 1 })
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { externalLinks, getRoutes } from '../src/config/routes'
|
||||
import { seedanceFaqs, seedanceReviews } from '../src/data/seedance'
|
||||
import { t } from '../src/i18n/translations'
|
||||
import { test } from './fixtures/blockExternalMedia'
|
||||
|
||||
const PATH = '/seedance-2.5'
|
||||
const HERO_TITLE = t('seedance.hero.title', 'en')
|
||||
const MODELS_HEADING = t('seedance.models.heading', 'en')
|
||||
const MODELS_ROUTE = getRoutes('en').models
|
||||
const CTA_HEADING = t('seedance.cta.heading', 'en')
|
||||
const CTA_PRIMARY = t('seedance.cta.primaryCta', 'en')
|
||||
const WORKFLOWS_URL = externalLinks.workflows
|
||||
const FAQ_COUNT = seedanceFaqs.length
|
||||
const FIRST_FAQ = seedanceFaqs[0]
|
||||
const REVIEWS_HEADING = t('seedance.reviews.heading', 'en')
|
||||
const HIGHLIGHT_CTA = t('seedance.reviews.highlightCta', 'en')
|
||||
const MCP_ROUTE = getRoutes('en').mcp
|
||||
const FIRST_REVIEW = seedanceReviews[0]
|
||||
|
||||
test.describe('Seedance 2.5 page — desktop @smoke', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(PATH)
|
||||
})
|
||||
|
||||
test('renders the hero heading and is indexable', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { level: 1, name: HERO_TITLE })
|
||||
).toBeVisible()
|
||||
|
||||
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('renders the models section heading', async ({ page }) => {
|
||||
const heading = page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: MODELS_HEADING
|
||||
})
|
||||
await heading.scrollIntoViewIfNeeded()
|
||||
await expect(heading).toBeVisible()
|
||||
})
|
||||
|
||||
test('renders the reviews section heading and first quote', async ({
|
||||
page
|
||||
}) => {
|
||||
const heading = page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: REVIEWS_HEADING
|
||||
})
|
||||
await heading.scrollIntoViewIfNeeded()
|
||||
await expect(heading).toBeVisible()
|
||||
await expect(page.getByText(FIRST_REVIEW.name)).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Seedance 2.5 page — link targets', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(PATH)
|
||||
})
|
||||
|
||||
test('breadcrumb trail links to the models catalog', async ({ page }) => {
|
||||
const modelsCrumb = page
|
||||
.getByRole('navigation', { name: 'Breadcrumb' })
|
||||
.getByRole('link', { name: t('models.breadcrumb.models', 'en') })
|
||||
await expect(modelsCrumb).toHaveAttribute('href', MODELS_ROUTE)
|
||||
})
|
||||
|
||||
test('closing CTA links to run Wan 2.2 in a new tab', async ({ page }) => {
|
||||
const ctaSection = page.locator('section').filter({
|
||||
has: page.getByRole('heading', { level: 2, name: CTA_HEADING })
|
||||
})
|
||||
const primary = ctaSection.getByRole('link', { name: CTA_PRIMARY })
|
||||
await primary.scrollIntoViewIfNeeded()
|
||||
await expect(primary).toBeVisible()
|
||||
await expect(primary).toHaveAttribute('href', WORKFLOWS_URL)
|
||||
await expect(primary).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
|
||||
test('MCP highlight card CTA links to the MCP page', async ({ page }) => {
|
||||
const reviewsSection = page.locator('section').filter({
|
||||
has: page.getByRole('heading', { level: 2, name: REVIEWS_HEADING })
|
||||
})
|
||||
const cta = reviewsSection.getByRole('link', { name: HIGHLIGHT_CTA })
|
||||
await cta.scrollIntoViewIfNeeded()
|
||||
await expect(cta).toHaveAttribute('href', MCP_ROUTE)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Seedance 2.5 page — interactions', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(PATH)
|
||||
})
|
||||
|
||||
test('emits FAQPage structured data with one entry per FAQ', async ({
|
||||
page
|
||||
}) => {
|
||||
const faqJsonLd = await page.evaluate(() => {
|
||||
const scripts = Array.from(
|
||||
document.querySelectorAll<HTMLScriptElement>(
|
||||
'script[type="application/ld+json"]'
|
||||
)
|
||||
)
|
||||
const match = scripts.find((s) =>
|
||||
(s.textContent ?? '').includes('FAQPage')
|
||||
)
|
||||
return match?.textContent ?? null
|
||||
})
|
||||
expect(faqJsonLd, 'FAQ JSON-LD script').not.toBeNull()
|
||||
const graph = JSON.parse(faqJsonLd!)['@graph'] as {
|
||||
'@type': string
|
||||
mainEntity?: unknown[]
|
||||
}[]
|
||||
const faqPage = graph.find((node) => node['@type'] === 'FAQPage')
|
||||
expect(faqPage, 'FAQPage node in @graph').toBeDefined()
|
||||
expect(faqPage!.mainEntity!.length).toBe(FAQ_COUNT)
|
||||
})
|
||||
|
||||
test('FAQ items toggle open and closed on click', async ({ page }) => {
|
||||
const firstQuestion = page.getByRole('button', {
|
||||
name: FIRST_FAQ.question.en
|
||||
})
|
||||
await firstQuestion.scrollIntoViewIfNeeded()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true')
|
||||
await expect(page.getByText(FIRST_FAQ.answer.en)).toBeVisible()
|
||||
|
||||
await firstQuestion.click()
|
||||
await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
test('reviews carousel advances when Next is clicked', async ({ page }) => {
|
||||
const reviewsSection = page.locator('section').filter({
|
||||
has: page.getByRole('heading', { level: 2, name: REVIEWS_HEADING })
|
||||
})
|
||||
const track = reviewsSection.getByRole('article').first().locator('..')
|
||||
const nextButton = reviewsSection.getByRole('button', { name: 'Next' })
|
||||
await nextButton.scrollIntoViewIfNeeded()
|
||||
|
||||
const startScroll = await track.evaluate((el) => el.scrollLeft)
|
||||
await nextButton.click()
|
||||
await expect
|
||||
.poll(() => track.evaluate((el) => el.scrollLeft))
|
||||
.toBeGreaterThan(startScroll)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Seedance 2.5 page — mobile @mobile', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(PATH)
|
||||
})
|
||||
|
||||
test('renders the hero heading at narrow viewports', async ({ page }) => {
|
||||
await expect(
|
||||
page.getByRole('heading', { level: 1, name: HERO_TITLE })
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('closing CTA heading stays within the viewport width', async ({
|
||||
page
|
||||
}) => {
|
||||
const ctaHeading = page.getByRole('heading', {
|
||||
level: 2,
|
||||
name: CTA_HEADING
|
||||
})
|
||||
await ctaHeading.scrollIntoViewIfNeeded()
|
||||
await expect(ctaHeading).toBeVisible()
|
||||
|
||||
const box = await ctaHeading.boundingBox()
|
||||
expect(box, 'CTA heading bounding box').not.toBeNull()
|
||||
expect(box!.x + box!.width).toBeLessThanOrEqual(
|
||||
page.viewportSize()!.width + 1
|
||||
)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
4
apps/website/public/icons/comfyicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="275" height="275" viewBox="0 0 275 275" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="274.66" height="274.66" rx="63.5555" fill="#211927"/>
|
||||
<path d="M177.456 174.409C177.713 173.538 177.854 172.621 177.854 171.656C177.854 166.313 173.546 161.983 168.232 161.983H125.108C122.791 162.006 120.894 160.124 120.894 157.794C120.894 157.37 120.965 156.97 121.058 156.594L132.67 115.926C133.162 114.137 134.801 112.819 136.72 112.819L180.008 112.772C189.138 112.772 196.84 106.582 199.158 98.1335L205.666 75.4696C205.877 74.6695 205.994 73.7987 205.994 72.9279C205.994 67.6091 201.71 63.3022 196.419 63.3022H144.048C134.965 63.3022 127.286 69.4448 124.921 77.7996L120.52 93.2618C120.005 95.0269 118.389 96.3213 116.47 96.3213H103.898C94.8846 96.3213 87.276 102.346 84.8412 110.607L69.0152 166.172C68.7811 166.996 68.6641 167.89 68.6641 168.784C68.6641 174.127 72.9717 178.457 78.2861 178.457H90.6472C92.9649 178.457 94.8612 180.34 94.8612 182.693C94.8612 183.093 94.8144 183.494 94.6973 183.87L90.3194 199.191C90.1087 200.015 89.9683 200.862 89.9683 201.733C89.9683 207.052 94.2525 211.359 99.5434 211.359L151.938 211.312C161.045 211.312 168.724 205.145 171.065 196.744L177.432 174.433L177.456 174.409Z" fill="#F2FF59"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
apps/website/public/videos/mcp/setup-claude-desktop-v1.mp4
Normal file
BIN
apps/website/public/videos/mcp/setup-codex-oauth-v1.mp4
Normal file
@@ -16,9 +16,8 @@ type TermsLink = {
|
||||
href: string
|
||||
}
|
||||
|
||||
const { heading, subtitle, primaryCta, secondaryCta, termsLink } = defineProps<{
|
||||
const { heading, primaryCta, secondaryCta, termsLink } = defineProps<{
|
||||
heading: string
|
||||
subtitle?: string
|
||||
primaryCta: Cta
|
||||
secondaryCta?: Cta
|
||||
termsLink?: TermsLink
|
||||
@@ -35,13 +34,6 @@ const { heading, subtitle, primaryCta, secondaryCta, termsLink } = defineProps<{
|
||||
{{ heading }}
|
||||
</h2>
|
||||
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="mt-4 max-w-2xl text-base/relaxed font-light text-primary-comfy-canvas/80 lg:text-lg/relaxed"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
|
||||
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
|
||||
<Button
|
||||
as="a"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { reactive, watch } from 'vue'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
|
||||
type Faq = { id: string; question: string; answer: string }
|
||||
|
||||
@@ -9,6 +9,31 @@ const { faqs } = defineProps<{
|
||||
faqs: readonly Faq[]
|
||||
}>()
|
||||
|
||||
type AnswerPart = { type: 'text' | 'link'; value: string }
|
||||
|
||||
function parseAnswer(answer: string): AnswerPart[] {
|
||||
const urlPattern = /https?:\/\/[\w\-./?=&#%~:@+,;]+/g
|
||||
const parts: AnswerPart[] = []
|
||||
let lastIndex = 0
|
||||
for (const match of answer.matchAll(urlPattern)) {
|
||||
const start = match.index ?? 0
|
||||
const url = match[0].replace(/[.,;:]+$/, '')
|
||||
if (start > lastIndex) {
|
||||
parts.push({ type: 'text', value: answer.slice(lastIndex, start) })
|
||||
}
|
||||
parts.push({ type: 'link', value: url })
|
||||
lastIndex = start + url.length
|
||||
}
|
||||
if (lastIndex < answer.length) {
|
||||
parts.push({ type: 'text', value: answer.slice(lastIndex) })
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
const parsedFaqs = computed(() =>
|
||||
faqs.map((faq) => ({ ...faq, answerParts: parseAnswer(faq.answer) }))
|
||||
)
|
||||
|
||||
const expanded = reactive<boolean[]>(faqs.map(() => false))
|
||||
|
||||
watch(
|
||||
@@ -40,7 +65,7 @@ function toggle(index: number) {
|
||||
<!-- Right FAQ list -->
|
||||
<div class="flex-1">
|
||||
<div
|
||||
v-for="(faq, index) in faqs"
|
||||
v-for="(faq, index) in parsedFaqs"
|
||||
:key="faq.id"
|
||||
class="border-b border-primary-comfy-canvas/20"
|
||||
>
|
||||
@@ -83,8 +108,23 @@ function toggle(index: number) {
|
||||
:aria-labelledby="`faq-trigger-${faq.id}`"
|
||||
class="pb-6"
|
||||
>
|
||||
<p class="text-sm whitespace-pre-line text-primary-comfy-canvas/70">
|
||||
{{ faq.answer }}
|
||||
<p
|
||||
class="text-sm wrap-break-word whitespace-pre-line text-primary-comfy-canvas/70"
|
||||
>
|
||||
<template
|
||||
v-for="(part, partIndex) in faq.answerParts"
|
||||
:key="partIndex"
|
||||
>
|
||||
<a
|
||||
v-if="part.type === 'link'"
|
||||
:href="part.value"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-comfy-yellow focus-visible:ring-primary-comfy-yellow/50 rounded-sm underline underline-offset-2 transition-opacity hover:opacity-70 focus-visible:ring-2 focus-visible:outline-none"
|
||||
>{{ part.value }}</a
|
||||
>
|
||||
<template v-else>{{ part.value }}</template>
|
||||
</template>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import CopyableField from '@/components/ui/copyable-field/CopyableField.vue'
|
||||
|
||||
import SectionHeader from '../common/SectionHeader.vue'
|
||||
|
||||
type CardAction =
|
||||
| {
|
||||
type: 'link'
|
||||
label: string
|
||||
href: string
|
||||
target?: '_blank'
|
||||
icon?: Component
|
||||
variant?: 'default' | 'outline'
|
||||
}
|
||||
| { type: 'code'; value: string }
|
||||
|
||||
export interface FeatureCard {
|
||||
id: string
|
||||
label?: string
|
||||
title: string
|
||||
description: string
|
||||
action?: CardAction
|
||||
}
|
||||
|
||||
type ColumnCount = 2 | 3 | 4
|
||||
|
||||
const {
|
||||
cards,
|
||||
columns = 3,
|
||||
copiedLabel,
|
||||
copyLabel,
|
||||
eyebrow,
|
||||
heading,
|
||||
subtitle
|
||||
} = defineProps<{
|
||||
cards: readonly FeatureCard[]
|
||||
columns?: ColumnCount
|
||||
copiedLabel?: string
|
||||
copyLabel?: string
|
||||
eyebrow?: string
|
||||
heading: string
|
||||
subtitle?: string
|
||||
}>()
|
||||
|
||||
const columnClass: Record<ColumnCount, string> = {
|
||||
2: 'lg:grid-cols-2',
|
||||
3: 'lg:grid-cols-3',
|
||||
4: 'lg:grid-cols-4'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
|
||||
<SectionHeader max-width="xl" :label="eyebrow" align="start">
|
||||
{{ heading }}
|
||||
<template v-if="subtitle" #subtitle>
|
||||
<p class="mt-4 max-w-xl text-sm text-smoke-700 lg:text-base">
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</template>
|
||||
</SectionHeader>
|
||||
|
||||
<div :class="cn('mt-16 grid grid-cols-1 gap-6', columnClass[columns])">
|
||||
<div
|
||||
v-for="card in cards"
|
||||
:key="card.id"
|
||||
class="bg-transparency-white-t4 flex flex-col rounded-3xl p-6 lg:p-8"
|
||||
>
|
||||
<p
|
||||
v-if="card.label"
|
||||
class="text-primary-comfy-yellow text-xs font-bold tracking-widest uppercase"
|
||||
>
|
||||
{{ card.label }}
|
||||
</p>
|
||||
<h3
|
||||
:class="
|
||||
cn(
|
||||
'text-xl font-light text-primary-comfy-canvas lg:text-2xl',
|
||||
card.label && 'mt-3'
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ card.title }}
|
||||
</h3>
|
||||
<p class="mt-3 text-sm text-smoke-700">
|
||||
{{ card.description }}
|
||||
</p>
|
||||
|
||||
<div v-if="card.action" class="mt-6">
|
||||
<Button
|
||||
v-if="card.action.type === 'link'"
|
||||
as="a"
|
||||
:href="card.action.href"
|
||||
:target="card.action.target"
|
||||
:rel="
|
||||
card.action.target === '_blank'
|
||||
? 'noopener noreferrer'
|
||||
: undefined
|
||||
"
|
||||
:variant="card.action.variant ?? 'outline'"
|
||||
:append-icon="card.action.icon"
|
||||
>
|
||||
{{ card.action.label }}
|
||||
</Button>
|
||||
<CopyableField
|
||||
v-else
|
||||
:value="card.action.value"
|
||||
:copy-label="copyLabel"
|
||||
:copied-label="copiedLabel"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -8,7 +8,7 @@ import VideoPlayer from '../common/VideoPlayer.vue'
|
||||
import type { VideoTrack } from '../common/VideoPlayer.vue'
|
||||
|
||||
type RowMedia =
|
||||
| { type: 'image'; src: string; alt?: string }
|
||||
| { type: 'image'; src: string; alt?: string; fit?: 'cover' | 'contain' }
|
||||
| {
|
||||
type: 'video'
|
||||
src: string
|
||||
@@ -20,6 +20,7 @@ type RowMedia =
|
||||
loop?: boolean
|
||||
minimal?: boolean
|
||||
hideControls?: boolean
|
||||
fit?: 'cover' | 'contain'
|
||||
}
|
||||
|
||||
export interface FeatureRow {
|
||||
@@ -58,7 +59,7 @@ const {
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'order-2 flex flex-col justify-center gap-4 p-6 lg:w-1/2 lg:p-12',
|
||||
'order-2 flex flex-col justify-center gap-4 p-6 lg:flex-1 lg:p-12',
|
||||
i % 2 === 0 ? 'lg:order-1' : 'lg:order-2'
|
||||
)
|
||||
"
|
||||
@@ -72,10 +73,11 @@ const {
|
||||
</div>
|
||||
|
||||
<!-- Media: image or video -->
|
||||
<!-- 620/364 and w-155 (620px) match the card media asset dimensions -->
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'order-1 flex lg:w-1/2',
|
||||
'relative order-1 aspect-620/364 w-full lg:w-155 lg:shrink-0',
|
||||
i % 2 === 0 ? 'lg:order-2' : 'lg:order-1'
|
||||
)
|
||||
"
|
||||
@@ -86,7 +88,12 @@ const {
|
||||
:alt="row.media.alt ?? row.title"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="aspect-4/3 w-full rounded-4xl object-cover"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-0 size-full rounded-4xl',
|
||||
row.media.fit === 'contain' ? 'object-contain' : 'object-cover'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<VideoPlayer
|
||||
v-else
|
||||
@@ -99,7 +106,13 @@ const {
|
||||
:loop="row.media.loop"
|
||||
:minimal="row.media.minimal"
|
||||
:hide-controls="row.media.hideControls"
|
||||
class="w-full"
|
||||
:fit="row.media.fit"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-0 size-full',
|
||||
row.media.fit === 'contain' && 'bg-transparent'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</GlassCard>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
const { resources } = defineProps<{
|
||||
resources: { label: string; href: string; display: string }[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="flex flex-col gap-3 text-base font-light lg:text-lg">
|
||||
<li v-for="resource in resources" :key="resource.href">
|
||||
<span class="text-primary-comfy-canvas">{{ resource.label }}</span>
|
||||
<span class="text-primary-warm-gray"> — </span>
|
||||
<a
|
||||
:href="resource.href"
|
||||
class="text-primary-comfy-yellow underline underline-offset-4 hover:no-underline"
|
||||
>
|
||||
{{ resource.display }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
type Crumb = { label: string; href?: string }
|
||||
|
||||
const { crumbs, updated } = defineProps<{
|
||||
crumbs: readonly Crumb[]
|
||||
updated?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
class="max-w-9xl mx-auto flex items-center justify-between gap-4 px-6 py-3 lg:px-20"
|
||||
>
|
||||
<ol
|
||||
class="text-primary-warm-gray flex flex-wrap items-center gap-2 text-xs tracking-wide uppercase"
|
||||
>
|
||||
<li
|
||||
v-for="(crumb, i) in crumbs"
|
||||
:key="`${i}-${crumb.label}`"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<span v-if="i > 0" aria-hidden="true" class="text-primary-warm-gray/60">
|
||||
/
|
||||
</span>
|
||||
<a
|
||||
v-if="crumb.href"
|
||||
:href="crumb.href"
|
||||
class="transition-colors hover:text-primary-comfy-canvas"
|
||||
>
|
||||
{{ crumb.label }}
|
||||
</a>
|
||||
<span v-else aria-current="page" class="text-primary-comfy-canvas">
|
||||
{{ crumb.label }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<p
|
||||
v-if="updated"
|
||||
class="text-primary-warm-gray shrink-0 text-xs tracking-wide uppercase"
|
||||
>
|
||||
{{ updated }}
|
||||
</p>
|
||||
</nav>
|
||||
</template>
|
||||
@@ -16,6 +16,7 @@ import type { NavItem } from '../../../data/mainNavigation'
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import NavColumn from './NavColumn.vue'
|
||||
import NavFeaturedCard from './NavFeaturedCard.vue'
|
||||
import NewBadge from './NewBadge.vue'
|
||||
|
||||
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
|
||||
const mainNavigation = getMainNavigation(locale)
|
||||
@@ -42,7 +43,10 @@ function isNavItemActive(navItem: NavItem, path: string): boolean {
|
||||
<NavigationMenuTrigger
|
||||
:active="isNavItemActive(navItem, currentPath)"
|
||||
>
|
||||
{{ navItem.label }}
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span class="ppformula-text-center">{{ navItem.label }}</span>
|
||||
<NewBadge v-if="navItem.badge" :locale="locale" size="xxs" />
|
||||
</span>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent class="w-auto" data-testid="nav-dropdown">
|
||||
<ul class="flex w-max gap-16">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { lockScroll, unlockScroll } from '../../../composables/scrollLock'
|
||||
import type { Locale } from '../../../i18n/translations.ts'
|
||||
import { t } from '../../../i18n/translations.ts'
|
||||
import NavLinkContent from './NavLinkContent.vue'
|
||||
import NewBadge from './NewBadge.vue'
|
||||
import Sheet from '@/components/ui/sheet/Sheet.vue'
|
||||
import SheetContent from '@/components/ui/sheet/SheetContent.vue'
|
||||
import SheetDescription from '@/components/ui/sheet/SheetDescription.vue'
|
||||
@@ -96,7 +97,8 @@ onUnmounted(() => {
|
||||
:href="item.columns ? undefined : item.href"
|
||||
@click="item.columns && (activeSection = item.label)"
|
||||
>
|
||||
{{ item.label }}
|
||||
<span class="ppformula-text-center">{{ item.label }}</span>
|
||||
<NewBadge v-if="item.badge" :locale="locale" size="xxs" />
|
||||
<template #append>
|
||||
<ChevronRight class="size-7" />
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
import { ArrowUpRight } from '@lucide/vue'
|
||||
|
||||
import type { NavColumnItem } from '../../../data/mainNavigation'
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import { t } from '../../../i18n/translations'
|
||||
import NewBadge from './NewBadge.vue'
|
||||
|
||||
defineProps<{ item: NavColumnItem; locale: Locale }>()
|
||||
</script>
|
||||
@@ -12,9 +11,7 @@ defineProps<{ item: NavColumnItem; locale: Locale }>()
|
||||
<template>
|
||||
<span class="flex items-center gap-2">
|
||||
<span class="ppformula-text-center">{{ item.label }}</span>
|
||||
<Badge v-if="item.badge" size="xs" variant="accent">
|
||||
{{ t('nav.badgeNew', locale) }}
|
||||
</Badge>
|
||||
<NewBadge v-if="item.badge" :locale="locale" size="xs" />
|
||||
<ArrowUpRight
|
||||
v-if="item.external"
|
||||
class="text-primary-comfy-yellow size-4"
|
||||
|
||||
15
apps/website/src/components/common/HeaderMain/NewBadge.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import Badge from '@/components/ui/badge/Badge.vue'
|
||||
|
||||
import type { BadgeVariants } from '@/components/ui/badge'
|
||||
import type { Locale } from '../../../i18n/translations'
|
||||
import { t } from '../../../i18n/translations'
|
||||
|
||||
defineProps<{ locale: Locale; size?: BadgeVariants['size'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Badge :size="size" variant="accent">
|
||||
{{ t('nav.badgeNew', locale) }}
|
||||
</Badge>
|
||||
</template>
|
||||
@@ -1,66 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { useScroll } from '@vueuse/core'
|
||||
import { computed, useTemplateRef } from 'vue'
|
||||
|
||||
const { prevLabel, nextLabel } = defineProps<{
|
||||
prevLabel: string
|
||||
nextLabel: string
|
||||
}>()
|
||||
|
||||
const track = useTemplateRef<HTMLElement>('track')
|
||||
const { x } = useScroll(track)
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
const el = track.value
|
||||
if (!el) return '0%'
|
||||
const max = el.scrollWidth - el.clientWidth
|
||||
return `${(max > 0 ? x.value / max : 0) * 100}%`
|
||||
})
|
||||
|
||||
function scroll(direction: -1 | 1) {
|
||||
const el = track.value
|
||||
if (!el) return
|
||||
el.scrollBy({ left: direction * el.clientWidth, behavior: 'smooth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-10">
|
||||
<div
|
||||
ref="track"
|
||||
class="flex snap-x snap-mandatory scrollbar-none gap-6 overflow-x-auto"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="h-1 flex-1 rounded-full bg-white/20">
|
||||
<div
|
||||
class="bg-primary-comfy-yellow h-full rounded-full"
|
||||
:style="{ width: progressPercent }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="prevLabel"
|
||||
class="flex size-10 items-center justify-center rounded-full border border-white/20 text-white/60 transition-colors hover:border-white/40"
|
||||
@click="scroll(-1)"
|
||||
>
|
||||
<img
|
||||
src="/icons/arrow-right.svg"
|
||||
alt=""
|
||||
class="size-3 rotate-180 opacity-60 invert"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:aria-label="nextLabel"
|
||||
class="bg-primary-comfy-yellow flex size-10 items-center justify-center rounded-full transition-opacity hover:opacity-90"
|
||||
@click="scroll(1)"
|
||||
>
|
||||
<img src="/icons/arrow-right.svg" alt="" class="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
const { class: className } = defineProps<{ class?: HTMLAttributes['class'] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="
|
||||
cn(
|
||||
'inline-flex h-8 -skew-x-12 items-center justify-center rounded-lg px-3.5',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
<span
|
||||
class="ppformula-text-center font-formula skew-x-12 text-sm leading-none font-extrabold tracking-wider uppercase"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -85,6 +85,7 @@ const companyColumn: { title: string; links: FooterLink[] } = {
|
||||
links: [
|
||||
{ label: t('footer.about', locale), href: routes.about },
|
||||
{ label: t('nav.careers', locale), href: routes.careers },
|
||||
{ label: t('nav.brand', locale), href: routes.brand },
|
||||
{ label: t('footer.termsOfService', locale), href: routes.termsOfService },
|
||||
{ label: t('footer.enterpriseMsa', locale), href: routes.enterpriseMsa },
|
||||
{ label: t('footer.privacyPolicy', locale), href: routes.privacyPolicy }
|
||||
@@ -175,10 +176,7 @@ const contactColumn: { title: string; links: FooterLink[] } = {
|
||||
</div>
|
||||
|
||||
<!-- Logo -->
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="pointer-events-none size-52 opacity-80 lg:mt-28"
|
||||
/>
|
||||
<canvas ref="canvasRef" class="pointer-events-none size-52 lg:mt-28" />
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
whenever
|
||||
} from '@vueuse/core'
|
||||
import { computed, shallowRef, useTemplateRef, watch } from 'vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { t } from '../../i18n/translations'
|
||||
import type { Locale } from '../../i18n/translations'
|
||||
@@ -30,7 +31,10 @@ const {
|
||||
autoplay = false,
|
||||
loop = false,
|
||||
minimal = false,
|
||||
hideControls = false
|
||||
hideControls = false,
|
||||
fit = 'cover',
|
||||
ariaLabel,
|
||||
class: className
|
||||
} = defineProps<{
|
||||
locale?: Locale
|
||||
src?: string
|
||||
@@ -40,6 +44,9 @@ const {
|
||||
loop?: boolean
|
||||
minimal?: boolean
|
||||
hideControls?: boolean
|
||||
fit?: 'cover' | 'contain'
|
||||
ariaLabel?: string
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
|
||||
const playerEl = useTemplateRef<HTMLDivElement>('playerEl')
|
||||
@@ -88,6 +95,28 @@ watch(videoEl, syncNativeDuration)
|
||||
useEventListener(videoEl, 'loadedmetadata', syncNativeDuration)
|
||||
useEventListener(videoEl, 'durationchange', syncNativeDuration)
|
||||
|
||||
// The muted attribute only sets defaultMuted, so SSR-rendered autoplay
|
||||
// videos count as unmuted and get blocked; force the property and kick
|
||||
// playback. Scoped to hideControls (decorative) clips so chrome-visible
|
||||
// consumers keep native semantics. flush: 'post' guarantees this runs
|
||||
// after useMediaControls' internal muted watcher on the same source.
|
||||
watch(
|
||||
[videoEl, () => src],
|
||||
([el]) => {
|
||||
if (!el || !autoplay || !hideControls) return
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||
el.pause()
|
||||
return
|
||||
}
|
||||
el.muted = true
|
||||
el.play().catch((error: unknown) => {
|
||||
if (error instanceof Error && error.name === 'AbortError') return
|
||||
console.warn('VideoPlayer autoplay failed', error)
|
||||
})
|
||||
},
|
||||
{ flush: 'post' }
|
||||
)
|
||||
|
||||
const effectiveDuration = computed(() => duration.value || nativeDuration.value)
|
||||
|
||||
// Scrubber (modeled after VueUse demo Scrubber.vue)
|
||||
@@ -189,7 +218,12 @@ function toggleFullscreen() {
|
||||
<template>
|
||||
<div
|
||||
ref="playerEl"
|
||||
class="relative aspect-video overflow-hidden rounded-4xl border border-white/10 bg-black"
|
||||
:class="
|
||||
cn(
|
||||
'relative aspect-video overflow-hidden rounded-4xl border border-white/10 bg-black',
|
||||
className
|
||||
)
|
||||
"
|
||||
@pointermove="showControls"
|
||||
@pointerdown="showControls"
|
||||
@focusin="showControls"
|
||||
@@ -197,7 +231,10 @@ function toggleFullscreen() {
|
||||
<video
|
||||
v-if="src"
|
||||
ref="videoEl"
|
||||
class="size-full object-cover"
|
||||
:aria-label="ariaLabel"
|
||||
:class="
|
||||
cn('size-full', fit === 'contain' ? 'object-contain' : 'object-cover')
|
||||
"
|
||||
:src
|
||||
:poster
|
||||
:preload="autoplay ? 'auto' : 'metadata'"
|
||||
|
||||
@@ -8,11 +8,9 @@ export const brandButtonVariants = cva({
|
||||
solid:
|
||||
'bg-primary-comfy-yellow text-primary-comfy-ink transition-opacity hover:opacity-90',
|
||||
outline:
|
||||
'border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow border hover:text-primary-comfy-ink',
|
||||
'border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink border',
|
||||
'outline-dark':
|
||||
'hover:text-primary-comfy-yellow border-2 border-primary-comfy-ink text-primary-comfy-ink uppercase hover:bg-primary-comfy-ink',
|
||||
inverse:
|
||||
'text-primary-comfy-yellow bg-primary-comfy-ink transition-opacity hover:opacity-90'
|
||||
'border-primary-comfy-ink text-primary-comfy-ink hover:bg-primary-comfy-ink hover:text-primary-comfy-yellow border-2 uppercase'
|
||||
},
|
||||
size: {
|
||||
xs: 'rounded-2xl px-6 py-3 text-xs font-bold',
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import Button from '../ui/button/Button.vue'
|
||||
|
||||
const { href, label } = defineProps<{
|
||||
href: string
|
||||
label: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-2 flex justify-center">
|
||||
<Button as="a" :href variant="default" size="lg">
|
||||
{{ label }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,12 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
const { title } = defineProps<{ title: string }>()
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
const { title, class: className } = defineProps<{
|
||||
title: string
|
||||
class?: HTMLAttributes['class']
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section
|
||||
class="flex items-center justify-center px-6 pt-20 pb-16 lg:pt-32 lg:pb-24"
|
||||
>
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light lg:text-6xl">
|
||||
<h1
|
||||
:class="
|
||||
cn(
|
||||
'text-4xl font-light text-primary-comfy-canvas lg:text-6xl',
|
||||
className
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
@@ -6,14 +6,15 @@ export const badgeVariants = cva({
|
||||
variants: {
|
||||
size: {
|
||||
md: 'px-4 py-1 text-xs',
|
||||
xs: 'px-2 py-0.5 text-[9px]'
|
||||
xs: 'px-2 py-0.5 text-[9px]',
|
||||
xxs: 'px-1.5 py-px text-[8px]'
|
||||
},
|
||||
variant: {
|
||||
default: 'bg-transparency-ink-t80',
|
||||
subtle: 'bg-transparency-white-t4 text-primary-comfy-canvas',
|
||||
category: 'text-primary-comfy-yellow px-0 font-semibold uppercase',
|
||||
accent:
|
||||
'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent px-2 py-0.5 text-[9px] font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
|
||||
'before:bg-primary-comfy-yellow relative isolate overflow-visible rounded-none bg-transparent font-bold tracking-wide text-primary-comfy-ink uppercase before:absolute before:inset-0 before:-z-10 before:-skew-x-12 before:rounded-sm'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -20,6 +20,8 @@ export const buttonVariants = cva(
|
||||
link: "text-primary-comfy-yellow h-auto justify-start px-0 py-1 text-base uppercase hover:opacity-90 [&_svg:not([class*='size-'])]:size-6",
|
||||
underlineLink:
|
||||
"text-primary-comfy-yellow relative h-auto justify-start px-0 py-1 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-full after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-200 hover:opacity-90 hover:after:scale-x-100 [&_svg:not([class*='size-'])]:size-6",
|
||||
inline:
|
||||
'text-primary-comfy-yellow inline h-auto rounded-none p-0 align-baseline text-sm font-normal tracking-normal whitespace-normal hover:opacity-90 [&>span]:top-0 [&>span]:underline',
|
||||
nav: 'text-primary-warm-white hover:text-primary-comfy-yellow h-auto justify-between px-0 py-1 text-start text-2xl font-medium',
|
||||
navMuted:
|
||||
'hover:text-primary-comfy-yellow h-auto w-full justify-between px-0 py-1 text-start text-2xl font-medium text-primary-comfy-canvas uppercase'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getRoutes, localizeHref } from './routes'
|
||||
import { localizeHref } from './routes'
|
||||
|
||||
describe('localizeHref', () => {
|
||||
it('prefixes an internal path for a non-default locale', () => {
|
||||
@@ -21,13 +21,3 @@ describe('localizeHref', () => {
|
||||
expect(localizeHref('/terms-of-service', 'zh-CN')).toBe('/terms-of-service')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getRoutes seedance', () => {
|
||||
it('serves the seedance page at its canonical path for en', () => {
|
||||
expect(getRoutes('en').seedance).toBe('/seedance-2.5')
|
||||
})
|
||||
|
||||
it('serves a localized seedance path for zh-CN', () => {
|
||||
expect(getRoutes('zh-CN').seedance).toBe('/zh-CN/seedance-2.5')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ const baseRoutes = {
|
||||
contact: '/contact',
|
||||
models: '/p/supported-models',
|
||||
mcp: '/mcp',
|
||||
seedance: '/seedance-2.5'
|
||||
brand: '/brand'
|
||||
} as const
|
||||
|
||||
type Routes = typeof baseRoutes
|
||||
@@ -88,6 +88,7 @@ export const externalLinks = {
|
||||
githubInstall: 'https://github.com/Comfy-Org/ComfyUI#installing',
|
||||
instagram: 'https://www.instagram.com/comfyui/',
|
||||
linkedin: 'https://www.linkedin.com/company/comfyui',
|
||||
mcpEndpoint: 'https://cloud.comfy.org/mcp',
|
||||
mcpSkills: 'https://github.com/Comfy-Org/comfy-skills',
|
||||
platform: 'https://platform.comfy.org',
|
||||
platformUsage: 'https://platform.comfy.org/profile/usage',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { LocalizedText } from '../i18n/translations'
|
||||
|
||||
import { BRAND_ASSETS_ZIP } from './brandAssets'
|
||||
|
||||
interface AffiliateBrandAsset {
|
||||
id: string
|
||||
title: LocalizedText
|
||||
@@ -7,9 +9,6 @@ interface AffiliateBrandAsset {
|
||||
preview: string
|
||||
}
|
||||
|
||||
const BRAND_ASSETS_ZIP =
|
||||
'https://media.comfy.org/website/comfy-org-brand-assets.zip'
|
||||
|
||||
export const affiliateBrandAssets: readonly AffiliateBrandAsset[] = [
|
||||
{
|
||||
id: 'core-logo',
|
||||
|
||||
9
apps/website/src/data/brandAssets.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Shared brand download URLs served from the media bucket, used by both the
|
||||
// affiliate page and the brand portal.
|
||||
export const BRAND_ASSETS_ZIP =
|
||||
'https://media.comfy.org/website/comfy-org-brand-assets.zip'
|
||||
|
||||
// Brand guidelines live in Google Drive, shared to Comfy Org only, so Google
|
||||
// enforces the comfy.org sign-in. Opened in a new tab rather than downloaded.
|
||||
export const BRAND_GUIDELINES_PDF =
|
||||
'https://drive.google.com/file/d/1EDt03JTfF_nbbY_H2n67aaUj6k11v3bS/view'
|
||||
91
apps/website/src/data/brandColors.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
interface BrandColor {
|
||||
name: string
|
||||
hex: string
|
||||
rgb: string
|
||||
hsl: string
|
||||
cmyk: string
|
||||
swatchClass: string
|
||||
textClass: string
|
||||
wide?: boolean
|
||||
border?: boolean
|
||||
}
|
||||
|
||||
export const brandColors: readonly BrandColor[] = [
|
||||
{
|
||||
name: 'Comfy Yellow',
|
||||
hex: '#F2FF59',
|
||||
rgb: '242, 255, 89',
|
||||
hsl: '65, 100, 67',
|
||||
cmyk: '5, 0, 65, 0',
|
||||
swatchClass: 'bg-primary-comfy-yellow',
|
||||
textClass: 'text-primary-comfy-ink',
|
||||
wide: true
|
||||
},
|
||||
{
|
||||
name: 'Comfy Ink',
|
||||
hex: '#211927',
|
||||
rgb: '33, 25, 39',
|
||||
hsl: '274, 22, 13',
|
||||
cmyk: '15, 36, 0, 85',
|
||||
swatchClass: 'bg-primary-comfy-ink',
|
||||
textClass: 'text-primary-warm-white',
|
||||
border: true
|
||||
},
|
||||
{
|
||||
name: 'Comfy Canvas',
|
||||
hex: '#C2BFB9',
|
||||
rgb: '194, 191, 185',
|
||||
hsl: '40, 7, 74',
|
||||
cmyk: '0, 2, 5, 24',
|
||||
swatchClass: 'bg-primary-comfy-canvas',
|
||||
textClass: 'text-primary-comfy-ink'
|
||||
},
|
||||
{
|
||||
name: 'Comfy Plum',
|
||||
hex: '#49378B',
|
||||
rgb: '73, 55, 139',
|
||||
hsl: '253, 43, 38',
|
||||
cmyk: '47, 60, 0, 45',
|
||||
swatchClass: 'bg-primary-comfy-plum',
|
||||
textClass: 'text-primary-comfy-canvas'
|
||||
},
|
||||
{
|
||||
name: 'Warm White',
|
||||
hex: '#F0EFED',
|
||||
rgb: '240, 239, 237',
|
||||
hsl: '40, 9, 94',
|
||||
cmyk: '0, 0, 1, 6',
|
||||
swatchClass: 'bg-primary-warm-white',
|
||||
textClass: 'text-primary-comfy-ink',
|
||||
wide: true
|
||||
},
|
||||
{
|
||||
name: 'Warm Gray',
|
||||
hex: '#7E7C78',
|
||||
rgb: '126, 124, 120',
|
||||
hsl: '40, 2, 48',
|
||||
cmyk: '0, 2, 5, 51',
|
||||
swatchClass: 'bg-primary-warm-gray',
|
||||
textClass: 'text-primary-warm-white',
|
||||
border: true
|
||||
},
|
||||
{
|
||||
name: 'Cool Gray',
|
||||
hex: '#3C3C3C',
|
||||
rgb: '60, 60, 60',
|
||||
hsl: '0, 0, 24',
|
||||
cmyk: '0, 0, 0, 76',
|
||||
swatchClass: 'bg-secondary-cool-gray',
|
||||
textClass: 'text-primary-warm-white',
|
||||
border: true
|
||||
},
|
||||
{
|
||||
name: 'Mauve',
|
||||
hex: '#4D3762',
|
||||
rgb: '77, 55, 98',
|
||||
hsl: '271, 28, 30',
|
||||
cmyk: '21, 44, 0, 62',
|
||||
swatchClass: 'bg-secondary-mauve',
|
||||
textClass: 'text-primary-warm-white'
|
||||
}
|
||||
] as const
|
||||
@@ -30,15 +30,23 @@ export type NavItem =
|
||||
label: string
|
||||
columns: NavColumn[]
|
||||
featured?: NavFeatured
|
||||
badge?: 'new'
|
||||
href?: never
|
||||
}
|
||||
| { label: string; href: string; columns?: never; featured?: never }
|
||||
| {
|
||||
label: string
|
||||
href: string
|
||||
badge?: 'new'
|
||||
columns?: never
|
||||
featured?: never
|
||||
}
|
||||
|
||||
export function getMainNavigation(locale: Locale): NavItem[] {
|
||||
const routes = getRoutes(locale)
|
||||
return [
|
||||
{
|
||||
label: t('nav.products', locale),
|
||||
badge: 'new',
|
||||
featured: {
|
||||
imageSrc: 'https://media.comfy.org/website/nav/mcp-card.webp',
|
||||
imageAlt: t('nav.featuredProductsAlt', locale),
|
||||
@@ -95,6 +103,7 @@ export function getMainNavigation(locale: Locale): NavItem[] {
|
||||
{ label: t('nav.pricing', locale), href: routes.cloudPricing },
|
||||
{
|
||||
label: t('nav.community', locale),
|
||||
badge: 'new',
|
||||
featured: {
|
||||
imageSrc: 'https://media.comfy.org/website/nav/featured-demo-card.jpg',
|
||||
imageAlt: t('nav.featuredCommunityAlt', locale),
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
import type { LocalizedText } from '../i18n/translations'
|
||||
|
||||
import { externalLinks } from '../config/routes'
|
||||
|
||||
// Placeholder media reused from existing hosted cloud assets so the page
|
||||
// renders end-to-end. Swap for the final Seedance renders and provider logos
|
||||
// from June before launch (CRE-145).
|
||||
const media = {
|
||||
hero: 'https://media.comfy.org/website/cloud/ai-models/seedance-20.webm',
|
||||
example1:
|
||||
'https://media.comfy.org/website/cloud/ai-models/nano-banana-pro.webp',
|
||||
example2:
|
||||
'https://media.comfy.org/website/cloud/ai-models/qwen-image-edit.webp',
|
||||
kling: 'https://media.comfy.org/website/cloud/ai-models/grok-video.webm',
|
||||
wan: 'https://media.comfy.org/website/cloud/ai-models/wan-22.webm',
|
||||
veo: 'https://media.comfy.org/website/cloud/ai-models/seedance-20.webm',
|
||||
hailuo: 'https://media.comfy.org/website/cloud/ai-models/gpt-image-2.webm'
|
||||
} as const
|
||||
|
||||
export const seedanceHeroVideo = media.hero
|
||||
|
||||
export interface SeedanceStep {
|
||||
id: string
|
||||
title: LocalizedText
|
||||
description: LocalizedText
|
||||
imageSrc: string
|
||||
}
|
||||
|
||||
export const seedanceSteps: readonly SeedanceStep[] = [
|
||||
{
|
||||
id: 'write-the-shot',
|
||||
title: { en: 'Write the shot', 'zh-CN': '写下镜头' },
|
||||
description: {
|
||||
en: 'Camera, subject, framing',
|
||||
'zh-CN': '镜头、主体、构图'
|
||||
},
|
||||
imageSrc: media.example1
|
||||
},
|
||||
{
|
||||
id: 'draft-on-wan',
|
||||
title: { en: 'Draft free on Wan 2.2', 'zh-CN': '用 Wan 2.2 免费打样' },
|
||||
description: {
|
||||
en: 'Same workflow, zero credits',
|
||||
'zh-CN': '相同工作流,零积分消耗'
|
||||
},
|
||||
imageSrc: media.example2
|
||||
},
|
||||
{
|
||||
id: 'switch-to-seedance',
|
||||
title: { en: 'Switch to Seedance 2.5', 'zh-CN': '切换到 Seedance 2.5' },
|
||||
description: {
|
||||
en: 'Final render, up to 4K',
|
||||
'zh-CN': '最终渲染,最高 4K'
|
||||
},
|
||||
imageSrc: media.example1
|
||||
}
|
||||
] as const
|
||||
|
||||
type SeedanceModelTier = 'free' | 'premium'
|
||||
|
||||
export interface SeedanceModel {
|
||||
id: string
|
||||
name: string
|
||||
tier: SeedanceModelTier
|
||||
note: LocalizedText
|
||||
description: LocalizedText
|
||||
imageSrc: string
|
||||
logoSrc: string
|
||||
logoAlt: string
|
||||
href: string
|
||||
}
|
||||
|
||||
// Six cards in a 2x3 grid to match the Figma (which repeats Veo/Hailuo as the
|
||||
// last two). June to supply the final roster + card art.
|
||||
export const seedanceModels: readonly SeedanceModel[] = [
|
||||
{
|
||||
id: 'kling-3',
|
||||
name: 'Kling 3.0',
|
||||
tier: 'free',
|
||||
note: { en: 'Included free', 'zh-CN': '免费包含' },
|
||||
description: {
|
||||
en: 'Precise camera and motion control.',
|
||||
'zh-CN': '精准的镜头与运动控制。'
|
||||
},
|
||||
imageSrc: media.kling,
|
||||
logoSrc: '/icons/ai-models/bytedance.svg',
|
||||
logoAlt: 'Kling',
|
||||
href: externalLinks.workflows
|
||||
},
|
||||
{
|
||||
id: 'wan-22',
|
||||
name: 'Wan 2.2',
|
||||
tier: 'free',
|
||||
note: { en: 'Included free', 'zh-CN': '免费包含' },
|
||||
description: {
|
||||
en: 'Draft free before you spend a credit.',
|
||||
'zh-CN': '在消耗积分前免费打样。'
|
||||
},
|
||||
imageSrc: media.wan,
|
||||
logoSrc: '/icons/ai-models/wan.svg',
|
||||
logoAlt: 'Wan',
|
||||
href: externalLinks.workflows
|
||||
},
|
||||
{
|
||||
id: 'veo-31',
|
||||
name: 'Veo 3.1',
|
||||
tier: 'premium',
|
||||
note: { en: 'Pay-as-you-go', 'zh-CN': '按量付费' },
|
||||
description: {
|
||||
en: 'Native audio with the frame.',
|
||||
'zh-CN': '画面自带原生音频。'
|
||||
},
|
||||
imageSrc: media.veo,
|
||||
logoSrc: '/icons/ai-models/gemini.svg',
|
||||
logoAlt: 'Veo',
|
||||
href: externalLinks.workflows
|
||||
},
|
||||
{
|
||||
id: 'hailuo',
|
||||
name: 'Hailuo',
|
||||
tier: 'premium',
|
||||
note: { en: 'Pay-as-you-go', 'zh-CN': '按量付费' },
|
||||
description: {
|
||||
en: 'Fast, expressive character motion.',
|
||||
'zh-CN': '快速且富有表现力的角色动作。'
|
||||
},
|
||||
imageSrc: media.hailuo,
|
||||
logoSrc: '/icons/ai-models/bytedance.svg',
|
||||
logoAlt: 'Hailuo',
|
||||
href: externalLinks.workflows
|
||||
},
|
||||
{
|
||||
id: 'veo-31-b',
|
||||
name: 'Veo 3.1',
|
||||
tier: 'premium',
|
||||
note: { en: 'Pay-as-you-go', 'zh-CN': '按量付费' },
|
||||
description: {
|
||||
en: 'Native audio with the frame.',
|
||||
'zh-CN': '画面自带原生音频。'
|
||||
},
|
||||
imageSrc: media.veo,
|
||||
logoSrc: '/icons/ai-models/gemini.svg',
|
||||
logoAlt: 'Veo',
|
||||
href: externalLinks.workflows
|
||||
},
|
||||
{
|
||||
id: 'hailuo-b',
|
||||
name: 'Hailuo',
|
||||
tier: 'premium',
|
||||
note: { en: 'Pay-as-you-go', 'zh-CN': '按量付费' },
|
||||
description: {
|
||||
en: 'Fast, expressive character motion.',
|
||||
'zh-CN': '快速且富有表现力的角色动作。'
|
||||
},
|
||||
imageSrc: media.hailuo,
|
||||
logoSrc: '/icons/ai-models/bytedance.svg',
|
||||
logoAlt: 'Hailuo',
|
||||
href: externalLinks.workflows
|
||||
}
|
||||
] as const
|
||||
|
||||
export interface SeedanceExample {
|
||||
id: string
|
||||
prompt: LocalizedText
|
||||
imageSrc: string
|
||||
imageAlt: LocalizedText
|
||||
}
|
||||
|
||||
// Prompt/result pairs are placeholder pending June's final examples.
|
||||
export const seedanceExamples: readonly SeedanceExample[] = [
|
||||
{
|
||||
id: 'dalmatian-loft',
|
||||
prompt: {
|
||||
en: 'A dalmatian stretches across a sunlit loft, slow push-in, shallow depth of field, warm morning light.',
|
||||
'zh-CN':
|
||||
'一只斑点狗在洒满阳光的阁楼里舒展身体,镜头缓缓推进,浅景深,温暖的晨光。'
|
||||
},
|
||||
imageSrc: media.example1,
|
||||
imageAlt: {
|
||||
en: 'Seedance render of a dalmatian in a sunlit loft',
|
||||
'zh-CN': 'Seedance 渲染的阁楼斑点狗画面'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'city-window',
|
||||
prompt: {
|
||||
en: 'Handheld shot drifting past a rain-streaked city window at dusk, neon reflections, cinematic grade.',
|
||||
'zh-CN':
|
||||
'黄昏时分,手持镜头掠过雨痕斑驳的城市窗户,霓虹倒影,电影级调色。'
|
||||
},
|
||||
imageSrc: media.example2,
|
||||
imageAlt: {
|
||||
en: 'Seedance render of a rain-streaked city window at dusk',
|
||||
'zh-CN': 'Seedance 渲染的黄昏雨窗画面'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'studio-portrait',
|
||||
prompt: {
|
||||
en: 'Slow orbit around a model on a seamless studio backdrop, soft key light, subtle rim, editorial motion.',
|
||||
'zh-CN':
|
||||
'镜头缓缓环绕纯色背景前的模特,柔和主光,微妙轮廓光,时尚编辑感的运镜。'
|
||||
},
|
||||
imageSrc: media.veo,
|
||||
imageAlt: {
|
||||
en: 'Seedance render of a studio portrait orbit',
|
||||
'zh-CN': 'Seedance 渲染的影棚人像环绕画面'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'street-tracking',
|
||||
prompt: {
|
||||
en: 'Tracking shot following a runner through a neon night market, shallow focus, motion blur, moody grade.',
|
||||
'zh-CN': '跟拍镜头追随一名跑者穿过霓虹夜市,浅焦,动态模糊,情绪化调色。'
|
||||
},
|
||||
imageSrc: media.kling,
|
||||
imageAlt: {
|
||||
en: 'Seedance render of a runner in a neon night market',
|
||||
'zh-CN': 'Seedance 渲染的霓虹夜市跑者画面'
|
||||
}
|
||||
}
|
||||
] as const
|
||||
|
||||
export interface SeedanceFaq {
|
||||
id: string
|
||||
question: LocalizedText
|
||||
answer: LocalizedText
|
||||
}
|
||||
|
||||
// FAQ questions follow the Figma; answers drafted from the page content
|
||||
// pending June/Nav sign-off (CRE-145).
|
||||
export const seedanceFaqs: readonly SeedanceFaq[] = [
|
||||
{
|
||||
id: 'cost-to-run',
|
||||
question: {
|
||||
en: 'How much does Seedance 2.5 cost to run?',
|
||||
'zh-CN': '运行 Seedance 2.5 的费用是多少?'
|
||||
},
|
||||
answer: {
|
||||
en: 'Seedance runs on pay-as-you-go credits or subscription credits — and you can draft the same shot free on Wan 2.2 before spending anything. No watermark either way.',
|
||||
'zh-CN':
|
||||
'Seedance 采用按量付费积分或订阅积分,你可以先在 Wan 2.2 上免费打样同一镜头,再决定是否消耗积分。两种方式都不带水印。'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'generate-4k',
|
||||
question: {
|
||||
en: 'Can Seedance 2.5 generate 4K video?',
|
||||
'zh-CN': 'Seedance 2.5 能生成 4K 视频吗?'
|
||||
},
|
||||
answer: {
|
||||
en: 'Yes. Seedance 2.5 renders up to 4K, with multi-shot sequences directed on the Comfy canvas.',
|
||||
'zh-CN':
|
||||
'可以。Seedance 2.5 最高支持 4K 渲染,并可在 Comfy 画布上执导多镜头序列。'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'text-or-image',
|
||||
question: {
|
||||
en: 'Text to video or image to video?',
|
||||
'zh-CN': '是文生视频还是图生视频?'
|
||||
},
|
||||
answer: {
|
||||
en: 'Both. Start from a text prompt or drop in a reference image; you direct the shot either way.',
|
||||
'zh-CN':
|
||||
'两者都支持。你可以从文本提示开始,也可以放入参考图像,无论哪种方式都由你执导镜头。'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'clip-length',
|
||||
question: {
|
||||
en: 'How long can clips be?',
|
||||
'zh-CN': '片段可以多长?'
|
||||
},
|
||||
answer: {
|
||||
en: 'Seedance 2.5 builds multi-shot sequences, so you stack shots and direct the full cut on the canvas rather than being capped at a single clip.',
|
||||
'zh-CN':
|
||||
'Seedance 2.5 可构建多镜头序列,你可以在画布上叠加镜头并执导完整成片,而不局限于单个片段。'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'commercial-use',
|
||||
question: {
|
||||
en: 'Can I use the videos commercially?',
|
||||
'zh-CN': '视频可以商用吗?'
|
||||
},
|
||||
answer: {
|
||||
en: 'Yes. Renders include commercial use and carry no watermark.',
|
||||
'zh-CN': '可以。渲染结果包含商业使用授权,且不带水印。'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'seedance-vs-kling',
|
||||
question: {
|
||||
en: 'Seedance vs Kling — which one for my shot?',
|
||||
'zh-CN': 'Seedance 与 Kling,我的镜头该选哪个?'
|
||||
},
|
||||
answer: {
|
||||
en: 'Kling gives precise camera and motion control; Seedance is built for multi-shot cinematic sequences. Draft on Wan 2.2, then finish on whichever the shot demands.',
|
||||
'zh-CN':
|
||||
'Kling 提供精准的镜头与运动控制,Seedance 专为多镜头电影级序列打造。先用 Wan 2.2 打样,再根据镜头需求选择合适的模型完成。'
|
||||
}
|
||||
}
|
||||
] as const
|
||||
|
||||
export interface SeedanceReview {
|
||||
id: string
|
||||
// Optional headline shown above the body (title-style card). When absent the
|
||||
// body renders as a larger pull quote (name + role card, e.g. Scott Belsky).
|
||||
title?: LocalizedText
|
||||
body: LocalizedText
|
||||
name: string
|
||||
role?: LocalizedText
|
||||
}
|
||||
|
||||
// Customer reviews for the "4+ million Comfy creators say" carousel. The Scott
|
||||
// Belsky quote is curated and already in the Figma. The remaining three are
|
||||
// clearly-marked placeholders pending Nav's hand-curated 5-star picks from G2
|
||||
// (https://www.g2.com/products/comfyui/reviews) — Nav will exclude some, so
|
||||
// these are swapped one-for-one, same as the placeholder media above (CRE-145).
|
||||
export const seedanceReviews: readonly SeedanceReview[] = [
|
||||
{
|
||||
id: 'scott-belsky',
|
||||
body: {
|
||||
en: 'Comfy has innovated a new and powerful ecosystem for creativity without compromising creative control. It has been amazing to watch technical artists and curious creative minds leverage Comfy to explore the full surface area of their ideas.',
|
||||
'zh-CN':
|
||||
'Comfy 打造了一个全新而强大的创意生态,同时毫不牺牲创作掌控力。看着技术型艺术家和充满好奇的创意人借助 Comfy 探索创意的每一个维度,令人惊叹。'
|
||||
},
|
||||
name: 'Scott Belsky',
|
||||
role: { en: 'Founder of Behance', 'zh-CN': 'Behance 创始人' }
|
||||
},
|
||||
{
|
||||
id: 'placeholder-1',
|
||||
title: {
|
||||
en: 'Intuitive UI and powerful node-based workflows',
|
||||
'zh-CN': '直观的界面与强大的节点式工作流'
|
||||
},
|
||||
body: {
|
||||
en: 'Placeholder review — Nav to replace with a curated 5-star review from G2.',
|
||||
'zh-CN': '占位评价 —— 待 Nav 从 G2 挑选并替换为 5 星好评。'
|
||||
},
|
||||
name: 'Customer Name'
|
||||
},
|
||||
{
|
||||
id: 'placeholder-2',
|
||||
title: {
|
||||
en: 'The most flexible creative pipeline we run',
|
||||
'zh-CN': '我们用过最灵活的创意流程'
|
||||
},
|
||||
body: {
|
||||
en: 'Placeholder review — Nav to replace with a curated 5-star review from G2.',
|
||||
'zh-CN': '占位评价 —— 待 Nav 从 G2 挑选并替换为 5 星好评。'
|
||||
},
|
||||
name: 'Customer Name'
|
||||
},
|
||||
{
|
||||
id: 'placeholder-3',
|
||||
title: {
|
||||
en: 'From prompt to final cut without leaving the canvas',
|
||||
'zh-CN': '从提示到成片,全程不离开画布'
|
||||
},
|
||||
body: {
|
||||
en: 'Placeholder review — Nav to replace with a curated 5-star review from G2.',
|
||||
'zh-CN': '占位评价 —— 待 Nav 从 G2 挑选并替换为 5 星好评。'
|
||||
},
|
||||
name: 'Customer Name'
|
||||
}
|
||||
] as const
|
||||
@@ -823,7 +823,7 @@ const translations = {
|
||||
'zh-CN': 'Comfy Cloud 支持的自定义节点包'
|
||||
},
|
||||
'cloudNodes.meta.title': {
|
||||
en: 'Custom-node packs on Comfy Cloud — supported by default',
|
||||
en: 'Custom-node packs on Comfy Cloud - supported by default',
|
||||
'zh-CN': 'Comfy Cloud 自定义节点包合集——开箱即用'
|
||||
},
|
||||
'cloudNodes.meta.description': {
|
||||
@@ -1845,8 +1845,8 @@ const translations = {
|
||||
|
||||
// MCP – Meta
|
||||
'mcp.meta.title': {
|
||||
en: 'Comfy MCP — Drive ComfyUI from any AI agent',
|
||||
'zh-CN': 'Comfy MCP — 让任何 AI 智能体驱动 ComfyUI'
|
||||
en: 'Comfy MCP - Drive ComfyUI from any AI agent',
|
||||
'zh-CN': 'Comfy MCP - 让任何 AI 智能体驱动 ComfyUI'
|
||||
},
|
||||
'mcp.meta.description': {
|
||||
en: 'Comfy MCP exposes the full ComfyUI engine over the Model Context Protocol. Generate images, video, audio, and 3D from Claude Code, Claude Desktop, and any MCP-compatible client.',
|
||||
@@ -1864,10 +1864,26 @@ const translations = {
|
||||
'zh-CN':
|
||||
'Comfy MCP 通过模型上下文协议暴露完整的 ComfyUI 引擎——让你的助手能够接入生态系统、构建工作流,并生成图像、视频、音频或 3D 内容。'
|
||||
},
|
||||
'mcp.hero.demoPrompt': {
|
||||
'mcp.hero.demoPromptMoodboard': {
|
||||
en: 'turn the brief in this email into a 6-up moodboard',
|
||||
'zh-CN': '把这封邮件里的需求做成六宫格情绪板'
|
||||
},
|
||||
'mcp.hero.demoPromptConcepts': {
|
||||
en: 'sketch three concept frames for the launch page',
|
||||
'zh-CN': '为发布页画三张概念稿'
|
||||
},
|
||||
'mcp.hero.demoPromptKeyart': {
|
||||
en: "match this frame's palette, make the hero key art",
|
||||
'zh-CN': '匹配这一帧的配色,生成主视觉关键画面'
|
||||
},
|
||||
'mcp.hero.demoPromptPbr': {
|
||||
en: 'make a tileable asphalt PBR material, all 5 maps',
|
||||
'zh-CN': '生成可平铺的沥青 PBR 材质,共 5 张贴图'
|
||||
},
|
||||
'mcp.hero.demoPromptUpscale': {
|
||||
en: 'upscale the neon kaiju shot to 4K',
|
||||
'zh-CN': '把霓虹怪兽画面放大到 4K'
|
||||
},
|
||||
'mcp.hero.viewDocs': {
|
||||
en: 'VIEW DOCS',
|
||||
'zh-CN': '查看文档'
|
||||
@@ -1876,10 +1892,6 @@ const translations = {
|
||||
en: 'INSTALL MCP',
|
||||
'zh-CN': '安装 MCP'
|
||||
},
|
||||
'mcp.hero.runWorkflow': {
|
||||
en: 'RUN A WORKFLOW',
|
||||
'zh-CN': '运行工作流'
|
||||
},
|
||||
'mcp.hero.demoGenerate': {
|
||||
en: 'GENERATE',
|
||||
'zh-CN': '生成'
|
||||
@@ -1897,60 +1909,100 @@ const translations = {
|
||||
'zh-CN': '放大图像'
|
||||
},
|
||||
|
||||
// MCP – SetupStepsSection
|
||||
// MCP – SetupSection
|
||||
'mcp.setup.label': {
|
||||
en: 'GET STARTED',
|
||||
'zh-CN': '快速开始'
|
||||
},
|
||||
'mcp.setup.heading': {
|
||||
en: 'Set up Comfy MCP in three steps',
|
||||
'zh-CN': '三步完成 Comfy MCP 配置'
|
||||
en: 'Set up Comfy MCP',
|
||||
'zh-CN': '配置 Comfy MCP'
|
||||
},
|
||||
'mcp.setup.subtitle': {
|
||||
en: 'Add Comfy Cloud as a custom connector in Claude, Cursor, Codex, or any MCP-compatible client. Sign in once, and the full ComfyUI toolset is available right in your chat.',
|
||||
en: 'Two ways to connect: add the server yourself, or ask your agent to install it. Sign in once, and the full ComfyUI toolset is available right in your chat.',
|
||||
'zh-CN':
|
||||
'将 Comfy Cloud 添加为 Claude、Cursor、Codex 或任意兼容 MCP 客户端的自定义连接器。登录一次,ComfyUI 全套工具即可直接在对话中使用。'
|
||||
'两种接入方式:自行添加服务器,或让你的智能体自动安装。登录一次,ComfyUI 全套工具即可直接在对话中使用。'
|
||||
},
|
||||
'mcp.setup.step1.label': { en: 'STEP 1', 'zh-CN': '第 1 步' },
|
||||
'mcp.setup.step1.title': {
|
||||
'mcp.setup.manual.title': {
|
||||
en: 'Install manually',
|
||||
'zh-CN': '手动安装'
|
||||
},
|
||||
'mcp.setup.manual.description': {
|
||||
en: 'Add this URL as a custom connector or remote MCP server in your client, then sign in when prompted.',
|
||||
'zh-CN':
|
||||
'将此 URL 添加为客户端的自定义连接器或远程 MCP 服务器,然后按提示登录。'
|
||||
},
|
||||
'mcp.setup.manual.tabsLabel': {
|
||||
en: 'Pick your client',
|
||||
'zh-CN': '选择你的客户端'
|
||||
},
|
||||
'mcp.setup.agent.title': {
|
||||
en: 'Ask your agent to install Comfy MCP',
|
||||
'zh-CN': '让你的智能体安装 Comfy MCP'
|
||||
},
|
||||
'mcp.setup.step1.command': {
|
||||
'mcp.setup.agent.command': {
|
||||
en: 'Help me install Comfy MCP.\nFollow the setup guide at {url}',
|
||||
'zh-CN': '帮我安装 Comfy MCP。\n请按照 {url} 上的设置指南操作。'
|
||||
},
|
||||
'mcp.setup.step1.description': {
|
||||
en: 'Paste this into Claude, Cursor, Codex, or any MCP-compatible agent. It reads the docs and adds the connector for you.',
|
||||
'mcp.setup.agent.description': {
|
||||
en: 'Prefer to let your agent do it? Paste this into Claude, Cursor, Codex, or any MCP-compatible agent. It reads the docs and adds the connector for you.',
|
||||
'zh-CN':
|
||||
'将它粘贴到 Claude、Cursor、Codex 或任意兼容 MCP 的智能体中。它会读取文档并为你添加连接器。'
|
||||
'想让智能体代劳?将它粘贴到 Claude、Cursor、Codex 或任意兼容 MCP 的智能体中。它会读取文档并为你添加连接器。'
|
||||
},
|
||||
'mcp.setup.step2.label': { en: 'STEP 2', 'zh-CN': '第 2 步' },
|
||||
'mcp.setup.step2.title': {
|
||||
en: 'Or add it by hand',
|
||||
'zh-CN': '或手动添加'
|
||||
'mcp.setup.clients.claudeCode.step': {
|
||||
en: 'Run this in your terminal, then use /mcp to pick comfy-cloud and authenticate.',
|
||||
'zh-CN': '在终端运行以下命令,然后通过 /mcp 选择 comfy-cloud 并完成认证。'
|
||||
},
|
||||
'mcp.setup.step2.description': {
|
||||
en: 'Prefer manual setup? Add Comfy Cloud as a custom connector with the MCP URL. The docs cover every client.',
|
||||
'mcp.setup.walkthroughAlt': {
|
||||
en: '{client} setup walkthrough',
|
||||
'zh-CN': '{client} 设置演示'
|
||||
},
|
||||
'mcp.setup.clients.claudeDesktop.manualTitle': {
|
||||
en: 'Add Custom Connector',
|
||||
'zh-CN': '添加自定义连接器'
|
||||
},
|
||||
'mcp.setup.clients.claudeDesktop.step': {
|
||||
en: 'Click Customize in the sidebar, open Connectors, choose Add custom connector, paste the URL above, and sign in.',
|
||||
'zh-CN':
|
||||
'想手动配置?用 MCP URL 将 Comfy Cloud 添加为自定义连接器。文档涵盖各类客户端。'
|
||||
'点击侧边栏的 Customize,进入 Connectors,选择添加自定义连接器,粘贴上方 URL 并登录。'
|
||||
},
|
||||
'mcp.setup.step2.cta': {
|
||||
en: 'COMFY CLOUD MCP DOCS',
|
||||
'zh-CN': 'COMFY CLOUD MCP 文档'
|
||||
'mcp.setup.clients.cursor.step': {
|
||||
en: 'Add the URL above to ~/.cursor/mcp.json with an X-API-Key header. Create your key at ',
|
||||
'zh-CN':
|
||||
'将上方 URL 添加到 ~/.cursor/mcp.json,并附带 X-API-Key 请求头。在此创建密钥:'
|
||||
},
|
||||
'mcp.setup.step3.label': { en: 'STEP 3', 'zh-CN': '第 3 步' },
|
||||
'mcp.setup.step3.title': {
|
||||
en: 'Connect and sign in',
|
||||
'zh-CN': '连接并登录'
|
||||
'mcp.setup.clients.cursor.linkLabel': {
|
||||
en: 'platform.comfy.org',
|
||||
'zh-CN': 'platform.comfy.org'
|
||||
},
|
||||
'mcp.setup.step3.description': {
|
||||
en: 'Click Connect, sign in, and every Comfy Cloud skill is ready in your client.',
|
||||
'zh-CN': '点击"连接"并登录,所有 Comfy Cloud 技能即可在你的客户端中使用。'
|
||||
'mcp.setup.clients.codex.step': {
|
||||
en: 'Run this in your terminal, then codex mcp login comfy-cloud to sign in.',
|
||||
'zh-CN': '在终端运行以下命令,然后执行 codex mcp login comfy-cloud 登录。'
|
||||
},
|
||||
'mcp.setup.step3.cta': {
|
||||
en: 'COMFY CLOUD SKILLS',
|
||||
'zh-CN': 'COMFY CLOUD 技能'
|
||||
'mcp.setup.clients.openclaw.step': {
|
||||
en: 'Run these in your terminal, then openclaw mcp login comfy to sign in.',
|
||||
'zh-CN': '在终端运行以下命令,然后执行 openclaw mcp login comfy 登录。'
|
||||
},
|
||||
'mcp.setup.clients.other.name': {
|
||||
en: 'Others',
|
||||
'zh-CN': '其他'
|
||||
},
|
||||
'mcp.setup.clients.other.step': {
|
||||
en: 'Add the URL above as a remote MCP server. No OAuth in your client? Use an X-API-Key header instead. Full walkthroughs live in the ',
|
||||
'zh-CN':
|
||||
'将上方 URL 添加为远程 MCP 服务器。客户端不支持 OAuth?改用 X-API-Key 请求头。完整教程见'
|
||||
},
|
||||
'mcp.setup.clients.other.linkLabel': {
|
||||
en: 'setup docs',
|
||||
'zh-CN': '设置文档'
|
||||
},
|
||||
'mcp.setup.skillsNote': {
|
||||
en: 'Using Claude Code? The Comfy skills plugin adds ready-made slash commands. ',
|
||||
'zh-CN': '在用 Claude Code?Comfy 技能插件提供现成的斜杠命令。'
|
||||
},
|
||||
'mcp.setup.skillsLink': {
|
||||
en: 'View on GitHub',
|
||||
'zh-CN': '在 GitHub 上查看'
|
||||
},
|
||||
|
||||
// MCP – WhyBuildSection
|
||||
@@ -1971,9 +2023,9 @@ const translations = {
|
||||
'zh-CN': '开放协议,\n任意客户端。'
|
||||
},
|
||||
'mcp.why.1.description': {
|
||||
en: 'MCP is an open standard, so any MCP-compatible client can connect. Today Comfy supports Claude Code and Claude Desktop, with more clients coming.',
|
||||
en: 'MCP is an open standard, so any MCP-compatible client can connect. Claude Code, Claude Desktop, and Codex sign in with OAuth; every other agent connects with an API key.',
|
||||
'zh-CN':
|
||||
'MCP 是开放标准,因此任何兼容 MCP 的客户端都能接入。目前 Comfy 支持 Claude Code 和 Claude Desktop,更多客户端即将推出。'
|
||||
'MCP 是开放标准,因此任何兼容 MCP 的客户端都能接入。Claude Code、Claude Desktop 和 Codex 通过 OAuth 登录,其他智能体使用 API 密钥连接。'
|
||||
},
|
||||
'mcp.why.2.title': {
|
||||
en: 'The full engine,\nnot a sandbox.',
|
||||
@@ -2037,14 +2089,53 @@ const translations = {
|
||||
'zh-CN': '运行真实工作流'
|
||||
},
|
||||
'mcp.tools.3.description': {
|
||||
en: 'Turn any ComfyUI workflow into a callable tool. The full power of the engine, driven by your agent.',
|
||||
en: 'Submit graphs, track jobs, and pull outputs back. Save and share workflows, reuse a saved one, or open any run on the ComfyUI canvas — the full engine, driven by tool calls.',
|
||||
'zh-CN':
|
||||
'将任何 ComfyUI 工作流转换为可调用的工具。由你的智能体驱动完整的引擎能力。'
|
||||
'提交计算图、跟踪任务并取回输出。保存和分享工作流,复用已保存的工作流,或在 ComfyUI 画布上打开任意运行——完整的引擎,由工具调用驱动。'
|
||||
},
|
||||
'mcp.tools.3.alt': {
|
||||
en: 'Comfy MCP running a ComfyUI workflow as a callable tool from a chat',
|
||||
'zh-CN': 'Comfy MCP 在对话中将 ComfyUI 工作流作为可调用工具运行'
|
||||
},
|
||||
'mcp.tools.4.title': {
|
||||
en: 'Direct any model',
|
||||
'zh-CN': '直接调用任意模型'
|
||||
},
|
||||
'mcp.tools.4.description': {
|
||||
en: 'Kling, Veo, Seedance, Flux, GPT-Image, Nano Banana, and ElevenLabs. Closed partner APIs and open-source models, reached through one set of tools.',
|
||||
'zh-CN':
|
||||
'Kling、Veo、Seedance、Flux、GPT-Image、Nano Banana 和 ElevenLabs。封闭的合作伙伴 API 与开源模型,通过同一套工具即可调用。'
|
||||
},
|
||||
'mcp.tools.4.alt': {
|
||||
en: 'Comfy MCP directing closed partner APIs and open-source models through one set of tools',
|
||||
'zh-CN': 'Comfy MCP 通过同一套工具调用封闭合作伙伴 API 和开源模型'
|
||||
},
|
||||
'mcp.tools.5.title': {
|
||||
en: 'Generate in batches',
|
||||
'zh-CN': '批量生成'
|
||||
},
|
||||
'mcp.tools.5.description': {
|
||||
en: 'Stack a batch on the Queue, track it, and pull back every output. Dozens of runs from a single call.',
|
||||
'zh-CN':
|
||||
'将一批任务加入队列,跟踪进度,并取回每一个输出。一次调用即可完成数十次运行。'
|
||||
},
|
||||
'mcp.tools.5.alt': {
|
||||
en: 'Comfy MCP stacking a batch on the Queue and pulling back every output',
|
||||
'zh-CN': 'Comfy MCP 将一批任务加入队列并取回每个输出'
|
||||
},
|
||||
'mcp.tools.6.title': {
|
||||
en: 'Ship it as an app',
|
||||
'zh-CN': '作为应用发布'
|
||||
},
|
||||
'mcp.tools.6.description': {
|
||||
en: 'Turn any workflow into an app with a shareable URL. Collaborators run it in the browser — only the inputs you expose, nothing to install.',
|
||||
'zh-CN':
|
||||
'将任意工作流变成带可分享链接的应用。协作者在浏览器中运行——只暴露你开放的输入,无需安装任何东西。'
|
||||
},
|
||||
'mcp.tools.6.alt': {
|
||||
en: 'Comfy MCP turning a workflow into a shareable browser app',
|
||||
'zh-CN': 'Comfy MCP 将工作流变成可在浏览器中分享的应用'
|
||||
},
|
||||
|
||||
// MCP – HowItWorksSection
|
||||
'mcp.howItWorks.heading': {
|
||||
@@ -2091,71 +2182,81 @@ const translations = {
|
||||
'zh-CN': '支持哪些客户端?'
|
||||
},
|
||||
'mcp.faq.1.a': {
|
||||
en: 'Claude Code and Claude Desktop today, both signing in with OAuth. Support for more clients is coming.',
|
||||
en: "For Claude Code, Claude Desktop, or Codex, add https://cloud.comfy.org/mcp as a custom connector or remote MCP server in any client, then sign in when prompted.\nFor clients that don't support OAuth, connect with a Comfy API key. Send the docs https://docs.comfy.org/agent-tools/cloud to your agent and it will figure out the installation for you.",
|
||||
'zh-CN':
|
||||
'目前支持 Claude Code 和 Claude Desktop,均通过 OAuth 登录。更多客户端的支持即将推出。'
|
||||
'对于 Claude Code、Claude Desktop 或 Codex,在任意客户端中将 https://cloud.comfy.org/mcp 添加为自定义连接器或远程 MCP 服务器,然后在提示时登录。\n对于不支持 OAuth 的客户端,请使用 Comfy API 密钥连接。将文档 https://docs.comfy.org/agent-tools/cloud 发送给你的智能体,它会为你完成安装。'
|
||||
},
|
||||
'mcp.faq.2.q': {
|
||||
en: "What's the server URL?",
|
||||
'zh-CN': '服务器 URL 是什么?'
|
||||
},
|
||||
'mcp.faq.2.a': {
|
||||
en: 'https://cloud.comfy.org/mcp — add it as a custom connector or remote MCP server in any client, then sign in when prompted.',
|
||||
'zh-CN':
|
||||
'https://cloud.comfy.org/mcp——在任意客户端中将它添加为自定义连接器或远程 MCP 服务器,然后在提示时登录。'
|
||||
},
|
||||
'mcp.faq.3.q': {
|
||||
en: 'Do I need an API key?',
|
||||
'zh-CN': '我需要 API 密钥吗?'
|
||||
},
|
||||
'mcp.faq.2.a': {
|
||||
en: 'Not for Claude Code or Claude Desktop. They use OAuth. An API key is only needed for headless or CI setups with no browser.',
|
||||
'zh-CN':
|
||||
'Claude Code 和 Claude Desktop 不需要,它们使用 OAuth。仅在没有浏览器的无头或 CI 环境中才需要 API 密钥。'
|
||||
},
|
||||
'mcp.faq.3.q': {
|
||||
en: 'Do the slash commands work in Claude Desktop?',
|
||||
'zh-CN': '斜杠命令在 Claude Desktop 中可以使用吗?'
|
||||
},
|
||||
'mcp.faq.3.a': {
|
||||
en: 'No. They ship in the Claude Code plugin. Desktop connects to the same MCP server, so the tools work; just ask in plain language.',
|
||||
en: 'Not for Claude Code, Claude Desktop, Codex, or OpenClaw. You need a Comfy API key for Cursor and Hermes for now. Just copy https://docs.comfy.org/agent-tools/cloud and your agent will figure out the installation for you.',
|
||||
'zh-CN':
|
||||
'不可以。斜杠命令包含在 Claude Code 插件中。Claude Desktop 连接的是同一个 MCP 服务器,因此工具可以正常使用;直接用自然语言提问即可。'
|
||||
'Claude Code、Claude Desktop、Codex 和 OpenClaw 不需要。Cursor 和 Hermes 目前需要 Comfy API 密钥。只需复制 https://docs.comfy.org/agent-tools/cloud,你的智能体就会为你完成安装。'
|
||||
},
|
||||
'mcp.faq.4.q': {
|
||||
en: "The sign-in didn't open a browser.",
|
||||
'zh-CN': '登录时没有打开浏览器。'
|
||||
en: 'Does it cost anything?',
|
||||
'zh-CN': '需要付费吗?'
|
||||
},
|
||||
'mcp.faq.4.a': {
|
||||
en: 'In Claude Code, run /mcp, select comfy-cloud, and choose Authenticate. In Claude Desktop, reopen the connector from Customize → Connectors.',
|
||||
en: "Connecting is free with a Comfy account, and searching models, nodes, and templates doesn't cost credits. Running a generation uses Comfy Cloud credits and needs a subscription or credit balance. Your agent confirms with you before it spends.",
|
||||
'zh-CN':
|
||||
'在 Claude Code 中,运行 /mcp,选择 comfy-cloud,然后选择 Authenticate(授权)。在 Claude Desktop 中,从“自定义 → 连接器”重新打开该连接器。'
|
||||
'使用 Comfy 账户连接是免费的,搜索模型、节点和模板也不消耗积分。运行生成会使用 Comfy Cloud 积分,需要订阅或积分余额。智能体在消费前会先与你确认。'
|
||||
},
|
||||
'mcp.faq.5.q': {
|
||||
en: 'How do I connect in Claude Code?',
|
||||
'zh-CN': '如何在 Claude Code 中连接?'
|
||||
en: 'Can I use it with my local ComfyUI?',
|
||||
'zh-CN': '可以配合我的本地 ComfyUI 使用吗?'
|
||||
},
|
||||
'mcp.faq.5.a': {
|
||||
en: 'Add the marketplace and install the comfy-cloud plugin, then run /mcp → comfy-cloud → Authenticate. It adds the connection and slash commands in one step.',
|
||||
en: 'Coming soon. Today, to drive a local ComfyUI, you can use comfy-cli: https://github.com/Comfy-Org/comfy-cli',
|
||||
'zh-CN':
|
||||
'添加插件市场并安装 comfy-cloud 插件,然后运行 /mcp → comfy-cloud → Authenticate(授权)。一步即可添加连接和斜杠命令。'
|
||||
'即将推出。目前,若要操作本地 ComfyUI,你可以使用 comfy-cli:https://github.com/Comfy-Org/comfy-cli'
|
||||
},
|
||||
'mcp.faq.6.q': {
|
||||
en: "What's the server URL for Claude Desktop?",
|
||||
'zh-CN': 'Claude Desktop 的服务器 URL 是什么?'
|
||||
},
|
||||
'mcp.faq.6.a': {
|
||||
en: 'Add a custom connector in Customize → Connectors pointing to https://cloud.comfy.org/mcp, then sign in when prompted.',
|
||||
'zh-CN':
|
||||
'在“自定义 → 连接器”中添加一个指向 https://cloud.comfy.org/mcp 的自定义连接器,然后在提示时登录。'
|
||||
},
|
||||
'mcp.faq.7.q': {
|
||||
en: 'What can my agent do once connected?',
|
||||
'zh-CN': '连接后我的智能体能做什么?'
|
||||
},
|
||||
'mcp.faq.7.a': {
|
||||
en: 'Generate images, video, audio, and 3D; search models, nodes, and templates; and run ComfyUI workflows, all from a chat.',
|
||||
'mcp.faq.6.a': {
|
||||
en: "• Generate images, video, audio, and 3D — including all open-source workflows and partner models like Seedance, GPT-Image, Nano Banana, and Kling\n• Build, edit, and run workflows; save and re-run workflows\n• Run and read in large batches\n• Search models, nodes, and template workflows\n• Read and execute shared workflow URLs\n• Upload and download assets for you\n\nEverything is now in natural language. No nodes, no downloads, no GPU, no node graphs if you don't want them.",
|
||||
'zh-CN':
|
||||
'生成图像、视频、音频和 3D;搜索模型、节点和模板;并运行 ComfyUI 工作流——全部在对话中完成。'
|
||||
'• 生成图像、视频、音频和 3D——包括所有开源工作流以及 Seedance、GPT-Image、Nano Banana 和 Kling 等合作伙伴模型\n• 构建、编辑和运行工作流;保存并重新运行工作流\n• 大批量运行和读取\n• 搜索模型、节点和模板工作流\n• 读取并执行分享的工作流链接\n• 为你上传和下载资产\n\n现在一切都用自然语言完成。如果你愿意,无需节点、无需下载、无需 GPU、无需节点图。'
|
||||
},
|
||||
'mcp.faq.7.q': {
|
||||
en: 'Where do my outputs go?',
|
||||
'zh-CN': '我的输出会保存到哪里?'
|
||||
},
|
||||
'mcp.faq.7.a': {
|
||||
en: 'Into your Comfy Cloud asset library, so you can reuse, remix, and share them — and open any run on the canvas to keep editing. You can also ask your agent to download the assets locally for you.',
|
||||
'zh-CN':
|
||||
'保存到你的 Comfy Cloud 资产库,你可以复用、二次创作和分享——还能在画布上打开任意运行继续编辑。你也可以让智能体把资产下载到本地。'
|
||||
},
|
||||
'mcp.faq.8.q': {
|
||||
en: 'Do slash commands work in Claude Desktop?',
|
||||
'zh-CN': '斜杠命令在 Claude Desktop 中可以使用吗?'
|
||||
},
|
||||
'mcp.faq.8.a': {
|
||||
en: 'No. They ship with the Claude Code comfy-cloud plugin. Desktop connects to the same MCP server, so every tool works; just ask in plain language.',
|
||||
'zh-CN':
|
||||
'不可以。斜杠命令随 Claude Code 的 comfy-cloud 插件一起提供。Claude Desktop 连接的是同一个 MCP 服务器,因此所有工具都能使用;直接用自然语言提问即可。'
|
||||
},
|
||||
'mcp.faq.9.q': {
|
||||
en: 'Is it generally available?',
|
||||
'zh-CN': '现已正式发布了吗?'
|
||||
},
|
||||
'mcp.faq.8.a': {
|
||||
en: 'Comfy Cloud MCP is in open beta and available to everyone.',
|
||||
'zh-CN': 'Comfy Cloud MCP 目前处于公开测试阶段,所有人均可使用。'
|
||||
'mcp.faq.9.a': {
|
||||
en: 'Yes. Comfy Cloud MCP is in open beta and available to everyone with a Comfy account.',
|
||||
'zh-CN':
|
||||
'是的。Comfy Cloud MCP 目前处于公开测试阶段,任何拥有 Comfy 账户的人都可以使用。'
|
||||
},
|
||||
|
||||
// SiteNav
|
||||
@@ -2181,6 +2282,7 @@ const translations = {
|
||||
'nav.youtube': { en: 'YouTube', 'zh-CN': 'YouTube' },
|
||||
'nav.aboutUs': { en: 'About Us', 'zh-CN': '关于我们' },
|
||||
'nav.careers': { en: 'Careers', 'zh-CN': '招聘' },
|
||||
'nav.brand': { en: 'Brand', 'zh-CN': '品牌' },
|
||||
'nav.customerStories': { en: 'Customer Stories', 'zh-CN': '客户故事' },
|
||||
'nav.launches': { en: 'Launches', 'zh-CN': '发布' },
|
||||
'nav.downloadLocal': { en: 'DOWNLOAD DESKTOP', 'zh-CN': '下载桌面版' },
|
||||
@@ -3483,8 +3585,8 @@ const translations = {
|
||||
},
|
||||
|
||||
'affiliate-terms.page.title': {
|
||||
en: 'Affiliate Terms — Comfy',
|
||||
'zh-CN': 'Affiliate Terms — Comfy'
|
||||
en: 'Affiliate Terms - Comfy',
|
||||
'zh-CN': 'Affiliate Terms - Comfy'
|
||||
},
|
||||
'affiliate-terms.page.description': {
|
||||
en: 'Comfy.org Affiliate Program Terms and Conditions.',
|
||||
@@ -3896,8 +3998,8 @@ const translations = {
|
||||
'This document reproduces the current template of the Enterprise Customer Agreement for reference only. The executed Agreement between Comfy and Customer, together with any signed Order Forms, governs the relationship between the parties. To request an executable copy, please contact <a href="mailto:sales@comfy.org" class="text-white underline">sales@comfy.org</a>.'
|
||||
},
|
||||
'enterprise-msa.page.title': {
|
||||
en: 'Enterprise MSA — Comfy',
|
||||
'zh-CN': 'Enterprise MSA — Comfy'
|
||||
en: 'Enterprise MSA - Comfy',
|
||||
'zh-CN': 'Enterprise MSA - Comfy'
|
||||
},
|
||||
'enterprise-msa.page.description': {
|
||||
en: 'Comfy Enterprise Customer Agreement — the master services agreement that governs Comfy Enterprise deployments of Comfy Cloud, Comfy API, and related products.',
|
||||
@@ -4361,8 +4463,8 @@ const translations = {
|
||||
|
||||
// Affiliate page (/affiliates) — head metadata
|
||||
'affiliate.page.title': {
|
||||
en: 'Comfy.org Affiliate Program — Become a Partner',
|
||||
'zh-CN': 'Comfy.org 联盟计划 — 成为合作伙伴'
|
||||
en: 'Comfy.org Affiliate Program - Become a Partner',
|
||||
'zh-CN': 'Comfy.org 联盟计划 - 成为合作伙伴'
|
||||
},
|
||||
'affiliate.page.description': {
|
||||
en: 'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.',
|
||||
@@ -4387,8 +4489,8 @@ const translations = {
|
||||
// Launches page (/launches) — head metadata
|
||||
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
|
||||
'launches.page.title': {
|
||||
en: 'ComfyUI Live Demo & Q&A — June 29 Launch Livestream',
|
||||
'zh-CN': 'ComfyUI 直播演示与问答 — 6 月 29 日发布直播'
|
||||
en: 'ComfyUI Live Demo & Q&A - June 29 Launch Livestream',
|
||||
'zh-CN': 'ComfyUI 直播演示与问答 - 6 月 29 日发布直播'
|
||||
},
|
||||
'launches.page.description': {
|
||||
en: 'Join the ComfyUI livestream on June 29 for a hands-on product demo and live Q&A. See what’s new across desktop, cloud, and community, and get your questions answered.',
|
||||
@@ -4448,118 +4550,160 @@ const translations = {
|
||||
'zh-CN': '最新发布'
|
||||
},
|
||||
|
||||
// Seedance 2.5 SEO page (/seedance-2.5). zh-CN hand-translated; some body
|
||||
// copy carries placeholder intent from Figma and may change (June, CRE-145).
|
||||
'seedance.meta.title': {
|
||||
en: 'Seedance 2.5 on Comfy — Cinematic AI Video Model',
|
||||
'zh-CN': 'Comfy 上的 Seedance 2.5 — 电影级 AI 视频模型'
|
||||
// Brand Portal page (/brand)
|
||||
'brand.page.title': {
|
||||
en: 'Brand — Comfy',
|
||||
'zh-CN': '品牌 — Comfy'
|
||||
},
|
||||
'seedance.meta.description': {
|
||||
en: 'Run ByteDance Seedance 2.5 on Comfy: multi-shot cinematic video from text or image, 1080p to 4K. Draft free on Wan 2.2 and spend credits only on the final render.',
|
||||
'brand.page.description': {
|
||||
en: 'The Comfy brand portal: logos, color, typography, and voice. Everything you need to build something that looks and sounds like Comfy.',
|
||||
'zh-CN':
|
||||
'在 Comfy 上运行字节跳动 Seedance 2.5:从文本或图像生成多镜头电影级视频,支持 1080p 至 4K。先用 Wan 2.2 免费打样,只在最终渲染时消耗积分。'
|
||||
'Comfy 品牌门户:标志、色彩、字体与语调。打造与 Comfy 观感一致、表达一致所需的一切。'
|
||||
},
|
||||
'seedance.breadcrumb.model': { en: 'Seedance 2.5', 'zh-CN': 'Seedance 2.5' },
|
||||
'seedance.breadcrumb.updated': {
|
||||
en: 'Updated July 2026',
|
||||
'zh-CN': '更新于 2026 年 7 月'
|
||||
'brand.hero.label': {
|
||||
en: 'Brand Portal',
|
||||
'zh-CN': '品牌门户'
|
||||
},
|
||||
'seedance.hero.badge': { en: 'Now on', 'zh-CN': '现已登陆' },
|
||||
'seedance.hero.title': { en: 'Seedance 2.5', 'zh-CN': 'Seedance 2.5' },
|
||||
'seedance.hero.description': {
|
||||
en: "ByteDance's cinematic video model — multi-shot sequences, 1080p to 4K, text or image in. You direct on the canvas; Seedance renders the cut. Draft free on Wan 2.2 and spend credits only on the final.",
|
||||
'brand.hero.heading': {
|
||||
en: 'Create with ComfyUI',
|
||||
'zh-CN': '用 ComfyUI 创作'
|
||||
},
|
||||
'brand.hero.subheading': {
|
||||
en: 'Logo, color, type, and voice. Everything you need to build something that looks and sounds like us.',
|
||||
'zh-CN': '标志、色彩、字体与语调。打造与我们观感一致、表达一致所需的一切。'
|
||||
},
|
||||
'brand.hero.viewGuidelines': {
|
||||
en: 'View brand guidelines',
|
||||
'zh-CN': '查看品牌规范'
|
||||
},
|
||||
'brand.hero.downloadLogos': {
|
||||
en: 'Download logos',
|
||||
'zh-CN': '下载标志'
|
||||
},
|
||||
'brand.logos.heading': {
|
||||
en: 'One mark, many dimensions.',
|
||||
'zh-CN': '一个标志,多种维度。'
|
||||
},
|
||||
'brand.logos.subheading': {
|
||||
en: 'Logos come in light and dark options. Use as provided. Do not distort, recolor, or outline. Make sure the logo is legible against its background.',
|
||||
'zh-CN':
|
||||
'字节跳动的电影级视频模型:多镜头序列,1080p 至 4K,支持文本或图像输入。你在画布上执导,Seedance 负责渲染成片。先用 Wan 2.2 免费打样,只在最终成片时消耗积分。'
|
||||
'标志提供浅色和深色两种版本。请按原样使用,不要变形、改色或描边。确保标志在其背景上清晰可辨。'
|
||||
},
|
||||
'seedance.hero.primaryCta': {
|
||||
en: 'RUN SEEDANCE 2.5',
|
||||
'zh-CN': '运行 Seedance 2.5'
|
||||
'brand.colors.heading': {
|
||||
en: 'Every color earns its place.',
|
||||
'zh-CN': '每种颜色都各得其所。'
|
||||
},
|
||||
'seedance.hero.secondaryCta': { en: 'SEE THE PRESETS', 'zh-CN': '查看预设' },
|
||||
'seedance.hero.footnote': {
|
||||
en: 'Pay-as-you-go credits · No watermark · Commercial use included',
|
||||
'zh-CN': '按量付费积分 · 无水印 · 包含商业使用授权'
|
||||
},
|
||||
'seedance.showcase.heading': {
|
||||
en: 'Multi-shot sequences, 4K',
|
||||
'zh-CN': '多镜头序列,4K'
|
||||
},
|
||||
'seedance.showcase.promptLabel': { en: 'Prompt', 'zh-CN': '提示词' },
|
||||
'seedance.showcase.cta': { en: 'RUN SEEDANCE', 'zh-CN': '运行 Seedance' },
|
||||
'seedance.showcase.prev': {
|
||||
en: 'Previous example',
|
||||
'zh-CN': '上一个示例'
|
||||
},
|
||||
'seedance.showcase.next': { en: 'Next example', 'zh-CN': '下一个示例' },
|
||||
'seedance.steps.heading': {
|
||||
en: 'Direct your first Seedance shot',
|
||||
'zh-CN': '执导你的第一个 Seedance 镜头'
|
||||
},
|
||||
'seedance.steps.leadIn': {
|
||||
en: 'The model is the artist. You write the brief.',
|
||||
'zh-CN': '模型即艺术家,你只需写下创意简报。'
|
||||
},
|
||||
'seedance.steps.step': { en: 'Step', 'zh-CN': '步骤' },
|
||||
'seedance.models.eyebrow': {
|
||||
en: 'Every model · one canvas',
|
||||
'zh-CN': '每个模型 · 同一画布'
|
||||
},
|
||||
'seedance.models.heading': {
|
||||
en: 'The rest of the video stack',
|
||||
'zh-CN': '完整的视频模型阵容'
|
||||
},
|
||||
'seedance.models.subtitle': {
|
||||
en: 'Same canvas, same workflow. Draft on the free anchors, finish on the model the shot demands.',
|
||||
'brand.colors.subheading': {
|
||||
en: 'Our color palette helps build brand recognition. When people think of Comfy, we want them to associate it with the following colors.',
|
||||
'zh-CN':
|
||||
'同一画布,同一工作流。先用免费模型打样,再用最合适的模型完成镜头。'
|
||||
'我们的调色板有助于建立品牌辨识度。当人们想到 Comfy 时,我们希望他们联想到以下这些颜色。'
|
||||
},
|
||||
'seedance.models.tagFree': { en: 'Free', 'zh-CN': '免费' },
|
||||
'seedance.models.tagPremium': { en: 'Premium', 'zh-CN': '高级' },
|
||||
'seedance.faq.heading': {
|
||||
en: 'Q&A',
|
||||
'zh-CN': '问答'
|
||||
'brand.colors.copy': {
|
||||
en: 'Copy',
|
||||
'zh-CN': '复制'
|
||||
},
|
||||
'seedance.cta.heading': {
|
||||
en: 'Multi-shot. One director.',
|
||||
'zh-CN': '多镜头,一位导演。'
|
||||
'brand.colors.copied': {
|
||||
en: 'Copied',
|
||||
'zh-CN': '已复制'
|
||||
},
|
||||
'seedance.cta.subtitle': {
|
||||
en: 'Draft free on Wan 2.2 — switch to Seedance when the shot is ready.',
|
||||
'zh-CN': '先用 Wan 2.2 免费打样,镜头就绪后再切换到 Seedance。'
|
||||
'brand.voice.heading': {
|
||||
en: 'Precise, never cute.',
|
||||
'zh-CN': '精准,绝不卖弄。'
|
||||
},
|
||||
'seedance.cta.primaryCta': {
|
||||
en: 'RUN WAN 2.2 FOR FREE',
|
||||
'zh-CN': '免费运行 Wan 2.2'
|
||||
'brand.voice.direct.title': {
|
||||
en: 'Direct',
|
||||
'zh-CN': '直接'
|
||||
},
|
||||
'seedance.cta.secondaryCta': {
|
||||
en: 'RUN SEEDANCE 2.5',
|
||||
'zh-CN': '运行 Seedance 2.5'
|
||||
},
|
||||
'seedance.runOptions.heading': {
|
||||
en: 'One engine, every way to run it',
|
||||
'zh-CN': '同一引擎,多种运行方式'
|
||||
},
|
||||
'seedance.runOptions.subtitle': {
|
||||
en: 'Run Seedance in the browser today. Batch campaigns with the API, or bring it in-house.',
|
||||
'brand.voice.direct.body': {
|
||||
en: 'We state things. We don’t hedge, qualify, or suggest. Short sentences. Active voice. One idea at a time.',
|
||||
'zh-CN':
|
||||
'今天就在浏览器中运行 Seedance。用 API 批量制作,或部署到自有环境。'
|
||||
'我们直陈其事。不含糊、不设限、不暗示。短句。主动语态。一次只讲一个观点。'
|
||||
},
|
||||
'seedance.reviews.heading': {
|
||||
en: '4+ million Comfy creators say',
|
||||
'zh-CN': '400 万+ Comfy 创作者这样说'
|
||||
'brand.voice.precise.title': {
|
||||
en: 'Precise',
|
||||
'zh-CN': '精准'
|
||||
},
|
||||
'seedance.reviews.highlightTitle': {
|
||||
en: 'Comfy MCP: now turn your agent into a creative technologist.',
|
||||
'zh-CN': 'Comfy MCP:让你的智能体成为创意技术专家。'
|
||||
},
|
||||
'seedance.reviews.highlightDescription': {
|
||||
en: 'Your AI assistant can access the ecosystem, build workflows, and generate images, video, audio, or 3D.',
|
||||
'brand.voice.precise.body': {
|
||||
en: 'We use the real names for things. Nodes, samplers, seeds, checkpoints. We don’t talk around the product or reach for metaphor when the technical term is already good.',
|
||||
'zh-CN':
|
||||
'你的 AI 助手可以接入整个生态、构建工作流,并生成图像、视频、音频或 3D 内容。'
|
||||
'我们直呼其名:nodes、samplers、seeds、checkpoints。当技术术语已经足够贴切时,我们不绕弯子,也不借用比喻。'
|
||||
},
|
||||
'seedance.reviews.highlightCta': { en: 'GET STARTED', 'zh-CN': '开始使用' },
|
||||
'seedance.reviews.prev': { en: 'Previous', 'zh-CN': '上一条' },
|
||||
'seedance.reviews.next': { en: 'Next', 'zh-CN': '下一条' }
|
||||
'brand.voice.human.title': {
|
||||
en: 'Human-first',
|
||||
'zh-CN': '以人为先'
|
||||
},
|
||||
'brand.voice.human.body': {
|
||||
en: 'The human creates. Comfy makes every step visible. We never write as though the AI is doing the work.',
|
||||
'zh-CN':
|
||||
'创作的是人。Comfy 让每一步都清晰可见。我们绝不把功劳写成是 AI 完成的。'
|
||||
},
|
||||
'brand.voice.antihype.title': {
|
||||
en: 'Anti-hype',
|
||||
'zh-CN': '拒绝浮夸'
|
||||
},
|
||||
'brand.voice.antihype.body': {
|
||||
en: 'We don’t write “stunning,” “revolutionary,” or “effortless.” We don’t promise magic. Our tagline says exactly what we mean: Method, not magic.',
|
||||
'zh-CN':
|
||||
'我们不写“惊艳”“革命性”或“毫不费力”。我们不承诺魔法。我们的口号恰如其分:方法,而非魔法。'
|
||||
},
|
||||
'brand.voice.doLabel': {
|
||||
en: 'Do',
|
||||
'zh-CN': '推荐'
|
||||
},
|
||||
'brand.voice.dontLabel': {
|
||||
en: 'Don’t',
|
||||
'zh-CN': '避免'
|
||||
},
|
||||
'brand.voice.do.0': {
|
||||
en: 'Route your prompt through a ControlNet. Wire the output to the VAE decode.',
|
||||
'zh-CN': '让你的 prompt 经过 ControlNet,再将输出连接到 VAE decode。'
|
||||
},
|
||||
'brand.voice.do.1': {
|
||||
en: 'Comfy runs on your hardware. Nothing leaves your machine.',
|
||||
'zh-CN': 'Comfy 在你自己的硬件上运行。任何数据都不会离开你的机器。'
|
||||
},
|
||||
'brand.voice.dont.0': {
|
||||
en: 'Simply connect your AI blocks and watch the magic happen!',
|
||||
'zh-CN': '只需连接你的 AI 模块,见证奇迹的发生!'
|
||||
},
|
||||
'brand.voice.dont.1': {
|
||||
en: 'Oops! Something went wrong. Please try again later.',
|
||||
'zh-CN': '哎呀!出了点问题,请稍后再试。'
|
||||
},
|
||||
'brand.trademark.heading': {
|
||||
en: 'Trademark guidelines.',
|
||||
'zh-CN': '商标使用规范。'
|
||||
},
|
||||
'brand.trademark.body1': {
|
||||
en: 'Comfy and ComfyUI are trademarks of Comfy Org. You’re welcome to reference them in content that accurately describes your work with our platform. Tutorials, reviews, integrations, and affiliate content all qualify.',
|
||||
'zh-CN':
|
||||
'Comfy 和 ComfyUI 是 Comfy Org 的商标。欢迎在准确描述你与我们平台相关工作的内容中引用它们。教程、评测、集成以及联盟内容均可。'
|
||||
},
|
||||
'brand.trademark.body2': {
|
||||
en: 'A few rules: don’t modify the logo, don’t use the Comfy name in your own product or company name, and don’t present your content in a way that implies official endorsement or partnership beyond what’s been agreed.',
|
||||
'zh-CN':
|
||||
'几条规则:不要修改标志,不要在你自己的产品或公司名称中使用 Comfy 这一名称,也不要以暗示官方认可或合作关系(超出双方已达成的约定)的方式呈现你的内容。'
|
||||
},
|
||||
'brand.trademark.body3': {
|
||||
en: 'For permissions outside these guidelines,',
|
||||
'zh-CN': '如需本规范之外的授权,请'
|
||||
},
|
||||
'brand.trademark.contact': {
|
||||
en: 'Contact Us',
|
||||
'zh-CN': '联系我们'
|
||||
},
|
||||
'brand.questions.heading': {
|
||||
en: 'Questions?',
|
||||
'zh-CN': '有疑问?'
|
||||
},
|
||||
'brand.questions.body': {
|
||||
en: 'For press, partnerships, or anything outside these guidelines,',
|
||||
'zh-CN': '如涉及媒体、合作,或本规范未涵盖的任何事宜,请'
|
||||
},
|
||||
'brand.questions.contact': {
|
||||
en: 'Contact Us',
|
||||
'zh-CN': '联系我们'
|
||||
}
|
||||
} as const satisfies Record<string, Record<Locale, string>>
|
||||
|
||||
type TranslationKey = keyof typeof translations
|
||||
|
||||
@@ -3,7 +3,7 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="404 — Page Not Found — Comfy"
|
||||
title="404 - Page Not Found - Comfy"
|
||||
noindex>
|
||||
<div
|
||||
class="flex w-full flex-col items-center p-4 h-[calc(100dvh-12rem)]">
|
||||
|
||||
@@ -16,7 +16,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="About Us — Comfy"
|
||||
title="About Us - Comfy"
|
||||
pageType="AboutPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
54
apps/website/src/pages/booking-confirmation.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ResourceList from '../components/booking-confirmation/ResourceList.vue'
|
||||
import HeroSection from '../components/legal/HeroSection.vue'
|
||||
import { externalLinks, getRoutes } from '../config/routes'
|
||||
|
||||
const routes = getRoutes('en')
|
||||
|
||||
const resources = [
|
||||
{
|
||||
label: 'Learning Center',
|
||||
href: routes.learning,
|
||||
display: 'comfy.org/learning'
|
||||
},
|
||||
{
|
||||
label: 'Workflow templates',
|
||||
href: externalLinks.workflows,
|
||||
display: 'comfy.org/workflows'
|
||||
},
|
||||
{
|
||||
label: 'Customer stories',
|
||||
href: routes.customers,
|
||||
display: 'comfy.org/customers'
|
||||
},
|
||||
{
|
||||
label: 'Docs',
|
||||
href: externalLinks.docs,
|
||||
display: 'docs.comfy.org'
|
||||
}
|
||||
]
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="You're booked - Comfy"
|
||||
description="Your meeting is booked. Check your email for the calendar invite and meeting link."
|
||||
noindex
|
||||
>
|
||||
<HeroSection title="You're booked." class="text-center" />
|
||||
|
||||
<section class="-mt-8 px-6 pb-24 lg:-mt-12 lg:pb-40">
|
||||
<div
|
||||
class="text-primary-comfy-canvas mx-auto flex max-w-2xl flex-col gap-10 text-center text-base font-light lg:text-lg"
|
||||
>
|
||||
<p>Check your email for the calendar invite and meeting link!</p>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-primary-comfy-yellow text-xl font-semibold italic lg:text-2xl">
|
||||
Resources while you wait
|
||||
</h2>
|
||||
<ResourceList resources={resources} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
28
apps/website/src/pages/brand.astro
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import BrandBackground from '../templates/brand/BrandBackground.vue'
|
||||
import BrandHeroSection from '../templates/brand/BrandHeroSection.vue'
|
||||
import BrandLogosSection from '../templates/brand/BrandLogosSection.vue'
|
||||
import BrandColorSection from '../templates/brand/BrandColorSection.vue'
|
||||
import BrandVoiceSection from '../templates/brand/BrandVoiceSection.vue'
|
||||
import BrandTrademarkSection from '../templates/brand/BrandTrademarkSection.vue'
|
||||
import BrandQuestionsSection from '../templates/brand/BrandQuestionsSection.vue'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
const locale = 'en' as const
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('brand.page.title', locale)}
|
||||
description={t('brand.page.description', locale)}
|
||||
>
|
||||
<div class="relative">
|
||||
<BrandBackground client:idle />
|
||||
<BrandHeroSection />
|
||||
<BrandLogosSection />
|
||||
<BrandColorSection client:visible />
|
||||
<BrandVoiceSection />
|
||||
<BrandTrademarkSection />
|
||||
<BrandQuestionsSection />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -42,7 +42,7 @@ const roles = itemListNode(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Careers — Comfy"
|
||||
title="Careers - Comfy"
|
||||
description="Join the team building the operating system for generative AI. Open roles in engineering, design, marketing, and more."
|
||||
pageType="CollectionPage"
|
||||
mainEntityId={jsonLdId(url, 'itemlist')}
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Case Studies — Comfy">
|
||||
<BaseLayout title="Case Studies - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { t } from '../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy Cloud — AI in the Cloud"
|
||||
title="Comfy Cloud - AI in the Cloud"
|
||||
description={t('cloud.hero.subtitle', 'en')}
|
||||
keywords={['comfyui web app', 'comfyui app', 'comfyui online', 'comfyui cloud', 'comfy cloud', 'comfy ui application', 'comfyui browser', 'cloud comfyui', 'managed comfyui']}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ const productId = jsonLdId(url, 'product')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Pricing — Comfy Cloud"
|
||||
title="Pricing - Comfy Cloud"
|
||||
mainEntityId={productId}
|
||||
breadcrumbs={[
|
||||
{ name: t('breadcrumb.home', locale), url: absoluteUrl(Astro.site, '/') },
|
||||
|
||||
@@ -13,7 +13,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Contact — Comfy"
|
||||
title="Contact - Comfy"
|
||||
pageType="ContactPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -11,7 +11,7 @@ import { loadStories } from '../utils/loadStories'
|
||||
const stories = (await loadStories('en')).map(toCardProps)
|
||||
---
|
||||
|
||||
<BaseLayout title="Customer Stories — Comfy">
|
||||
<BaseLayout title="Customer Stories - Comfy">
|
||||
<HeroSection client:load />
|
||||
<StorySection stories={stories} />
|
||||
<FeedbackSection client:load />
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
|
||||
const { entry, next } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={`${entry.data.title} — Comfy`}>
|
||||
<BaseLayout title={`${entry.data.title} - Comfy`}>
|
||||
<DetailHeroSection
|
||||
label={entry.data.category}
|
||||
title={entry.data.title}
|
||||
|
||||
@@ -58,7 +58,7 @@ const learningResource: JsonLdNode = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${title} — Comfy`}
|
||||
title={`${title} - Comfy`}
|
||||
description={description}
|
||||
ogImage={demo.ogImage}
|
||||
mainEntityId={learningId}
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Demos — Comfy" description="Interactive demos and tutorials for ComfyUI.">
|
||||
<BaseLayout title="Demos - Comfy" description="Interactive demos and tutorials for ComfyUI.">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
absoluteUrl,
|
||||
comfyUiApplicationNode,
|
||||
comfyUiSoftwareId,
|
||||
comfyUiSourceCodeNode,
|
||||
pageContext,
|
||||
} from '../utils/jsonLd'
|
||||
|
||||
@@ -23,14 +24,14 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Download Comfy Desktop — Run AI on Your Hardware"
|
||||
title="Download Comfy Desktop - Run AI on Your Hardware"
|
||||
description={t('download.hero.subtitle', 'en')}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
{ name: t('breadcrumb.home', locale), url: absoluteUrl(Astro.site, '/') },
|
||||
{ name: t('breadcrumb.download', locale) },
|
||||
]}
|
||||
extraJsonLd={[comfyUiApplicationNode(siteUrl)]}
|
||||
extraJsonLd={[comfyUiApplicationNode(siteUrl), comfyUiSourceCodeNode(siteUrl)]}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfyui desktop', 'comfy ui application', 'comfyui download', 'download comfyui', 'comfyui windows', 'comfyui mac', 'comfyui linux']}
|
||||
>
|
||||
<CloudBannerSection />
|
||||
|
||||
@@ -5,7 +5,7 @@ import GallerySection from '../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../components/gallery/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="Gallery — Comfy">
|
||||
<BaseLayout title="Gallery - Comfy">
|
||||
<HeroSection />
|
||||
<GallerySection client:load />
|
||||
<ContactSection />
|
||||
|
||||
@@ -24,7 +24,7 @@ const { siteUrl } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy — Professional Control of Visual AI"
|
||||
title="Comfy - Professional Control of Visual AI"
|
||||
description={t("hero.subtitle", "en")}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
extraJsonLd={[
|
||||
|
||||
60
apps/website/src/pages/individual-submission.astro
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import PlansPricingCta from '../components/individual-submission/PlansPricingCta.vue'
|
||||
import HeroSection from '../components/legal/HeroSection.vue'
|
||||
import { getRoutes } from '../config/routes'
|
||||
|
||||
const routes = getRoutes('en')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Thanks for reaching out - Comfy"
|
||||
description="Thanks for reaching out. Based on what you shared, one of our self-serve plans is probably a better fit."
|
||||
noindex
|
||||
>
|
||||
<HeroSection title="Thanks for reaching out." class="text-center" />
|
||||
|
||||
<section class="-mt-8 px-6 pb-24 lg:-mt-12 lg:pb-40">
|
||||
<div
|
||||
class="text-primary-comfy-canvas mx-auto flex max-w-2xl flex-col gap-6 text-center text-base font-light lg:text-lg"
|
||||
>
|
||||
<p>
|
||||
Based on what you shared, one of our self-serve plans is probably a
|
||||
better fit.
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Standard</strong
|
||||
>,
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Creator</strong
|
||||
>, and
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Pro</strong
|
||||
> for individual creators, plus our new
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Teams</strong
|
||||
> plan for multiple users under shared billing.
|
||||
</p>
|
||||
|
||||
<PlansPricingCta
|
||||
href={routes.cloudPricing}
|
||||
label="See plans and pricing →"
|
||||
/>
|
||||
|
||||
<p class="text-primary-warm-gray mt-8 text-sm">
|
||||
Still think you have an enterprise need?<br /> We're happy to assist. Email: <a
|
||||
href="mailto:gtm-team@comfy.org"
|
||||
class="text-primary-comfy-yellow whitespace-nowrap underline underline-offset-4 hover:no-underline"
|
||||
>gtm-team@comfy.org</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<p class="text-primary-warm-gray text-sm">
|
||||
Need help with something else? Email: <a
|
||||
href="mailto:support@comfy.org"
|
||||
class="text-primary-comfy-yellow whitespace-nowrap underline underline-offset-4 hover:no-underline"
|
||||
>support@comfy.org</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -12,7 +12,7 @@ import { externalLinks } from '../config/routes'
|
||||
const routes = getRoutes('en')
|
||||
---
|
||||
|
||||
<BaseLayout title="Learning — Comfy">
|
||||
<BaseLayout title="Learning - Comfy">
|
||||
<HeroSection client:load />
|
||||
<FeaturedWorkflowSection client:visible />
|
||||
<TutorialsSection client:visible />
|
||||
|
||||
@@ -7,7 +7,7 @@ import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vu
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Models — Comfy"
|
||||
title="Models - Comfy"
|
||||
description="Run the world's leading AI models in ComfyUI. Browse every supported model with community workflow templates ready to run."
|
||||
>
|
||||
<ModelsHeroSection
|
||||
|
||||
@@ -103,7 +103,7 @@ const faqPage: JsonLdNode = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${pageTitle} — Comfy`}
|
||||
title={`${pageTitle} - Comfy`}
|
||||
description={pageDescription}
|
||||
ogImage={model.thumbnailUrl}
|
||||
mainEntityId={softwareId}
|
||||
|
||||
@@ -43,7 +43,7 @@ const dirLabel: Record<string, string> = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${title} — Comfy`}
|
||||
title={`${title} - Comfy`}
|
||||
description={subtitle}
|
||||
pageType="CollectionPage"
|
||||
mainEntityId={jsonLdId(url, 'itemlist')}
|
||||
|
||||
@@ -4,7 +4,7 @@ import PaymentStatusSection from '../../components/payment/PaymentStatusSection.
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Payment Failed — Comfy"
|
||||
title="Payment Failed - Comfy"
|
||||
description="Your payment was not completed."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import PaymentStatusSection from '../../components/payment/PaymentStatusSection.
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Payment Successful — Comfy"
|
||||
title="Payment Successful - Comfy"
|
||||
description="Your payment was processed successfully."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeroSection from '../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Privacy Policy — Comfy"
|
||||
title="Privacy Policy - Comfy"
|
||||
description="Comfy privacy policy. Learn how we collect, use, and protect your personal information."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeroSection from '../../components/legal/HeroSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Desktop Privacy Policy — Comfy"
|
||||
title="Desktop Privacy Policy - Comfy"
|
||||
description="Privacy policy for Comfy Desktop. Named processors, lawful basis under GDPR/UK GDPR, retention periods, and your rights."
|
||||
>
|
||||
<HeroSection title="Desktop Privacy Policy" />
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import BreadcrumbBar from '../components/common/BreadcrumbBar.vue'
|
||||
import PriceSection from '../components/pricing/PriceSection.vue'
|
||||
import SeedanceCtaSection from '../templates/seedance/SeedanceCtaSection.vue'
|
||||
import SeedanceFaqSection from '../templates/seedance/SeedanceFaqSection.vue'
|
||||
import SeedanceHeroSection from '../templates/seedance/SeedanceHeroSection.vue'
|
||||
import SeedanceModelsSection from '../templates/seedance/SeedanceModelsSection.vue'
|
||||
import SeedanceReviewsSection from '../templates/seedance/SeedanceReviewsSection.vue'
|
||||
import SeedanceRunOptionsSection from '../templates/seedance/SeedanceRunOptionsSection.vue'
|
||||
import SeedanceShowcaseSection from '../templates/seedance/SeedanceShowcaseSection.vue'
|
||||
import SeedanceStepsSection from '../templates/seedance/SeedanceStepsSection.vue'
|
||||
import { getRoutes } from '../config/routes'
|
||||
import { seedanceFaqs } from '../data/seedance'
|
||||
import { t } from '../i18n/translations'
|
||||
import type { JsonLdNode } from '../utils/jsonLd'
|
||||
import { absoluteUrl, jsonLdId, pageContext } from '../utils/jsonLd'
|
||||
|
||||
const { locale, url } = pageContext(
|
||||
Astro.site,
|
||||
Astro.url.pathname,
|
||||
Astro.currentLocale,
|
||||
)
|
||||
const routes = getRoutes(locale)
|
||||
|
||||
const crumbs = [
|
||||
{ label: t('breadcrumb.home', locale), href: routes.home },
|
||||
{ label: t('models.breadcrumb.models', locale), href: routes.models },
|
||||
{ label: t('seedance.breadcrumb.model', locale) },
|
||||
]
|
||||
|
||||
// Derive the JSON-LD breadcrumb trail from the visible crumbs so the two
|
||||
// cannot drift apart.
|
||||
const breadcrumbs = crumbs.map((crumb) =>
|
||||
crumb.href
|
||||
? { name: crumb.label, url: absoluteUrl(Astro.site, crumb.href) }
|
||||
: { name: crumb.label },
|
||||
)
|
||||
|
||||
const faqPage: JsonLdNode = {
|
||||
'@type': 'FAQPage',
|
||||
'@id': jsonLdId(url, 'faq'),
|
||||
mainEntity: seedanceFaqs.map((faq) => ({
|
||||
'@type': 'Question',
|
||||
name: faq.question[locale],
|
||||
acceptedAnswer: { '@type': 'Answer', text: faq.answer[locale] },
|
||||
})),
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('seedance.meta.title', locale)}
|
||||
description={t('seedance.meta.description', locale)}
|
||||
breadcrumbs={breadcrumbs}
|
||||
extraJsonLd={[faqPage]}
|
||||
>
|
||||
<BreadcrumbBar
|
||||
crumbs={crumbs}
|
||||
updated={t('seedance.breadcrumb.updated', locale)}
|
||||
/>
|
||||
<SeedanceHeroSection client:load />
|
||||
<SeedanceShowcaseSection client:visible />
|
||||
<SeedanceStepsSection />
|
||||
<SeedanceModelsSection />
|
||||
<PriceSection client:load />
|
||||
<SeedanceFaqSection client:visible />
|
||||
<SeedanceCtaSection />
|
||||
<SeedanceRunOptionsSection />
|
||||
<SeedanceReviewsSection client:visible />
|
||||
</BaseLayout>
|
||||
@@ -6,7 +6,7 @@ import { t } from '../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Terms of Service — Comfy"
|
||||
title="Terms of Service - Comfy"
|
||||
description="Terms of Service governing use of the Comfy Products, including Comfy Cloud, Comfy API, and Comfy Enterprise."
|
||||
noindex
|
||||
>
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="Videos — Comfy">
|
||||
<BaseLayout title="Videos - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -16,7 +16,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="关于我们 — Comfy"
|
||||
title="关于我们 - Comfy"
|
||||
description="了解 ComfyUI 背后的团队和使命——开源的生成式 AI 平台。"
|
||||
pageType="AboutPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
|
||||
54
apps/website/src/pages/zh-CN/booking-confirmation.astro
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ResourceList from '../../components/booking-confirmation/ResourceList.vue'
|
||||
import HeroSection from '../../components/legal/HeroSection.vue'
|
||||
import { externalLinks, getRoutes } from '../../config/routes'
|
||||
|
||||
const routes = getRoutes('zh-CN')
|
||||
|
||||
const resources = [
|
||||
{
|
||||
label: '学习中心',
|
||||
href: routes.learning,
|
||||
display: 'comfy.org/learning'
|
||||
},
|
||||
{
|
||||
label: '工作流模板',
|
||||
href: externalLinks.workflows,
|
||||
display: 'comfy.org/workflows'
|
||||
},
|
||||
{
|
||||
label: '客户案例',
|
||||
href: routes.customers,
|
||||
display: 'comfy.org/customers'
|
||||
},
|
||||
{
|
||||
label: '文档',
|
||||
href: externalLinks.docs,
|
||||
display: 'docs.comfy.org'
|
||||
}
|
||||
]
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="预约成功 - Comfy"
|
||||
description="您的会议已预约成功。请查收邮件中的日历邀请和会议链接。"
|
||||
noindex
|
||||
>
|
||||
<HeroSection title="预约成功。" class="text-center" />
|
||||
|
||||
<section class="-mt-8 px-6 pb-24 lg:-mt-12 lg:pb-40">
|
||||
<div
|
||||
class="text-primary-comfy-canvas mx-auto flex max-w-2xl flex-col gap-10 text-center text-base font-light lg:text-lg"
|
||||
>
|
||||
<p>请查收邮件中的日历邀请和会议链接!</p>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="text-primary-comfy-yellow text-xl font-semibold italic lg:text-2xl">
|
||||
等待期间的资源
|
||||
</h2>
|
||||
<ResourceList resources={resources} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
26
apps/website/src/pages/zh-CN/brand.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import BrandBackground from '../../templates/brand/BrandBackground.vue'
|
||||
import BrandHeroSection from '../../templates/brand/BrandHeroSection.vue'
|
||||
import BrandLogosSection from '../../templates/brand/BrandLogosSection.vue'
|
||||
import BrandColorSection from '../../templates/brand/BrandColorSection.vue'
|
||||
import BrandVoiceSection from '../../templates/brand/BrandVoiceSection.vue'
|
||||
import BrandTrademarkSection from '../../templates/brand/BrandTrademarkSection.vue'
|
||||
import BrandQuestionsSection from '../../templates/brand/BrandQuestionsSection.vue'
|
||||
import { t } from '../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={t('brand.page.title', 'zh-CN')}
|
||||
description={t('brand.page.description', 'zh-CN')}
|
||||
>
|
||||
<div class="relative">
|
||||
<BrandBackground client:idle />
|
||||
<BrandHeroSection locale="zh-CN" />
|
||||
<BrandLogosSection locale="zh-CN" />
|
||||
<BrandColorSection client:visible locale="zh-CN" />
|
||||
<BrandVoiceSection locale="zh-CN" />
|
||||
<BrandTrademarkSection locale="zh-CN" />
|
||||
<BrandQuestionsSection locale="zh-CN" />
|
||||
</div>
|
||||
</BaseLayout>
|
||||
@@ -42,7 +42,7 @@ const roles = itemListNode(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="招聘 — Comfy"
|
||||
title="招聘 - Comfy"
|
||||
description="加入构建生成式 AI 操作系统的团队。工程、设计、市场营销等岗位开放招聘中。"
|
||||
pageType="CollectionPage"
|
||||
mainEntityId={jsonLdId(url, 'itemlist')}
|
||||
|
||||
@@ -3,6 +3,6 @@ import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import ComingSoon from '../../components/common/ComingSoon.astro'
|
||||
---
|
||||
|
||||
<BaseLayout title="案例研究 — Comfy">
|
||||
<BaseLayout title="案例研究 - Comfy">
|
||||
<ComingSoon />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { t } from '../../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy Cloud — 云端 AI"
|
||||
title="Comfy Cloud - 云端 AI"
|
||||
description={t('cloud.hero.subtitle', 'zh-CN')}
|
||||
keywords={['comfyui web app', 'comfyui app', 'comfyui online', 'comfyui cloud', 'ComfyUI 网页版', 'ComfyUI 云端', 'ComfyUI 应用', 'Comfy Cloud', '云端 ComfyUI']}
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ const productId = jsonLdId(url, 'product')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="定价 — Comfy Cloud"
|
||||
title="定价 - Comfy Cloud"
|
||||
mainEntityId={productId}
|
||||
breadcrumbs={[
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="联系我们 — Comfy"
|
||||
title="联系我们 - Comfy"
|
||||
pageType="ContactPage"
|
||||
mainEntityId={organizationId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
|
||||
@@ -11,7 +11,7 @@ import { loadStories } from '../../utils/loadStories'
|
||||
const stories = (await loadStories('zh-CN')).map(toCardProps)
|
||||
---
|
||||
|
||||
<BaseLayout title="客户故事 — Comfy">
|
||||
<BaseLayout title="客户故事 - Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<StorySection stories={stories} locale="zh-CN" />
|
||||
<FeedbackSection locale="zh-CN" client:load />
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function getStaticPaths() {
|
||||
const { entry, next } = Astro.props
|
||||
---
|
||||
|
||||
<BaseLayout title={`${entry.data.title} — Comfy`}>
|
||||
<BaseLayout title={`${entry.data.title} - Comfy`}>
|
||||
<DetailHeroSection
|
||||
label={entry.data.category}
|
||||
title={entry.data.title}
|
||||
|
||||
@@ -58,7 +58,7 @@ const learningResource: JsonLdNode = {
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={`${title} — Comfy`}
|
||||
title={`${title} - Comfy`}
|
||||
description={description}
|
||||
ogImage={demo.ogImage}
|
||||
mainEntityId={learningId}
|
||||
|
||||
@@ -3,7 +3,7 @@ import BaseLayout from '../../../layouts/BaseLayout.astro'
|
||||
import { t } from '../../../i18n/translations'
|
||||
---
|
||||
|
||||
<BaseLayout title="演示 — Comfy" description="ComfyUI 的互动演示和教程。">
|
||||
<BaseLayout title="演示 - Comfy" description="ComfyUI 的互动演示和教程。">
|
||||
<section class="flex min-h-[60vh] items-center justify-center px-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-primary-comfy-canvas text-4xl font-light">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
absoluteUrl,
|
||||
comfyUiApplicationNode,
|
||||
comfyUiSoftwareId,
|
||||
comfyUiSourceCodeNode,
|
||||
pageContext,
|
||||
} from '../../utils/jsonLd'
|
||||
|
||||
@@ -23,7 +24,7 @@ const { siteUrl, locale } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="下载 Comfy 桌面版 — 在您的硬件上运行 AI"
|
||||
title="下载 Comfy 桌面版 - 在您的硬件上运行 AI"
|
||||
description={t('download.hero.subtitle', 'zh-CN')}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
breadcrumbs={[
|
||||
@@ -33,7 +34,7 @@ const { siteUrl, locale } = pageContext(
|
||||
},
|
||||
{ name: t('breadcrumb.download', locale) },
|
||||
]}
|
||||
extraJsonLd={[comfyUiApplicationNode(siteUrl)]}
|
||||
extraJsonLd={[comfyUiApplicationNode(siteUrl), comfyUiSourceCodeNode(siteUrl)]}
|
||||
keywords={['comfyui app', 'comfyui desktop app', 'comfyui download', 'ComfyUI 下载', 'ComfyUI 桌面应用', 'ComfyUI 应用', 'ComfyUI Windows', 'ComfyUI macOS', 'ComfyUI Linux']}
|
||||
>
|
||||
<CloudBannerSection locale="zh-CN" />
|
||||
|
||||
@@ -5,7 +5,7 @@ import GallerySection from '../../components/gallery/GallerySection.vue'
|
||||
import ContactSection from '../../components/gallery/ContactSection.vue'
|
||||
---
|
||||
|
||||
<BaseLayout title="作品集 — Comfy">
|
||||
<BaseLayout title="作品集 - Comfy">
|
||||
<HeroSection locale="zh-CN" />
|
||||
<GallerySection locale="zh-CN" client:load />
|
||||
<ContactSection locale="zh-CN" />
|
||||
|
||||
@@ -24,7 +24,7 @@ const { siteUrl } = pageContext(
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Comfy — 视觉 AI 的最强可控性"
|
||||
title="Comfy - 视觉 AI 的最强可控性"
|
||||
description={t('hero.subtitle', 'zh-CN')}
|
||||
mainEntityId={comfyUiSoftwareId(siteUrl)}
|
||||
extraJsonLd={[comfyUiApplicationNode(siteUrl), comfyUiSourceCodeNode(siteUrl)]}
|
||||
|
||||
57
apps/website/src/pages/zh-CN/individual-submission.astro
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro'
|
||||
import PlansPricingCta from '../../components/individual-submission/PlansPricingCta.vue'
|
||||
import HeroSection from '../../components/legal/HeroSection.vue'
|
||||
import { getRoutes } from '../../config/routes'
|
||||
|
||||
const routes = getRoutes('zh-CN')
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="感谢您的联系 - Comfy"
|
||||
description="感谢您的联系。根据您提供的信息,我们的自助服务套餐之一可能更适合您。"
|
||||
noindex
|
||||
>
|
||||
<HeroSection title="感谢您的联系。" class="text-center" />
|
||||
|
||||
<section class="-mt-8 px-6 pb-24 lg:-mt-12 lg:pb-40">
|
||||
<div
|
||||
class="text-primary-comfy-canvas mx-auto flex max-w-2xl flex-col gap-6 text-center text-base font-light lg:text-lg"
|
||||
>
|
||||
<p>
|
||||
根据您提供的信息,我们的自助服务套餐之一可能更适合您。面向个人创作者的
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Standard</strong
|
||||
>、
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Creator</strong
|
||||
>
|
||||
和
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Pro</strong
|
||||
>,以及我们全新的
|
||||
<strong class="text-primary-comfy-yellow font-semibold italic">
|
||||
Teams</strong
|
||||
> 套餐,可让多位用户共享账单。
|
||||
</p>
|
||||
|
||||
<PlansPricingCta href={routes.cloudPricing} label="查看套餐与价格 →" />
|
||||
|
||||
<p class="text-primary-warm-gray mt-8 text-sm">
|
||||
仍然认为您有企业级需求?我们很乐意为您提供帮助。邮箱:<a
|
||||
href="mailto:gtm-team@comfy.org"
|
||||
class="text-primary-comfy-yellow whitespace-nowrap underline underline-offset-4 hover:no-underline"
|
||||
>gtm-team@comfy.org</a
|
||||
>
|
||||
</p>
|
||||
|
||||
<p class="text-primary-warm-gray text-sm">
|
||||
需要其他方面的帮助?邮箱:<a
|
||||
href="mailto:support@comfy.org"
|
||||
class="text-primary-comfy-yellow whitespace-nowrap underline underline-offset-4 hover:no-underline"
|
||||
>support@comfy.org</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
@@ -11,7 +11,7 @@ import { learningEvents } from '../../data/events'
|
||||
const routes = getRoutes('zh-CN')
|
||||
---
|
||||
|
||||
<BaseLayout title="学习 — Comfy">
|
||||
<BaseLayout title="学习 - Comfy">
|
||||
<HeroSection locale="zh-CN" client:load />
|
||||
<FeaturedWorkflowSection locale="zh-CN" client:visible />
|
||||
<TutorialsSection locale="zh-CN" client:visible />
|
||||
|
||||