feat(history): normalize URLs to prevent duplicate entries
- commit
- 1c2f59f00ecc660fca744af513cfd74b4153cbe7
- author
- bnema · · 2025-12-16 21:41 UTC
- parent
- b9f594c
- signature
- unsigned
1 file · +25 -0
M internal/application/usecase/navigate.go +25 -0
| @@ -3,6 +3,7 @@ package usecase | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "fmt" | |
| 6 | + | "strings" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| 8 | 9 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -99,6 +100,9 @@ func (uc *NavigateUseCase) Execute(ctx context.Context, input NavigateInput) (*N | |||
| 99 | 100 | func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) { | |
| 100 | 101 | log := logging.FromContext(ctx) | |
| 101 | 102 | ||
| 103 | + | // Normalize URL to avoid duplicates (e.g., github.com vs github.com/) | |
| 104 | + | url = normalizeURLForHistory(url) | |
| 105 | + | ||
| 102 | 106 | // Check if entry exists | |
| 103 | 107 | existing, err := uc.historyRepo.FindByURL(ctx, url) | |
| 104 | 108 | if err != nil { | |
| @@ -123,6 +127,9 @@ func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) { | |||
| 123 | 127 | // UpdateHistoryTitle updates the title of a history entry after page load. | |
| 124 | 128 | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error { | |
| 125 | 129 | log := logging.FromContext(ctx) | |
| 130 | + | ||
| 131 | + | // Normalize URL to match storage (e.g., github.com/ -> github.com) | |
| 132 | + | url = normalizeURLForHistory(url) | |
| 126 | 133 | log.Debug().Str("url", url).Str("title", title).Msg("updating history title") | |
| 127 | 134 | ||
| 128 | 135 | entry, err := uc.historyRepo.FindByURL(ctx, url) | |
| @@ -191,3 +198,21 @@ func (uc *NavigateUseCase) Stop(ctx context.Context, webview port.WebView) error | |||
| 191 | 198 | log.Debug().Msg("stopping page load") | |
| 192 | 199 | return webview.Stop(ctx) | |
| 193 | 200 | } | |
| 201 | + | ||
| 202 | + | // normalizeURLForHistory normalizes a URL for history storage. | |
| 203 | + | // Strips trailing slash to avoid duplicates like github.com vs github.com/ | |
| 204 | + | func normalizeURLForHistory(url string) string { | |
| 205 | + | // Don't strip if URL is just the protocol + domain (e.g., "https://github.com") | |
| 206 | + | // Only strip trailing slash from paths | |
| 207 | + | if strings.HasSuffix(url, "/") { | |
| 208 | + | // Check if it's just domain with trailing slash (e.g., "https://github.com/") | |
| 209 | + | // by counting slashes - protocol has 2, domain adds 0, path adds more | |
| 210 | + | if strings.Count(url, "/") == 3 { | |
| 211 | + | // It's "https://domain.com/" - strip the trailing slash | |
| 212 | + | return strings.TrimSuffix(url, "/") | |
| 213 | + | } | |
| 214 | + | // For paths like "https://github.com/user/" - also strip | |
| 215 | + | return strings.TrimSuffix(url, "/") | |
| 216 | + | } | |
| 217 | + | return url | |
| 218 | + | } | |
| @@ -3,6 +3,7 @@ package usecase | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | + | "strings" | |||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 9 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -99,6 +100,9 @@ func (uc *NavigateUseCase) Execute(ctx context.Context, input NavigateInput) (*N | |||||
| 99 | func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) { | 100 | func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) { | ||
| 100 | log := logging.FromContext(ctx) | 101 | log := logging.FromContext(ctx) | ||
| 101 | 102 | ||||
| 103 | + | // Normalize URL to avoid duplicates (e.g., github.com vs github.com/) | |||
| 104 | + | url = normalizeURLForHistory(url) | |||
| 105 | + | ||||
| 102 | // Check if entry exists | 106 | // Check if entry exists | ||
| 103 | existing, err := uc.historyRepo.FindByURL(ctx, url) | 107 | existing, err := uc.historyRepo.FindByURL(ctx, url) | ||
| 104 | if err != nil { | 108 | if err != nil { | ||
| @@ -123,6 +127,9 @@ func (uc *NavigateUseCase) recordHistory(ctx context.Context, url string) { | |||||
| 123 | // UpdateHistoryTitle updates the title of a history entry after page load. | 127 | // UpdateHistoryTitle updates the title of a history entry after page load. | ||
| 124 | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error { | 128 | func (uc *NavigateUseCase) UpdateHistoryTitle(ctx context.Context, url, title string) error { | ||
| 125 | log := logging.FromContext(ctx) | 129 | log := logging.FromContext(ctx) | ||
| 130 | + | ||||
| 131 | + | // Normalize URL to match storage (e.g., github.com/ -> github.com) | |||
| 132 | + | url = normalizeURLForHistory(url) | |||
| 126 | log.Debug().Str("url", url).Str("title", title).Msg("updating history title") | 133 | log.Debug().Str("url", url).Str("title", title).Msg("updating history title") | ||
| 127 | 134 | ||||
| 128 | entry, err := uc.historyRepo.FindByURL(ctx, url) | 135 | entry, err := uc.historyRepo.FindByURL(ctx, url) | ||
| @@ -191,3 +198,21 @@ func (uc *NavigateUseCase) Stop(ctx context.Context, webview port.WebView) error | |||||
| 191 | log.Debug().Msg("stopping page load") | 198 | log.Debug().Msg("stopping page load") | ||
| 192 | return webview.Stop(ctx) | 199 | return webview.Stop(ctx) | ||
| 193 | } | 200 | } | ||
| 201 | + | ||||
| 202 | + | // normalizeURLForHistory normalizes a URL for history storage. | |||
| 203 | + | // Strips trailing slash to avoid duplicates like github.com vs github.com/ | |||
| 204 | + | func normalizeURLForHistory(url string) string { | |||
| 205 | + | // Don't strip if URL is just the protocol + domain (e.g., "https://github.com") | |||
| 206 | + | // Only strip trailing slash from paths | |||
| 207 | + | if strings.HasSuffix(url, "/") { | |||
| 208 | + | // Check if it's just domain with trailing slash (e.g., "https://github.com/") | |||
| 209 | + | // by counting slashes - protocol has 2, domain adds 0, path adds more | |||
| 210 | + | if strings.Count(url, "/") == 3 { | |||
| 211 | + | // It's "https://domain.com/" - strip the trailing slash | |||
| 212 | + | return strings.TrimSuffix(url, "/") | |||
| 213 | + | } | |||
| 214 | + | // For paths like "https://github.com/user/" - also strip | |||
| 215 | + | return strings.TrimSuffix(url, "/") | |||
| 216 | + | } | |||
| 217 | + | return url | |||
| 218 | + | } | |||