mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 07:06:36 +08:00
seo 检索开关
This commit is contained in:
@@ -14,3 +14,6 @@ build-embedded:
|
|||||||
rm -rf internal/router/dist
|
rm -rf internal/router/dist
|
||||||
cp -R frontend/out internal/router/dist
|
cp -R frontend/out internal/router/dist
|
||||||
go build -tags embed_frontend -o bin/wavelet main.go
|
go build -tags embed_frontend -o bin/wavelet main.go
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
cd frontend && npx eslint . --max-warnings 0
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 74 KiB |
@@ -6,6 +6,7 @@ import {BellRingProvider} from "@/contexts/bell-ring-context";
|
|||||||
import {NotificationSettingsProvider} from "@/contexts/notification-settings-context";
|
import {NotificationSettingsProvider} from "@/contexts/notification-settings-context";
|
||||||
import {UserProvider} from "@/contexts/user-context";
|
import {UserProvider} from "@/contexts/user-context";
|
||||||
import {AppQueryProvider} from "@/components/providers/query-provider";
|
import {AppQueryProvider} from "@/components/providers/query-provider";
|
||||||
|
import {RobotsMeta} from "@/components/layout/robots-meta";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -35,6 +36,7 @@ export default function RootLayout({
|
|||||||
>
|
>
|
||||||
<CustomThemeProvider>
|
<CustomThemeProvider>
|
||||||
<AppQueryProvider>
|
<AppQueryProvider>
|
||||||
|
<RobotsMeta />
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<NotificationSettingsProvider>
|
<NotificationSettingsProvider>
|
||||||
<BellRingProvider>
|
<BellRingProvider>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {useMutation, useQueryClient, type UseQueryResult} from "@tanstack/react-query"
|
||||||
|
import {Globe, Search} from "lucide-react"
|
||||||
|
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
|
||||||
|
import {Switch} from "@/components/ui/switch"
|
||||||
|
import {AdminService} from "@/lib/services"
|
||||||
|
import type {SystemConfig} from "@/lib/services/admin"
|
||||||
|
import {TemplatesManager} from "./templates"
|
||||||
|
import {toast} from "sonner"
|
||||||
|
|
||||||
|
interface OperationTabProps {
|
||||||
|
configs: Record<string, SystemConfig>
|
||||||
|
systemConfigsQuery: UseQueryResult<SystemConfig[], Error>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OperationTab({ configs, systemConfigsQuery }: OperationTabProps) {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const updateConfigMutation = useMutation({
|
||||||
|
mutationFn: async ({ key, value }: { key: string; value: boolean }) => {
|
||||||
|
const config = configs[key]
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`缺少配置项: ${key}`)
|
||||||
|
}
|
||||||
|
await AdminService.updateSystemConfig(key, {
|
||||||
|
value: value ? "true" : "false",
|
||||||
|
description: config.description,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["public-config"] })
|
||||||
|
toast.success("运营配置已更新")
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
toast.error(error.message || "更新配置失败")
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const indexingEnabled = configs["search_engine_indexing_enabled"]?.value === "true"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 搜索引擎检索设置 */}
|
||||||
|
<Card className="border border-dashed shadow-sm">
|
||||||
|
<CardHeader className="border-b border-dashed pb-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-500">
|
||||||
|
<Globe className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-base font-semibold">SEO 与搜索引擎检索</CardTitle>
|
||||||
|
<CardDescription className="text-xs">配置站点是否允许被搜索引擎抓取和检索</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex items-center justify-between gap-4 rounded-xl border border-dashed p-4 bg-card hover:bg-muted/10 hover:border-indigo-500/30 transition-all duration-300 shadow-sm">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Search className="size-4 text-indigo-500" />
|
||||||
|
<span className="font-medium text-sm text-foreground">允许被搜索引擎检索</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground leading-relaxed pr-2">
|
||||||
|
默认关闭。关闭后系统将自动下发 noindex 指令并限制 robots.txt,禁止搜索引擎爬虫抓取本站页面。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={indexingEnabled}
|
||||||
|
disabled={updateConfigMutation.isPending || systemConfigsQuery.isPending}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
updateConfigMutation.mutate({ key: "search_engine_indexing_enabled", value: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 通知模板管理 */}
|
||||||
|
<TemplatesManager />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import {motion} from "motion/react"
|
|||||||
|
|
||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
|
||||||
import {useAuth} from "@/components/providers/auth-provider"
|
import {useAuth} from "@/components/providers/auth-provider"
|
||||||
import {TemplatesManager} from "@/components/common/settings/templates"
|
import {OperationTab} from "./operation-tab"
|
||||||
import {AdminService} from "@/lib/services"
|
import {AdminService} from "@/lib/services"
|
||||||
import type {SystemConfig} from "@/lib/services/admin"
|
import type {SystemConfig} from "@/lib/services/admin"
|
||||||
import {SystemStatusManager} from "@/components/common/admin/status"
|
import {SystemStatusManager} from "@/components/common/admin/status"
|
||||||
@@ -93,7 +93,7 @@ export function SystemSettingsMain() {
|
|||||||
<SecurityTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
<SecurityTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="operation" className="pt-4">
|
<TabsContent value="operation" className="pt-4">
|
||||||
<TemplatesManager />
|
<OperationTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="system" className="pt-4">
|
<TabsContent value="system" className="pt-4">
|
||||||
<SystemTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
<SystemTab configs={configs} systemConfigsQuery={systemConfigsQuery} />
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import {usePublicConfig} from "@/hooks/use-public-config"
|
||||||
|
|
||||||
|
export function RobotsMeta() {
|
||||||
|
const { config } = usePublicConfig()
|
||||||
|
|
||||||
|
// Default to noindex, nofollow if config is loading or if it's explicitly disabled
|
||||||
|
const enabled = config?.search_engine_indexing_enabled === true
|
||||||
|
|
||||||
|
return (
|
||||||
|
<meta
|
||||||
|
name="robots"
|
||||||
|
content={enabled ? "index, follow" : "noindex, nofollow"}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -26,4 +26,6 @@ export interface PublicConfigResponse {
|
|||||||
email_register_verification_enabled: boolean;
|
email_register_verification_enabled: boolean;
|
||||||
/** 目录显示配置 */
|
/** 目录显示配置 */
|
||||||
menu_display_config?: Record<string, boolean>;
|
menu_display_config?: Record<string, boolean>;
|
||||||
|
/** 是否允许搜索引擎检索 */
|
||||||
|
search_engine_indexing_enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
* @returns API 基础 URL
|
* @returns API 基础 URL
|
||||||
*/
|
*/
|
||||||
export function getApiBaseUrl(): string {
|
export function getApiBaseUrl(): string {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return process.env.WAVELET_BACKEND_URL || process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || 'http://localhost:8000';
|
||||||
|
}
|
||||||
return process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || '';
|
return process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ const nextConfig: NextConfig = {
|
|||||||
source: '/f/:id',
|
source: '/f/:id',
|
||||||
destination: `${ backendUrl }/f/:id`,
|
destination: `${ backendUrl }/f/:id`,
|
||||||
},
|
},
|
||||||
|
// robots.txt 路由代理到后端动态接口
|
||||||
|
{
|
||||||
|
source: '/robots.txt',
|
||||||
|
destination: `${ backendUrl }/robots.txt`,
|
||||||
|
},
|
||||||
// 标准 RESTful API 接口
|
// 标准 RESTful API 接口
|
||||||
{
|
{
|
||||||
source: '/api/:path*',
|
source: '/api/:path*',
|
||||||
|
|||||||
@@ -146,9 +146,9 @@ func TestListSystemConfigs(t *testing.T) {
|
|||||||
var configs []model.SystemConfig
|
var configs []model.SystemConfig
|
||||||
json.Unmarshal(dataBytes, &configs)
|
json.Unmarshal(dataBytes, &configs)
|
||||||
|
|
||||||
// Defaults seed 22 configurations
|
// Defaults seed 23 configurations
|
||||||
if len(configs) != 22 {
|
if len(configs) != 23 {
|
||||||
t.Errorf("expected 22 default configs, got %d", len(configs))
|
t.Errorf("expected 23 default configs, got %d", len(configs))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type PublicConfigResponse struct {
|
|||||||
EmailLoginVerificationEnabled bool `json:"email_login_verification_enabled"` // 是否启用邮箱登录验证
|
EmailLoginVerificationEnabled bool `json:"email_login_verification_enabled"` // 是否启用邮箱登录验证
|
||||||
EmailRegisterVerificationEnabled bool `json:"email_register_verification_enabled"` // 是否启用邮箱注册验证
|
EmailRegisterVerificationEnabled bool `json:"email_register_verification_enabled"` // 是否启用邮箱注册验证
|
||||||
MenuDisplayConfig map[string]bool `json:"menu_display_config"` // 目录显示配置
|
MenuDisplayConfig map[string]bool `json:"menu_display_config"` // 目录显示配置
|
||||||
|
SearchEngineIndexingEnabled bool `json:"search_engine_indexing_enabled"` // 是否允许搜索引擎检索
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPublicConfig 获取公共配置
|
// GetPublicConfig 获取公共配置
|
||||||
@@ -117,6 +118,11 @@ func GetPublicConfig(c *gin.Context) {
|
|||||||
emailRegisterVerificationEnabled = val
|
emailRegisterVerificationEnabled = val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var searchEngineIndexingEnabled bool
|
||||||
|
if val, err := model.GetBoolByKey(ctx, model.ConfigKeySearchEngineIndexingEnabled); err == nil {
|
||||||
|
searchEngineIndexingEnabled = val
|
||||||
|
}
|
||||||
|
|
||||||
menuDisplayConfig, err := model.GetMenuDisplayConfig(ctx)
|
menuDisplayConfig, err := model.GetMenuDisplayConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
menuDisplayConfig = make(map[string]bool)
|
menuDisplayConfig = make(map[string]bool)
|
||||||
@@ -135,7 +141,25 @@ func GetPublicConfig(c *gin.Context) {
|
|||||||
EmailLoginVerificationEnabled: emailLoginVerificationEnabled,
|
EmailLoginVerificationEnabled: emailLoginVerificationEnabled,
|
||||||
EmailRegisterVerificationEnabled: emailRegisterVerificationEnabled,
|
EmailRegisterVerificationEnabled: emailRegisterVerificationEnabled,
|
||||||
MenuDisplayConfig: menuDisplayConfig,
|
MenuDisplayConfig: menuDisplayConfig,
|
||||||
|
SearchEngineIndexingEnabled: searchEngineIndexingEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, util.OK(response))
|
c.JSON(http.StatusOK, util.OK(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRobotsTXT 动态生成 robots.txt
|
||||||
|
// @Summary 获取 robots.txt
|
||||||
|
// @Description 根据系统配置决定是否允许搜索引擎检索,并返回相应的 robots.txt 文件内容
|
||||||
|
// @Tags config
|
||||||
|
// @Produce text/plain
|
||||||
|
// @Success 200 {string} string "robots.txt 内容"
|
||||||
|
// @Router /robots.txt [get]
|
||||||
|
func GetRobotsTXT(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeySearchEngineIndexingEnabled)
|
||||||
|
content := "User-Agent: *\nDisallow: /\n"
|
||||||
|
if err == nil && enabled {
|
||||||
|
content = "User-Agent: *\nAllow: /\n"
|
||||||
|
}
|
||||||
|
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(content))
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func init() {
|
|||||||
// Disable standard DB/Redis initializations during tests to prevent connection attempts.
|
// Disable standard DB/Redis initializations during tests to prevent connection attempts.
|
||||||
if isTest() {
|
if isTest() {
|
||||||
c.Database.Enabled = false
|
c.Database.Enabled = false
|
||||||
|
c.Database.SQLitePath = ":memory:"
|
||||||
c.Redis.Enabled = false
|
c.Redis.Enabled = false
|
||||||
c.ClickHouse.Enabled = false
|
c.ClickHouse.Enabled = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func initSystemConfigs() {
|
|||||||
ensureConfigKeyExists(model.ConfigKeyEmailLoginVerificationEnabled, "false", "system", "是否开启邮箱登录验证(true/false)")
|
ensureConfigKeyExists(model.ConfigKeyEmailLoginVerificationEnabled, "false", "system", "是否开启邮箱登录验证(true/false)")
|
||||||
ensureConfigKeyExists(model.ConfigKeyEmailRegisterVerificationEnabled, "false", "system", "是否开启邮箱注册验证(true/false)")
|
ensureConfigKeyExists(model.ConfigKeyEmailRegisterVerificationEnabled, "false", "system", "是否开启邮箱注册验证(true/false)")
|
||||||
ensureConfigKeyExists(model.ConfigKeyMenuDisplayConfig, "{}", "system", "目录显示配置(JSON 字符串,格式为 {url: enabled})")
|
ensureConfigKeyExists(model.ConfigKeyMenuDisplayConfig, "{}", "system", "目录显示配置(JSON 字符串,格式为 {url: enabled})")
|
||||||
|
ensureConfigKeyExists(model.ConfigKeySearchEngineIndexingEnabled, "false", "system", "是否允许搜索引擎爬取/检索该站点(true/false)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +242,12 @@ func initSystemConfigs() {
|
|||||||
Type: "system",
|
Type: "system",
|
||||||
Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})",
|
Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: model.ConfigKeySearchEngineIndexingEnabled,
|
||||||
|
Value: "false",
|
||||||
|
Type: "system",
|
||||||
|
Description: "是否允许搜索引擎爬取/检索该站点(true/false)",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const (
|
|||||||
ConfigKeyEmailLoginVerificationEnabled = "email_login_verification_enabled" // 是否启用邮箱登录验证
|
ConfigKeyEmailLoginVerificationEnabled = "email_login_verification_enabled" // 是否启用邮箱登录验证
|
||||||
ConfigKeyEmailRegisterVerificationEnabled = "email_register_verification_enabled" // 是否启用邮箱注册验证
|
ConfigKeyEmailRegisterVerificationEnabled = "email_register_verification_enabled" // 是否启用邮箱注册验证
|
||||||
ConfigKeyMenuDisplayConfig = "menu_display_config" // 目录显示配置 (JSON 字符串)
|
ConfigKeyMenuDisplayConfig = "menu_display_config" // 目录显示配置 (JSON 字符串)
|
||||||
|
ConfigKeySearchEngineIndexingEnabled = "search_engine_indexing_enabled" // 是否允许搜索引擎检索
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ func Serve() {
|
|||||||
// Serve files by ID
|
// Serve files by ID
|
||||||
r.GET("/f/:id", upload.ServeFileByID)
|
r.GET("/f/:id", upload.ServeFileByID)
|
||||||
|
|
||||||
|
// Dynamic robots.txt serving
|
||||||
|
r.GET("/robots.txt", publicconfig.GetRobotsTXT)
|
||||||
|
|
||||||
apiGroup := r.Group(config.Config.App.APIPrefix)
|
apiGroup := r.Group(config.Config.App.APIPrefix)
|
||||||
{
|
{
|
||||||
if !config.Config.App.IsProduction() {
|
if !config.Config.App.IsProduction() {
|
||||||
|
|||||||
@@ -218,6 +218,12 @@ func seedDefaultConfigs(t *testing.T, tx *gorm.DB) {
|
|||||||
Type: "system",
|
Type: "system",
|
||||||
Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})",
|
Description: "目录显示配置(JSON 字符串,格式为 {url: enabled})",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: model.ConfigKeySearchEngineIndexingEnabled,
|
||||||
|
Value: "false",
|
||||||
|
Type: "system",
|
||||||
|
Description: "是否允许搜索引擎检索",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
if err := tx.Create(&defaultConfigs).Error; err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user