refactor: adapt docs links for product prefixes (#37565)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Stephen Zhou 2026-06-23 11:24:29 +08:00 committed by GitHub
parent f380bbaa10
commit 855bb32306
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 692 additions and 112 deletions

View File

@ -17,7 +17,7 @@ vi.mock('@/next/navigation', () => ({
// Mock useDocLink hook
vi.mock('@/context/i18n', () => ({
useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path || ''}`,
useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path?.startsWith('/use-dify/') ? `/cloud${path}` : path || ''}`,
}))
// Mock external context providers (these are external dependencies)
@ -155,7 +155,7 @@ describe('ExternalKnowledgeBaseCreate', () => {
renderComponent()
const docLink = screen.getByText('dataset.connectHelper.helper4')
expect(docLink)!.toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/knowledge/connect-external-knowledge-base')
expect(docLink)!.toHaveAttribute('href', 'https://docs.dify.ai/en/cloud/use-dify/knowledge/connect-external-knowledge-base')
expect(docLink)!.toHaveAttribute('target', '_blank')
expect(docLink)!.toHaveAttribute('rel', 'noopener noreferrer')
})

View File

@ -22,6 +22,7 @@ vi.mock('react-i18next', () => ({
vi.mock('@/context/i18n', () => ({
defaultDocBaseUrl: 'https://docs.dify.ai',
getDocHomePath: () => '/home',
}))
vi.mock('@/i18n-config/language', () => ({
@ -45,7 +46,7 @@ describe('docsCommand', () => {
docsCommand.execute?.()
expect(openSpy).toHaveBeenCalledWith(
expect.stringContaining('https://docs.dify.ai'),
'https://docs.dify.ai/en/home',
'_blank',
'noopener,noreferrer',
)
@ -85,7 +86,11 @@ describe('docsCommand', () => {
const handlers = vi.mocked(registerCommands).mock.calls[0]![0]
await handlers['navigation.doc']!()
expect(openSpy).toHaveBeenCalledWith('https://docs.dify.ai/en', '_blank', 'noopener,noreferrer')
expect(openSpy).toHaveBeenCalledWith(
'https://docs.dify.ai/en/home',
'_blank',
'noopener,noreferrer',
)
openSpy.mockRestore()
})

View File

@ -2,13 +2,20 @@ import type { SlashCommandHandler } from './types'
import { RiBookOpenLine } from '@remixicon/react'
import * as React from 'react'
import { getI18n } from 'react-i18next'
import { defaultDocBaseUrl } from '@/context/i18n'
import { defaultDocBaseUrl, getDocHomePath } from '@/context/i18n'
import { getDocLanguage } from '@/i18n-config/language'
import { registerCommands, unregisterCommands } from './command-bus'
// Documentation command dependency types - no external dependencies needed
type DocDeps = Record<string, never>
const getDocsHomeUrl = () => {
const i18n = getI18n()
const currentLocale = i18n.language
const docLanguage = getDocLanguage(currentLocale)
return `${defaultDocBaseUrl}/${docLanguage}${getDocHomePath()}`
}
/**
* Documentation command - Opens help documentation
*/
@ -19,11 +26,7 @@ export const docsCommand: SlashCommandHandler<DocDeps> = {
// Direct execution function
execute: () => {
const i18n = getI18n()
const currentLocale = i18n.language
const docLanguage = getDocLanguage(currentLocale)
const url = `${defaultDocBaseUrl}/${docLanguage}`
window.open(url, '_blank', 'noopener,noreferrer')
window.open(getDocsHomeUrl(), '_blank', 'noopener,noreferrer')
},
async search(args: string, locale: string = 'en') {
@ -43,14 +46,9 @@ export const docsCommand: SlashCommandHandler<DocDeps> = {
},
register(_deps: DocDeps) {
const i18n = getI18n()
registerCommands({
'navigation.doc': async (_args) => {
// Get the current language from i18n
const currentLocale = i18n.language
const docLanguage = getDocLanguage(currentLocale)
const url = `${defaultDocBaseUrl}/${docLanguage}`
window.open(url, '_blank', 'noopener,noreferrer')
window.open(getDocsHomeUrl(), '_blank', 'noopener,noreferrer')
},
})
},

View File

@ -11,8 +11,8 @@ describe('Empty State', () => {
expect(screen.getByText('common.apiBasedExtension.title')).toBeInTheDocument()
const link = screen.getByText('common.apiBasedExtension.link')
expect(link).toBeInTheDocument()
// The real useDocLink includes the language prefix (defaulting to /en in tests)
expect(link.closest('a')).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/api-extension/api-extension')
// The real useDocLink includes language and product prefixes in tests.
expect(link.closest('a')).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/api-extension/api-extension')
})
})
})

View File

@ -578,7 +578,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('common.toolsPage.toolPlugin')).toHaveLength(2)
expect(screen.getByText('common.toolsPage.description')).toBeInTheDocument()
expect(screen.getByText('common.toolsPage.description').closest('[class*="max-w-[1600px]"]')).toHaveClass('px-6')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/tools')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools')
})
it('aligns model provider headers to the unified content frame', () => {
@ -600,7 +600,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('MCP')).toHaveLength(2)
expect(screen.getByText('common.mcpPage.description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/build/mcp')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/build/mcp')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})
@ -609,7 +609,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('common.settings.swaggerAPIAsTool')).toHaveLength(2)
expect(screen.getByText('common.swaggerAPIAsToolPage.description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/tools#custom-tool')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#custom-tool')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})
@ -630,7 +630,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('common.settings.customEndpoint')).toHaveLength(2)
expect(screen.getByText('common.apiBasedExtensionPage.description')).toBeInTheDocument()
expect(screen.getByTestId('api-extension-toolbar')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/api-extension/api-extension')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/api-extension/api-extension')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})
@ -652,7 +652,7 @@ describe('IntegrationsPage', () => {
expect(screen.getAllByText('workflow.common.workflowAsTool')).toHaveLength(2)
expect(screen.getByText('common.workflowAsToolPage.description')).toBeInTheDocument()
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/tools#workflow-tool')
expect(screen.getByRole('link', { name: /common\.modelProvider\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#workflow-tool')
expect(screen.queryByText('common.toolsPage.description')).not.toBeInTheDocument()
})

View File

@ -15,7 +15,7 @@ vi.mock('@/context/app-context', () => ({
// Mock useLocale and useDocLink
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
useDocLink: () => (path: string) => `https://docs.dify.ai/en/${path?.startsWith('/') ? path.slice(1) : path}`,
useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path?.startsWith('/use-dify/') ? `/cloud${path}` : path || ''}`,
}))
// Mock getLanguage
@ -132,7 +132,7 @@ describe('CustomCreateCard', () => {
render(<CustomCreateCard onRefreshData={mockOnRefreshData} />)
const docLink = screen.getByText('tools.swaggerAPIAsToolTip').closest('a')
expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/tools#custom-tool')
expect(docLink).toHaveAttribute('href', 'https://docs.dify.ai/en/cloud/use-dify/workspace/tools#custom-tool')
expect(docLink).toHaveAttribute('target', '_blank')
expect(docLink).toHaveAttribute('rel', 'noopener noreferrer')
})

View File

@ -84,7 +84,7 @@ describe('Empty', () => {
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.goToStudio/i })).toHaveAttribute('href', '/apps')
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.learnMore/i })).toHaveAttribute('target', '_blank')
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/use-dify/workspace/tools#workflow-tool')
expect(screen.getByRole('link', { name: /tools\.workflowToolEmpty\.learnMore/i })).toHaveAttribute('href', 'https://docs.dify.ai/en/self-host/use-dify/workspace/tools#workflow-tool')
})
})

View File

@ -54,6 +54,18 @@ describe('useAvailableNodesMetaData', () => {
})
})
it('should use explicit docs pages and skip nodes without generated docs pages', () => {
mockUseIsChatMode.mockReturnValue(false)
const { result } = renderHook(() => useAvailableNodesMetaData())
expect(result.current.nodesMap?.[BlockEnum.End]?.metaData.helpLinkUri).toBe('/docs/use-dify/nodes/output')
expect(result.current.nodesMap?.[BlockEnum.IterationStart]?.metaData.helpLinkUri).toBeUndefined()
expect(result.current.nodesMap?.[BlockEnum.LoopStart]?.metaData.helpLinkUri).toBeUndefined()
expect(result.current.nodesMap?.[BlockEnum.LoopEnd]?.metaData.helpLinkUri).toBeUndefined()
expect(result.current.nodesMap?.[BlockEnum.Start]?.metaData.helpLinkUri).toBe('/docs/use-dify/nodes/user-input')
})
it('should expose Agent v2 instead of legacy Agent when Agent v2 is enabled', () => {
mockUseIsChatMode.mockReturnValue(false)

View File

@ -14,8 +14,20 @@ import TriggerWebhookDefault from '@/app/components/workflow/nodes/trigger-webho
import { BlockEnum } from '@/app/components/workflow/types'
import { useDocLink } from '@/context/i18n'
import { isAgentV2Enabled } from '@/features/agent-v2/feature-flag'
import { docPathProductAvailability } from '@/types/doc-paths'
import { useIsChatMode } from './use-is-chat-mode'
const getNodeHelpLinkPath = (helpLinkUri?: string): DocPathWithoutLang | undefined => {
if (!helpLinkUri)
return undefined
const helpLinkPath = `/use-dify/nodes/${helpLinkUri}`
if (!docPathProductAvailability[helpLinkPath])
return undefined
return helpLinkPath as DocPathWithoutLang
}
export const useAvailableNodesMetaData = () => {
const { t } = useTranslation()
const isChatMode = useIsChatMode()
@ -57,14 +69,14 @@ export const useAvailableNodesMetaData = () => {
const { metaData } = node
const title = t(`blocks.${metaData.type}`, { ns: 'workflow' })
const description = t(`blocksAbout.${metaData.type}` as I18nKeysWithPrefix<'workflow', 'blocksAbout.'>, { ns: 'workflow' })
const helpLinkPath = `/use-dify/nodes/${metaData.helpLinkUri}` as DocPathWithoutLang
const helpLinkPath = getNodeHelpLinkPath(metaData.helpLinkUri)
return {
...node,
metaData: {
...metaData,
title,
description,
helpLinkUri: docLink(helpLinkPath),
helpLinkUri: helpLinkPath ? docLink(helpLinkPath) : undefined,
},
defaultValue: {
...node.defaultValue,

View File

@ -318,7 +318,7 @@ describe('NodeSelector', () => {
expect(await screen.findByText('workflow.tabs.unconfiguredStartDisabledTip')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'workflow.tabs.startDisabledTipLearnMore' })).toHaveAttribute(
'href',
'https://docs.dify.ai/en/use-dify/nodes/trigger/overview',
'https://docs.dify.ai/en/self-host/use-dify/nodes/trigger/overview',
)
expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument()
})

View File

@ -6,6 +6,7 @@ import { genNodeMetaData } from '@/app/components/workflow/utils'
const metaData = genNodeMetaData({
sort: 2.1,
type: BlockEnum.End,
helpLinkUri: 'output',
isRequired: false,
})
const nodeDefault: NodeDefault<EndNodeType> = {

View File

@ -5,6 +5,10 @@ import { useTranslation } from '#i18n'
import { getDocLanguage } from '@/i18n-config/language'
import { defaultDocBaseUrl, useDocLink } from './i18n'
const mockConfig = vi.hoisted(() => ({
IS_CLOUD_EDITION: true,
}))
// Mock dependencies
vi.mock('#i18n', () => ({
useTranslation: vi.fn(() => ({
@ -12,6 +16,12 @@ vi.mock('#i18n', () => ({
})),
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() {
return mockConfig.IS_CLOUD_EDITION
},
}))
vi.mock('@/i18n-config/language', () => ({
getDocLanguage: vi.fn((locale: string) => {
const map: Record<string, string> = {
@ -28,6 +38,7 @@ vi.mock('@/i18n-config/language', () => ({
describe('useDocLink', () => {
beforeEach(() => {
vi.clearAllMocks()
mockConfig.IS_CLOUD_EDITION = true
vi.mocked(useTranslation).mockReturnValue({
i18n: { language: 'en-US' },
} as ReturnType<typeof useTranslation>)
@ -45,28 +56,28 @@ describe('useDocLink', () => {
it('should use default base URL when no baseUrl provided', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current()
expect(url).toBe(`${defaultDocBaseUrl}/en`)
expect(url).toBe(`${defaultDocBaseUrl}/en/home`)
})
it('should use custom base URL when provided', () => {
const customBaseUrl = 'https://custom.docs.com'
const { result } = renderHook(() => useDocLink(customBaseUrl))
const url = result.current()
expect(url).toBe(`${customBaseUrl}/en`)
expect(url).toBe(`${customBaseUrl}/en/home`)
})
it('should remove trailing slash from base URL', () => {
const baseUrlWithSlash = 'https://docs.dify.ai/'
const { result } = renderHook(() => useDocLink(baseUrlWithSlash))
const url = result.current('/use-dify/getting-started/introduction')
expect(url).toBe('https://docs.dify.ai/en/use-dify/getting-started/introduction')
expect(url).toBe('https://docs.dify.ai/en/cloud/use-dify/getting-started/introduction')
})
it('should handle base URL without trailing slash', () => {
const baseUrlWithoutSlash = 'https://docs.dify.ai'
const { result } = renderHook(() => useDocLink(baseUrlWithoutSlash))
const url = result.current('/use-dify/getting-started/introduction')
expect(url).toBe('https://docs.dify.ai/en/use-dify/getting-started/introduction')
expect(url).toBe('https://docs.dify.ai/en/cloud/use-dify/getting-started/introduction')
})
})
@ -74,19 +85,31 @@ describe('useDocLink', () => {
it('should handle path parameter', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/getting-started/introduction')
expect(url).toBe(`${defaultDocBaseUrl}/en/use-dify/getting-started/introduction`)
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/getting-started/introduction`)
})
it('should handle empty path', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current()
expect(url).toBe(`${defaultDocBaseUrl}/en`)
expect(url).toBe(`${defaultDocBaseUrl}/en/home`)
})
it('should handle undefined path', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current(undefined)
expect(url).toBe(`${defaultDocBaseUrl}/en`)
expect(url).toBe(`${defaultDocBaseUrl}/en/home`)
})
it('should keep common docs path without product prefix', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/learn/key-concepts' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/learn/key-concepts`)
})
it('should keep explicit product docs path without adding another product prefix', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/cloud/use-dify/build/mcp' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/build/mcp`)
})
})
@ -99,12 +122,12 @@ describe('useDocLink', () => {
const pathMap: DocPathMap = {
'zh-Hans': '/use-dify/getting-started/introduction',
'en-US': '/use-dify/getting-started/quick-start',
'en-US': '/use-dify/build/mcp',
}
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/getting-started/quick-start' as DocPathWithoutLang, pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/zh/use-dify/getting-started/introduction`)
const url = result.current('/use-dify/build/mcp', pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/zh/cloud/use-dify/getting-started/introduction`)
})
it('should use default path when locale not in pathMap', () => {
@ -115,18 +138,76 @@ describe('useDocLink', () => {
const pathMap: DocPathMap = {
'zh-Hans': '/use-dify/getting-started/introduction',
'en-US': '/use-dify/getting-started/quick-start',
'en-US': '/use-dify/build/mcp',
}
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/getting-started/quick-start' as DocPathWithoutLang, pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/ja/use-dify/getting-started/quick-start`)
const url = result.current('/use-dify/build/mcp', pathMap)
expect(url).toBe(`${defaultDocBaseUrl}/ja/cloud/use-dify/build/mcp`)
})
it('should handle undefined pathMap', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/getting-started/introduction', undefined)
expect(url).toBe(`${defaultDocBaseUrl}/en/use-dify/getting-started/introduction`)
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/getting-started/introduction`)
})
})
describe('Product prefix handling', () => {
it('should add cloud product prefix for product docs available in both editions', () => {
mockConfig.IS_CLOUD_EDITION = true
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/build/mcp')
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/build/mcp`)
})
it('should add self-host product prefix for product docs available in both editions outside cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = false
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/build/mcp')
expect(url).toBe(`${defaultDocBaseUrl}/en/self-host/use-dify/build/mcp`)
})
it('should use the existing cloud docs path for cloud-only product docs outside cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = false
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/workspace/subscription-management#dify-for-education')
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/workspace/subscription-management#dify-for-education`)
})
it('should use the existing self-host docs path for self-host-only product docs in cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = true
const { result } = renderHook(() => useDocLink())
const url = result.current('/deploy/overview')
expect(url).toBe(`${defaultDocBaseUrl}/en/self-host/deploy/overview`)
})
it('should not add a product prefix for unknown productless paths', () => {
mockConfig.IS_CLOUD_EDITION = false
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/unknown-page' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/use-dify/unknown-page`)
})
it('should open shared docs home when no path is provided outside cloud edition', () => {
mockConfig.IS_CLOUD_EDITION = false
const { result } = renderHook(() => useDocLink())
const url = result.current()
expect(url).toBe(`${defaultDocBaseUrl}/en/home`)
})
it('should keep self-host deploy paths without adding use-dify product prefix', () => {
mockConfig.IS_CLOUD_EDITION = true
const { result } = renderHook(() => useDocLink())
const url = result.current('/self-host/deploy/overview' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/self-host/deploy/overview`)
})
})
@ -232,7 +313,7 @@ describe('useDocLink', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/getting-started/introduction')
expect(url).toBe(`${defaultDocBaseUrl}/zh/use-dify/getting-started/introduction`)
expect(url).toBe(`${defaultDocBaseUrl}/zh/cloud/use-dify/getting-started/introduction`)
})
})
@ -240,15 +321,15 @@ describe('useDocLink', () => {
it('should handle path with anchor', () => {
const { result } = renderHook(() => useDocLink())
const url = result.current('/use-dify/getting-started/introduction#overview' as DocPathWithoutLang)
expect(url).toBe(`${defaultDocBaseUrl}/en/use-dify/getting-started/introduction#overview`)
expect(url).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/getting-started/introduction#overview`)
})
it('should handle multiple calls with same hook instance', () => {
const { result } = renderHook(() => useDocLink())
const url1 = result.current('/use-dify/getting-started/introduction')
const url2 = result.current('/use-dify/getting-started/quick-start')
expect(url1).toBe(`${defaultDocBaseUrl}/en/use-dify/getting-started/introduction`)
expect(url2).toBe(`${defaultDocBaseUrl}/en/use-dify/getting-started/quick-start`)
const url2 = result.current('/use-dify/build/mcp')
expect(url1).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/getting-started/introduction`)
expect(url2).toBe(`${defaultDocBaseUrl}/en/cloud/use-dify/build/mcp`)
})
})
})

View File

@ -1,9 +1,10 @@
import type { Locale } from '@/i18n-config/language'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import type { DocPathWithoutLang, DocsProduct } from '@/types/doc-paths'
import { useCallback } from 'react'
import { useTranslation } from '#i18n'
import { IS_CLOUD_EDITION } from '@/config'
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
import { apiReferencePathTranslations } from '@/types/doc-paths'
import { apiReferencePathTranslations, docPathProductAvailability } from '@/types/doc-paths'
export const useLocale = () => {
const { i18n } = useTranslation()
@ -24,6 +25,44 @@ export const useGetPricingPageLanguage = () => {
export const defaultDocBaseUrl = 'https://docs.dify.ai'
export type DocPathMap = Partial<Record<Locale, DocPathWithoutLang>>
export const getDocHomePath = () => '/home'
const getCurrentDocsProduct = (): DocsProduct => {
return IS_CLOUD_EDITION ? 'cloud' : 'self-host'
}
const splitPathHash = (path: string) => {
const hashIndex = path.indexOf('#')
if (hashIndex === -1) {
return {
pathname: path,
hash: '',
}
}
return {
pathname: path.slice(0, hashIndex),
hash: path.slice(hashIndex),
}
}
const getProductAwarePath = (path: string): string => {
const { pathname, hash } = splitPathHash(path)
const availableProducts = docPathProductAvailability[pathname]
if (!availableProducts?.length)
return path
const currentProduct = getCurrentDocsProduct()
const targetProduct = availableProducts.includes(currentProduct)
? currentProduct
: availableProducts[0]
if (!targetProduct)
return path
return `/${targetProduct}${pathname}${hash}`
}
export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathMap?: DocPathMap) => string) => {
let baseDocUrl = baseUrl || defaultDocBaseUrl
baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl
@ -44,6 +83,12 @@ export const useDocLink = (baseUrl?: string): ((path?: DocPathWithoutLang, pathM
}
}
}
else if (!targetPath) {
targetPath = getDocHomePath()
}
else {
targetPath = getProductAwarePath(targetPath)
}
return `${baseDocUrl}${languagePrefix}${targetPath}`
},

View File

@ -84,7 +84,7 @@ export function AgentAccessPage({
<p className="mt-1 flex min-w-0 flex-wrap items-center gap-x-0.5 system-xs-regular text-text-tertiary">
<span>{t('agentDetail.access.description')}</span>
<a
href={docLink('/use-dify/publish/webapp/web-app-access')}
href={docLink('/use-dify/publish/webapp/web-app-settings')}
target="_blank"
rel="noreferrer"
className="inline-flex shrink-0 items-center gap-0.5 rounded-sm text-text-accent hover:underline focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"

View File

@ -11,7 +11,8 @@ import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const DOCS_JSON_URL = 'https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json'
const DEFAULT_DOCS_JSON_URL = 'https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json'
const DOCS_JSON_URL = process.env.DOCS_JSON_URL || DEFAULT_DOCS_JSON_URL
const OUTPUT_PATH = path.resolve(__dirname, '../types/doc-paths.ts')
type NavItem = string | NavObject | NavItem[]
@ -21,6 +22,9 @@ type NavObject = {
groups?: NavItem[]
dropdowns?: NavItem[]
languages?: NavItem[]
products?: NavItem[]
tabs?: NavItem[]
menu?: NavItem[]
versions?: NavItem[]
openapi?: string
[key: string]: unknown
@ -58,7 +62,15 @@ type DocsJson = {
[key: string]: unknown
}
const OPENAPI_BASE_URL = 'https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/'
const OPENAPI_BASE_URL = (process.env.DOCS_OPENAPI_BASE_URL || new URL('.', DOCS_JSON_URL).toString()).replace(/\/?$/, '/')
const DOCS_PRODUCTS = ['cloud', 'self-host'] as const
type DocsProduct = typeof DOCS_PRODUCTS[number]
type ProductAvailability = Record<string, Set<DocsProduct>>
function isDocsProduct(segment: string): segment is DocsProduct {
return DOCS_PRODUCTS.includes(segment as DocsProduct)
}
/**
* Convert summary to URL slug
@ -112,6 +124,15 @@ function extractOpenAPIPaths(item: NavItem | undefined, paths: Set<string> = new
if (item.languages)
extractOpenAPIPaths(item.languages, paths)
if (item.products)
extractOpenAPIPaths(item.products, paths)
if (item.tabs)
extractOpenAPIPaths(item.tabs, paths)
if (item.menu)
extractOpenAPIPaths(item.menu, paths)
if (item.versions)
extractOpenAPIPaths(item.versions, paths)
}
@ -199,6 +220,15 @@ function extractPaths(item: NavItem | undefined, paths: Set<string> = new Set())
if (item.languages)
extractPaths(item.languages, paths)
if (item.products)
extractPaths(item.products, paths)
if (item.tabs)
extractPaths(item.tabs, paths)
if (item.menu)
extractPaths(item.menu, paths)
// Handle versions in navigation
if (item.versions)
extractPaths(item.versions, paths)
@ -207,33 +237,68 @@ function extractPaths(item: NavItem | undefined, paths: Set<string> = new Set())
return paths
}
function addPathToGroup(groups: Record<string, Set<string>>, pathWithoutLang: string): void {
const parts = pathWithoutLang.split('/')
const section = parts[0]
if (!section)
return
if (!groups[section])
groups[section] = new Set()
groups[section]!.add(pathWithoutLang)
}
function getProductPathInfo(pathWithoutLang: string): { product: DocsProduct, pathWithoutProduct: string } | undefined {
const parts = pathWithoutLang.split('/')
const [product, ...rest] = parts
if (!product || !isDocsProduct(product) || rest.length === 0)
return undefined
return {
product,
pathWithoutProduct: rest.join('/'),
}
}
/**
* Group paths by their prefix structure
*/
function groupPathsBySection(paths: Set<string>): Record<string, Set<string>> {
function groupPathsBySection(paths: Set<string>): { groups: Record<string, Set<string>>, productAvailability: ProductAvailability } {
const groups: Record<string, Set<string>> = {}
const productAvailability: ProductAvailability = {}
for (const fullPath of paths) {
// Skip non-doc paths (like .json files for OpenAPI)
if (fullPath.endsWith('.json'))
continue
// Remove language prefix (en/, zh/, ja/)
const withoutLang = fullPath.replace(/^(en|zh|ja)\//, '')
if (!withoutLang || withoutLang === fullPath)
continue
// Get section (first part of path)
const parts = withoutLang.split('/')
const section = parts[0]
// Skip non-doc paths (like .json files for OpenAPI)
if (withoutLang.endsWith('.json') || withoutLang === 'None')
continue
if (!groups[section!])
groups[section!] = new Set()
addPathToGroup(groups, withoutLang)
groups[section!]!.add(withoutLang)
const productPathInfo = getProductPathInfo(withoutLang)
if (productPathInfo) {
const productlessPath = productPathInfo.pathWithoutProduct
const normalizedPath = `/${productlessPath}`
addPathToGroup(groups, productlessPath)
if (!productAvailability[normalizedPath])
productAvailability[normalizedPath] = new Set()
productAvailability[normalizedPath]!.add(productPathInfo.product)
}
}
return groups
return {
groups,
productAvailability,
}
}
/**
@ -251,6 +316,7 @@ function sectionToTypeName(section: string): string {
*/
function generateTypeDefinitions(
groups: Record<string, Set<string>>,
productAvailability: ProductAvailability,
apiReferencePaths: string[],
apiPathTranslations: Record<string, { zh?: string, ja?: string }>,
): string {
@ -258,11 +324,12 @@ function generateTypeDefinitions(
'// GENERATE BY script',
'// DON NOT EDIT IT MANUALLY',
'//',
'// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json',
`// Generated from: ${DOCS_JSON_URL}`,
`// Generated at: ${new Date().toISOString()}`,
'',
'// Language prefixes',
'export type DocLanguage = \'en\' | \'zh\' | \'ja\'',
'export type DocsProduct = \'cloud\' | \'self-host\'',
'',
]
@ -321,10 +388,14 @@ function generateTypeDefinitions(
lines.push(' | `${DocPathWithoutLangBase}#${string}`')
lines.push('')
// Generate full path type with language prefix
lines.push('// Full documentation path with language prefix')
// eslint-disable-next-line no-template-curly-in-string
lines.push('export type DifyDocPath = `${DocLanguage}/${DocPathWithoutLang}`')
// Generate product availability map for productless runtime links.
lines.push('// Product availability for productless docs paths')
lines.push('export const docPathProductAvailability: Record<string, readonly DocsProduct[]> = {')
for (const path of Object.keys(productAvailability).sort()) {
const products = [...productAvailability[path]!].sort((a, b) => DOCS_PRODUCTS.indexOf(a) - DOCS_PRODUCTS.indexOf(b))
lines.push(` '${path}': [${products.map(product => `'${product}'`).join(', ')}],`)
}
lines.push('}')
lines.push('')
// Generate API reference path translations map
@ -423,12 +494,13 @@ async function main(): Promise<void> {
console.log(`Generated ${Object.keys(apiPathTranslations).length} API path translations`)
// Group by section
const groups = groupPathsBySection(allPaths)
const { groups, productAvailability } = groupPathsBySection(allPaths)
console.log(`Grouped into ${Object.keys(groups).length} sections:`, Object.keys(groups))
console.log(`Found ${Object.keys(productAvailability).length} product-aware paths`)
// Generate TypeScript
const tsContent = generateTypeDefinitions(groups, uniqueEnApiPaths, apiPathTranslations)
const tsContent = generateTypeDefinitions(groups, productAvailability, uniqueEnApiPaths, apiPathTranslations)
// Write to file
await writeFile(OUTPUT_PATH, tsContent, 'utf-8')

View File

@ -1,28 +1,127 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
//
// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/refs/heads/main/docs.json
// Generated at: 2026-03-25T03:18:49.626Z
// Generated from: https://raw.githubusercontent.com/langgenius/dify-docs/feat/audience-products/docs.json
// Generated at: 2026-06-17T04:42:51.293Z
// Language prefixes
export type DocLanguage = 'en' | 'zh' | 'ja'
export type DocsProduct = 'cloud' | 'self-host'
// Cloud paths
type CloudPath =
| '/cloud/use-dify/build/additional-features'
| '/cloud/use-dify/build/agent'
| '/cloud/use-dify/build/chatbot'
| '/cloud/use-dify/build/mcp'
| '/cloud/use-dify/build/orchestrate-node'
| '/cloud/use-dify/build/predefined-error-handling-logic'
| '/cloud/use-dify/build/shortcut-key'
| '/cloud/use-dify/build/text-generator'
| '/cloud/use-dify/build/version-control'
| '/cloud/use-dify/build/workflow-chatflow'
| '/cloud/use-dify/build/workflow-collaboration'
| '/cloud/use-dify/debug/error-type'
| '/cloud/use-dify/debug/history-and-logs'
| '/cloud/use-dify/debug/step-run'
| '/cloud/use-dify/debug/variable-inspect'
| '/cloud/use-dify/getting-started/introduction'
| '/cloud/use-dify/knowledge/connect-external-knowledge-base'
| '/cloud/use-dify/knowledge/create-knowledge/chunking-and-cleaning-text'
| '/cloud/use-dify/knowledge/create-knowledge/import-text-data/readme'
| '/cloud/use-dify/knowledge/create-knowledge/import-text-data/sync-from-notion'
| '/cloud/use-dify/knowledge/create-knowledge/import-text-data/sync-from-website'
| '/cloud/use-dify/knowledge/create-knowledge/introduction'
| '/cloud/use-dify/knowledge/create-knowledge/setting-indexing-methods'
| '/cloud/use-dify/knowledge/external-knowledge-api'
| '/cloud/use-dify/knowledge/integrate-knowledge-within-application'
| '/cloud/use-dify/knowledge/knowledge-pipeline/authorize-data-source'
| '/cloud/use-dify/knowledge/knowledge-pipeline/create-knowledge-pipeline'
| '/cloud/use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration'
| '/cloud/use-dify/knowledge/knowledge-pipeline/manage-knowledge-base'
| '/cloud/use-dify/knowledge/knowledge-pipeline/publish-knowledge-pipeline'
| '/cloud/use-dify/knowledge/knowledge-pipeline/readme'
| '/cloud/use-dify/knowledge/knowledge-pipeline/upload-files'
| '/cloud/use-dify/knowledge/knowledge-request-rate-limit'
| '/cloud/use-dify/knowledge/manage-knowledge/introduction'
| '/cloud/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api'
| '/cloud/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents'
| '/cloud/use-dify/knowledge/metadata'
| '/cloud/use-dify/knowledge/readme'
| '/cloud/use-dify/knowledge/test-retrieval'
| '/cloud/use-dify/monitor/analysis'
| '/cloud/use-dify/monitor/annotation-reply'
| '/cloud/use-dify/monitor/integrations/integrate-aliyun'
| '/cloud/use-dify/monitor/integrations/integrate-arize'
| '/cloud/use-dify/monitor/integrations/integrate-langfuse'
| '/cloud/use-dify/monitor/integrations/integrate-langsmith'
| '/cloud/use-dify/monitor/integrations/integrate-opik'
| '/cloud/use-dify/monitor/integrations/integrate-phoenix'
| '/cloud/use-dify/monitor/integrations/integrate-weave'
| '/cloud/use-dify/monitor/logs'
| '/cloud/use-dify/nodes/agent'
| '/cloud/use-dify/nodes/answer'
| '/cloud/use-dify/nodes/code'
| '/cloud/use-dify/nodes/doc-extractor'
| '/cloud/use-dify/nodes/http-request'
| '/cloud/use-dify/nodes/human-input'
| '/cloud/use-dify/nodes/ifelse'
| '/cloud/use-dify/nodes/iteration'
| '/cloud/use-dify/nodes/knowledge-retrieval'
| '/cloud/use-dify/nodes/list-operator'
| '/cloud/use-dify/nodes/llm'
| '/cloud/use-dify/nodes/loop'
| '/cloud/use-dify/nodes/output'
| '/cloud/use-dify/nodes/parameter-extractor'
| '/cloud/use-dify/nodes/question-classifier'
| '/cloud/use-dify/nodes/template'
| '/cloud/use-dify/nodes/tools'
| '/cloud/use-dify/nodes/trigger/overview'
| '/cloud/use-dify/nodes/trigger/plugin-trigger'
| '/cloud/use-dify/nodes/trigger/schedule-trigger'
| '/cloud/use-dify/nodes/trigger/webhook-trigger'
| '/cloud/use-dify/nodes/user-input'
| '/cloud/use-dify/nodes/variable-aggregator'
| '/cloud/use-dify/nodes/variable-assigner'
| '/cloud/use-dify/publish/README'
| '/cloud/use-dify/publish/developing-with-apis'
| '/cloud/use-dify/publish/publish-mcp'
| '/cloud/use-dify/publish/publish-to-marketplace'
| '/cloud/use-dify/publish/webapp/chatflow-webapp'
| '/cloud/use-dify/publish/webapp/embedding-in-websites'
| '/cloud/use-dify/publish/webapp/web-app-settings'
| '/cloud/use-dify/publish/webapp/workflow-webapp'
| '/cloud/use-dify/workspace/api-extension/api-extension'
| '/cloud/use-dify/workspace/api-extension/cloudflare-worker'
| '/cloud/use-dify/workspace/api-extension/external-data-tool-api-extension'
| '/cloud/use-dify/workspace/api-extension/moderation-api-extension'
| '/cloud/use-dify/workspace/app-management'
| '/cloud/use-dify/workspace/model-providers'
| '/cloud/use-dify/workspace/personal-account-management'
| '/cloud/use-dify/workspace/plugins'
| '/cloud/use-dify/workspace/readme'
| '/cloud/use-dify/workspace/subscription-management'
| '/cloud/use-dify/workspace/team-members-management'
| '/cloud/use-dify/workspace/tools'
// UseDify paths
type UseDifyPath =
| '/use-dify/build/additional-features'
| '/use-dify/build/goto-anything'
| '/use-dify/build/agent'
| '/use-dify/build/chatbot'
| '/use-dify/build/mcp'
| '/use-dify/build/orchestrate-node'
| '/use-dify/build/predefined-error-handling-logic'
| '/use-dify/build/shortcut-key'
| '/use-dify/build/text-generator'
| '/use-dify/build/version-control'
| '/use-dify/build/workflow-chatflow'
| '/use-dify/build/workflow-collaboration'
| '/use-dify/debug/error-type'
| '/use-dify/debug/history-and-logs'
| '/use-dify/debug/step-run'
| '/use-dify/debug/variable-inspect'
| '/use-dify/getting-started/introduction'
| '/use-dify/getting-started/key-concepts'
| '/use-dify/getting-started/quick-start'
| '/use-dify/knowledge/connect-external-knowledge-base'
| '/use-dify/knowledge/create-knowledge/chunking-and-cleaning-text'
| '/use-dify/knowledge/create-knowledge/import-text-data/readme'
@ -86,24 +185,8 @@ type UseDifyPath =
| '/use-dify/publish/publish-to-marketplace'
| '/use-dify/publish/webapp/chatflow-webapp'
| '/use-dify/publish/webapp/embedding-in-websites'
| '/use-dify/publish/webapp/web-app-access'
| '/use-dify/publish/webapp/web-app-settings'
| '/use-dify/publish/webapp/workflow-webapp'
| '/use-dify/tutorials/article-reader'
| '/use-dify/tutorials/build-ai-image-generation-app'
| '/use-dify/tutorials/customer-service-bot'
| '/use-dify/tutorials/simple-chatbot'
| '/use-dify/tutorials/twitter-chatflow'
| '/use-dify/tutorials/workflow-101/lesson-01'
| '/use-dify/tutorials/workflow-101/lesson-02'
| '/use-dify/tutorials/workflow-101/lesson-03'
| '/use-dify/tutorials/workflow-101/lesson-04'
| '/use-dify/tutorials/workflow-101/lesson-05'
| '/use-dify/tutorials/workflow-101/lesson-06'
| '/use-dify/tutorials/workflow-101/lesson-07'
| '/use-dify/tutorials/workflow-101/lesson-08'
| '/use-dify/tutorials/workflow-101/lesson-09'
| '/use-dify/tutorials/workflow-101/lesson-10'
| '/use-dify/workspace/api-extension/api-extension'
| '/use-dify/workspace/api-extension/cloudflare-worker'
| '/use-dify/workspace/api-extension/external-data-tool-api-extension'
@ -115,25 +198,42 @@ type UseDifyPath =
| '/use-dify/workspace/readme'
| '/use-dify/workspace/subscription-management'
| '/use-dify/workspace/team-members-management'
| '/use-dify/workspace/tools'
// UseDify node paths (without prefix)
type ExtractNodesPath<T> = T extends `/use-dify/nodes/${infer Path}` ? Path : never
export type UseDifyNodesPath = ExtractNodesPath<UseDifyPath>
// SelfHost paths
type SelfHostPath =
| '/self-host/advanced-deployments/local-source-code'
| '/self-host/advanced-deployments/start-the-frontend-docker-container'
| '/self-host/configuration/environments'
| '/self-host/platform-guides/bt-panel'
| '/self-host/platform-guides/dify-premium'
| '/self-host/quick-start/docker-compose'
| '/self-host/quick-start/faqs'
| '/self-host/troubleshooting/common-issues'
| '/self-host/troubleshooting/docker-issues'
| '/self-host/troubleshooting/integrations'
| '/self-host/troubleshooting/storage-and-migration'
| '/self-host/troubleshooting/weaviate-v4-migration'
// Home paths
type HomePath =
| '/home'
// Learn paths
type LearnPath =
| '/learn/key-concepts'
| '/learn/tutorials/article-reader'
| '/learn/tutorials/build-ai-image-generation-app'
| '/learn/tutorials/customer-service-bot'
| '/learn/tutorials/simple-chatbot'
| '/learn/tutorials/twitter-chatflow'
| '/learn/tutorials/workflow-101/lesson-01'
| '/learn/tutorials/workflow-101/lesson-02'
| '/learn/tutorials/workflow-101/lesson-03'
| '/learn/tutorials/workflow-101/lesson-04'
| '/learn/tutorials/workflow-101/lesson-05'
| '/learn/tutorials/workflow-101/lesson-06'
| '/learn/tutorials/workflow-101/lesson-07'
| '/learn/tutorials/workflow-101/lesson-08'
| '/learn/tutorials/workflow-101/lesson-09'
| '/learn/tutorials/workflow-101/lesson-10'
// QuickStart paths
type QuickStartPath =
| '/quick-start'
// Cli paths
type CliPath =
| '/cli/coming-soon'
// DevelopPlugin paths
type DevelopPluginPath =
@ -165,6 +265,7 @@ type DevelopPluginPath =
| '/develop-plugin/features-and-specs/plugin-types/plugin-logging'
| '/develop-plugin/features-and-specs/plugin-types/remote-debug-a-plugin'
| '/develop-plugin/features-and-specs/plugin-types/tool'
| '/develop-plugin/getting-started/choose-plugin-type'
| '/develop-plugin/getting-started/cli'
| '/develop-plugin/getting-started/getting-started-dify-plugin'
| '/develop-plugin/publishing/faq/faq'
@ -177,6 +278,129 @@ type DevelopPluginPath =
| '/develop-plugin/publishing/standards/privacy-protection-guidelines'
| '/develop-plugin/publishing/standards/third-party-signature-verification'
// SelfHost paths
type SelfHostPath =
| '/self-host/deploy/advanced-deployments/local-source-code'
| '/self-host/deploy/advanced-deployments/start-the-frontend-docker-container'
| '/self-host/deploy/configuration/environments'
| '/self-host/deploy/overview'
| '/self-host/deploy/platform-guides/bt-panel'
| '/self-host/deploy/platform-guides/dify-premium'
| '/self-host/deploy/quick-start/docker-compose'
| '/self-host/deploy/quick-start/faqs'
| '/self-host/deploy/troubleshooting/common-issues'
| '/self-host/deploy/troubleshooting/docker-issues'
| '/self-host/deploy/troubleshooting/integrations'
| '/self-host/deploy/troubleshooting/storage-and-migration'
| '/self-host/deploy/troubleshooting/weaviate-v4-migration'
| '/self-host/use-dify/build/additional-features'
| '/self-host/use-dify/build/agent'
| '/self-host/use-dify/build/chatbot'
| '/self-host/use-dify/build/mcp'
| '/self-host/use-dify/build/orchestrate-node'
| '/self-host/use-dify/build/predefined-error-handling-logic'
| '/self-host/use-dify/build/shortcut-key'
| '/self-host/use-dify/build/text-generator'
| '/self-host/use-dify/build/version-control'
| '/self-host/use-dify/build/workflow-chatflow'
| '/self-host/use-dify/build/workflow-collaboration'
| '/self-host/use-dify/debug/error-type'
| '/self-host/use-dify/debug/history-and-logs'
| '/self-host/use-dify/debug/step-run'
| '/self-host/use-dify/debug/variable-inspect'
| '/self-host/use-dify/getting-started/introduction'
| '/self-host/use-dify/knowledge/connect-external-knowledge-base'
| '/self-host/use-dify/knowledge/create-knowledge/chunking-and-cleaning-text'
| '/self-host/use-dify/knowledge/create-knowledge/import-text-data/readme'
| '/self-host/use-dify/knowledge/create-knowledge/import-text-data/sync-from-notion'
| '/self-host/use-dify/knowledge/create-knowledge/import-text-data/sync-from-website'
| '/self-host/use-dify/knowledge/create-knowledge/introduction'
| '/self-host/use-dify/knowledge/create-knowledge/setting-indexing-methods'
| '/self-host/use-dify/knowledge/external-knowledge-api'
| '/self-host/use-dify/knowledge/integrate-knowledge-within-application'
| '/self-host/use-dify/knowledge/knowledge-pipeline/authorize-data-source'
| '/self-host/use-dify/knowledge/knowledge-pipeline/create-knowledge-pipeline'
| '/self-host/use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration'
| '/self-host/use-dify/knowledge/knowledge-pipeline/manage-knowledge-base'
| '/self-host/use-dify/knowledge/knowledge-pipeline/publish-knowledge-pipeline'
| '/self-host/use-dify/knowledge/knowledge-pipeline/readme'
| '/self-host/use-dify/knowledge/knowledge-pipeline/upload-files'
| '/self-host/use-dify/knowledge/manage-knowledge/introduction'
| '/self-host/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api'
| '/self-host/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents'
| '/self-host/use-dify/knowledge/metadata'
| '/self-host/use-dify/knowledge/readme'
| '/self-host/use-dify/knowledge/test-retrieval'
| '/self-host/use-dify/monitor/analysis'
| '/self-host/use-dify/monitor/annotation-reply'
| '/self-host/use-dify/monitor/integrations/integrate-aliyun'
| '/self-host/use-dify/monitor/integrations/integrate-arize'
| '/self-host/use-dify/monitor/integrations/integrate-langfuse'
| '/self-host/use-dify/monitor/integrations/integrate-langsmith'
| '/self-host/use-dify/monitor/integrations/integrate-opik'
| '/self-host/use-dify/monitor/integrations/integrate-phoenix'
| '/self-host/use-dify/monitor/integrations/integrate-weave'
| '/self-host/use-dify/monitor/logs'
| '/self-host/use-dify/nodes/agent'
| '/self-host/use-dify/nodes/answer'
| '/self-host/use-dify/nodes/code'
| '/self-host/use-dify/nodes/doc-extractor'
| '/self-host/use-dify/nodes/http-request'
| '/self-host/use-dify/nodes/human-input'
| '/self-host/use-dify/nodes/ifelse'
| '/self-host/use-dify/nodes/iteration'
| '/self-host/use-dify/nodes/knowledge-retrieval'
| '/self-host/use-dify/nodes/list-operator'
| '/self-host/use-dify/nodes/llm'
| '/self-host/use-dify/nodes/loop'
| '/self-host/use-dify/nodes/output'
| '/self-host/use-dify/nodes/parameter-extractor'
| '/self-host/use-dify/nodes/question-classifier'
| '/self-host/use-dify/nodes/template'
| '/self-host/use-dify/nodes/tools'
| '/self-host/use-dify/nodes/trigger/overview'
| '/self-host/use-dify/nodes/trigger/plugin-trigger'
| '/self-host/use-dify/nodes/trigger/schedule-trigger'
| '/self-host/use-dify/nodes/trigger/webhook-trigger'
| '/self-host/use-dify/nodes/user-input'
| '/self-host/use-dify/nodes/variable-aggregator'
| '/self-host/use-dify/nodes/variable-assigner'
| '/self-host/use-dify/publish/README'
| '/self-host/use-dify/publish/developing-with-apis'
| '/self-host/use-dify/publish/publish-mcp'
| '/self-host/use-dify/publish/publish-to-marketplace'
| '/self-host/use-dify/publish/webapp/chatflow-webapp'
| '/self-host/use-dify/publish/webapp/embedding-in-websites'
| '/self-host/use-dify/publish/webapp/web-app-settings'
| '/self-host/use-dify/publish/webapp/workflow-webapp'
| '/self-host/use-dify/workspace/api-extension/api-extension'
| '/self-host/use-dify/workspace/api-extension/cloudflare-worker'
| '/self-host/use-dify/workspace/api-extension/external-data-tool-api-extension'
| '/self-host/use-dify/workspace/api-extension/moderation-api-extension'
| '/self-host/use-dify/workspace/app-management'
| '/self-host/use-dify/workspace/model-providers'
| '/self-host/use-dify/workspace/personal-account-management'
| '/self-host/use-dify/workspace/plugins'
| '/self-host/use-dify/workspace/readme'
| '/self-host/use-dify/workspace/team-members-management'
| '/self-host/use-dify/workspace/tools'
// Deploy paths
type DeployPath =
| '/deploy/advanced-deployments/local-source-code'
| '/deploy/advanced-deployments/start-the-frontend-docker-container'
| '/deploy/configuration/environments'
| '/deploy/overview'
| '/deploy/platform-guides/bt-panel'
| '/deploy/platform-guides/dify-premium'
| '/deploy/quick-start/docker-compose'
| '/deploy/quick-start/faqs'
| '/deploy/troubleshooting/common-issues'
| '/deploy/troubleshooting/docker-issues'
| '/deploy/troubleshooting/integrations'
| '/deploy/troubleshooting/storage-and-migration'
| '/deploy/troubleshooting/weaviate-v4-migration'
// API Reference paths (English, use apiReferencePathTranslations for other languages)
type ApiReferencePath =
| '/api-reference/annotations/configure-annotation-reply'
@ -189,6 +413,12 @@ type ApiReferencePath =
| '/api-reference/applications/get-app-meta'
| '/api-reference/applications/get-app-parameters'
| '/api-reference/applications/get-app-webapp-settings'
| '/api-reference/chatflows/get-next-suggested-questions'
| '/api-reference/chatflows/get-workflow-run-detail'
| '/api-reference/chatflows/list-workflow-logs'
| '/api-reference/chatflows/send-chat-message'
| '/api-reference/chatflows/stop-chat-message-generation'
| '/api-reference/chatflows/stream-workflow-events'
| '/api-reference/chats/get-next-suggested-questions'
| '/api-reference/chats/send-chat-message'
| '/api-reference/chats/stop-chat-message-generation'
@ -225,6 +455,8 @@ type ApiReferencePath =
| '/api-reference/feedback/submit-message-feedback'
| '/api-reference/files/download-file'
| '/api-reference/files/upload-file'
| '/api-reference/human-input/get-human-input-form'
| '/api-reference/human-input/submit-human-input-form'
| '/api-reference/knowledge-bases/create-an-empty-knowledge-base'
| '/api-reference/knowledge-bases/delete-knowledge-base'
| '/api-reference/knowledge-bases/get-knowledge-base'
@ -252,19 +484,24 @@ type ApiReferencePath =
| '/api-reference/tags/update-knowledge-tag'
| '/api-reference/tts/convert-audio-to-text'
| '/api-reference/tts/convert-text-to-audio'
| '/api-reference/workflow-runs/get-workflow-run-detail'
| '/api-reference/workflow-runs/list-workflow-logs'
| '/api-reference/workflows/get-workflow-run-detail'
| '/api-reference/workflows/list-workflow-logs'
| '/api-reference/workflows/run-workflow'
| '/api-reference/workflows/run-workflow-by-id'
| '/api-reference/workflows/stop-workflow-task'
| '/api-reference/workflows/stream-workflow-events'
// Base path without language prefix
type DocPathWithoutLangBase =
| CloudPath
| UseDifyPath
| SelfHostPath
| HomePath
| LearnPath
| QuickStartPath
| CliPath
| DevelopPluginPath
| SelfHostPath
| DeployPath
| ApiReferencePath
// Combined path without language prefix (supports optional #anchor)
@ -272,6 +509,116 @@ export type DocPathWithoutLang =
| DocPathWithoutLangBase
| `${DocPathWithoutLangBase}#${string}`
// Product availability for productless docs paths
export const docPathProductAvailability: Record<string, readonly DocsProduct[]> = {
'/deploy/advanced-deployments/local-source-code': ['self-host'],
'/deploy/advanced-deployments/start-the-frontend-docker-container': ['self-host'],
'/deploy/configuration/environments': ['self-host'],
'/deploy/overview': ['self-host'],
'/deploy/platform-guides/bt-panel': ['self-host'],
'/deploy/platform-guides/dify-premium': ['self-host'],
'/deploy/quick-start/docker-compose': ['self-host'],
'/deploy/quick-start/faqs': ['self-host'],
'/deploy/troubleshooting/common-issues': ['self-host'],
'/deploy/troubleshooting/docker-issues': ['self-host'],
'/deploy/troubleshooting/integrations': ['self-host'],
'/deploy/troubleshooting/storage-and-migration': ['self-host'],
'/deploy/troubleshooting/weaviate-v4-migration': ['self-host'],
'/use-dify/build/additional-features': ['cloud', 'self-host'],
'/use-dify/build/agent': ['cloud', 'self-host'],
'/use-dify/build/chatbot': ['cloud', 'self-host'],
'/use-dify/build/mcp': ['cloud', 'self-host'],
'/use-dify/build/orchestrate-node': ['cloud', 'self-host'],
'/use-dify/build/predefined-error-handling-logic': ['cloud', 'self-host'],
'/use-dify/build/shortcut-key': ['cloud', 'self-host'],
'/use-dify/build/text-generator': ['cloud', 'self-host'],
'/use-dify/build/version-control': ['cloud', 'self-host'],
'/use-dify/build/workflow-chatflow': ['cloud', 'self-host'],
'/use-dify/build/workflow-collaboration': ['cloud', 'self-host'],
'/use-dify/debug/error-type': ['cloud', 'self-host'],
'/use-dify/debug/history-and-logs': ['cloud', 'self-host'],
'/use-dify/debug/step-run': ['cloud', 'self-host'],
'/use-dify/debug/variable-inspect': ['cloud', 'self-host'],
'/use-dify/getting-started/introduction': ['cloud', 'self-host'],
'/use-dify/knowledge/connect-external-knowledge-base': ['cloud', 'self-host'],
'/use-dify/knowledge/create-knowledge/chunking-and-cleaning-text': ['cloud', 'self-host'],
'/use-dify/knowledge/create-knowledge/import-text-data/readme': ['cloud', 'self-host'],
'/use-dify/knowledge/create-knowledge/import-text-data/sync-from-notion': ['cloud', 'self-host'],
'/use-dify/knowledge/create-knowledge/import-text-data/sync-from-website': ['cloud', 'self-host'],
'/use-dify/knowledge/create-knowledge/introduction': ['cloud', 'self-host'],
'/use-dify/knowledge/create-knowledge/setting-indexing-methods': ['cloud', 'self-host'],
'/use-dify/knowledge/external-knowledge-api': ['cloud', 'self-host'],
'/use-dify/knowledge/integrate-knowledge-within-application': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/authorize-data-source': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/create-knowledge-pipeline': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/knowledge-pipeline-orchestration': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/manage-knowledge-base': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/publish-knowledge-pipeline': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/readme': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-pipeline/upload-files': ['cloud', 'self-host'],
'/use-dify/knowledge/knowledge-request-rate-limit': ['cloud'],
'/use-dify/knowledge/manage-knowledge/introduction': ['cloud', 'self-host'],
'/use-dify/knowledge/manage-knowledge/maintain-dataset-via-api': ['cloud', 'self-host'],
'/use-dify/knowledge/manage-knowledge/maintain-knowledge-documents': ['cloud', 'self-host'],
'/use-dify/knowledge/metadata': ['cloud', 'self-host'],
'/use-dify/knowledge/readme': ['cloud', 'self-host'],
'/use-dify/knowledge/test-retrieval': ['cloud', 'self-host'],
'/use-dify/monitor/analysis': ['cloud', 'self-host'],
'/use-dify/monitor/annotation-reply': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-aliyun': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-arize': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-langfuse': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-langsmith': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-opik': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-phoenix': ['cloud', 'self-host'],
'/use-dify/monitor/integrations/integrate-weave': ['cloud', 'self-host'],
'/use-dify/monitor/logs': ['cloud', 'self-host'],
'/use-dify/nodes/agent': ['cloud', 'self-host'],
'/use-dify/nodes/answer': ['cloud', 'self-host'],
'/use-dify/nodes/code': ['cloud', 'self-host'],
'/use-dify/nodes/doc-extractor': ['cloud', 'self-host'],
'/use-dify/nodes/http-request': ['cloud', 'self-host'],
'/use-dify/nodes/human-input': ['cloud', 'self-host'],
'/use-dify/nodes/ifelse': ['cloud', 'self-host'],
'/use-dify/nodes/iteration': ['cloud', 'self-host'],
'/use-dify/nodes/knowledge-retrieval': ['cloud', 'self-host'],
'/use-dify/nodes/list-operator': ['cloud', 'self-host'],
'/use-dify/nodes/llm': ['cloud', 'self-host'],
'/use-dify/nodes/loop': ['cloud', 'self-host'],
'/use-dify/nodes/output': ['cloud', 'self-host'],
'/use-dify/nodes/parameter-extractor': ['cloud', 'self-host'],
'/use-dify/nodes/question-classifier': ['cloud', 'self-host'],
'/use-dify/nodes/template': ['cloud', 'self-host'],
'/use-dify/nodes/tools': ['cloud', 'self-host'],
'/use-dify/nodes/trigger/overview': ['cloud', 'self-host'],
'/use-dify/nodes/trigger/plugin-trigger': ['cloud', 'self-host'],
'/use-dify/nodes/trigger/schedule-trigger': ['cloud', 'self-host'],
'/use-dify/nodes/trigger/webhook-trigger': ['cloud', 'self-host'],
'/use-dify/nodes/user-input': ['cloud', 'self-host'],
'/use-dify/nodes/variable-aggregator': ['cloud', 'self-host'],
'/use-dify/nodes/variable-assigner': ['cloud', 'self-host'],
'/use-dify/publish/README': ['cloud', 'self-host'],
'/use-dify/publish/developing-with-apis': ['cloud', 'self-host'],
'/use-dify/publish/publish-mcp': ['cloud', 'self-host'],
'/use-dify/publish/publish-to-marketplace': ['cloud', 'self-host'],
'/use-dify/publish/webapp/chatflow-webapp': ['cloud', 'self-host'],
'/use-dify/publish/webapp/embedding-in-websites': ['cloud', 'self-host'],
'/use-dify/publish/webapp/web-app-settings': ['cloud', 'self-host'],
'/use-dify/publish/webapp/workflow-webapp': ['cloud', 'self-host'],
'/use-dify/workspace/api-extension/api-extension': ['cloud', 'self-host'],
'/use-dify/workspace/api-extension/cloudflare-worker': ['cloud', 'self-host'],
'/use-dify/workspace/api-extension/external-data-tool-api-extension': ['cloud', 'self-host'],
'/use-dify/workspace/api-extension/moderation-api-extension': ['cloud', 'self-host'],
'/use-dify/workspace/app-management': ['cloud', 'self-host'],
'/use-dify/workspace/model-providers': ['cloud', 'self-host'],
'/use-dify/workspace/personal-account-management': ['cloud', 'self-host'],
'/use-dify/workspace/plugins': ['cloud', 'self-host'],
'/use-dify/workspace/readme': ['cloud', 'self-host'],
'/use-dify/workspace/subscription-management': ['cloud'],
'/use-dify/workspace/team-members-management': ['cloud', 'self-host'],
'/use-dify/workspace/tools': ['cloud', 'self-host'],
}
// API Reference path translations (English -> other languages)
export const apiReferencePathTranslations: Record<string, { zh?: string; ja?: string }> = {
'/api-reference/annotations/configure-annotation-reply': { zh: '/api-reference/标注管理/配置标注回复', ja: '/api-reference/アノテーション管理/アノテーション返信を設定' },
@ -284,6 +631,12 @@ export const apiReferencePathTranslations: Record<string, { zh?: string; ja?: st
'/api-reference/applications/get-app-meta': { zh: '/api-reference/应用配置/获取应用元数据', ja: '/api-reference/アプリケーション設定/アプリケーションのメタ情報を取得' },
'/api-reference/applications/get-app-parameters': { zh: '/api-reference/应用配置/获取应用参数', ja: '/api-reference/アプリケーション設定/アプリケーションのパラメータ情報を取得' },
'/api-reference/applications/get-app-webapp-settings': { zh: '/api-reference/应用配置/获取应用-webapp-设置', ja: '/api-reference/アプリケーション設定/アプリの-webapp-設定を取得' },
'/api-reference/chatflows/get-next-suggested-questions': { zh: '/api-reference/对话流/获取下一轮建议问题列表', ja: '/api-reference/チャットフロー/次の推奨質問を取得' },
'/api-reference/chatflows/get-workflow-run-detail': { zh: '/api-reference/对话流/获取工作流执行情况', ja: '/api-reference/チャットフロー/ワークフロー実行詳細を取得' },
'/api-reference/chatflows/list-workflow-logs': { zh: '/api-reference/对话流/获取工作流日志', ja: '/api-reference/チャットフロー/ワークフローログ一覧を取得' },
'/api-reference/chatflows/send-chat-message': { zh: '/api-reference/对话流/发送对话消息', ja: '/api-reference/チャットフロー/チャットメッセージを送信' },
'/api-reference/chatflows/stop-chat-message-generation': { zh: '/api-reference/对话流/停止响应', ja: '/api-reference/チャットフロー/生成を停止' },
'/api-reference/chatflows/stream-workflow-events': { zh: '/api-reference/对话流/流式获取工作流事件', ja: '/api-reference/チャットフロー/ワークフローイベントをストリーム' },
'/api-reference/chats/get-next-suggested-questions': { zh: '/api-reference/对话消息/获取下一轮建议问题列表', ja: '/api-reference/チャットメッセージ/次の推奨質問を取得' },
'/api-reference/chats/send-chat-message': { zh: '/api-reference/对话消息/发送对话消息', ja: '/api-reference/チャットメッセージ/チャットメッセージを送信' },
'/api-reference/chats/stop-chat-message-generation': { zh: '/api-reference/对话消息/停止响应', ja: '/api-reference/チャットメッセージ/生成を停止' },
@ -320,6 +673,8 @@ export const apiReferencePathTranslations: Record<string, { zh?: string; ja?: st
'/api-reference/feedback/submit-message-feedback': { zh: '/api-reference/消息反馈/提交消息反馈', ja: '/api-reference/メッセージフィードバック/メッセージフィードバックを送信' },
'/api-reference/files/download-file': { zh: '/api-reference/文件操作/下载文件', ja: '/api-reference/ファイル操作/ファイルをダウンロード' },
'/api-reference/files/upload-file': { zh: '/api-reference/文件操作/上传文件', ja: '/api-reference/ファイル操作/ファイルをアップロード' },
'/api-reference/human-input/get-human-input-form': { zh: '/api-reference/人工介入/获取人工介入表单', ja: '/api-reference/人間の入力/人間の入力フォームを取得' },
'/api-reference/human-input/submit-human-input-form': { zh: '/api-reference/人工介入/提交人工介入表单', ja: '/api-reference/人間の入力/人間の入力フォームを送信' },
'/api-reference/knowledge-bases/create-an-empty-knowledge-base': { zh: '/api-reference/知识库/创建空知识库', ja: '/api-reference/データセット/空のナレッジベースを作成' },
'/api-reference/knowledge-bases/delete-knowledge-base': { zh: '/api-reference/知识库/删除知识库', ja: '/api-reference/データセット/ナレッジベースを削除' },
'/api-reference/knowledge-bases/get-knowledge-base': { zh: '/api-reference/知识库/获取知识库详情', ja: '/api-reference/データセット/ナレッジベース詳細を取得' },
@ -347,11 +702,10 @@ export const apiReferencePathTranslations: Record<string, { zh?: string; ja?: st
'/api-reference/tags/update-knowledge-tag': { zh: '/api-reference/标签/修改知识库标签', ja: '/api-reference/タグ管理/ナレッジベースタグを変更' },
'/api-reference/tts/convert-audio-to-text': { zh: '/api-reference/语音与文字转换/语音转文字', ja: '/api-reference/音声・テキスト変換/音声をテキストに変換' },
'/api-reference/tts/convert-text-to-audio': { zh: '/api-reference/语音与文字转换/文字转语音', ja: '/api-reference/音声・テキスト変換/テキストを音声に変換' },
'/api-reference/workflow-runs/get-workflow-run-detail': { zh: '/api-reference/工作流执行/获取工作流执行情况', ja: '/api-reference/ワークフロー実行/ワークフロー実行詳細を取得' },
'/api-reference/workflow-runs/list-workflow-logs': { zh: '/api-reference/工作流执行/获取工作流日志', ja: '/api-reference/ワークフロー実行/ワークフローログ一覧を取得' },
'/api-reference/workflows/get-workflow-run-detail': { zh: '/api-reference/工作流/获取工作流执行情况', ja: '/api-reference/ワークフロー/ワークフロー実行詳細を取得' },
'/api-reference/workflows/list-workflow-logs': { zh: '/api-reference/工作流/获取工作流日志', ja: '/api-reference/ワークフロー/ワークフローログ一覧を取得' },
'/api-reference/workflows/run-workflow': { zh: '/api-reference/工作流/执行工作流', ja: '/api-reference/ワークフロー/ワークフローを実行' },
'/api-reference/workflows/run-workflow-by-id': { zh: '/api-reference/工作流/按-id-执行工作流', ja: '/api-reference/ワークフロー/id-でワークフローを実行' },
'/api-reference/workflows/stop-workflow-task': { zh: '/api-reference/工作流/停止工作流任务', ja: '/api-reference/ワークフロー/ワークフロータスクを停止' },
'/api-reference/workflows/stream-workflow-events': { zh: '/api-reference/工作流/流式获取工作流事件', ja: '/api-reference/ワークフロー/ワークフローイベントをストリーム' },
}