feat(filtering): add global content filtering toggle in config
Adds ability to completely disable ad blocking via configuration while
maintaining the whitelist system for granular control.
Changes:
- Refactored ContentFilteringWhitelist into ContentFilteringConfig struct
with separate 'enabled' boolean and 'whitelist' array
- FilterManager now checks config.ContentFiltering.Enabled before applying filters
- Defaults to enabled=true for backward compatibility
- When disabled, no network rules or cosmetic filters are loaded
- Whitelist cache optimization with hash-based change detection to avoid
rebuilding rules on every filter application
Config structure:
content_filtering:
enabled: true
whitelist:
- "twitch.tv"
- "passport.twitch.tv"
- "gql.twitch.tv"
This allows users to completely bypass ad blocking when needed (e.g., debugging,
site compatibility) without removing the configuration file.- commit
- 202ed5abb8da138fbf616a6ab9f45e5cb96fe6c4
- author
- bnema · · 2025-11-17 22:14 UTC
- committer
- bnema · · 2025-11-17 21:58 UTC
- parent
- 72ad164
- signature
- unsigned
3 files · +78 -7
M internal/config/config.go +14 -2
| @@ -42,8 +42,8 @@ type Config struct { | |||
| 42 | 42 | DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"` | |
| 43 | 43 | // Workspace defines workspace, pane, and tab handling behaviour. | |
| 44 | 44 | Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"` | |
| 45 | - | // ContentFilteringWhitelist domains skip ad blocking (e.g. Twitch breaks with bot detection) | |
| 46 | - | ContentFilteringWhitelist []string `mapstructure:"content_filtering_whitelist" yaml:"content_filtering_whitelist"` | |
| 45 | + | // ContentFiltering controls ad blocking and content filtering | |
| 46 | + | ContentFiltering ContentFilteringConfig `mapstructure:"content_filtering" yaml:"content_filtering"` | |
| 47 | 47 | } | |
| 48 | 48 | ||
| 49 | 49 | // RenderingMode selects GPU vs CPU rendering. | |
| @@ -175,6 +175,14 @@ type CodecConfig struct { | |||
| 175 | 175 | } | |
| 176 | 176 | ||
| 177 | 177 | // DebugConfig holds debug and troubleshooting options | |
| 178 | + | // ContentFilteringConfig holds content filtering and ad blocking preferences | |
| 179 | + | type ContentFilteringConfig struct { | |
| 180 | + | // Enabled controls whether ad blocking is active (default: true) | |
| 181 | + | Enabled bool `mapstructure:"enabled" yaml:"enabled"` | |
| 182 | + | // Whitelist domains that should skip ad blocking (e.g. Twitch breaks with bot detection) | |
| 183 | + | Whitelist []string `mapstructure:"whitelist" yaml:"whitelist"` | |
| 184 | + | } | |
| 185 | + | ||
| 178 | 186 | type DebugConfig struct { | |
| 179 | 187 | // Enable WebKit internal debug logging | |
| 180 | 188 | EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"` | |
| @@ -744,6 +752,10 @@ func (m *Manager) setDefaults() { | |||
| 744 | 752 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | |
| 745 | 753 | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | |
| 746 | 754 | m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale) | |
| 755 | + | ||
| 756 | + | // Content filtering | |
| 757 | + | m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled) | |
| 758 | + | m.viper.SetDefault("content_filtering.whitelist", defaults.ContentFiltering.Whitelist) | |
| 747 | 759 | } | |
| 748 | 760 | ||
| 749 | 761 | // createDefaultConfig creates a default configuration file. | |
| @@ -42,8 +42,8 @@ type Config struct { | |||||
| 42 | DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"` | 42 | DefaultZoom float64 `mapstructure:"default_zoom" yaml:"default_zoom"` | ||
| 43 | // Workspace defines workspace, pane, and tab handling behaviour. | 43 | // Workspace defines workspace, pane, and tab handling behaviour. | ||
| 44 | Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"` | 44 | Workspace WorkspaceConfig `mapstructure:"workspace" yaml:"workspace"` | ||
| 45 | - | // ContentFilteringWhitelist domains skip ad blocking (e.g. Twitch breaks with bot detection) | 45 | + | // ContentFiltering controls ad blocking and content filtering |
| 46 | - | ContentFilteringWhitelist []string `mapstructure:"content_filtering_whitelist" yaml:"content_filtering_whitelist"` | 46 | + | ContentFiltering ContentFilteringConfig `mapstructure:"content_filtering" yaml:"content_filtering"` |
| 47 | } | 47 | } | ||
| 48 | 48 | ||||
| 49 | // RenderingMode selects GPU vs CPU rendering. | 49 | // RenderingMode selects GPU vs CPU rendering. | ||
| @@ -175,6 +175,14 @@ type CodecConfig struct { | |||||
| 175 | } | 175 | } | ||
| 176 | 176 | ||||
| 177 | // DebugConfig holds debug and troubleshooting options | 177 | // DebugConfig holds debug and troubleshooting options | ||
| 178 | + | // ContentFilteringConfig holds content filtering and ad blocking preferences | |||
| 179 | + | type ContentFilteringConfig struct { | |||
| 180 | + | // Enabled controls whether ad blocking is active (default: true) | |||
| 181 | + | Enabled bool `mapstructure:"enabled" yaml:"enabled"` | |||
| 182 | + | // Whitelist domains that should skip ad blocking (e.g. Twitch breaks with bot detection) | |||
| 183 | + | Whitelist []string `mapstructure:"whitelist" yaml:"whitelist"` | |||
| 184 | + | } | |||
| 185 | + | ||||
| 178 | type DebugConfig struct { | 186 | type DebugConfig struct { | ||
| 179 | // Enable WebKit internal debug logging | 187 | // Enable WebKit internal debug logging | ||
| 180 | EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"` | 188 | EnableWebKitDebug bool `mapstructure:"enable_webkit_debug" yaml:"enable_webkit_debug"` | ||
| @@ -744,6 +752,10 @@ func (m *Manager) setDefaults() { | |||||
| 744 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | 752 | m.viper.SetDefault("workspace.styling.transition_duration", defaults.Workspace.Styling.TransitionDuration) | ||
| 745 | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | 753 | m.viper.SetDefault("workspace.styling.border_radius", defaults.Workspace.Styling.BorderRadius) | ||
| 746 | m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale) | 754 | m.viper.SetDefault("workspace.styling.ui_scale", defaults.Workspace.Styling.UIScale) | ||
| 755 | + | ||||
| 756 | + | // Content filtering | |||
| 757 | + | m.viper.SetDefault("content_filtering.enabled", defaults.ContentFiltering.Enabled) | |||
| 758 | + | m.viper.SetDefault("content_filtering.whitelist", defaults.ContentFiltering.Whitelist) | |||
| 747 | } | 759 | } | ||
| 748 | 760 | ||||
| 749 | // createDefaultConfig creates a default configuration file. | 761 | // createDefaultConfig creates a default configuration file. | ||
M internal/config/defaults.go +8 -5
| @@ -129,7 +129,7 @@ func DefaultConfig() *Config { | |||
| 129 | 129 | DisableVP9Hardware: false, // Allow VP9 hardware for high res content | |
| 130 | 130 | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams | |
| 131 | 131 | QueueBufferTimeSec: 10, // More buffering time for smooth playback | |
| 132 | - | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", // Chrome UA with AV1 support | |
| 132 | + | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // WebKitGTK default (same as Epiphany) | |
| 133 | 133 | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res | |
| 134 | 134 | DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing) | |
| 135 | 135 | }, | |
| @@ -206,10 +206,13 @@ func DefaultConfig() *Config { | |||
| 206 | 206 | UIScale: defaultUIScale, | |
| 207 | 207 | }, | |
| 208 | 208 | }, | |
| 209 | - | ContentFilteringWhitelist: []string{ | |
| 210 | - | "twitch.tv", // Arkose Labs bot detection breaks with filtering | |
| 211 | - | "passport.twitch.tv", // Auth subdomain | |
| 212 | - | "gql.twitch.tv", // GraphQL API | |
| 209 | + | ContentFiltering: ContentFilteringConfig{ | |
| 210 | + | Enabled: true, // Ad blocking enabled by default | |
| 211 | + | Whitelist: []string{ | |
| 212 | + | "twitch.tv", // Arkose Labs bot detection breaks with filtering | |
| 213 | + | "passport.twitch.tv", // Auth subdomain | |
| 214 | + | "gql.twitch.tv", // GraphQL API | |
| 215 | + | }, | |
| 213 | 216 | }, | |
| 214 | 217 | } | |
| 215 | 218 | } | |
| @@ -129,7 +129,7 @@ func DefaultConfig() *Config { | |||||
| 129 | DisableVP9Hardware: false, // Allow VP9 hardware for high res content | 129 | DisableVP9Hardware: false, // Allow VP9 hardware for high res content | ||
| 130 | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams | 130 | VideoBufferSizeMB: 16, // Larger buffer for AV1 streams | ||
| 131 | QueueBufferTimeSec: 10, // More buffering time for smooth playback | 131 | QueueBufferTimeSec: 10, // More buffering time for smooth playback | ||
| 132 | - | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36", // Chrome UA with AV1 support | 132 | + | CustomUserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15", // WebKitGTK default (same as Epiphany) |
| 133 | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res | 133 | AV1MaxResolution: "1080p", // Optimal AV1 up to 1080p, fallback to VP9 for higher res | ||
| 134 | DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing) | 134 | DisableTwitchCodecControl: true, // Disable codec control on Twitch by default (prevents theater/fullscreen freezing) | ||
| 135 | }, | 135 | }, | ||
| @@ -206,10 +206,13 @@ func DefaultConfig() *Config { | |||||
| 206 | UIScale: defaultUIScale, | 206 | UIScale: defaultUIScale, | ||
| 207 | }, | 207 | }, | ||
| 208 | }, | 208 | }, | ||
| 209 | - | ContentFilteringWhitelist: []string{ | 209 | + | ContentFiltering: ContentFilteringConfig{ |
| 210 | - | "twitch.tv", // Arkose Labs bot detection breaks with filtering | 210 | + | Enabled: true, // Ad blocking enabled by default |
| 211 | - | "passport.twitch.tv", // Auth subdomain | 211 | + | Whitelist: []string{ |
| 212 | - | "gql.twitch.tv", // GraphQL API | 212 | + | "twitch.tv", // Arkose Labs bot detection breaks with filtering |
| 213 | + | "passport.twitch.tv", // Auth subdomain | |||
| 214 | + | "gql.twitch.tv", // GraphQL API | |||
| 215 | + | }, | |||
| 213 | }, | 216 | }, | ||
| 214 | } | 217 | } | ||
| 215 | } | 218 | } | ||
M internal/filtering/manager.go +56 -0
| @@ -7,9 +7,11 @@ import ( | |||
| 7 | 7 | "encoding/json" | |
| 8 | 8 | "fmt" | |
| 9 | 9 | "net/http" | |
| 10 | + | "strings" | |
| 10 | 11 | "sync" | |
| 11 | 12 | "time" | |
| 12 | 13 | ||
| 14 | + | "github.com/bnema/dumber/internal/config" | |
| 13 | 15 | "github.com/bnema/dumber/internal/filtering/converter" | |
| 14 | 16 | "github.com/bnema/dumber/internal/filtering/cosmetic" | |
| 15 | 17 | "github.com/bnema/dumber/internal/logging" | |
| @@ -33,6 +35,9 @@ type FilterManager struct { | |||
| 33 | 35 | compileMutex sync.RWMutex | |
| 34 | 36 | compiled *CompiledFilters | |
| 35 | 37 | onFiltersReady FilterReadyCallback | |
| 38 | + | // Cached whitelist rules to avoid recomputing on every filter application | |
| 39 | + | cachedWhitelistRules []converter.WebKitRule | |
| 40 | + | cachedWhitelistHash string | |
| 36 | 41 | } | |
| 37 | 42 | ||
| 38 | 43 | // FilterStore defines the interface for filter storage operations | |
| @@ -76,6 +81,41 @@ func NewFilterManager(store FilterStore, compiler FilterCompiler) *FilterManager | |||
| 76 | 81 | } | |
| 77 | 82 | } | |
| 78 | 83 | ||
| 84 | + | // updateWhitelistCache rebuilds the cached whitelist rules from config if needed | |
| 85 | + | func (fm *FilterManager) updateWhitelistCache(whitelist []string) { | |
| 86 | + | // Create a hash of the whitelist to detect changes | |
| 87 | + | currentHash := strings.Join(whitelist, "|") | |
| 88 | + | ||
| 89 | + | // Only rebuild if whitelist changed | |
| 90 | + | if fm.cachedWhitelistHash == currentHash && fm.cachedWhitelistRules != nil { | |
| 91 | + | return | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | // Rebuild cache with pre-allocation | |
| 95 | + | rules := make([]converter.WebKitRule, 0, len(whitelist)) | |
| 96 | + | for _, domain := range whitelist { | |
| 97 | + | // Create a regex pattern that matches all URLs containing this domain | |
| 98 | + | // Escape special regex characters in the domain | |
| 99 | + | escapedDomain := strings.ReplaceAll(domain, ".", "\\.") | |
| 100 | + | // Match any URL containing this domain (with protocol and optional path) | |
| 101 | + | urlPattern := ".*" + escapedDomain | |
| 102 | + | ||
| 103 | + | rules = append(rules, converter.WebKitRule{ | |
| 104 | + | Trigger: converter.Trigger{ | |
| 105 | + | URLFilter: urlPattern, | |
| 106 | + | }, | |
| 107 | + | Action: converter.Action{ | |
| 108 | + | Type: converter.ActionTypeIgnorePreviousRules, | |
| 109 | + | }, | |
| 110 | + | }) | |
| 111 | + | logging.Info(fmt.Sprintf("[filtering] Created whitelist rule: %s → pattern: %s", domain, urlPattern)) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | fm.cachedWhitelistRules = rules | |
| 115 | + | fm.cachedWhitelistHash = currentHash | |
| 116 | + | logging.Info(fmt.Sprintf("[filtering] Whitelist cache updated with %d domains", len(whitelist))) | |
| 117 | + | } | |
| 118 | + | ||
| 79 | 119 | // SetFiltersReadyCallback sets a callback to be called when filters are loaded/updated | |
| 80 | 120 | func (fm *FilterManager) SetFiltersReadyCallback(callback FilterReadyCallback) { | |
| 81 | 121 | fm.onFiltersReady = callback | |
| @@ -269,6 +309,22 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) { | |||
| 269 | 309 | fm.compileMutex.Lock() | |
| 270 | 310 | defer fm.compileMutex.Unlock() | |
| 271 | 311 | ||
| 312 | + | // Add whitelist rules from config (ignore-previous-rules for whitelisted domains) | |
| 313 | + | // Update cache if whitelist changed, then use cached rules | |
| 314 | + | cfg := config.Get() | |
| 315 | + | ||
| 316 | + | // Only apply filters if content filtering is enabled | |
| 317 | + | if !cfg.ContentFiltering.Enabled { | |
| 318 | + | logging.Info("[filtering] Content filtering is DISABLED in config - no ad blocking active") | |
| 319 | + | return | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | fm.updateWhitelistCache(cfg.ContentFiltering.Whitelist) | |
| 323 | + | if len(fm.cachedWhitelistRules) > 0 { | |
| 324 | + | filters.NetworkRules = append(filters.NetworkRules, fm.cachedWhitelistRules...) | |
| 325 | + | logging.Info(fmt.Sprintf("[filtering] Applied %d whitelist rules (ignore-previous-rules)", len(fm.cachedWhitelistRules))) | |
| 326 | + | } | |
| 327 | + | ||
| 272 | 328 | // Add uBlock Origin style scriptlets to handle broken page loading | |
| 273 | 329 | ||
| 274 | 330 | // Instead of blocking completely, add exception rules for critical ad scripts that break page loading | |
| @@ -7,9 +7,11 @@ import ( | |||||
| 7 | "encoding/json" | 7 | "encoding/json" | ||
| 8 | "fmt" | 8 | "fmt" | ||
| 9 | "net/http" | 9 | "net/http" | ||
| 10 | + | "strings" | |||
| 10 | "sync" | 11 | "sync" | ||
| 11 | "time" | 12 | "time" | ||
| 12 | 13 | ||||
| 14 | + | "github.com/bnema/dumber/internal/config" | |||
| 13 | "github.com/bnema/dumber/internal/filtering/converter" | 15 | "github.com/bnema/dumber/internal/filtering/converter" | ||
| 14 | "github.com/bnema/dumber/internal/filtering/cosmetic" | 16 | "github.com/bnema/dumber/internal/filtering/cosmetic" | ||
| 15 | "github.com/bnema/dumber/internal/logging" | 17 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -33,6 +35,9 @@ type FilterManager struct { | |||||
| 33 | compileMutex sync.RWMutex | 35 | compileMutex sync.RWMutex | ||
| 34 | compiled *CompiledFilters | 36 | compiled *CompiledFilters | ||
| 35 | onFiltersReady FilterReadyCallback | 37 | onFiltersReady FilterReadyCallback | ||
| 38 | + | // Cached whitelist rules to avoid recomputing on every filter application | |||
| 39 | + | cachedWhitelistRules []converter.WebKitRule | |||
| 40 | + | cachedWhitelistHash string | |||
| 36 | } | 41 | } | ||
| 37 | 42 | ||||
| 38 | // FilterStore defines the interface for filter storage operations | 43 | // FilterStore defines the interface for filter storage operations | ||
| @@ -76,6 +81,41 @@ func NewFilterManager(store FilterStore, compiler FilterCompiler) *FilterManager | |||||
| 76 | } | 81 | } | ||
| 77 | } | 82 | } | ||
| 78 | 83 | ||||
| 84 | + | // updateWhitelistCache rebuilds the cached whitelist rules from config if needed | |||
| 85 | + | func (fm *FilterManager) updateWhitelistCache(whitelist []string) { | |||
| 86 | + | // Create a hash of the whitelist to detect changes | |||
| 87 | + | currentHash := strings.Join(whitelist, "|") | |||
| 88 | + | ||||
| 89 | + | // Only rebuild if whitelist changed | |||
| 90 | + | if fm.cachedWhitelistHash == currentHash && fm.cachedWhitelistRules != nil { | |||
| 91 | + | return | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | // Rebuild cache with pre-allocation | |||
| 95 | + | rules := make([]converter.WebKitRule, 0, len(whitelist)) | |||
| 96 | + | for _, domain := range whitelist { | |||
| 97 | + | // Create a regex pattern that matches all URLs containing this domain | |||
| 98 | + | // Escape special regex characters in the domain | |||
| 99 | + | escapedDomain := strings.ReplaceAll(domain, ".", "\\.") | |||
| 100 | + | // Match any URL containing this domain (with protocol and optional path) | |||
| 101 | + | urlPattern := ".*" + escapedDomain | |||
| 102 | + | ||||
| 103 | + | rules = append(rules, converter.WebKitRule{ | |||
| 104 | + | Trigger: converter.Trigger{ | |||
| 105 | + | URLFilter: urlPattern, | |||
| 106 | + | }, | |||
| 107 | + | Action: converter.Action{ | |||
| 108 | + | Type: converter.ActionTypeIgnorePreviousRules, | |||
| 109 | + | }, | |||
| 110 | + | }) | |||
| 111 | + | logging.Info(fmt.Sprintf("[filtering] Created whitelist rule: %s → pattern: %s", domain, urlPattern)) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | fm.cachedWhitelistRules = rules | |||
| 115 | + | fm.cachedWhitelistHash = currentHash | |||
| 116 | + | logging.Info(fmt.Sprintf("[filtering] Whitelist cache updated with %d domains", len(whitelist))) | |||
| 117 | + | } | |||
| 118 | + | ||||
| 79 | // SetFiltersReadyCallback sets a callback to be called when filters are loaded/updated | 119 | // SetFiltersReadyCallback sets a callback to be called when filters are loaded/updated | ||
| 80 | func (fm *FilterManager) SetFiltersReadyCallback(callback FilterReadyCallback) { | 120 | func (fm *FilterManager) SetFiltersReadyCallback(callback FilterReadyCallback) { | ||
| 81 | fm.onFiltersReady = callback | 121 | fm.onFiltersReady = callback | ||
| @@ -269,6 +309,22 @@ func (fm *FilterManager) applyFilters(filters *CompiledFilters) { | |||||
| 269 | fm.compileMutex.Lock() | 309 | fm.compileMutex.Lock() | ||
| 270 | defer fm.compileMutex.Unlock() | 310 | defer fm.compileMutex.Unlock() | ||
| 271 | 311 | ||||
| 312 | + | // Add whitelist rules from config (ignore-previous-rules for whitelisted domains) | |||
| 313 | + | // Update cache if whitelist changed, then use cached rules | |||
| 314 | + | cfg := config.Get() | |||
| 315 | + | ||||
| 316 | + | // Only apply filters if content filtering is enabled | |||
| 317 | + | if !cfg.ContentFiltering.Enabled { | |||
| 318 | + | logging.Info("[filtering] Content filtering is DISABLED in config - no ad blocking active") | |||
| 319 | + | return | |||
| 320 | + | } | |||
| 321 | + | ||||
| 322 | + | fm.updateWhitelistCache(cfg.ContentFiltering.Whitelist) | |||
| 323 | + | if len(fm.cachedWhitelistRules) > 0 { | |||
| 324 | + | filters.NetworkRules = append(filters.NetworkRules, fm.cachedWhitelistRules...) | |||
| 325 | + | logging.Info(fmt.Sprintf("[filtering] Applied %d whitelist rules (ignore-previous-rules)", len(fm.cachedWhitelistRules))) | |||
| 326 | + | } | |||
| 327 | + | ||||
| 272 | // Add uBlock Origin style scriptlets to handle broken page loading | 328 | // Add uBlock Origin style scriptlets to handle broken page loading | ||
| 273 | 329 | ||||
| 274 | // Instead of blocking completely, add exception rules for critical ad scripts that break page loading | 330 | // Instead of blocking completely, add exception rules for critical ad scripts that break page loading | ||