diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f49b634..b6f7b57 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,6 +10,7 @@ repos: exclude: ^web/tsconfig\.json$ - id: check-merge-conflict - id: check-added-large-files + exclude: ^backend/internal/domain/discovery/crawler/testdata/ - id: no-commit-to-branch args: ['--branch', 'main'] diff --git a/backend/cmd/discovery-compare/main.go b/backend/cmd/discovery-compare/main.go new file mode 100644 index 0000000..1b32fdd --- /dev/null +++ b/backend/cmd/discovery-compare/main.go @@ -0,0 +1,233 @@ +// discovery-compare: Run the crawler and Mistral Pass 0 against a sample of +// (land, region, year-month) buckets; emit markdown diff to stdout or --out. +// Purpose: verify crawler coverage before MR 2 deletes the Mistral path. +// +// Requires the usual backend env vars. JWT_SECRET must be set (any value works +// for this tool since no HTTP server is started). +package main + +import ( + "context" + "flag" + "fmt" + "os" + "sort" + "strings" + "time" + + "marktvogt.de/backend/internal/config" + "marktvogt.de/backend/internal/domain/discovery" + "marktvogt.de/backend/internal/domain/discovery/crawler" + "marktvogt.de/backend/internal/pkg/ai" +) + +type sampleBucket struct { + Land string + Region string + YearMonth string +} + +func main() { + var ( + bucketsFlag = flag.String("buckets", "", "comma-separated LAND:REGION:YYYY-MM list (e.g., Deutschland:Bayern:2026-04)") + outFlag = flag.String("out", "", "write markdown report to this file (default: stdout)") + ) + flag.Parse() + + if *bucketsFlag == "" { + fmt.Fprintln(os.Stderr, "--buckets required") + os.Exit(2) + } + buckets, err := parseBuckets(*bucketsFlag) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(2) + } + + if err := run(buckets, *outFlag); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(buckets []sampleBucket, outPath string) error { + cfg, err := config.Load() + if err != nil { + return fmt.Errorf("load config: %w", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + // 1. Crawler run (dry-run — no DB inserts). + cr := crawler.NewCrawler(cfg.Discovery.CrawlerUserAgent, crawler.DefaultSourceConfigs()) + res, err := cr.RunAll(ctx) + if err != nil { + return fmt.Errorf("crawler: %w", err) + } + crawlerEvents := make([]crawler.RawEvent, 0) + for _, evs := range res.PerSource { + crawlerEvents = append(crawlerEvents, evs...) + } + merged := crawler.Merge(crawlerEvents) + crawlerByBucket := groupCrawlerByBucket(merged, buckets) + + // 2. Mistral Pass 0 run (one per sample bucket). + aiClient := ai.New(cfg.AI.APIKey, cfg.AI.AgentSimple, cfg.AI.ModelComplex, cfg.AI.RateLimitRPS) + agentClient := discovery.NewAgentClient(aiClient, cfg.AI.AgentDiscovery) + mistralByBucket := runMistralForBuckets(ctx, agentClient, buckets) + + // 3. Emit report. + report := buildMarkdownReport(buckets, crawlerByBucket, mistralByBucket) + if outPath == "" { + fmt.Println(report) + return nil + } + if err := os.WriteFile(outPath, []byte(report), 0644); err != nil { + return fmt.Errorf("write out: %w", err) + } + return nil +} + +func parseBuckets(s string) ([]sampleBucket, error) { + parts := strings.Split(s, ",") + out := make([]sampleBucket, 0, len(parts)) + for _, item := range parts { + fields := strings.Split(strings.TrimSpace(item), ":") + if len(fields) != 3 { + return nil, fmt.Errorf("bucket %q: want LAND:REGION:YYYY-MM", item) + } + out = append(out, sampleBucket{Land: fields[0], Region: fields[1], YearMonth: fields[2]}) + } + return out, nil +} + +// groupCrawlerByBucket assigns merged crawler events to sample buckets. +// +// NOTE: this is an approximation for the diagnostic CLI only — not for +// production dedup. The Bundesland match uses `strings.Contains` so a merged +// event with Bundesland="Bayern" will join a bucket with Region="Bay" (or +// "ern"). Good enough to compare coverage between the crawler and Mistral +// Pass 0 at bucket granularity; not safe for business-logic routing. +func groupCrawlerByBucket(merged []crawler.MergedEvent, buckets []sampleBucket) map[string][]crawler.MergedEvent { + result := make(map[string][]crawler.MergedEvent) + for _, b := range buckets { + result[bucketKey(b)] = nil + } + for _, m := range merged { + if m.StartDate == nil { + continue + } + ym := m.StartDate.Format("2006-01") + for _, b := range buckets { + if b.YearMonth != ym { + continue + } + if m.Land != "" && m.Land != b.Land { + continue + } + if m.Bundesland != "" && !strings.Contains(m.Bundesland, b.Region) { + continue + } + key := bucketKey(b) + result[key] = append(result[key], m) + } + } + return result +} + +func runMistralForBuckets(ctx context.Context, ac *discovery.AgentClient, buckets []sampleBucket) map[string][]discovery.Pass0Market { + out := make(map[string][]discovery.Pass0Market) + for _, b := range buckets { + bk := discovery.Bucket{Land: b.Land, Region: b.Region, YearMonth: b.YearMonth, Halbmonat: "H1"} + resp, err := ac.Discover(ctx, bk) + if err != nil { + fmt.Fprintf(os.Stderr, "mistral %s: %v\n", bucketKey(b), err) + continue + } + out[bucketKey(b)] = resp.Maerkte + } + return out +} + +func bucketKey(b sampleBucket) string { + return b.Land + ":" + b.Region + ":" + b.YearMonth +} + +func buildMarkdownReport(buckets []sampleBucket, c map[string][]crawler.MergedEvent, m map[string][]discovery.Pass0Market) string { + var sb strings.Builder + sb.WriteString("# Discovery coverage comparison\n\n") + sb.WriteString(fmt.Sprintf("Generated: %s\n\n", time.Now().Format(time.RFC3339))) + for _, b := range buckets { + key := bucketKey(b) + ce := c[key] + me := m[key] + sb.WriteString(fmt.Sprintf("## %s\n\n", key)) + sb.WriteString(fmt.Sprintf("- crawler: %d events\n", len(ce))) + sb.WriteString(fmt.Sprintf("- mistral: %d events\n\n", len(me))) + + crawlerNames := nameSet(ceNames(ce)) + mistralNames := nameSet(meNames(me)) + + sb.WriteString("### Only in crawler\n") + for _, n := range diff(crawlerNames, mistralNames) { + sb.WriteString(fmt.Sprintf("- %s\n", n)) + } + sb.WriteString("\n### Only in mistral\n") + for _, n := range diff(mistralNames, crawlerNames) { + sb.WriteString(fmt.Sprintf("- %s\n", n)) + } + sb.WriteString("\n### In both\n") + for _, n := range intersect(crawlerNames, mistralNames) { + sb.WriteString(fmt.Sprintf("- %s\n", n)) + } + sb.WriteString("\n") + } + return sb.String() +} + +func ceNames(ce []crawler.MergedEvent) []string { + out := make([]string, len(ce)) + for i, e := range ce { + out[i] = discovery.NormalizeName(e.Name) + } + return out +} + +func meNames(me []discovery.Pass0Market) []string { + out := make([]string, len(me)) + for i, e := range me { + out[i] = discovery.NormalizeName(e.MarktName) + } + return out +} + +func nameSet(names []string) map[string]bool { + s := make(map[string]bool, len(names)) + for _, n := range names { + s[n] = true + } + return s +} + +func diff(a, b map[string]bool) []string { + out := make([]string, 0, len(a)) + for n := range a { + if !b[n] { + out = append(out, n) + } + } + sort.Strings(out) + return out +} + +func intersect(a, b map[string]bool) []string { + out := make([]string, 0) + for n := range a { + if b[n] { + out = append(out, n) + } + } + sort.Strings(out) + return out +} diff --git a/backend/go.mod b/backend/go.mod index 11d6849..d8813ee 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,6 +3,7 @@ module marktvogt.de/backend go 1.26 require ( + github.com/PuerkitoBio/goquery v1.12.0 github.com/VikingOwl91/mistral-go-sdk v1.3.0 github.com/gin-gonic/gin v1.11.0 github.com/go-playground/validator/v10 v10.30.1 @@ -11,12 +12,13 @@ require ( github.com/jackc/pgx/v5 v5.8.0 github.com/pquerna/otp v1.5.0 github.com/valkey-io/valkey-go v1.0.72 - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.49.0 golang.org/x/oauth2 v0.35.0 golang.org/x/time v0.14.0 ) require ( + github.com/andybalholm/cascadia v1.3.3 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect @@ -42,9 +44,9 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 782c9f8..c640361 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,9 @@ +github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo= +github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ= github.com/VikingOwl91/mistral-go-sdk v1.3.0 h1:OkTsodDE5lmdf7p2cwScqD2vIk8sScQ2IGk65dUjuz0= github.com/VikingOwl91/mistral-go-sdk v1.3.0/go.mod h1:f4emNtHUx2zSqY3V0LBz6lNI1jE6q/zh+SEU+/hJ0i4= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= @@ -31,6 +35,7 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -84,27 +89,91 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/valkey-io/valkey-go v1.0.72 h1:iRWt1hJyOchcEgbHSkRY3aKkcBudxvMaVMsmxuYxuxE= github.com/valkey-io/valkey-go v1.0.72/go.mod h1:VGhZ6fs68Qrn2+OhH+6waZH27bjpgQOiLyUQyXuYK5k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index a5e4a7b..ed97540 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -26,9 +26,11 @@ type Config struct { } type DiscoveryConfig struct { - Token string // bearer token for /tick endpoint - BatchSize int // buckets per tick (default 4) - ForwardMonths int // forward window in months (default 12) + Token string // bearer token for /tick endpoint + BatchSize int // buckets per tick (default 4) + ForwardMonths int // forward window in months (default 12) + CrawlerUserAgent string // user-agent for crawler HTTP requests + CrawlerManualRateLimitPerHour int // max manual crawl requests per hour (1-3600, default 1) } type AIConfig struct { @@ -199,6 +201,11 @@ func Load() (*Config, error) { slog.Warn("DISCOVERY_TOKEN is empty; /api/v1/admin/discovery/tick is disabled") } + crawlerRateLimit, err := envInt("DISCOVERY_CRAWLER_MANUAL_RATE_LIMIT_PER_HOUR", 1) + if err != nil { + return nil, fmt.Errorf("DISCOVERY_CRAWLER_MANUAL_RATE_LIMIT_PER_HOUR: %w", err) + } + jwtSecret := envStr("JWT_SECRET", "") if jwtSecret == "" { return nil, fmt.Errorf("JWT_SECRET is required") @@ -285,9 +292,11 @@ func Load() (*Config, error) { RateLimitRPS: rpsAI, }, Discovery: DiscoveryConfig{ - Token: discoveryToken, - BatchSize: batchSize, - ForwardMonths: forwardMonths, + Token: discoveryToken, + BatchSize: batchSize, + ForwardMonths: forwardMonths, + CrawlerUserAgent: envStr("DISCOVERY_CRAWLER_USER_AGENT", "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"), + CrawlerManualRateLimitPerHour: crawlerRateLimit, }, }, nil } diff --git a/backend/internal/domain/discovery/crawler/constants.go b/backend/internal/domain/discovery/crawler/constants.go new file mode 100644 index 0000000..c4f63e0 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/constants.go @@ -0,0 +1,20 @@ +package crawler + +// Land names used in RawEvent/MergedEvent.Land and throughout discovery. ASCII +// forms (Oesterreich, not Österreich) to keep string comparisons and log output +// stable across locales. Defined in one place so every source parser references +// the same constants. +const ( + landDeutschland = "Deutschland" + landOesterreich = "Oesterreich" + landSchweiz = "Schweiz" +) + +// Source name constants — used in SourceConfig.Name, switch cases, and tests. +const ( + sourceMarktkalendarium = "marktkalendarium" + sourceMittelalterkalender = "mittelalterkalender" + sourceFestivalAlarm = "festival_alarm" + sourceMittelaltermarktOnline = "mittelaltermarkt_online" + sourceSuendenfrei = "suendenfrei" +) diff --git a/backend/internal/domain/discovery/crawler/crawler.go b/backend/internal/domain/discovery/crawler/crawler.go new file mode 100644 index 0000000..66ed81d --- /dev/null +++ b/backend/internal/domain/discovery/crawler/crawler.go @@ -0,0 +1,81 @@ +package crawler + +import ( + "context" + "fmt" + "log/slog" + "time" +) + +// Crawler orchestrates per-source fetch, collects results + errors. +type Crawler struct { + sources []Source +} + +// NewCrawler builds source instances from the supplied configs. The UA is +// injected into the shared Fetcher. +func NewCrawler(userAgent string, configs []SourceConfig) *Crawler { + f := NewFetcher(FetcherOptions{UserAgent: userAgent}) + c := &Crawler{} + for _, cfg := range configs { + switch cfg.Name { + case sourceMarktkalendarium: + c.sources = append(c.sources, NewMarktkalendarium(f, cfg.URLs)) + case sourceMittelalterkalender: + c.sources = append(c.sources, NewMittelalterkalender(f, cfg.URLs)) + case sourceFestivalAlarm: + y2u := make(map[int]string, len(cfg.URLs)) + for _, u := range cfg.URLs { + // Year is the last 4 digits of the URL. + if len(u) < 4 { + continue + } + var year int + if _, err := fmt.Sscanf(u[len(u)-4:], "%d", &year); err == nil { + y2u[year] = u + } + } + c.sources = append(c.sources, NewFestivalAlarm(f, y2u)) + case sourceMittelaltermarktOnline: + c.sources = append(c.sources, NewMittelaltermarktOnline(f, cfg.URLs)) + case sourceSuendenfrei: + if len(cfg.URLs) > 0 { + c.sources = append(c.sources, NewSuendenfrei(f, cfg.URLs[0])) + } + default: + slog.Warn("unknown crawler source, skipping", "name", cfg.Name) + } + } + return c +} + +// RunAll fetches each source sequentially, isolating errors so one bad source +// doesn't kill the whole run. Per-source elapsed time is recorded. +func (c *Crawler) RunAll(ctx context.Context) (CrawlResult, error) { + res := CrawlResult{ + PerSource: make(map[string][]RawEvent), + PerSourceMS: make(map[string]int64), + } + for i, src := range c.sources { + if err := ctx.Err(); err != nil { + return res, err + } + if i > 0 { + if err := sleepCtx(ctx, 1*time.Second); err != nil { + return res, err + } + } + start := time.Now() + events, err := src.Fetch(ctx) + elapsed := time.Since(start).Milliseconds() + res.PerSourceMS[src.Name()] = elapsed + if err != nil { + res.SourceErrors = append(res.SourceErrors, SourceError{Name: src.Name(), Err: err}) + slog.ErrorContext(ctx, "crawler source failed", "source", src.Name(), "error", err) + continue + } + res.PerSource[src.Name()] = events + slog.InfoContext(ctx, "crawler source ok", "source", src.Name(), "events", len(events), "elapsed_ms", elapsed) + } + return res, nil +} diff --git a/backend/internal/domain/discovery/crawler/crawler_test.go b/backend/internal/domain/discovery/crawler/crawler_test.go new file mode 100644 index 0000000..86e6fba --- /dev/null +++ b/backend/internal/domain/discovery/crawler/crawler_test.go @@ -0,0 +1,52 @@ +package crawler + +import ( + "context" + "errors" + "testing" + "time" +) + +type stubSource struct { + name string + events []RawEvent + err error +} + +func (s *stubSource) Name() string { return s.name } +func (s *stubSource) Fetch(ctx context.Context) ([]RawEvent, error) { + return s.events, s.err +} + +func TestCrawlerRunAllCollectsEventsAndErrors(t *testing.T) { + c := &Crawler{ + sources: []Source{ + &stubSource{name: "a", events: []RawEvent{{SourceName: "a", Name: "X"}}}, + &stubSource{name: "b", err: errors.New("boom")}, + &stubSource{name: "c", events: []RawEvent{{SourceName: "c", Name: "Y"}}}, + }, + } + res, err := c.RunAll(context.Background()) + if err != nil { + t.Fatal(err) + } + if len(res.PerSource["a"]) != 1 || len(res.PerSource["c"]) != 1 { + t.Errorf("per-source events missing: %+v", res.PerSource) + } + if len(res.SourceErrors) != 1 || res.SourceErrors[0].Name != "b" { + t.Errorf("expected one SourceError for b; got %+v", res.SourceErrors) + } +} + +func TestCrawlerRunAllCancellation(t *testing.T) { + c := &Crawler{sources: []Source{ + &stubSource{name: "slow", events: []RawEvent{{Name: "X"}}}, + }} + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := c.RunAll(ctx) + if err == nil { + t.Fatal("expected ctx error, got nil") + } + _ = time.Second // prevent unused import +} diff --git a/backend/internal/domain/discovery/crawler/festival_alarm.go b/backend/internal/domain/discovery/crawler/festival_alarm.go new file mode 100644 index 0000000..f1c1496 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/festival_alarm.go @@ -0,0 +1,139 @@ +package crawler + +import ( + "bytes" + "context" + "fmt" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" +) + +// FestivalAlarmSource scrapes festival-alarm.com. The Mittelalter category +// page renders a flat with tr.event-wrapper rows. Genre is in +// td.event-genre; only rows containing "Mittelalter" are emitted. ISO dates +// are available as content attributes on span[itemprop=startDate/endDate]. +type FestivalAlarmSource struct { + fetcher *Fetcher + urls []festivalAlarmURL +} + +type festivalAlarmURL struct { + URL string + Year int +} + +func NewFestivalAlarm(f *Fetcher, yearToURL map[int]string) *FestivalAlarmSource { + urls := make([]festivalAlarmURL, 0, len(yearToURL)) + for y, u := range yearToURL { + urls = append(urls, festivalAlarmURL{URL: u, Year: y}) + } + return &FestivalAlarmSource{fetcher: f, urls: urls} +} + +func (s *FestivalAlarmSource) Name() string { return "festival_alarm" } + +func (s *FestivalAlarmSource) Fetch(ctx context.Context) ([]RawEvent, error) { + var all []RawEvent + for i, u := range s.urls { + if i > 0 { + if err := sleepCtx(ctx, 2*time.Second); err != nil { + return all, err + } + } + body, err := s.fetcher.Get(ctx, u.URL, "") + if err != nil { + return all, fmt.Errorf("festival_alarm %s: %w", u.URL, err) + } + events, err := parseFestivalAlarm(body, u.URL, u.Year) + if err != nil { + return all, fmt.Errorf("festival_alarm parse %s: %w", u.URL, err) + } + all = append(all, events...) + } + return all, nil +} + +func parseFestivalAlarm(data []byte, sourceURL string, year int) ([]RawEvent, error) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + seen := make(map[string]struct{}) + var events []RawEvent + + doc.Find("tr.event-wrapper").Each(func(_ int, tr *goquery.Selection) { + // Genre filter — must contain "Mittelalter". + genre := strings.TrimSpace(tr.Find("td.event-genre").Text()) + if !strings.Contains(strings.ToLower(genre), "mittelalter") { + return + } + + // Name + detail URL from the event-title cell. + titleAnchor := tr.Find("td.event-title a").First() + name := strings.TrimSpace(titleAnchor.Text()) + if name == "" { + return + } + + href, _ := titleAnchor.Attr("href") + detailURL := resolveURL(sourceURL, strings.TrimSpace(href)) + + // Dedup by detail URL (same event can be listed multiple times for + // multi-day spans on some pages). + dedupKey := detailURL + if dedupKey == "" { + dedupKey = name + } + if _, exists := seen[dedupKey]; exists { + return + } + seen[dedupKey] = struct{}{} + + // Dates — prefer ISO content attributes; fall back to year parameter for year. + startDate := parseDateAttr(tr.Find("span[itemprop='startDate']").First(), year) + endDate := parseDateAttr(tr.Find("span[itemprop='endDate']").First(), year) + if startDate == nil { + return + } + + // Venue fields from the hidden event-venue cell. + venue := strings.TrimSpace(tr.Find("span[itemprop='name']").First().Text()) + city := strings.TrimSpace(tr.Find("span[itemprop='addressLocality']").Text()) + plz := strings.TrimSpace(tr.Find("span[itemprop='postalCode']").Text()) + + events = append(events, RawEvent{ + SourceName: "festival_alarm", + SourceURL: sourceURL, + DetailURL: detailURL, + Name: name, + City: city, + PLZ: plz, + Land: InferLand(plz), + Venue: venue, + StartDate: startDate, + EndDate: endDate, + }) + }) + return events, nil +} + +// parseDateAttr reads the content="YYYY-MM-DD" attribute from an itemprop +// span and returns a *time.Time in UTC. Returns nil if absent or malformed. +// The year parameter is accepted for signature symmetry with future sources +// that may not include full dates in their markup; it is intentionally unused +// here because festival-alarm always emits complete ISO dates. +func parseDateAttr(s *goquery.Selection, year int) *time.Time { + _ = year + content, exists := s.Attr("content") + if !exists || content == "" { + return nil + } + t, err := time.Parse("2006-01-02", content) + if err != nil { + return nil + } + return &t +} diff --git a/backend/internal/domain/discovery/crawler/festival_alarm_test.go b/backend/internal/domain/discovery/crawler/festival_alarm_test.go new file mode 100644 index 0000000..9ca4298 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/festival_alarm_test.go @@ -0,0 +1,31 @@ +package crawler + +import ( + "os" + "testing" +) + +func TestFestivalAlarmParse(t *testing.T) { + data, err := os.ReadFile("testdata/festival_alarm.html") + if err != nil { + t.Fatal(err) + } + events, err := parseFestivalAlarm(data, "https://www.festival-alarm.com/Kategorien/Mittelalter-Festivals/(year)/2026", 2026) + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(events) == 0 { + t.Fatal("expected at least one Mittelalter event") + } + for _, e := range events { + if e.SourceName != "festival_alarm" { + t.Errorf("SourceName = %q", e.SourceName) + } + if e.Name == "" { + t.Errorf("Name empty for %+v", e) + } + if e.StartDate == nil || e.StartDate.Year() != 2026 { + t.Errorf("StartDate = %v (want year 2026)", e.StartDate) + } + } +} diff --git a/backend/internal/domain/discovery/crawler/fetch.go b/backend/internal/domain/discovery/crawler/fetch.go new file mode 100644 index 0000000..dc2cf8a --- /dev/null +++ b/backend/internal/domain/discovery/crawler/fetch.go @@ -0,0 +1,170 @@ +package crawler + +import ( + "context" + "fmt" + "io" + "net/http" + "time" +) + +// Fetcher is the shared HTTP client across all crawler sources. Enforces polite +// defaults: retry on 5xx/network, back off on 429, stop immediately on 4xx. +// Per-host connection cap is on the Transport so all callers share the gate. +type Fetcher struct { + client *http.Client + userAgent string + retryWait []time.Duration + rateLimitWaits []time.Duration +} + +type FetcherOptions struct { + UserAgent string + Timeout time.Duration + RetryWait []time.Duration + RateLimitWaits []time.Duration +} + +func NewFetcher(opts FetcherOptions) *Fetcher { + if opts.Timeout == 0 { + opts.Timeout = 30 * time.Second + } + if opts.RetryWait == nil { + opts.RetryWait = []time.Duration{2 * time.Second, 5 * time.Second} + } + if opts.RateLimitWaits == nil { + opts.RateLimitWaits = []time.Duration{60 * time.Second, 180 * time.Second} + } + return &Fetcher{ + client: &http.Client{ + Timeout: opts.Timeout, + Transport: &http.Transport{ + MaxIdleConnsPerHost: 2, + MaxConnsPerHost: 1, + IdleConnTimeout: 30 * time.Second, + }, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= 10 { + return http.ErrUseLastResponse + } + return nil + }, + }, + userAgent: opts.UserAgent, + retryWait: opts.RetryWait, + rateLimitWaits: opts.RateLimitWaits, + } +} + +// Get fetches a URL and returns the body bytes. accept overrides the default +// Accept header ("" -> HTML default). +func (f *Fetcher) Get(ctx context.Context, url, accept string) ([]byte, error) { + if accept == "" { + accept = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + } + + attempt := func() (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", f.userAgent) + req.Header.Set("Accept-Language", "de-DE,de;q=0.9,en;q=0.8") + req.Header.Set("Accept", accept) + return f.client.Do(req) + } + + // Initial + 5xx retries. + var lastErr error + waits := append([]time.Duration{0}, f.retryWait...) + for i, wait := range waits { + if wait > 0 { + if err := sleepCtx(ctx, wait); err != nil { + return nil, err + } + } + resp, err := attempt() + if err != nil { + lastErr = err + if i < len(waits)-1 { + continue + } + return nil, fmt.Errorf("fetch %s: %w", url, err) + } + + // 4xx other than 429: stop immediately. + if resp.StatusCode == http.StatusTooManyRequests { + _ = resp.Body.Close() + return f.handle429(ctx, url, accept) + } + if resp.StatusCode >= 400 && resp.StatusCode < 500 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + _ = resp.Body.Close() + return nil, fmt.Errorf("fetch %s: http %d: %s", url, resp.StatusCode, string(body)) + } + + // 5xx: retry unless this was the last attempt. + if resp.StatusCode >= 500 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + _ = resp.Body.Close() + lastErr = fmt.Errorf("http %d: %s", resp.StatusCode, string(body)) + if i < len(waits)-1 { + continue + } + return nil, fmt.Errorf("fetch %s: %w", url, lastErr) + } + + body, err := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("read body %s: %w", url, err) + } + return body, nil + } + return nil, lastErr +} + +// handle429 applies longer 429-specific backoffs, then one more attempt per +// wait in rateLimitWaits. Gives up after the last wait still sees 429 or worse. +func (f *Fetcher) handle429(ctx context.Context, url, accept string) ([]byte, error) { + for _, wait := range f.rateLimitWaits { + if err := sleepCtx(ctx, wait); err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", f.userAgent) + req.Header.Set("Accept-Language", "de-DE,de;q=0.9,en;q=0.8") + req.Header.Set("Accept", accept) + resp, err := f.client.Do(req) + if err != nil { + return nil, fmt.Errorf("fetch %s after 429 wait: %w", url, err) + } + if resp.StatusCode == http.StatusOK { + body, rerr := io.ReadAll(resp.Body) + _ = resp.Body.Close() + if rerr != nil { + return nil, fmt.Errorf("read body %s: %w", url, rerr) + } + return body, nil + } + _ = resp.Body.Close() + if resp.StatusCode != http.StatusTooManyRequests { + return nil, fmt.Errorf("fetch %s after 429 wait: http %d", url, resp.StatusCode) + } + } + return nil, fmt.Errorf("fetch %s: rate-limited (429) after all retries", url) +} + +func sleepCtx(ctx context.Context, d time.Duration) error { + t := time.NewTimer(d) + defer t.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-t.C: + return nil + } +} diff --git a/backend/internal/domain/discovery/crawler/fetch_test.go b/backend/internal/domain/discovery/crawler/fetch_test.go new file mode 100644 index 0000000..8bc475c --- /dev/null +++ b/backend/internal/domain/discovery/crawler/fetch_test.go @@ -0,0 +1,117 @@ +package crawler + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + "time" +) + +func TestFetcherSuccess(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if ua := r.Header.Get("User-Agent"); !strings.Contains(ua, "Firefox") { + t.Errorf("unexpected UA: %q", ua) + } + w.WriteHeader(200) + _, _ = io.WriteString(w, "ok") + })) + defer srv.Close() + + f := NewFetcher(FetcherOptions{UserAgent: "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"}) + body, err := f.Get(context.Background(), srv.URL, "") + if err != nil { + t.Fatal(err) + } + if string(body) != "ok" { + t.Errorf("body = %q; want %q", body, "ok") + } +} + +func TestFetcherRetriesOn5xx(t *testing.T) { + var hits int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n := atomic.AddInt32(&hits, 1) + if n < 3 { + w.WriteHeader(503) + return + } + w.WriteHeader(200) + _, _ = io.WriteString(w, "ok") + })) + defer srv.Close() + + f := NewFetcher(FetcherOptions{ + UserAgent: "t", + RetryWait: []time.Duration{10 * time.Millisecond, 20 * time.Millisecond}, + }) + body, err := f.Get(context.Background(), srv.URL, "") + if err != nil { + t.Fatal(err) + } + if string(body) != "ok" { + t.Errorf("body = %q; want %q", body, "ok") + } + if got := atomic.LoadInt32(&hits); got != 3 { + t.Errorf("hits = %d; want 3", got) + } +} + +func TestFetcherGivesUpOn4xx(t *testing.T) { + var hits int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&hits, 1) + w.WriteHeader(404) + })) + defer srv.Close() + + f := NewFetcher(FetcherOptions{UserAgent: "t", RetryWait: []time.Duration{1 * time.Millisecond}}) + _, err := f.Get(context.Background(), srv.URL, "") + if err == nil { + t.Fatal("expected error, got nil") + } + if got := atomic.LoadInt32(&hits); got != 1 { + t.Errorf("hits = %d; want 1 (no retry on 4xx)", got) + } +} + +func TestFetcherBacksOffOn429(t *testing.T) { + var hits int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&hits, 1) + w.WriteHeader(429) + })) + defer srv.Close() + + f := NewFetcher(FetcherOptions{ + UserAgent: "t", + RetryWait: []time.Duration{1 * time.Millisecond}, + RateLimitWaits: []time.Duration{1 * time.Millisecond, 2 * time.Millisecond}, + }) + _, err := f.Get(context.Background(), srv.URL, "") + if err == nil { + t.Fatal("expected error after 429 retries exhausted") + } + // One initial + two rate-limit retries = 3 hits total. + if got := atomic.LoadInt32(&hits); got != 3 { + t.Errorf("hits = %d; want 3", got) + } +} + +func TestFetcherHonorsContextCancel(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(500 * time.Millisecond) + })) + defer srv.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + f := NewFetcher(FetcherOptions{UserAgent: "t"}) + if _, err := f.Get(ctx, srv.URL, ""); err == nil { + t.Fatal("expected context error, got nil") + } +} diff --git a/backend/internal/domain/discovery/crawler/marktkalendarium.go b/backend/internal/domain/discovery/crawler/marktkalendarium.go new file mode 100644 index 0000000..5a638bd --- /dev/null +++ b/backend/internal/domain/discovery/crawler/marktkalendarium.go @@ -0,0 +1,151 @@ +package crawler + +import ( + "bytes" + "context" + "fmt" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" +) + +// MarktkalendariumSource scrapes www.marktkalendarium.de. Table rows: +// Von | Bis | Veranstaltung | Ort | Platz | Webseite | Veranstalter +type MarktkalendariumSource struct { + fetcher *Fetcher + urls []string +} + +func NewMarktkalendarium(f *Fetcher, urls []string) *MarktkalendariumSource { + return &MarktkalendariumSource{fetcher: f, urls: urls} +} + +func (s *MarktkalendariumSource) Name() string { return "marktkalendarium" } + +func (s *MarktkalendariumSource) Fetch(ctx context.Context) ([]RawEvent, error) { + var all []RawEvent + for i, url := range s.urls { + if i > 0 { + if err := sleepCtx(ctx, 2*time.Second); err != nil { + return all, err + } + } + body, err := s.fetcher.Get(ctx, url, "") + if err != nil { + return all, fmt.Errorf("marktkalendarium %s: %w", url, err) + } + events, err := parseMarktkalendarium(body, url) + if err != nil { + return all, fmt.Errorf("marktkalendarium parse %s: %w", url, err) + } + all = append(all, events...) + } + return all, nil +} + +func parseMarktkalendarium(data []byte, sourceURL string) ([]RawEvent, error) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + var events []RawEvent + doc.Find("table tr").Each(func(_ int, tr *goquery.Selection) { + cells := tr.Find("td") + if cells.Length() < 4 { + return // header row or layout + } + von := strings.TrimSpace(cells.Eq(0).Text()) + bis := strings.TrimSpace(cells.Eq(1).Text()) + name := strings.TrimSpace(cells.Eq(2).Text()) + ort := strings.TrimSpace(cells.Eq(3).Text()) + platz := "" + website := "" + organizer := "" + if cells.Length() >= 5 { + platz = strings.TrimSpace(cells.Eq(4).Text()) + } + if cells.Length() >= 6 { + // First anchor href, not the text — text may be "[Facebook link]". + href, _ := cells.Eq(5).Find("a").First().Attr("href") + website = strings.TrimSpace(href) + if website == "" { + website = strings.TrimSpace(cells.Eq(5).Text()) + } + } + if cells.Length() >= 7 { + organizer = strings.TrimSpace(cells.Eq(6).Text()) + } + + if name == "" || von == "" { + return + } + + start := parseDEDate(von) + end := parseDEDate(bis) + if start == nil { + return + } + + land, plz, city := splitMarktkalendariumOrt(ort) + + events = append(events, RawEvent{ + SourceName: "marktkalendarium", + SourceURL: sourceURL, + Name: name, + City: city, + PLZ: plz, + Land: land, + StartDate: start, + EndDate: end, + Website: website, + Venue: platz, + Organizer: organizer, + }) + }) + return events, nil +} + +// splitMarktkalendariumOrt parses "D-49186 Bad Iburg" into (land, PLZ, city). +// Returns ("", "", raw) when the prefix or PLZ doesn't match the expected shape. +func splitMarktkalendariumOrt(ort string) (land, plz, city string) { + idx := strings.Index(ort, "-") + if idx < 1 || idx > 3 { + return "", "", ort + } + prefix := ort[:idx] + rest := ort[idx+1:] + + switch prefix { + case "D": + land = landDeutschland + case "A": + land = landOesterreich + case "CH": + land = landSchweiz + default: + return "", "", ort + } + + sp := strings.IndexByte(rest, ' ') + if sp < 0 { + return land, "", strings.TrimSpace(rest) + } + plz = strings.TrimSpace(rest[:sp]) + city = strings.TrimSpace(rest[sp+1:]) + return land, plz, city +} + +// parseDEDate parses "3.4.2026" and returns nil on any failure. +func parseDEDate(s string) *time.Time { + s = strings.TrimSpace(s) + if s == "" { + return nil + } + for _, layout := range []string{"2.1.2006", "02.01.2006"} { + if t, err := time.Parse(layout, s); err == nil { + return &t + } + } + return nil +} diff --git a/backend/internal/domain/discovery/crawler/marktkalendarium_test.go b/backend/internal/domain/discovery/crawler/marktkalendarium_test.go new file mode 100644 index 0000000..6454eab --- /dev/null +++ b/backend/internal/domain/discovery/crawler/marktkalendarium_test.go @@ -0,0 +1,64 @@ +package crawler + +import ( + "os" + "testing" +) + +func TestMarktkalendariumParse(t *testing.T) { + data, err := os.ReadFile("testdata/marktkalendarium.html") + if err != nil { + t.Fatal(err) + } + + events, err := parseMarktkalendarium(data, "https://www.marktkalendarium.de/maerkte2026.php") + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(events) < 10 { + t.Fatalf("got %d events; expected at least 10", len(events)) + } + + // Basic shape checks on the first event — tighten once fixture is stable. + e := events[0] + if e.SourceName != sourceMarktkalendarium { + t.Errorf("SourceName = %q; want marktkalendarium", e.SourceName) + } + if e.Name == "" { + t.Error("Name empty") + } + if e.City == "" { + t.Error("City empty") + } + if e.Land != "Deutschland" && e.Land != "Oesterreich" && e.Land != "Schweiz" { + t.Errorf("Land = %q; want DACH country", e.Land) + } + if e.StartDate == nil { + t.Error("StartDate nil") + } + if e.SourceURL != "https://www.marktkalendarium.de/maerkte2026.php" { + t.Errorf("SourceURL = %q", e.SourceURL) + } +} + +func TestParseMarktkalendariumOrtField(t *testing.T) { + tests := []struct { + in string + wantLand, wantPLZ, wantCity string + }{ + {"D-49186 Bad Iburg", "Deutschland", "49186", "Bad Iburg"}, + {"A-1010 Wien", "Oesterreich", "1010", "Wien"}, + {"CH-8001 Zuerich", "Schweiz", "8001", "Zuerich"}, + {"D-94152 Neuhaus am Inn", "Deutschland", "94152", "Neuhaus am Inn"}, + {"garbage", "", "", "garbage"}, + } + for _, tc := range tests { + t.Run(tc.in, func(t *testing.T) { + land, plz, city := splitMarktkalendariumOrt(tc.in) + if land != tc.wantLand || plz != tc.wantPLZ || city != tc.wantCity { + t.Errorf("splitMarktkalendariumOrt(%q) = (%q, %q, %q); want (%q, %q, %q)", + tc.in, land, plz, city, tc.wantLand, tc.wantPLZ, tc.wantCity) + } + }) + } +} diff --git a/backend/internal/domain/discovery/crawler/merger.go b/backend/internal/domain/discovery/crawler/merger.go new file mode 100644 index 0000000..8f9422c --- /dev/null +++ b/backend/internal/domain/discovery/crawler/merger.go @@ -0,0 +1,216 @@ +package crawler + +import ( + "sort" + "strings" + "time" + + "marktvogt.de/backend/internal/domain/discovery/normalize" +) + +// sourceRank: lower = better. Used for field-by-field tie-breaking. +var sourceRank = map[string]int{ + "mittelaltermarkt_online": 1, + "marktkalendarium": 2, + "mittelalterkalender": 3, + "festival_alarm": 4, + "suendenfrei": 5, +} + +// Merge groups RawEvents by normalized (name, city, start_date) and picks the +// best value for each field using the source rank. Pure function. +// +// Second-pass fold: when NormalizeName collapses an event name entirely to "" +// (e.g. "Mittelaltermarkt" — a pure strip-word), the resulting empty-name bucket +// is folded into any same-(city, date) bucket that has a non-empty normalized +// name. This handles sources that use only generic names for an event that other +// sources describe more specifically. +func Merge(raws []RawEvent) []MergedEvent { + groups := make(map[string][]RawEvent) + order := []string{} + for _, r := range raws { + key := mergeKey(r) + if _, seen := groups[key]; !seen { + order = append(order, key) + } + groups[key] = append(groups[key], r) + } + + // Second pass: fold empty-name keys into matching (city, date) keys. + cityDateToKey := make(map[string]string) // cityDate → first non-empty-name key + for _, key := range order { + name, cityDate := splitMergeKey(key) + if name != "" { + if _, exists := cityDateToKey[cityDate]; !exists { + cityDateToKey[cityDate] = key + } + } + } + // Remap empty-name keys and rebuild order without orphan keys. + remapped := make(map[string]string) // emptyKey → targetKey + for _, key := range order { + name, cityDate := splitMergeKey(key) + if name == "" { + if target, ok := cityDateToKey[cityDate]; ok { + remapped[key] = target + } + } + } + for emptyKey, targetKey := range remapped { + groups[targetKey] = append(groups[targetKey], groups[emptyKey]...) + delete(groups, emptyKey) + } + var filteredOrder []string + for _, key := range order { + if _, removed := remapped[key]; !removed { + filteredOrder = append(filteredOrder, key) + } + } + + out := make([]MergedEvent, 0, len(filteredOrder)) + for _, key := range filteredOrder { + out = append(out, mergeGroup(groups[key])) + } + return out +} + +// splitMergeKey returns the normalized-name part and the "city|date" part of a +// merge key (format: "name|city|date"). +func splitMergeKey(key string) (name, cityDate string) { + idx := strings.Index(key, "|") + if idx < 0 { + return key, "" + } + return key[:idx], key[idx+1:] +} + +func mergeKey(r RawEvent) string { + date := "" + if r.StartDate != nil { + date = r.StartDate.Format("2006-01-02") + } + return normalize.Name(r.Name) + "|" + normalize.City(r.City) + "|" + date +} + +func mergeGroup(raws []RawEvent) MergedEvent { + // Sort by source rank so "first non-empty" == "best source's value". + sort.SliceStable(raws, func(i, j int) bool { + return rankOf(raws[i].SourceName) < rankOf(raws[j].SourceName) + }) + + m := MergedEvent{} + sourceSet := map[string]bool{} + quellenSet := map[string]bool{} + + for _, r := range raws { + sourceSet[r.SourceName] = true + if r.SourceURL != "" { + quellenSet[r.SourceURL] = true + } + if r.DetailURL != "" { + quellenSet[r.DetailURL] = true + } + + // Longest name wins (regardless of rank). + if len(r.Name) > len(m.Name) { + m.Name = r.Name + } + if m.City == "" { + m.City = r.City + } + if m.PLZ == "" { + m.PLZ = r.PLZ + } + if m.Land == "" { + m.Land = r.Land + } + if m.Bundesland == "" { + m.Bundesland = r.Bundesland + } + if m.StartDate == nil && r.StartDate != nil { + m.StartDate = r.StartDate + } + if m.Venue == "" { + m.Venue = r.Venue + } + if m.Organizer == "" { + m.Organizer = r.Organizer + } + + // Website: prefer non-empty from best rank, but only if not social-media. + if m.Website == "" && r.Website != "" && !isSocialURL(r.Website) { + m.Website = r.Website + } + + // EndDate: best-rank non-empty, then detect conflict <= 2 days. + if m.EndDate == nil && r.EndDate != nil { + m.EndDate = r.EndDate + continue + } + if m.EndDate != nil && r.EndDate != nil && !sameDay(m.EndDate, r.EndDate) { + diff := r.EndDate.Sub(*m.EndDate) + if diff < 0 { + diff = -diff + } + if diff <= 48*time.Hour { + m.Hinweis = appendHinweis(m.Hinweis, "date_conflict") + } + // differences > 2 days: events were likely different in the first place; + // merge key already collapsed them, but we don't tag noise as a conflict. + } + } + + // Fallback: if no non-social website, take best-rank social URL. + if m.Website == "" { + for _, r := range raws { + if r.Website != "" { + m.Website = r.Website + break + } + } + } + + m.Quellen = sortedKeys(quellenSet) + m.Sources = sortedKeys(sourceSet) + return m +} + +func rankOf(name string) int { + if r, ok := sourceRank[name]; ok { + return r + } + return 999 +} + +func isSocialURL(u string) bool { + lu := strings.ToLower(u) + for _, domain := range []string{"facebook.com", "instagram.com", "twitter.com", "x.com", "tiktok.com"} { + if strings.Contains(lu, domain) { + return true + } + } + return false +} + +func sameDay(a, b *time.Time) bool { + return a.Year() == b.Year() && a.Month() == b.Month() && a.Day() == b.Day() +} + +func sortedKeys(m map[string]bool) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + sort.Strings(out) + return out +} + +func appendHinweis(cur, add string) string { + if cur == "" { + return add + } + if strings.Contains(cur, add) { + return cur + } + return cur + "; " + add +} diff --git a/backend/internal/domain/discovery/crawler/merger_test.go b/backend/internal/domain/discovery/crawler/merger_test.go new file mode 100644 index 0000000..759b76c --- /dev/null +++ b/backend/internal/domain/discovery/crawler/merger_test.go @@ -0,0 +1,106 @@ +package crawler + +import ( + "testing" + "time" +) + +func mkTime(t *testing.T, s string) *time.Time { + t.Helper() + tm, err := time.Parse("2006-01-02", s) + if err != nil { + t.Fatal(err) + } + return &tm +} + +func TestMergeSingleSourcePassthrough(t *testing.T) { + raws := []RawEvent{ + {SourceName: "marktkalendarium", SourceURL: "https://a/", Name: "X", City: "Y", StartDate: mkTime(t, "2026-05-01"), EndDate: mkTime(t, "2026-05-03")}, + } + merged := Merge(raws) + if len(merged) != 1 { + t.Fatalf("len = %d; want 1", len(merged)) + } + if merged[0].Name != "X" || merged[0].City != "Y" { + t.Errorf("unexpected shape: %+v", merged[0]) + } + if len(merged[0].Quellen) != 1 || merged[0].Quellen[0] != "https://a/" { + t.Errorf("Quellen = %v", merged[0].Quellen) + } +} + +func TestMergeTwoSourcesByRank(t *testing.T) { + raws := []RawEvent{ + // marktkalendarium: rich organizer + website + {SourceName: "marktkalendarium", SourceURL: "https://mk/", Name: "Mittelaltermarkt X", City: "Dresden", PLZ: "01067", StartDate: mkTime(t, "2026-05-01"), EndDate: mkTime(t, "2026-05-03"), Website: "https://organizer.de", Organizer: "Verein Y"}, + // mittelaltermarkt_online (rank 1): adds detail URL and venue + {SourceName: "mittelaltermarkt_online", SourceURL: "https://mo/", DetailURL: "https://mo/e/1", Name: "Mittelaltermarkt X", City: "Dresden", PLZ: "01067", StartDate: mkTime(t, "2026-05-01"), EndDate: mkTime(t, "2026-05-03"), Venue: "Stallhof", Land: "Deutschland"}, + } + merged := Merge(raws) + if len(merged) != 1 { + t.Fatalf("len = %d; want 1", len(merged)) + } + m := merged[0] + if m.Organizer != "Verein Y" { + t.Errorf("Organizer = %q; want 'Verein Y'", m.Organizer) + } + if m.Venue != "Stallhof" { + t.Errorf("Venue = %q; want 'Stallhof'", m.Venue) + } + if m.Website != "https://organizer.de" { + t.Errorf("Website = %q; want 'https://organizer.de'", m.Website) + } + if len(m.Quellen) != 3 { // SourceURL + DetailURL from rank-1 + SourceURL from rank-2 + t.Errorf("Quellen = %v", m.Quellen) + } +} + +func TestMergeDateConflictHinweis(t *testing.T) { + raws := []RawEvent{ + {SourceName: "mittelaltermarkt_online", SourceURL: "https://a/", Name: "X", City: "Y", StartDate: mkTime(t, "2026-05-01"), EndDate: mkTime(t, "2026-05-03")}, + {SourceName: "marktkalendarium", SourceURL: "https://b/", Name: "X", City: "Y", StartDate: mkTime(t, "2026-05-01"), EndDate: mkTime(t, "2026-05-05")}, + } + merged := Merge(raws) + if len(merged) != 1 { + t.Fatalf("len = %d", len(merged)) + } + if !containsSubstr(merged[0].Hinweis, "date_conflict") { + t.Errorf("Hinweis = %q; want date_conflict note", merged[0].Hinweis) + } + // Winning EndDate comes from rank-1 source. + if merged[0].EndDate.Day() != 3 { + t.Errorf("EndDate day = %d; want 3 (rank-1 wins)", merged[0].EndDate.Day()) + } +} + +func TestMergeSocialURLFilter(t *testing.T) { + raws := []RawEvent{ + {SourceName: "marktkalendarium", SourceURL: "https://a/", Name: "X", City: "Y", StartDate: mkTime(t, "2026-05-01"), Website: "https://facebook.com/event/1"}, + {SourceName: "mittelalterkalender", SourceURL: "https://b/", Name: "X", City: "Y", StartDate: mkTime(t, "2026-05-01"), Website: "https://realsite.de"}, + } + merged := Merge(raws) + if merged[0].Website != "https://realsite.de" { + t.Errorf("Website = %q; want realsite (facebook filtered)", merged[0].Website) + } +} + +func TestMergeLongestNameWins(t *testing.T) { + raws := []RawEvent{ + {SourceName: "mittelalterkalender", SourceURL: "https://a/", Name: "Mittelaltermarkt", City: "Dresden", StartDate: mkTime(t, "2026-05-01")}, + {SourceName: "marktkalendarium", SourceURL: "https://b/", Name: "Mittelaltermarkt zu Dresden im Stallhof", City: "Dresden", StartDate: mkTime(t, "2026-05-01")}, + } + merged := Merge(raws) + if merged[0].Name != "Mittelaltermarkt zu Dresden im Stallhof" { + t.Errorf("Name = %q; want longest", merged[0].Name) + } +} + +func containsSubstr(s, sub string) bool { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} diff --git a/backend/internal/domain/discovery/crawler/mittelalterkalender.go b/backend/internal/domain/discovery/crawler/mittelalterkalender.go new file mode 100644 index 0000000..9ce64e7 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/mittelalterkalender.go @@ -0,0 +1,127 @@ +package crawler + +import ( + "bytes" + "context" + "fmt" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" +) + +// MittelalterkalenderSource scrapes www.mittelalterkalender.info. Page has +// twelve monthly
s; each has columns: Beginn | Ende | Titel | PLZ | Ort | +// [Details link]. +type MittelalterkalenderSource struct { + fetcher *Fetcher + urls []string +} + +func NewMittelalterkalender(f *Fetcher, urls []string) *MittelalterkalenderSource { + return &MittelalterkalenderSource{fetcher: f, urls: urls} +} + +func (s *MittelalterkalenderSource) Name() string { return "mittelalterkalender" } + +func (s *MittelalterkalenderSource) Fetch(ctx context.Context) ([]RawEvent, error) { + var all []RawEvent + for i, url := range s.urls { + if i > 0 { + if err := sleepCtx(ctx, 2*time.Second); err != nil { + return all, err + } + } + body, err := s.fetcher.Get(ctx, url, "") + if err != nil { + return all, fmt.Errorf("mittelalterkalender %s: %w", url, err) + } + events, err := parseMittelalterkalender(body, url) + if err != nil { + return all, fmt.Errorf("mittelalterkalender parse %s: %w", url, err) + } + all = append(all, events...) + } + return all, nil +} + +func parseMittelalterkalender(data []byte, sourceURL string) ([]RawEvent, error) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + var events []RawEvent + doc.Find("table tr").Each(func(_ int, tr *goquery.Selection) { + cells := tr.Find("td") + if cells.Length() < 5 { + return + } + // First cell contains start date followed by " bis " span; extract just the date. + beginnText := strings.TrimSpace(cells.Eq(0).Text()) + // Remove the "bis" suffix (cell text is "DD.MM.YYYY bis") + if idx := strings.Index(beginnText, " bis"); idx > 0 { + beginnText = beginnText[:idx] + } + beginn := strings.TrimSpace(beginnText) + ende := strings.TrimSpace(cells.Eq(1).Text()) + titel := strings.TrimSpace(cells.Eq(2).Text()) + plz := strings.TrimSpace(cells.Eq(3).Text()) + ort := strings.TrimSpace(cells.Eq(4).Text()) + + if titel == "" || beginn == "" { + return + } + start := parseDEDate(beginn) + if start == nil { + return + } + end := parseDEDate(ende) + + detailURL := "" + if cells.Length() >= 6 { + href, ok := cells.Eq(5).Find("a").First().Attr("href") + if ok { + detailURL = resolveURL(sourceURL, strings.TrimSpace(href)) + } + } + + events = append(events, RawEvent{ + SourceName: "mittelalterkalender", + SourceURL: sourceURL, + DetailURL: detailURL, + Name: titel, + City: ort, + PLZ: plz, + Land: InferLand(plz), + StartDate: start, + EndDate: end, + }) + }) + return events, nil +} + +// resolveURL joins a relative href against the source URL. Leaves absolute +// URLs untouched; empty input returns empty. +func resolveURL(source, href string) string { + if href == "" { + return "" + } + if strings.HasPrefix(href, "http://") || strings.HasPrefix(href, "https://") { + return href + } + if strings.HasPrefix(href, "/") { + // Strip path from source, keep scheme://host. + // Simple impl — source is always a full URL from our config. + end := strings.Index(source[len("https://"):], "/") + if end < 0 { + return source + href + } + return source[:len("https://")+end] + href + } + // Relative to current dir — drop filename from source. + lastSlash := strings.LastIndex(source, "/") + if lastSlash < 0 { + return source + "/" + href + } + return source[:lastSlash+1] + href +} diff --git a/backend/internal/domain/discovery/crawler/mittelalterkalender_test.go b/backend/internal/domain/discovery/crawler/mittelalterkalender_test.go new file mode 100644 index 0000000..3e3e3b1 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/mittelalterkalender_test.go @@ -0,0 +1,38 @@ +package crawler + +import ( + "os" + "testing" +) + +func TestMittelalterkalenderParse(t *testing.T) { + data, err := os.ReadFile("testdata/mittelalterkalender.html") + if err != nil { + t.Fatal(err) + } + events, err := parseMittelalterkalender(data, "https://www.mittelalterkalender.info/mittelaltermarkt/mittelalterfeste-2026-nach-datum.php") + if err != nil { + t.Fatalf("parse: %v", err) + } + t.Logf("Parsed %d events", len(events)) + if len(events) < 10 { + t.Fatalf("got %d events; expected at least 10", len(events)) + } + e := events[0] + if e.SourceName != sourceMittelalterkalender { + t.Errorf("SourceName = %q", e.SourceName) + } + if e.Name == "" { + t.Error("Name empty") + } + if e.City == "" { + t.Error("City empty") + } + if e.StartDate == nil { + t.Error("StartDate nil") + } + // Land inferred from PLZ via InferLand. + if e.Land == "" && e.PLZ != "" { + t.Errorf("Land empty but PLZ=%q", e.PLZ) + } +} diff --git a/backend/internal/domain/discovery/crawler/mittelaltermarkt_online.go b/backend/internal/domain/discovery/crawler/mittelaltermarkt_online.go new file mode 100644 index 0000000..3a9d110 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/mittelaltermarkt_online.go @@ -0,0 +1,212 @@ +package crawler + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" +) + +// MittelaltermarktOnlineSource pulls events from the Tribe Events REST API on +// mittelaltermarkt.online. Pagination is driven by the next_rest_url field. +type MittelaltermarktOnlineSource struct { + fetcher *Fetcher + startURLs []string +} + +func NewMittelaltermarktOnline(f *Fetcher, startURLs []string) *MittelaltermarktOnlineSource { + return &MittelaltermarktOnlineSource{fetcher: f, startURLs: startURLs} +} + +func (s *MittelaltermarktOnlineSource) Name() string { return "mittelaltermarkt_online" } + +func (s *MittelaltermarktOnlineSource) Fetch(ctx context.Context) ([]RawEvent, error) { + var all []RawEvent + const perHostCap = 100 + requests := 0 + + for _, startURL := range s.startURLs { + url := startURL + for url != "" { + if requests >= perHostCap { + return all, fmt.Errorf("mittelaltermarkt_online: per-host request cap (%d) reached", perHostCap) + } + if requests > 0 { + if err := sleepCtx(ctx, 2*time.Second); err != nil { + return all, err + } + } + body, err := s.fetcher.Get(ctx, url, "application/json") + if err != nil { + return all, fmt.Errorf("mittelaltermarkt_online %s: %w", url, err) + } + events, next, err := parseTribeEvents(body, url) + if err != nil { + return all, fmt.Errorf("mittelaltermarkt_online parse %s: %w", url, err) + } + all = append(all, events...) + url = next + requests++ + } + } + return all, nil +} + +// tribeCategorySlugAllowlist is the set of category slugs we accept. Empty +// category lists are accepted (the site is Mittelalter-focused overall). +// Slugs are taken from the actual API responses on mittelaltermarkt.online. +var tribeCategorySlugAllowlist = map[string]bool{ + "mittelaltermaerkten": true, + "mittelalterfeste": true, + "mittelalterspektakel": true, + "wikingerspektakel": true, + "ritterspektakel": true, + "historisches-fest": true, +} + +type tribeEventsResponse struct { + Events []tribeEvent `json:"events"` + NextRestURL string `json:"next_rest_url"` +} + +type tribeEvent struct { + ID int `json:"id"` + Title string `json:"title"` + URL string `json:"url"` + Description string `json:"description"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + Website string `json:"website"` + Venue *tribeVenue `json:"venue"` + Organizer json.RawMessage `json:"organizer"` // sometimes object, sometimes [] + Categories []tribeTerm `json:"categories"` +} + +type tribeVenue struct { + Venue string `json:"venue"` + City string `json:"city"` + Zip string `json:"zip"` + State string `json:"state"` + Country string `json:"country"` +} + +type tribeTerm struct { + Slug string `json:"slug"` + Name string `json:"name"` +} + +func parseTribeEvents(data []byte, sourceURL string) ([]RawEvent, string, error) { + var resp tribeEventsResponse + if err := json.Unmarshal(data, &resp); err != nil { + return nil, "", fmt.Errorf("unmarshal: %w", err) + } + + events := make([]RawEvent, 0, len(resp.Events)) + for _, te := range resp.Events { + if te.Title == "" || te.StartDate == "" { + continue + } + if !tribePassesCategoryFilter(te.Categories) { + continue + } + start, err := parseTribeDate(te.StartDate) + if err != nil { + continue + } + var end *time.Time + if e, err := parseTribeDate(te.EndDate); err == nil { + end = e + } + + ev := RawEvent{ + SourceName: "mittelaltermarkt_online", + SourceURL: sourceURL, + DetailURL: te.URL, + Name: strings.TrimSpace(te.Title), + StartDate: start, + EndDate: end, + Website: te.Website, + } + if te.Venue != nil { + ev.City = te.Venue.City + ev.PLZ = te.Venue.Zip + ev.Venue = te.Venue.Venue + ev.Bundesland = te.Venue.State + ev.Land = tribeCountryToLand(te.Venue.Country) + if ev.Land == "" && ev.PLZ != "" { + ev.Land = InferLand(ev.PLZ) + } + } + ev.Organizer = extractTribeOrganizerName(te.Organizer) + events = append(events, ev) + } + return events, resp.NextRestURL, nil +} + +func tribePassesCategoryFilter(cats []tribeTerm) bool { + if len(cats) == 0 { + return true + } + for _, c := range cats { + if tribeCategorySlugAllowlist[c.Slug] { + return true + } + } + return false +} + +func tribeCountryToLand(country string) string { + switch strings.ToLower(strings.TrimSpace(country)) { + case "de", "germany", "deutschland": + return landDeutschland + case "at", "austria", "oesterreich", "österreich": + return landOesterreich + case "ch", "switzerland", "schweiz": + return landSchweiz + } + return "" +} + +// parseTribeDate parses the Tribe Events JSON date format +// "YYYY-MM-DD HH:MM:SS" in the site's local time. If the site ever switches +// to ISO-8601 with T-separator or an explicit timezone, this function will +// start returning errors and events will be silently dropped. The symptom +// will show up as "eventsFetched > 0 but Discovered = 0" in CrawlSummary — +// at that point switch to the iCal endpoint at +// https://mittelaltermarkt.online/events/?ical=1 which uses the standard +// VEVENT DTSTART format and is more durable across plugin upgrades. +func parseTribeDate(s string) (*time.Time, error) { + s = strings.TrimSpace(s) + if s == "" { + return nil, fmt.Errorf("empty") + } + t, err := time.Parse("2006-01-02 15:04:05", s) + if err != nil { + return nil, err + } + return &t, nil +} + +// extractTribeOrganizerName handles the Tribe quirk where organizer is either +// an object, a list of objects, or an empty list. +func extractTribeOrganizerName(raw json.RawMessage) string { + if len(raw) == 0 || string(raw) == "null" || string(raw) == "[]" { + return "" + } + // Try single object first. + var obj struct { + Organizer string `json:"organizer"` + } + if err := json.Unmarshal(raw, &obj); err == nil && obj.Organizer != "" { + return obj.Organizer + } + // Try list. + var list []struct { + Organizer string `json:"organizer"` + } + if err := json.Unmarshal(raw, &list); err == nil && len(list) > 0 { + return list[0].Organizer + } + return "" +} diff --git a/backend/internal/domain/discovery/crawler/mittelaltermarkt_online_test.go b/backend/internal/domain/discovery/crawler/mittelaltermarkt_online_test.go new file mode 100644 index 0000000..178e0a3 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/mittelaltermarkt_online_test.go @@ -0,0 +1,58 @@ +package crawler + +import ( + "os" + "testing" +) + +func TestMittelaltermarktOnlineParse(t *testing.T) { + data, err := os.ReadFile("testdata/mittelaltermarkt_online_page1.json") + if err != nil { + t.Fatal(err) + } + events, nextURL, err := parseTribeEvents(data, "https://mittelaltermarkt.online/wp-json/tribe/events/v1/events?per_page=20&start_date=2026-01-01&end_date=2026-12-31") + if err != nil { + t.Fatalf("parse: %v", err) + } + if len(events) == 0 { + t.Fatal("got zero events from page 1") + } + if nextURL == "" { + t.Error("expected next_rest_url on page 1 fixture") + } + e := events[0] + if e.SourceName != sourceMittelaltermarktOnline { + t.Errorf("SourceName = %q", e.SourceName) + } + if e.Name == "" { + t.Error("Name empty") + } + if e.StartDate == nil { + t.Error("StartDate nil") + } + if e.DetailURL == "" { + t.Error("DetailURL empty") + } +} + +func TestMittelaltermarktOnlineCategoryFilter(t *testing.T) { + // Event whose categories don't include any of our allowlist should be skipped. + jsonBody := []byte(`{ + "events": [ + {"id":1, "title":"Skip Me", "url":"https://x/1", "start_date":"2026-05-01 00:00:00", + "end_date":"2026-05-02 00:00:00", "categories":[{"slug":"sommerfest"}]}, + {"id":2, "title":"Keep Me", "url":"https://x/2", "start_date":"2026-05-01 00:00:00", + "end_date":"2026-05-02 00:00:00", "categories":[{"slug":"mittelaltermaerkten"}]} + ] + }`) + events, _, err := parseTribeEvents(jsonBody, "") + if err != nil { + t.Fatal(err) + } + if len(events) != 1 { + t.Fatalf("got %d events; want 1 (category filter)", len(events)) + } + if events[0].Name != "Keep Me" { + t.Errorf("kept = %q; want 'Keep Me'", events[0].Name) + } +} diff --git a/backend/internal/domain/discovery/crawler/plz.go b/backend/internal/domain/discovery/crawler/plz.go new file mode 100644 index 0000000..4bcc786 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/plz.go @@ -0,0 +1,47 @@ +package crawler + +import "strings" + +// InferLand maps a PLZ to a DACH country. 5-digit -> DE. 4-digit is ambiguous +// between AT and CH because Vienna (1000-1239) overlaps Swiss Geneva/Vaud +// (1000-1299). We pick the common case: Vienna wins 1000-1199, and only +// 1200-1299 is classified as CH. Other CH ranges: 3000-3999 Bern, +// 4000-4999 Basel/Aarau, 6000-6999 Luzern/Ticino/Wallis, 8000-8999 Zurich, +// 9000-9999 St. Gallen. Everything else 4-digit falls back to AT. +// Unknown -> "". The Land string is the form used throughout discovery +// (not ISO-2). +func InferLand(plz string) string { + plz = strings.TrimSpace(plz) + switch len(plz) { + case 5: + if !isAllDigits(plz) { + return "" + } + return landDeutschland + case 4: + if !isAllDigits(plz) { + return "" + } + switch plz[0] { + case '1': + if plz >= "1200" && plz <= "1299" { + return landSchweiz + } + return landOesterreich + case '3', '4', '6', '8', '9': + return landSchweiz + default: + return landOesterreich + } + } + return "" +} + +func isAllDigits(s string) bool { + for _, r := range s { + if r < '0' || r > '9' { + return false + } + } + return true +} diff --git a/backend/internal/domain/discovery/crawler/plz_test.go b/backend/internal/domain/discovery/crawler/plz_test.go new file mode 100644 index 0000000..b71a128 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/plz_test.go @@ -0,0 +1,35 @@ +package crawler + +import "testing" + +func TestInferLand(t *testing.T) { + tests := []struct { + name string + plz string + want string + }{ + {"empty", "", ""}, + {"de 5-digit", "49186", "Deutschland"}, + {"de 5-digit low", "01067", "Deutschland"}, + {"at 4-digit typical", "1010", "Oesterreich"}, + {"at 4-digit boundary 1199", "1199", "Oesterreich"}, + {"ch 4-digit boundary 1200", "1200", "Schweiz"}, + {"ch 4-digit boundary 1299", "1299", "Schweiz"}, + {"ch 4-digit typical", "8001", "Schweiz"}, + {"ch 4-digit zurich range", "8000", "Schweiz"}, + {"ch 4-digit bern range", "3000", "Schweiz"}, + {"ch 4-digit lucerne", "6000", "Schweiz"}, + {"ch 4-digit st gallen", "9000", "Schweiz"}, + {"at 4-digit outside ch ranges", "2500", "Oesterreich"}, + {"short garbage", "12", ""}, + {"4-digit non-numeric", "1a1a", ""}, + {"non-numeric", "abcde", ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := InferLand(tc.plz); got != tc.want { + t.Errorf("InferLand(%q) = %q; want %q", tc.plz, got, tc.want) + } + }) + } +} diff --git a/backend/internal/domain/discovery/crawler/sources.go b/backend/internal/domain/discovery/crawler/sources.go new file mode 100644 index 0000000..2f420a4 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/sources.go @@ -0,0 +1,37 @@ +package crawler + +// SourceConfig defines the per-source URL list (years, start pages, etc.). +type SourceConfig struct { + Name string + URLs []string +} + +// DefaultSourceConfigs returns the hardcoded Ship 1 source list. Update the +// per-source URL arrays each January (or sooner if a site adds a new year). +func DefaultSourceConfigs() []SourceConfig { + return []SourceConfig{ + {Name: sourceMarktkalendarium, URLs: []string{ + "https://www.marktkalendarium.de/maerkte2026.php", + "https://www.marktkalendarium.de/maerkte2027.php", + "https://www.marktkalendarium.de/maerkte2028.php", + }}, + {Name: sourceMittelalterkalender, URLs: []string{ + "https://www.mittelalterkalender.info/mittelaltermarkt/mittelalterfeste-2026-nach-datum.php", + "https://www.mittelalterkalender.info/mittelaltermarkt/historische-feste-mittelaltermaerkte-und-fantasy-festivals-2027-nach-datum.php", + // 2028 slug unknown until the site publishes it — add here when available. + }}, + {Name: sourceFestivalAlarm, URLs: []string{ + "https://www.festival-alarm.com/Kategorien/Mittelalter-Festivals/(year)/2026", + "https://www.festival-alarm.com/Kategorien/Mittelalter-Festivals/(year)/2027", + "https://www.festival-alarm.com/Kategorien/Mittelalter-Festivals/(year)/2028", + }}, + {Name: sourceMittelaltermarktOnline, URLs: []string{ + "https://mittelaltermarkt.online/wp-json/tribe/events/v1/events?per_page=100&start_date=2026-01-01&end_date=2026-12-31", + "https://mittelaltermarkt.online/wp-json/tribe/events/v1/events?per_page=100&start_date=2027-01-01&end_date=2027-12-31", + "https://mittelaltermarkt.online/wp-json/tribe/events/v1/events?per_page=100&start_date=2028-01-01&end_date=2028-12-31", + }}, + {Name: sourceSuendenfrei, URLs: []string{ + "https://www.suendenfrei.tv/veranstaltungen", + }}, + } +} diff --git a/backend/internal/domain/discovery/crawler/suendenfrei.go b/backend/internal/domain/discovery/crawler/suendenfrei.go new file mode 100644 index 0000000..c960d2f --- /dev/null +++ b/backend/internal/domain/discovery/crawler/suendenfrei.go @@ -0,0 +1,172 @@ +package crawler + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "regexp" + "strconv" + "strings" + "time" + + "github.com/PuerkitoBio/goquery" +) + +// SuendenfreiSource scrapes www.suendenfrei.tv/veranstaltungen. Events are +//

with free-form text. A regex parses " [in ]". +// Unparseable entries are logged at INFO and skipped; Ship 2's local LLM is +// the long-term rescue for prose that doesn't fit the regex. +type SuendenfreiSource struct { + fetcher *Fetcher + baseURL string +} + +func truncateForLog(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} + +func NewSuendenfrei(f *Fetcher, baseURL string) *SuendenfreiSource { + return &SuendenfreiSource{fetcher: f, baseURL: baseURL} +} + +func (s *SuendenfreiSource) Name() string { return "suendenfrei" } + +func (s *SuendenfreiSource) Fetch(ctx context.Context) ([]RawEvent, error) { + var all []RawEvent + const perHostCap = 100 + requests := 0 + + page := 1 + for { + if requests >= perHostCap { + return all, fmt.Errorf("suendenfrei: per-host request cap (%d) reached", perHostCap) + } + var url string + if page == 1 { + url = s.baseURL + } else { + url = fmt.Sprintf("%s/page-%d", s.baseURL, page) + } + if requests > 0 { + if err := sleepCtx(ctx, 2*time.Second); err != nil { + return all, err + } + } + body, err := s.fetcher.Get(ctx, url, "") + if err != nil { + // 404 on missing page ends pagination cleanly. + if strings.Contains(err.Error(), "http 404") { + break + } + return all, fmt.Errorf("suendenfrei %s: %w", url, err) + } + events, more := parseSuendenfreiPage(body, url) + all = append(all, events...) + if !more { + break + } + page++ + requests++ + } + return all, nil +} + +// parseSuendenfreiPage extracts events from one listing page. Returns the +// events and whether there were any

elements (= continue paginating). +func parseSuendenfreiPage(data []byte, sourceURL string) ([]RawEvent, bool) { + doc, err := goquery.NewDocumentFromReader(bytes.NewReader(data)) + if err != nil { + return nil, false + } + var events []RawEvent + anchors := doc.Find("h3 a") + anchors.Each(func(_ int, a *goquery.Selection) { + text := strings.TrimSpace(a.Text()) + if text == "" { + return + } + parsed, ok := parseSuendenfreiHeader(text) + if !ok { + // Unparseable entries are real — footer links, layout anchors, + // or event headers in a prose shape our regex doesn't cover. + // Log at INFO so operators can grep the count post-run without + // introducing a counter we'd have to thread through CrawlSummary. + slog.Info("suendenfrei: unparseable h3 anchor; skipping", + "source_url", sourceURL, + "text", truncateForLog(text, 120), + ) + return + } + href, _ := a.Attr("href") + parsed.SourceName = "suendenfrei" + parsed.SourceURL = sourceURL + parsed.DetailURL = resolveURL(sourceURL, strings.TrimSpace(href)) + parsed.Land = InferLand(parsed.PLZ) + events = append(events, parsed) + }) + return events, anchors.Length() > 0 +} + +// Month names we accept (lowercase, with and without umlauts). Maps to time.Month. +var suendenfreiMonths = map[string]time.Month{ + "januar": time.January, + "februar": time.February, + "maerz": time.March, + "märz": time.March, + "april": time.April, + "mai": time.May, + "juni": time.June, + "juli": time.July, + "august": time.August, + "september": time.September, + "oktober": time.October, + "november": time.November, + "dezember": time.December, +} + +// Header like "18. bis 19. April 2026 Mittelaltermarkt in 41849 Tueschenbroich OT von Wegberg". +// Also handles missing space "19.April" and absent " in " suffix. +var suendenfreiHeaderRE = regexp.MustCompile( + `^(\d{1,2})\.?\s*(?:bis|-|–)\s*(\d{1,2})\.?\s*([A-Za-zäöüÄÖÜ]+)\s+(\d{4})\s+(.+?)$`, +) +var suendenfreiLocationRE = regexp.MustCompile(`\s+in\s+(\d{5})\s+(.+)$`) + +func parseSuendenfreiHeader(text string) (RawEvent, bool) { + m := suendenfreiHeaderRE.FindStringSubmatch(text) + if m == nil { + return RawEvent{}, false + } + startDay, _ := strconv.Atoi(m[1]) + endDay, _ := strconv.Atoi(m[2]) + monthName := strings.ToLower(m[3]) + year, _ := strconv.Atoi(m[4]) + rest := strings.TrimSpace(m[5]) + + month, ok := suendenfreiMonths[monthName] + if !ok { + return RawEvent{}, false + } + + name := rest + plz := "" + city := "" + if locM := suendenfreiLocationRE.FindStringSubmatchIndex(rest); locM != nil { + name = strings.TrimSpace(rest[:locM[0]]) + plz = rest[locM[2]:locM[3]] + city = strings.TrimSpace(rest[locM[4]:locM[5]]) + } + + start := time.Date(year, month, startDay, 0, 0, 0, 0, time.UTC) + end := time.Date(year, month, endDay, 0, 0, 0, 0, time.UTC) + return RawEvent{ + Name: name, + PLZ: plz, + City: city, + StartDate: &start, + EndDate: &end, + }, true +} diff --git a/backend/internal/domain/discovery/crawler/suendenfrei_test.go b/backend/internal/domain/discovery/crawler/suendenfrei_test.go new file mode 100644 index 0000000..4c0bf04 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/suendenfrei_test.go @@ -0,0 +1,67 @@ +package crawler + +import ( + "testing" + "time" +) + +func TestSuendenfreiParseHeader(t *testing.T) { + tests := []struct { + in string + wantName string + wantPLZ string + wantCity string + wantStartY int + wantStartM time.Month + wantStartD int + wantEndD int + }{ + { + in: "18. bis 19. April 2026 Mittelaltermarkt in 41849 Tueschenbroich OT von Wegberg", + wantName: "Mittelaltermarkt", + wantPLZ: "41849", + wantCity: "Tueschenbroich OT von Wegberg", + wantStartY: 2026, wantStartM: time.April, wantStartD: 18, wantEndD: 19, + }, + { + in: "18. bis 19.April 2026 Mittelalterspektakel in 02829 Koenigshain bei Goerlitz", + wantName: "Mittelalterspektakel", + wantPLZ: "02829", + wantCity: "Koenigshain bei Goerlitz", + wantStartY: 2026, wantStartM: time.April, wantStartD: 18, wantEndD: 19, + }, + { + in: "25. bis 26. April 2026 Turnierspiele Weissensee", + wantName: "Turnierspiele Weissensee", + wantPLZ: "", + wantCity: "", + wantStartY: 2026, wantStartM: time.April, wantStartD: 25, wantEndD: 26, + }, + } + for _, tc := range tests { + t.Run(tc.in, func(t *testing.T) { + got, ok := parseSuendenfreiHeader(tc.in) + if !ok { + t.Fatalf("parse failed for %q", tc.in) + } + if got.Name != tc.wantName { + t.Errorf("Name = %q; want %q", got.Name, tc.wantName) + } + if got.PLZ != tc.wantPLZ { + t.Errorf("PLZ = %q; want %q", got.PLZ, tc.wantPLZ) + } + if got.City != tc.wantCity { + t.Errorf("City = %q; want %q", got.City, tc.wantCity) + } + if got.StartDate == nil { + t.Fatal("StartDate nil") + } + if got.StartDate.Year() != tc.wantStartY || got.StartDate.Month() != tc.wantStartM || got.StartDate.Day() != tc.wantStartD { + t.Errorf("StartDate = %v; want %d-%02d-%02d", got.StartDate, tc.wantStartY, tc.wantStartM, tc.wantStartD) + } + if got.EndDate == nil || got.EndDate.Day() != tc.wantEndD { + t.Errorf("EndDate = %v; want day %d", got.EndDate, tc.wantEndD) + } + }) + } +} diff --git a/backend/internal/domain/discovery/crawler/testdata/README.md b/backend/internal/domain/discovery/crawler/testdata/README.md new file mode 100644 index 0000000..5163ccd --- /dev/null +++ b/backend/internal/domain/discovery/crawler/testdata/README.md @@ -0,0 +1,16 @@ +# Crawler test fixtures + +Captured with curl using the default Firefox UA on 2026-04-18. + +These are the exact bytes each source served at capture time. If a parser starts +failing after a site redesign, re-capture the corresponding file with the curl +commands documented in `docs/superpowers/plans/2026-04-18-dach-discovery-crawler.md` +(Task 2) and update the parser's expected assertions. + +- `marktkalendarium.html` — https://www.marktkalendarium.de/maerkte2026.php +- `mittelalterkalender.html` — https://www.mittelalterkalender.info/mittelaltermarkt/mittelalterfeste-2026-nach-datum.php +- `festival_alarm.html` — https://www.festival-alarm.com/Kategorien/Mittelalter-Festivals/(year)/2026 +- `mittelaltermarkt_online_page1.json` — Tribe REST API page 1 +- `mittelaltermarkt_online_page2.json` — Tribe REST API page 2 +- `suendenfrei_page1.html` — https://www.suendenfrei.tv/veranstaltungen +- `suendenfrei_page2.html` — https://www.suendenfrei.tv/veranstaltungen/page-2 diff --git a/backend/internal/domain/discovery/crawler/testdata/festival_alarm.html b/backend/internal/domain/discovery/crawler/testdata/festival_alarm.html new file mode 100644 index 0000000..b857a0c --- /dev/null +++ b/backend/internal/domain/discovery/crawler/testdata/festival_alarm.html @@ -0,0 +1,1753 @@ + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + Übersicht: alle Mittelalter Festivals + 2026 + | Festival Alarm + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+ +
+
+ +
+ +
+
+
+
+

Mittelalter Festivals

+ +
+
+
+
+
+
+
+
Deine Mithilfe
+

Fehlt dir ein Termin in der Datenbank? Dann trag ihn in weniger als einer Minute ohne Anmeldung ein.

+
+ +
+ + + + + + + + + +
+
+
+ +
+
+
+
+
+ + + + + + +
+
+ + + +

Mittelalter Festivals 2026

+ +

+ Alle Mittelalter Festivals aus Deutschland, Europa und der Welt. +

+

+ Alle Mittelalter Festivals nach Jahren +

+

+ + + 2018 + + + 2019 + + + 2020 + + + 2021 + + + 2022 + + + 2023 + + + 2024 + + + 2025 + + 2026 + + 2027 + +

+ +
+
+
+
+ Festival-Filter +
+
+ einblenden +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDatum 2026DauerWoGenresLand BesucherPreis(ab)
+ Hexentanz Walpurgisschlacht Festival + + + 24.04. + - + + 26.04. + + 3 TagedraußenFolk, Gothic, Metal, Mittelalter, Rock + + + Saarland + + keine Daten + + + € 119,99 + + + + + + + + + + + + +
+ Sternenklang Festival + + + 11.06. + - + + 13.06. + + 3 TagedraußenFolk, Metal, Mittelalter + + + Thüringen + + +2500 + + n.v. + + + + + + + + + + + + +
+ Hussitenfest Bernau + + + 12.06. + - + + 14.06. + + 3 TagedraußenKunst & Kultur, Mittelalter + + + Brandenburg + + +20000 + + n.v. + + + + + + + + + +
+ Vegan Fantasy Fair - Das vegane Fantasy Festival + + + 04.07. + - + + 05.07. + + 2 TagedraußenFolk, Gothic, Kunst & Kultur, Lesung, Mittelalter, Pirat + + + Saarland + + +2000 + + n.v. + + + + + + + + + + + + +
+ Hörnerfest + + + 25.06. + - + + 27.06. + + 3 TagedraußenFolk, Metal, Mittelalter, Rock + + + Schleswig-Holstein + + +1500 + + n.v. + + + + + + + + + + + + +
+ Freienfelser Ritterspiele + + + 14.05. + - + + 17.05. + + 4 TagedraußenMittelalter + + + Hessen + + +20000 + + n.v. + + + + + + + + + +
+ Feuertanz Festival + + + 12.06. + - + + 13.06. + + 2 TagedraußenFolk, Irish, Metal, Mittelalter + + + Bayern + + +5000 + + € 115,00 + + + + + + + + + +
+ Spectaculum Magdeburgense + + + 22.05. + - + + 25.05. + + 4 TagedraußenFolk, Gothic, Kunst & Kultur, Mittelalter + + + Sachsen-Anhalt + + +16000 + + n.v. + + + + + + + + + + + + +
+ Das Grosse Treffen - Fantasy Festival + + + 07.08. + - + + 09.08. + + 3 TagedraußenIrish, Metal, Mittelalter, Rock + + + Baden-Württemberg + + +12000 + + € 75,00 + + + + + + + + + + + + +
+ Schlosshof Festival + + + 07.08. + - + + 08.08. + + 2 TagedraußenFolk, Irish, Mittelalter, Pirat + + + Bayern + + keine Daten + + + € 110,00 + + + + + + + + + +
+ Festival-Mediaval + + + 11.09. + - + + 13.09. + + 3 TagedraußenFolk, Irish, Lesung, Metal, Mittelalter, Pirat, Rock + + + Bayern + + keine Daten + + + n.v. + + + + + + + + + + + + +
+ Hayner Burgfest + + + 11.09. + - + + 13.09. + + 3 TagedraußenMittelalter + + + Deutschland + + + +14000 + + n.v. + + + + + + + + + +
+ Folkfield Festival + + + 11.09. + - + + 12.09. + + 2 TagedraußenFolk, Gothic, Irish, Mittelalter, Rock + + + Nordrhein-Westfalen + + +6000 + + € 132,00 + + + + + + + + + + + + +
+ Pfingst Spectaculum + + + 23.05. + - + + 25.05. + + 3 TagedraußenMittelalter, Rock, Sonstiges + + + Brandenburg + + keine Daten + + + € 115,00 + + + + + + + + + +
+ Ritterturnier Stetten + + + 31.07. + - + + 02.08. + + 3 TagedraußenKunst & Kultur, Mittelalter, Sonstiges, Sport + + + Baden-Württemberg + + keine Daten + + + n.v. + + + + + + + + + +
+ + + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/internal/domain/discovery/crawler/testdata/marktkalendarium.html b/backend/internal/domain/discovery/crawler/testdata/marktkalendarium.html new file mode 100644 index 0000000..7caeb0d --- /dev/null +++ b/backend/internal/domain/discovery/crawler/testdata/marktkalendarium.html @@ -0,0 +1,8329 @@ + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 0 + + + + + + + + + + + + + + + + + + + + + + + + Märkte 2026 + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + IP-Log schreiben + + + + + + + + + + + + + + + + + + +
+   
+
+
+ + + Startseite +

+ 2022  + 2023  + 2024  + 2025 + +

+

+ + 2026 aktuelle
+ 2026 alle
+
+

+ +

+ + 2027
+
+

+ + +

+ Termin melden
+


+ + + + magische Wege : +
+ Veranstalter +
+ Mittelalter +
+ + Partner +
+ + + Reisen ins Mittelalter +
+
+
+
+ + + Impressum +
+
+
+ + + Werben +
+
+
+
+ + Infos zu Barrierefreiheit + +
+ + + + +
+ Seit mehr als 20 Jahren verlässliche Informationsquelle für Termine historischer Märkte !
 
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Werbung +
+ +
+ +Asilion Markt
+ hierwerben
+  
+ + + + + + + + + + + + +
+ Werbung +
+ +Historia Aktiv + hierwerben
+ + + + + + + + +
+
+ + +

+ Zurück zur Startseite
+ Zum Anfang der Terminübersicht
+

+ +

+ 

+ + + + + + +
+ In eigener Sache: + Das Marktkalendarium verursacht, + neben viel Arbeit, auch einige + Kosten. Wer das Marktkalendarium + unterstützen möchte kann dies in + Form einer Spende tun.
+ +
+

+ +
+  + Radio Marktkalendarium
+ Laut.fm-Homepage
+ Homepage
+
+ +
+ +

+ Werbung +

+ +

+ +Tanja Aquaristik

+ +

+ +Radio Matchbox

+ + + + + +
+ +
+ + + +
+ + + + + + + + +
+ + +
+

Suchtext: + + +  Sortierung: + + +  Datenbereich: + + + +

+ +

+ Die Suche erstreckt sich über alle Felder der Tabelle. Es kann daher sowohl nach Datum, Postleitzahl, Ortsname usw. gesucht werden. +
Ein leeres Suchfeld bewirkt Anzeige der kompletten Tabelle.
Mit Klick auf den Ortsnamen wird der Ort, jedoch nicht der genaue Platz der Veranstaltung, in Google-Maps angezeigt. +

+
+ +
+ + +Termine 2026 wurden aktualisiert am : 17.04.2026 19:13:23. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VonBisVeranstaltungOrtPlatzWebseiteVeranstalter
Organisation

Termine im April

Werbung
+Erlebbar Heute
3.4.20266.5.2026Osterspecktakel zu Bad IburgD-49186 Bad IburgCharlottenseeparkDer WildwechselDer Wildwechsel
KW 15
KW 16
17.4.202619.4.2026Das FrühlingserwachenD-94152 Neuhaus am InnHöchfelden 7FrühlingserwachenUlv Den Røde
18.4.202619.4.20269. MittelalterspektakelD-01848 HohnsteinBurgCOEX GmbHCOEX GmbH
18.4.202619.4.2026MittelalterspektakelD-02829 Königshain SündenfreiSündenfrei
18.4.202619.4.20269. RitterturnierD-15831 DiedersdorfSchloss DiedersdorfCarnica SpectaculiCarnica Spectaculi
18.4.202619.4.2026MittelalterspektakelD-41844 WegbergSchloss TüschenbroichSündenfreiSündenfrei
18.4.202619.4.20268. Mittelalterlicher FrühlingsmarktD-74541 VellbergStädtle innerhalb des MauerringsMittelalterfestMittelalterfest
18.4.202619.4.2026Fantasy- & Mittelaltermarkt D-95505 Immenreuth all for you eventsall for you events
KW 17
24.4.202626.4.2026Walpurgis Mittelalterspektakel zu UsterCH-8610 UsterZeughausarealMittelalterspektakel zu UsterTurnei
24.4.202626.4.2026Mittelaltermarkt BurgdorfD-31303 BurgdorfAm Schloss Grimm Veranstaltung
E-Mail an die Orga
24.4.202625.4.2026MittelalterfestD-39343 BebertalVeltheimsburgVeltheimsburgVeltheimsburg
24.4.202626.4.2026HunnenlagerD-53572 UnkelBürgerpark - Linzer Straße 25  
24.4.202627.4.202613. Unkeler Mittelalterspectakulum
Lager der Unkeler Hunnen
zugunsten Kinderkrebsinitiative
D-53572 UnkelBürgerpark UnkelUnkeler HunnenUnkeler Hunnen
24.4.202626.4.20263. Spektakulum in RamsteinD-66877 Ramstein-MiesenbachInnenstadt  
24.4.202626.4.2026Mittelalter zur BrückeneinweihungD-78628 Rottweil Mittelalter zur BrückeneinweihungFabula Corvinus
25.4.202626.4.202621. Ritterspektakel
Tanz auf dem Seil
D-04703 LeisnigMildensteinCOEX GmbH26.4COEX GmbH
25.4.202626.4.2026Mittelalter SpektakelD-25355 BarmstedtRantzauer SeeMittelalterspektakel 
25.4.202626.4.2026Hansefest WarburgD-34414 WarburgNeustadtWarburger HansefestWarburger Hanse
E-Mail an die Orga
25.4.202626.4.2026Mittelaltermarkt VenneD-49179 Ostercappeln / VenneVenner MühleMittelaltermarkt VenneZum froehlichen Normannen
Der Wildwechsel
25.4.202626.4.2026MittelaltermarktD-66333 Völklingen-GeislauternSchloßpark GeislauternMittelaltermarkt GeislauternLorraine Medievale
25.4.202626.4.2026MittelalterfestD-74889 Sinsheim-WeilerBurg SteinsbergMittelalterfestSinsheim
25.4.202626.4.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-79713 Bad SäckingenSchloßparkSpectaculumSpectaculum
25.4.202626.4.2026MittelaltermarktD-90513 Zirndorf b. NürnbergGut WolfgangshofSündenfreiSündenfrei
25.4.202626.4.2026TurnierspieleD-99631 Weißensee SündenfreiSündenfrei
25.4.202626.4.2026Mittelalterliches BlütenfestD-99880 NeufrankenrodaSILOAH-HofBlütenfestSiloah Hof
KW 18
30.4.202630.4.2026Walpurgisnacht A-5710 KaprunBurg KaprunWalpurgisnachtBurgf Kaprun
E-Mail an die Orga
30.4.20263.5.2026Mittelaltermarkt
auf Schloss Homburg
D- 51588 NümbrechtSchloss Homburg 1Kramerey und KurzweylVPZ Events
30.4.20263.5.2026Hexentanz im MittelalterD-06502    ThaleKurpark ThaleDas nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
30.4.20263.5.20262.Mittelalterfest am DrachenmoorD-15374 MünchebergSunbow Ranch Mittelalterfest am DrachenmoorFencheley
FencheleyE-Mail an die Orga
30.4.20263.5.202628. Burgspektakel    mit
Beltane & Walpurgisfeier
D-29389 Bad BodenteichBurg BodenteichFogelvreiFogelvrei
30.4.20262.5.2026Walpurgisnacht SchierkeD-38879 SchierkeKurparkWalpurgisnacht Schierke Walpurgisnacht Schierke
30.4.202630.4.2026HexennachtD-53894 Mechernich-SatzveyBurg SatzveyHexennachtBurg Satzvey
30.4.20263.5.2026Phantastischer Maimarkt
Walpurgisnacht
D-63549 RonneburgAuf der Burg 1OstermarktBurg Ronneburg
30.4.202630.4.2026Walpurgisnacht
Auf dem Fagati
D-99947 Bad LangensalzaNationalpark Hainich - BaumkronenpfadWalpurgisnacht 

Termine im Mai

Werbung
hierwerben
1.5.20263.5.2026RitterturnierD-06632 Freyburg / Neuenburg SündenfreiSündenfrei
1.5.20263.5.20265. Mittelalterspektakel
mit Ritterturnier
D-06862 RoßlauWasserburgCOEX GmbHCOEX GmbH
1.5.20263.5.20264. Großes Mittelalter-SpektakelD-18106 RostockIGA-Park - Schmarl-Dorf 40Mittelalter-SpektakelMittelalter-Spektakel
1.5.20263.5.20261. RitterfestD-39387 OscherslebenSchloss AmpfurthCarnica SpectaculiCarnica Spectaculi
1.5.20263.5.2026Ritterturnier Schloss LauersfortD-47447 MoersSchloss LauersfortSündenfreiSündenfrei
1.5.20263.5.2026Wikingertage am DümmerD-49459 LembruchDümmer - Wagenfelder Straße 5WikingertageSegelschule Schlick
1.5.20261.5.2026HexenmarktD-53894 Mechernich-SatzveyBurg SatzveyHexenmarktBurg Satzvey
1.5.20263.5.2026700 Jahrfeier Mittelalter-Markt    Untersotzbach D-63633 Birstein - UntersotzbachSotzbacher Kirchstraße Trimburger RitterschaftTrimburger Ritterschaft
E-Mail an die Orga
1.5.20263.5.2026mittelalterlicher FrühlingsmarktD-66629 FreisenNaturwildpark FreisenFrühlingsmarkt FreisenMittelaltermarkt Freisen
1.5.20264.5.2026Burgerlebnistage
Burg Nanstein
D-66849 LandstuhlBurg NansteinHeimatfreunde Landstuhl 
1.5.20263.5.2026Mittelaltermarkt GartenschauD-67659 Kaiserslautern Gartenschau Lauterstraße 51Historische Märkte & Feste e.V.Historische Märkte & Feste e.V.
1.5.20263.5.2026Mittelalterfest auf der MurginselD-76593 GernsbachMurginselProHistoryProHistory
1.5.20263.5.2026Ritterspiele zu DonaueschingenD-78166 Donaueschingen RitterspieleFabula Corvinus
1.5.20263.5.2026Historische Festtage
Neuburg a.d. Kammel
D-86476 Neuburg an der KammelSchloss Neuburg an der KammelHistorische FesttageE-Mail an die Orga
1.5.20263.5.2026MittelaltermarktD-92543 Schloss Guteneck TurbaeventsTurbaevents
1.5.20263.5.2026MittelaltermarktD-98597 BreitungenSchloss HerrenbreitungenMittelaltermarkt BreitungenLorraine Medievale
2.5.20263.5.2026MittelalterfestA-3400 KlosterneuburgStiftsplatz 1Stift Klosterneuburg 
2.5.20262.5.2026Bal Renaissance - Danse MedievaleD-81825 MünchenGasthof Obermaier - Truderinger Str. 306Bal RenaissanceBlack Flag Records
KW 19
8.5.202610.5.2026Viking Rock Festival
Mittelaltermarkt
CH-3112    Allmendingen Caligatus FeleusCaligatus Feleus
8.5.202610.5.2026Mittelaltermarkt
Viking Rock Festival
CH-3112 Almendingen Caligatus FeleusCaligatus Feleus
8.5.202610.5.2026mittelalterliches Glockenfest
der Gemeinde Zarpen
D-23619 ZarpenTeichstr. 6Das nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
8.5.202610.5.2026Burgspektakel WassenbergD-41849 WassenbergBurg WassenbergBurgspektakel WassenbergFesta Medievale
8.5.202610.5.2026Mittelaltermarkt Burg LindenfelsD-64678 LindenfelsBurgHighlander eVHighlander eV
8.5.202610.5.20264. Mittelaltermarkt zu NußlochD-69226 NußlochMax-Berk-Stadion4. MittelaltermarktBurgfreunde Nußloch
8.5.202610.5.2026Die kuriose altertümliche KirmesD-82024 TaufkirchenFestplatzEvent MittelalterEvent Mittelalter
8.5.202610.5.2026Fantasy- & Mittelaltermarkt
1100 Jahrfeier
D-83233 Bernau am Chiemsee all for you eventsall for you events
8.5.202610.5.2026MittelaltermarktD-84577 Tüssling TurbaeventsTurbaevents
9.5.202610.5.2026Die Wikinger kommen!D-04519 RackwitzSchladitzer BuchtSündenfreiSündenfrei
9.5.202610.5.2026KlostermarktD-18246 RühnKlosterhofKlosterhof RühnArs Vivendi
9.5.202610.5.2026Mittelaltermarkt
zu Spetzerfehn
D-26629 SpetzerfehnÜlkeweg Festplatz Feuerwehr Jens Berndson
E-Mail an die Orga
9.5.202610.5.202617. MittelalterspektakelD-31535 Neustadt a. RbgSchloss LandestrostCOEX GmbHCOEX GmbH
9.5.202610.5.2026Mittelalterliches Markttreiben
auf der Heldenburg
D-37574 EinbeckHeldenburg - SalzderheldenMittelalterfest
Mittelalterliches Markttreiben
Heldenburg
Draconis Events
9.5.202610.5.20265. Kalkarer RitterspieleD-47546 Kalkarim SchwanenhorstRitterspiele 
9.5.202610.5.2026MittelalterspektakelD-58455 WittenKemnader SeeSündenfreiSündenfrei
9.5.202610.5.2026Mittelalterliches SpektakelD-65203 Wiesbaden / BiebrichGibber KerbeplatzHeimdalls ErbenHeimdalls Erben
10.5.202611.5.2026traditionelles Berlepscher Frühlingsfest
mit Ritterturnier
D-37218 WitzenhausenSchloss BerlepschSchloss BerlepschSchloss Berlepsch
KW 20
13.5.202617.5.2026725 Jahre BergneustadtD-51702 BergneustadtAltstadtVPZ EventsVPZ Events
14.5.202617.5.2026Mittelaltermarkt PlankensteinA-3242 PlankensteinBurg PlankensteinMittelaltermarkt PlankensteinBurg Plankenstein
14.5.202617.5.2026BurgfestD-06543 HarzBurg FalkensteinSündenfreiSündenfrei
14.5.202617.5.20265. Historia Caraslan
Der Räuberkönig
D-07545 GeraHofwiesenpark Historia CaraslanCaraslan
14.5.202617.5.202633. BurgfestD-09429 WolkensteinBurgCOEX GmbHCOEX GmbH
14.5.202617.5.202617. MittelalterspektakelD-09648 KriebsteinBurgCOEX GmbHCOEX GmbH
14.5.202617.5.2026Ritterfest & HimmelfahrtD-15345 AltlandsbergSchlossgut AltlandsbergCarnica SpectaculiCarnica Spectaculi
14.5.202617.5.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-26180 Rastede bei OldenburgSchloßparkSpectaculumSpectaculum
14.5.202617.5.2026Elze im WunderlandD-31008 ElzeRund ums RathausElze im Wunderland
14.5.202617.5.2026Mythen Märchen MittelalterD-34388 Burghotel Trendelburg Mittelalterliches MärchenfestBurghotel Trendelburg
14.5.202617.5.202631. Freienfelser RitterspieleD-35796 Weinbach / Freienfels Freienfelser Ritterspiele 
14.5.202617.5.202622. SiegfriedspektakelD-46509 Xanten SündenfreiSündenfrei
14.5.202617.5.2026Mittelaltermarkt an der SalineD-49214 Bad RothenfeldeAm Neuen GradierwerkMittelaltermarkt an der SalineDer Wildwechsel
14.5.202614.5.2026torienspiele Ehenbreitstein
auch 16.5. und 17.5.
D-56077 KoblenzFestung EhrenbreitsteinHistory & EventHistory & Event
14.5.202617.5.2026Grünauer RittertageD-86633 Neuburg a. d. DonauJagdschloss GrünauGrünauer Rittertage 
14.5.202617.5.2026Mittelalter SpektakelD-89079 Ulm WiblingenKlosterhof WiblingenMarkt WiblingenSüdevent
15.5.202617.5.2026Mittelalterspektakel zu HinwilCH-8610 HinwilObererlosenMittelalterspektakel zu HinwilTurnei
15.5.202617.5.2026ANNO 1195
Das 3. Fest
D-33142 BürenAlmeauenANNO 1195Anno Events
15.5.202617.5.2026Keilertage Nörten-HardenbergD-37176 Nörten-Hardenberg KeilertageKeilertage
E-Mail an die Orga
Torsten Hill    015150623961
15.5.202617.5.2026Spectaculum zu WormsD-67547 Wormsim Wormser WäldchenSpectaculumWormser Spectaculum
15.5.202617.5.2026historischer Markt OettingenD-86732 OettingenAltstadtHistorischer Markt OettingenHistorischer Markt Verein
15.5.202617.5.20263. Mittelalterlager zu FensterbachD-92269 Fensterbach - OT DürnsrichtMühlweg - am SeeweiherMittelalterlager 
16.5.202617.5.2026Kuckucksteiner Burgspektakel
Zeitreise ins Mittelalter
D-01825 Liebstadt Mittelalter auf Burg KuckucksteinDomus Donin
16.5.202617.5.2026KinderritterD-53894 Mechernich-SatzveyBurg SatzveyKinderritterBurg Satzvey
16.5.202617.5.2026torienspiele EhenbreitsteinD-56077 KoblenzFestung EhrenbreitsteinHistory & EventHistory & Event
16.5.202617.5.2026MittelaltermarktD--66459 KirkelurgBurgsommer 
16.5.202617.5.2026FaRK
Fantasie und
Rollenspiel Konvent
D-66578 Schiffweiler    / RedenOT Landsweiler-RedenFaRK MesseFaRK Messe
16.5.202617.5.2026Mittelalterliche RitterspieleD-71297 MönsheimOberer GrenzbachhofMittelalterliche RitterspieleReitschule Popp
16.5.202617.5.2026Mittelalterliches MarktfestD-91459 Markt ErlbachOrtsmitteMarkt ErlbachMarkt Erlbach
16.5.202617.5.2026Burgenfest Bad BerneckD-95460 Bad BerneckZwischem altem Schloß und Hohen BerneckFörderverein Historischer StättenBad Berneck Historisch
KW 21
22.5.202625.5.2026Ritter-Fest KufsteinA-6330 KufsteinFestungRitter-Fest Kufstein 
22.5.202624.5.2026Königstage QuedlinburgD-06484 Quedlinburg  Königstage Quedlinburg 
22.5.202631.5.2026Phantasy & Mittelaltermarkt
zu Grote Gaste
D-26810 IhrhoveDeichstraße 7APhantasy & MittelaltermarktGrote Gaste
Schotten Events
22.5.202631.5.2026Mittelaltermarkt GrotegasteD-26810 Westoverledingen Deichstr. 7a
Freizeitpark Am Emsdeich
Mittelaltermarkt GrotegasteCamping Grotegaste
22.5.202625.5.202623. Spectaculum MagdeburgenseD-39104 MagdeburgMaybachstraße 1 - Ravelin 2Spectaculum Magdeburgense 
22.5.202625.5.202619.mittelalterliches Treiben
auf der Burg Bucherbach
D-66346 Püttlingen / KöllerbachBurg BucherbachDie TafelrundeDie Tafelrunde
22.5.202625.5.2026Der MeistertrunkD-91541 Rothenburg o.d. Tauber Der Meistertrunk 
22.5.202625.5.20261150 JahreD-99610 Sömmerda SündenfreiSündenfrei
23.5.202625.5.202620. RitterturnierD-04626 Burg Posterstein (bei Gera) COEX GmbHCOEX GmbH
23.5.202625.5.2026PfingstspectaculumD-16515 OranienburgSchlosspark OranienburgCarnica SpectaculiCarnica Spectaculi
23.5.202625.5.2026Wikingermarkt SchildeD-19322 Schilde (bei Weisen)Park an der KulturscheuneKulturscheuneSchildeMittelalter Feste FB
23.5.202625.5.2026Retro-MPSD-24802 EmkendorfGut EmkendorfRetro-MPS EmkendorfSpectaculum
23.5.202625.5.2026mittelalterliches HeerlagertreffenD-26629 TimmelTimmeler DorfteichHeerlagertreffen 
23.5.202625.5.2026PfingstspektakelD-27343 Rotenburg WümmeBurgstraße HeimathausFogelvreiFogelvrei
23.5.202625.5.2026Münzenberg
das legendäre Mittelalter Spektakulum
D-35516 MünzenbergUnterhalb der Burg MünzenbergMittelalter SpektakulumRitterschaft zu Münzenberg
23.5.202625.5.202616. MittelalterspektakelD-38889 BlankenburgBurg RegensteinCOEX GmbHCOEX GmbH
23.5.202625.5.2026PfingstspektakulumD-45479 Mülheim a.d. RuhrAm Schloß BroichHistory & EventHistory & Event
23.5.202625.5.2026FlachsmarktD-47809 Krefeld - LinnBurg LinnFlachsmarkt 
23.5.202625.5.2026EpochenfestD-52428 JülichBrückenkopf-ParkBrückenkopf-Park 
23.5.202625.5.2026Pfingstspectaculum D-53557 Bad HönningenRheinanlagenPfingstspectaculum E-Mail an die Orga
23.5.202625.5.2026RitterfestspieleD-53894 Mechernich-SatzveyBurg SatzveyRitterfestspieleBurg Satzvey
23.5.202625.5.2026Ritterturnier zu PfingstenD-63549 RonneburgAuf der Burg 1OstermarktBurg Ronneburg
23.5.202625.5.2026MittelaltermarktD-76661 PhilippsburgAusfallweg PfählmorgenMittelaltermarkt PhilippsburgLorraine Medievale
23.5.202625.5.2026TurnierspieleD-85399 Hallbergmoos SündenfreiSündenfrei
23.5.202625.5.2026Historische Tage ScherneckD-86508 RehlingSchloss Scherneckhistorischer MarktIm Team Event
23.5.202625.5.2026Mittelalter SpektakelD-88069 Tettnang  SüdeventSüdevent
23.5.202625.5.2026Burgfantasie SpektakelD-88289 WaldburgSchloß WaldburgBurgfantasieSchloß Waldburg
23.5.202625.5.2026BurgbelebungD-88422 KanzachBachritterburg KanzachBachritterburgBachritterburg
23.5.202625.5.2026Mittelaltermarkt mit RitterturnierD-89561 DischingenBurg KatzensteinBurg KatzensteinBurg Katzenstein
23.5.202625.5.2026Crana HistoricaD-96317 KronachFestung Rosenberg
Festungsstraße 1
Crana Historica FacebookCrana Historica
23.5.202625.5.2026Burgfest CreuzburgD-99831 Amt CreuzburgAuf der Creuzburg 1Burgfest CreuzburgBurgfest Creuzburg
23.5.202625.5.2026TurnierspieleD-99885 Ohrdruf SündenfreiSündenfrei
23.5.202625.5.2026Wikingerfestival
im preHistorisch Dorp
NL-5644 TV EindhovenBoutenslaan 161BWikingerfestivalpreHistorisch Dorp
24.5.202625.5.2026Römer- und GermanentageD-49565 Bramsche-KalkrieseVenner Straße 69Römer- und Germanentage 
KW 22
28.5.202631.5.2026Tolkien TageD-47608 Geldern-PontEichental Pont - Bruchweg 58Tolkien TageTolkien Tage
29.5.202631.5.2026Hansefest SalzwedelD-29410 SalzwedelInnenstadtDas nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
29.5.202631.5.2026Zeitreise IsenD-84424 IsenVolksfestplatz und MeindlparkZeitreise IsenKulturschock Isen
29.5.202631.5.2026Zeitreise IsenD-84424 IsenVolksfestplatz und MeindelparkKulturschock IsenKulturschock Isen
29.5.202631.5.2026MittelaltermarktD-91583 Schillingsfürst TurbaeventsTurbaevents
30.5.202631.5.2026Fantasy- & MittelaltermarktD- Hof all for you eventsall for you events
30.5.202631.5.202619. MittelalterspektakelD-14806 Bad BelzigBurg EisenhardtCOEX GmbHCOEX GmbH
30.5.202631.5.2026Heiden-SpektakelD-32791 LageSpielplatzgelände in HeidenHeiden-SpektakelHeiden-Spektakel
30.5.202631.5.2026MittelalterspektakelD-34346 Hann. Münden SündenfreiSündenfrei
30.5.202631.5.2026750 Jahr FeierD-40472 RatingenMarktplatzHistory & EventHistory & Event
30.5.202631.5.2026Sturm auf ZonsD-41541 Dormagen ZonsFreilichtbühne & BurgSturm auf ZonsSturm auf Zons
30.5.202631.5.2026Mittelaltermarkt SpachbrückenD-64354 ReinheimPfarrgarten-/ Evangelische Kirche  
31.5.202631.5.2026Burgbelebung und Ritterschlag
Besucher willkommen
D-67466 ErfensteinBurg SpangenbergLöwenbanner zu Hagenbach und Berg 

Termine im Juni

Werbung
+Stauferspektael
3.6.20265.6.2026Zeltlager der Landzknechtrotte
der Reichsstadt Schweinfurt A.D. 1516
mit Walpurgisgericht am 4.6.
D-97424 Schweinfurt / OberndorfFriedrich-Pfister-ParkZeltlager
der Landzknechtrotte
 
4.6.20267.6.2026ANNO 1280 - Das 16. FestD-33333 GüterslohRittergut KruseANNO 1280Anno Events
4.6.20267.6.2026Mittelalterlich GaudiumD-45731 WaltropMoselbachparkMittelalterlich GaudiumMittelalterlich Gaudium
4.6.20267.6.2026Mittelalterlicher Markt
mit Ritterturnier in Maxipark
D-59071 Hamm MaximilianparkFogelvreiFogelvrei
4.6.20267.6.2026Mittelaltermarkt
zu Hockenheim
D-68766 HockenheimGartenschaupark - EisenbahnstraßeMittelaltermarkt FreisenE-Mail an die Orga
4.6.20267.6.202620 Jahre StauferSpektakelD-73037 GöppingenStauferpark im StauferwaldStauferspektakelStauferspektakel
4.6.20267.6.2026MYSTIKA
Eine fantastische
magische WeltenReise
D-76726 GermersheimAn Fronte Beckers und UniparkClaudia MichaelisClaudia Michaelis
4.6.20267.6.2026Mittelalterspektakel SandizellD-86529 SchrobenhausenWasserschlossMittelalterfestMittelalterfest
4.6.20267.6.2026MittelalterspektakelD-86655 HarburgSchloss HarburgTurbaeventsTurbaevents
4.6.20267.6.20268. Ritterspiele LichtenauD-91586 LichtenauRenaissancefestungMittelalterfestMittelalterfest
4.6.20267.6.2026MittelaltermarktD-95491 AhorntalBurg RabensteinBurg RabensteinBurg Rabenstein
4.6.20264.6.2026Walpurgisgericht
im Rahmen des Zeltlager der Landzknechtrotte vom 3.6 - 5.6
D-97424 Schweinfurt / OberndorfFriedrich-Pfister-ParkBKV Oberndorf 
5.6.20267.6.2026Sachsen-Anhalt TagD-06406 BernburgParkCOEX GmbHCOEX GmbH
6.6.20267.6.2026De Quaeye Werelt Anno 1477
Mittelaltermarkt
B-2100 Deume / AntwerpenSterckshofDe Quaeye Wereltfer de lance
6.6.20267.7.2026RömerfestD-55232 AlzeyInnenstadtRömerjahr AlzeyRömerjahr Alzey
6.6.20267.6.202612. Burgfest an der alten Burg zu Rotenhain D-56459 RotenhainAlte Burghistorica-rotenhainhistorica-rotenhain
6.6.20267.6.20261. Mittelalterliches Spektakel D-64807 Dieburg SchlossgartenHeimdalls ErbenHeimdalls Erben
6.6.20267.6.2026MittelaltermarktD-75323 Bad WildbadKurparkBad WildbadFreie Ritterschaft Baden
6.6.20267.6.2026Fantasy- & MittelaltermarktD-91239 Henfenfeld all for you eventsall for you events
6.6.20267.6.2026Slag om Bourtange Spectaculum
Schlacht um Bourtange
NL-9545 Bourtange Schlacht um Bourtange 
KW 24
12.6.202614.6.2026Luther's HochzeitD-06886 WittenbergSchlosswieseLuther's Hochzeit 
12.6.202614.6.2026Brimborium D-32805 Horn Bad MeinbergRund um die Burg HornBardensprungBardensprung
12.6.202614.6.2026Historicus MercatusD-78532 TuttlingenRuine Honberghistoricus-mercatus/Fabula Corvinus
12.6.202614.6.2026SchaumburgfestD-96528 SchalkauBurgruine SchaumbergSchaumburgvereinSchaumburgverein
12.6.202614.6.2026Rittertage UffenheimD-97215 UffenheimSchlossplatz und SchlossparkRittertage UffenheimRittertage
13.6.202614.6.2026750-Jahr-Feier mit MittelalterD-15926 Luckau COEX GmbHCOEX GmbH
13.6.202614.6.2026Mittelalter im Wildpark
Schwarze Berge
D-21224 Rosengarten Wildpark Schwarze BergeMarktvagabunden
13.6.202614.6.2026MittelaltermarktD-31629 EstorfScheunenviertel - neue SchulstraßeScheunenviertel EstorfE-Mail an die Orga
13.6.202614.6.2026Zeitreise nach DötzdorfD-41569 RommerskirchenAuf den Feldern vom Dyxmannshof E-Mail an die Orga
13.6.202615.6.2026Historisches Burgfest FürstenauD-48549 Fürstenau Auf der SchlossinselZaunreiter MärkteZaunreiter Märkte
13.6.202614.6.2026MittelaltermarktD-65410 Montabaurrund um Kirche St. Peter in KettenMittelaltermarkt MontabaurLorraine Medievale
13.6.202614.6.202625. Mittelaltermarkt
Obermoschel
D-67823 ObermoschelAuf der LandsburgMittelaltermarktBurgverein Kontakt
Burgverein
13.6.202614.6.202639th medieval festivalF-77160 Provins Provins Medieval 
13.6.202614.6.2026MëttelalterfestL-8707 UseldingenBurg UseldingenUseldeng MedievalUseldeng Medieval
KW 25
19.6.202621.6.2026Merseburger SchlossfestspieleD-06217 MerseburgSchlossgartenSchlossfestspieleMerseburg
19.6.202621.6.2026 Asilions MittelaltermarktD-35759 DriedorfAm Weiher 3Asilions MittelaltermarktAsilions Mittelaltermärkte
E-Mail an die Orga
19.6.202621.6.2026Mittelaltermarkt zum RathausfestD-38855 Wernigerode AltstadtRathausfest 
19.6.202621.6.2026Mittelalterliches Treyben zu MengedeD-44359 DortmundMengeder VolksgartenMittelalterliches TreybenZum froehlichen Normannen
19.6.202621.6.2026Horber RitterspieleD-72160 Horb am NeckarTurnierwiese - Flößerwasen - InnenstadtHorber RitterspieleHorb
20.6.202621.6.2026Mittelalterfest Groß-SieghartsA-3812 Groß-SieghartsSchloßpark Groß-SieghartsMittelalterfestE-Mail an die Orga
20.6.202621.6.2026PlattenburgspektakelD-19336 PlattenburgAuf der Burg 1Plattenburgspektakel 
20.6.202621.6.2026Mittelalter Erlebnis MarktD-22145 BraakBirkenwiese 1Mittelalter Erlebnis MarktAnmeldung
20.6.202621.6.2026WikingersommerD-25889 Witzwortbeim roten HaubargDie RattenbäckerDie Rattenbäcker
20.6.202621.6.2026Spectaculum gebhardi hagensisD-38229 Salzgitter GebhardshagenWasserburg Sternbergstrasse 7bSpectaculum gebhardi hagensisSpectaculum gebhardi hagensis
20.6.202621.6.2026GaudiumD-45899 GelsenkirchenSchloss HorstSündenfreiSündenfrei
20.6.202621.6.2026Schottischen FestivalD-47574    GochStadtparkSchottischen Festival 
20.6.202621.6.2026MittelaltermarktD-47929 GrefrathNiederrheinisches Freilichtmuseum Niederrheinisches Freilichtmuseum 
20.6.202621.6.2026Catzenelnbogener RitterspieleD-56368 KatzenelnbogenWeiherwieseFogelvreiFogelvrei
20.6.202621.6.2026MarktspectaculumD-95671 BärnauGeschichtspark Bärnau-TachovGeschichtspark BärnauGeschichtspark Bärnau
KW 26
26.6.202627.6.202619. Irisch-keltiche MittsommernachtD-39356 WalbeckStiftskirchenruineMittsommernacht
IKM Walbeck
E-Mail an die Orga
26.6.202628.6.2026Aplerbecker SchlossfehdeD-44287 Dortmund-AplerbeckSchlossgartenAplerbecker Schloßfehde 
26.6.202628.6.2026Staufer-SpektakelD-71332 WaiblingenBrühlwieseStaufer SpektakelStaufer Spektakel
26.6.202628.6.2026Mittelalterfest
Castellum ad Louffi
D-83410 Laufen (Salzach) Castellum ad LouffiE-Mail an die Orga
26.6.202628.6.2026MittelaltermarktD-91550 Dinkelsbühl TurbaeventsTurbaevents
27.6.202628.6.20268. Junker-Hansen MittelaltermarkttD-35279 Neustadt HessenBürgerpark
Junker-Hansen-Turm
Mittelalter NeustadtMittelalter Neustadt
27.6.202628.6.2026mittelalterlicher Markt und HeerlagerD-40764 LangenfeldWasserburg Haus GravenHistory & EventHistory & Event
27.6.202628.6.2026MittelaltermarktD-55276 OppenheimBurg LandskronMittelaltermarkt OppenheimLorraine Medievale
27.6.202628.6.2026mittelalterlicher WeihnachtsmarktD-66871 ThallichtenbergBurg LichtenbergWeihnachtsmarktSommerlicher Mittelaltermarkt
27.6.202628.6.2026Sommerlicher MittelaltermarktD-66871 ThallichtenbergBurg LichtenbergSommerlicher MittelaltermarktSommerlicher Mittelaltermarkt
27.6.202628.6.2026Historisches Spektakel zu ZellD-86633    Neuburg/ZellSportgelände des FC Zell/Bruck Spektakel zu Zell 
27.6.202628.6.2026historisches SpektakelD-86633 Neuburg / ZellAlte Neuburger Str. 24  

Termine im Juli

Werbung
hierwerben
2.7.20265.7.2026Mittelalter- und PiratenmarktD-26382 WilhelmshavenPumpwerk Banter Deich 1a  
3.7.20265.7.2026Mittelaltermeile
Altstadtfest
67655 KaiserslauternAltstadtNord SternE-Mail an die Orga 2
3.7.20265.7.2026Agesthorp TurniertageD-27324 EystrupKirchstr. 24Agesthorp TurniertageAgesthorp
3.7.20265.7.2026Mittelalterliches SalinenfestD-48432 RheineAn den Salinen neben dem NaturzooZaunreiter MärkteZaunreiter Märkte
3.7.20265.7.2026Mittelalterspectaculum zu DenklingenD-51580 Reichshof-DenklingenRund um Die BurgVPZ EventsVPZ Events
3.7.20266.7.2026Peter und Paul FestD-75015 Brettenkomplette AltstadtPeter und Paul Fest BrettenPeter und Paul Fest Bretten
3.7.20265.7.2026Mittelaltermarkt
Am Fuße des Stampflschloss
D-83536 Gars / InnAm Fuße des StampflschlossesCanem Nigrum EventsCanem Nigrum Events
3.7.20265.7.2026MittelaltermarktD-84175 GerzenSchlossparkDer SchirrmeisterDer Schirrmeister
3.7.20265.7.2026Schlossfestpiele GrüningenD-88499 Riedlingen (Grüningen) SchlossfestpieleFabula Corvinus
3.7.20265.7.2026Kiliani- AltstadtfestD-91438 Bad Windsheim SündenfreiSündenfrei
3.7.20265.7.2026Mittelalter im LandlD-92360 Mühlhausen-HofenUnter den LindenMittelalter im LandlMittelalter im Landl
3.7.20265.7.2026Mittelaltermarkt und FestivalDK-5800 NyborgDanehofDanehof 
4.7.20265.7.2026MythanD-15569 Woltersdorf bei BerlinMaiwiese WoltersdorfMythanMythan
4.7.20265.7.20264. MittelalterspektakelD-15848 BeeskowBurgCOEX GmbHCOEX GmbH
4.7.20265.7.2026Mittelaltermarkt KroppD-24848 KroppGeestland EventgeländeMittelaltermarkt KroppPST Events
4.7.20265.7.2026Fantasy- & MittelaltermarktD-36404 Vacha all for you eventsall for you events
4.7.20265.7.2026Mittelalterfest auf Gut HeimendahlD-47906 KempenHaus BockdorfGut HeimendahlGut Heimendahl
4.7.20265.7.2026Vegan Fantasy Fair
Das vegane Fantasy Festival
D-66333 VölklingenSchlosspark GeislauternVegan Fantasy Fair 
4.7.20265.7.2026SlawentageD-95671 BärnauGeschichtspark Bärnau-TachovGeschichtspark BärnauGeschichtspark Bärnau
KW 28
9.7.202620.7.2026Tänzelfest
historisches Kinderfest
D-87600 Kaufbeuren Ganze Stadt Tänzelfest 
10.7.202612.7.2026WikingerfestD-18556 Putgarten / RügenPeilturma href="https://kap-arkona.de/veranstaltungen/56-wikingerfest-am-kap-arkona-die-schlacht-um-svantevit" target="extern">Wikingerfesta href="https://www.kap-arkona.de/" target="extern">Kap Arkona
10.7.202612.7.2026ANNO 1250 - Das 3. FestD-37671 HöxterArchäologiepark ANNO 1250Anno Events
10.7.202612.7.2026Mittelalterliches SchlossspektakelD-38364 SchöningenSchlossplatzMittelalterspaßMittelalterspaß
10.7.202612.7.2026Königreich Venhaus
Markt der Schwerter und des Handels
D-48480 Spelle / VenhausVenhauser Straße 50 Der Wildwechsel
10.7.202612.7.20268. historischer MarktD-63868 GroßwallstadtMainwieseGenii LociGenii Loci
10.7.202612.7.2026Mittelaltermarkt
Mandelbachtal
D-66399 OmmersheimOmmersheimer WeiherMarkt MandelbachtalHistorische Märkte und Feste
Bewerbungen
10.7.202612.7.2026Mittelalter zum SchwörtagD-72764 ReutlingenVolksparkMittelalter zum SchwörtagFabula Corvinus
10.7.202610.7.2026Kaltenberger Ritterturnier
Gauklernacht 17:00 - 2:00
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
11.7.202612.7.2026MittelalterfestA-5570 MauterndorfOrtszentrum und BurgMittelalterfest MauterndorfMittelalterfest Mauterndorf
11.7.202612.7.2026Elb-Spectaculum zu StoveD-21423 Stove / Drage MarktvagabundenMarktvagabunden
11.7.202612.7.2026MittelaltermarktD-24972 NorgaardholzSpielplatz vor dem Campingplatz NordsternMittelaltermarkt 
11.7.202612.7.2026MittelaltermarktD-65589 HadamarSchloss und SchlossplatzMittelaltermarkt HadamarLorraine Medievale
11.7.202612.7.2026Mittelalterliches Spektakel
auf der Schauenburg
D-77704 OberkirchBurg SchauenburgHeimattageFreie Ritterschaft Baden
Lions Oberkirch
11.7.202611.7.2026Kaltenberger Ritterturnier
Abendturnier 16:00 - 0:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
11.7.202612.7.20264. Mittelalterfest in EbelebenD-99713 EbelebenSchlossgartenMittelalterfest zu Ebeleben  
11.7.202612.7.2026Ritter Recken HeldenspieleD-99897 Tambach-Dietharz OchsenwieseRitter Recken HeldenspieleMittelalterverein ACW
12.7.202612.7.2026Kaltenberger Ritterturnier
Tagesturnier 12:00 - 20:00
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
KW 29
17.7.202619.7.202636. Burgfest Kaprun A-5710 KaprunBurg KaprunBurgfest KaprunBurgf Kaprun
E-Mail an die Orga
17.7.202619.7.2026MittelalterfestivalCH-3401 Burgdorf Caligatus FeleusCaligatus Feleus
17.7.202619.7.2026Die Wikinger kommen!D-23999 Insel Poel SündenfreiSündenfrei
17.7.202619.7.2026Mittelalterspectaculum GreifensteinD-35753 GreifensteinBurg GreifensteinVPZ EventsVPZ Events
17.7.202619.7.2026Burgspektakel OberhausenD-46117 OberhausenBurg VondernBurgspektakel OberhausenFesta Medievale
17.7.202617.7.2026Kaltenberger Ritterturnier
Nachtturnier 17:00 - 1:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
17.7.202619.7.2026Burgfest SulzbergD-87477 SulzbergBurgruine SulzbergBurgfreunde SulzbergE-Mail an die Orga
17.7.202619.7.2026Spektakulum BayerwaldD-94065 WaldkirchenCamping Resort BayerwaldSpektakulum BayerwaldDie Met-Taverne
Camping Bayerwald
18.7.202619.7.202622. MittelalterspektakelD-07985 ElsterbergBurgruineCOEX GmbHCOEX GmbH
18.7.202619.7.2026MittelaltermarktD-29451 DannenbergReiterstadion    (Hermann Stolte Stadion)MittelaltergildeMittelaltergilde
18.7.202619.7.2026Mittelaltermarkt MassenhausenD-34454 Bad ArolsenBriloner Straße 16 MM ProduktionMM Produktion
18.7.202619.7.2026mittelalterlicher MarktD-47608 GeldernSchloss WalbeckHistory & EventHistory & Event
18.7.202619.7.2026Beller SpectaculumD-55599 EckelsheimRuine der Beller KircheBeller Spektakulum 
18.7.202619.7.202628. Mittelaltermarkt NohfeldenD-66625 NohfeldenOrtskern - rund um die BurgMittelaltermarkt Nohfelden
Mittelaltermarkt Nohfelden
Nohfelden
18.7.202619.7.2026MittelaltermarktD-66954 PirmasensStrecktalparkMittelaltermarkt PirmasensLorraine Medievale
18.7.202619.7.20265ittelalterliches SpektakelD-67141 NeuhofenWoog Straße - Fahr und Reitverein Neuhofen - Woog StraßeHeimdalls ErbenHeimdalls Erben
18.7.202619.7.2026Burg Löwenstein im Mittelalter
Living History Veranstaltung
D-74245 LöwensteinBurgruine LöwensteinBurg Löwenstein im MittelalterYoutube-Kanal
A-Papst-Production
18.7.202618.7.2026Kaltenberger Ritterturnier
Abendturnier 16:00 - 0:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
18.7.202619.7.2026Fantasy- & MittelaltermarktD-98669 VeilsdorfFestplatz Am Berg
neben Sportplatz Eichigt
all for you eventsall for you events
19.7.202619.7.2026Kaltenberger Ritterturnier
Tagesturnier 12:00 - 20:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
KW 30
23.7.202626.7.2026WallensteintageD-18439 StralsundAlter MarktSündenfreiSündenfrei
24.7.202626.7.2026Bovelmarkt BassumD-27211 BassumFreudenburgBovelzumftBovelzumft
24.7.202626.7.2026SparrenburgfestD-33602 BielefeldSparrenburgSparrenburgfestFogelvrei
24.7.202626.7.202612. Gregorianmarkt zu SchönebergD-55444 SchönebergSchlossstraßeSchöneberg SoonwaldE-Mail Anmeldungen
24.7.202626.7.20261250 Jahr Feier BaierbrunnD-82065 BaierbrunnAm Wirtsacker1250 Jahr Feier1250 Jahr Feier
24.7.202624.7.2026Kaltenberger Ritterturnier
Nachtturnier 17:00 - 1:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
24.7.202626.7.2026Mittelalterspektakel
Maderbichl
D-86989 SteingadenMaderbichl 4Zum Tropfenden FassE-Mail an die Orga
24.7.202626.7.2026Aischgrunder ThingD-91486 Uehlfeld Peppenhöchstädt - SchäferwieseAischgrunder ThingSchatten der Wälder
E-Mail an die Orga
25.7.202626.7.202624. Spectaculum zu FriesachA-9360 FriesachInnerhalb der Stadtmauer von FriesachSpectaculum FriesachStadtgemeinde Friesach
25.7.20262.8.2026Raceburg WylagD-23909 RatzeburgRatzeburger SeeWylag 
25.7.202626.7.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-31675 BückeburgSchloßparkSpectaculum Bückeburg 1Spectaculum
25.7.202626.7.2026MittelaltermarktD-56154 BoppardKurfürstliche Burg und RheinaueLorraine MedievaleLorraine Medievale
25.7.202625.7.2026Kaltenberger Ritterturnier
Abendturnier 16:00 - 0:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
25.7.20262.8.2026Mittelalterspektakel
Festival Médiéval
L-9401 ViandenChâteau ViandenMittelalterfest ViandenSchloss Vianden
26.7.202626.7.2026Graf-Engelbert-FestD-58511 LüdenscheidGraf-Engelbert-PlatzAltstadt LüdenscheidAltstadt Lüdenscheid
26.7.202626.7.2026Kaltenberger Ritterturnier
Tagesturnier 12:00 - 20:30
D-82269 KaltenbergSchlossRitterturnier Kaltenberg 
KW 31
30.7.20262.8.2026Wikingerfest am MeerD-26506 Norden-NorddeichGrünwiese DrachenwieseWikinger NorddeichWikinger Norddeich
31.7.20262.8.2026Mittelalterspektakel
Anno 1291 Altstätten SG
CH-9450 Altstätten SGAllmendMittelalterspektakel AltstättenTurnei
31.7.20262.8.2026Piratenspektakel Eckernförder BuchtD-24340 EckernfördeHafenPiratenspektakelEckernförde
31.7.20262.8.2026Zaubermarkt CoesfeldD-48653 CoesfeldHaus LoburgZaubermärkteZaubermärkte Bewerbung
31.7.20262.8.2026Sommermarkt zu Freisen D-66629 FreisenNaturwildpark FreisenFrühlingsmarkt FreisenMittelaltermarkt Freisen
31.7.20262.8.2026Fantasy- & MittelaltermarktD-83236 Übersee am ChiemseeAlmdorado - Almfischer 3all for you eventsall for you events
31.7.20262.8.2026Mittelaltermarkt
Am Wasserschloss
D-84416 Taufkirchen / VilsAm WasserschlossCanem Nigrum EventsCanem Nigrum Events
31.7.20269.8.2026MittelaltermarktD-86956 Schongau Zum Tropfenden FassE-Mail an die Orga
31.7.20262.8.2026MittelaltermarktD-88630 Pfullendorf TurbaeventsTurbaevents
31.7.20262.8.2026RitterturnierD-89168 Niederstotzingen - StettenRittergut StettenRitterturnierWürttemberger Ritter
31.7.20263.8.2026BurgfestD-91161 Hilpoltstein BurgfestHilpoltstein
31.7.20262.8.2026MittelaltermarktD-95491 AhorntalBurg RabensteinBurg RabensteinBurg Rabenstein

Termine im August

Werbung
hierwerben
1.8.20262.8.2026Keltisches ErntefestD-06537 Kelbra / KyfhäuserKönigspfalz TilledaCorvos die Raben e.V Corvos die Raben e.V
1.8.20261.8.2026Zeitreise: die Germanen und Kelten kommenD-16303 Passow bei Schwedt/Oder Kulturpalast VeranstaltungenKulturpalast Uckermark
E-Mail an die Orga
1.8.20262.8.2026Ritterfest D-26553 DornumMaximilianparkFogelvreiFogelvrei
1.8.20262.8.202620. Mittelalterliches KlosterfestD-55546 Pfaffen-Schwabenheim KlosterfestFördergemeinschaft Pfaffen Schwabenheim
1.8.20262.8.2026Burgfest Reichsburg CochemD-56812 Cochem / MoselReichsburgBurgfest Reichsburg CochemReichsburg Cochem
1.8.20262.8.202616. Oberurseler FeyereyD-61440 OberurselMarxstraßeOberurseler FeyereyUrsellis Historica
E-Mail an die Orga
1.8.20262.8.2026KlosterfestD-76332 Bad HerrenalbKlosterruineKlosterfestFabula Corvinus
1.8.20262.8.2026SeelenPferd trifft MittelalterD-79871 EisenbachPferdehof Bubenbacher Straße 11Bubenbacher MühleBubenbacher Mühle
2.8.20262.8.2026BurgfestD-07768 Orlamünde Kemenate OrlamündeKemenate Orlamünde
2.8.20263.8.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-31675 BückeburgSchloßparkSpectaculum Bückeburg 2Spectaculum
KW 32
5.8.20266.8.2026Mittelalterlicher Pferdemarkt
zur Eröffnung der Kranger Kirmes
D-44653 HerneReitergut Steinhausen - Wiedehopfstr. 200TIWI-TheaterTIWI-TheaterE-Mail an die Orga
6.8.20268.8.2026Fantastica FestivalD-31628 LandesbergenRittergut BrokelohFantastica FestivalRealms of Mythodea
7.8.20269.8.2026Wikingertage SchleswigD-24837 SchleswigKönigswiesenWikingertageWikingertage
7.8.20269.8.2026Lux Noctis & Ritterfest D-26553 DornumMaximilianparkFogelvreiFogelvrei
7.8.20269.8.2026ANNO 1238 - Das 5. FestD-48291 TelgtePlanwiese-Dümmert ParkANNO 1238Anno Events
7.8.20269.8.2026Das Grosse Treffen
Fantasy-Festival
Homepage beachten
D-78267 Aach am BodenseeMega Event ParkDas Grosse TreffenDas Grosse Treffen
7.8.20269.8.2026BachrittertageD-88422 KanzachBachritterburg KanzachBachritterburgBachritterburg
7.8.20268.8.2026Schloßhof FestivalD-91315 Höchstadt / AischSchloss HöchstadtSchloßhof FestivalShop Schloßhof Festival
7.8.20269.8.202613.Amberger BrunnenfestD-92224 AmbergMaxplatzBrunnefestCantus Ferrum
8.8.20269.8.20266. Mittelalterspektakel
Wettstreit der Feuerspucker
D-08115 SchönfelsBurg SchönfelsCOEX GmbHCOEX GmbH
8.8.20269.8.2026Stargarder BurgfestD-17094 Höhenburg Stargard Burgfest StargarderStargarder Burgverein
8.8.20269.8.2026Kurzweyl und Gaukeley
Zeitenzauber rund ums
Duderstädter Rathaus
D-37115 DuderstadtHistorisches RathausKultursommer DuderstadtE-Mail an die Orga
8.8.20269.8.2026mittelalterlicher MarktD-41065 MönchengladbachSchloss WickrathHistory & EventHistory & Event
8.8.20269.8.202613. Festival der Kulturen
und Mittelaltermarkt
noch Händler und Lager gesucht
D-53840 TroisdorfBurg Wissem    - Burgallee 1Festival der KulturenKinderkulturwelt
E-Mail an die Orga
8.8.20269.8.2026Mittelalterliches SpektakulumD-74918 Angelbachtal-EichtersheimSchloßpark EichtersheimMittelalterliches Spektakulum AngelbachtalHistotainment Park Adventon
8.8.20269.8.2026Historisches MilitärmanöverD-95671 BärnauGeschichtspark Bärnau-TachovGeschichtspark BärnauGeschichtspark Bärnau
KW 33
13.8.202616.8.2026histoeisches FestD-86685Huisheim histoeisches Fest 
14.8.202616.8.2026Lichterfest Halle im MittelalterD-29221 CelleAm Französischen Garten 1Das nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
14.8.202616.8.2026Mittelaltermarkt am Osnabrücker RingwallD-49082 OsnabrückNahner WaldbahnMittelaltermarkt OsnabrückEvent-Dual - Ticketshop
14.8.202616.8.2026Mittelaltermarkt am Osnabrücker RingwallD-49082 Osnabrück / SutthausenNahner RennbahnMittelaltermarkt OsnabrückMittelaltermarkt Osnabrück
14.8.202616.8.202636. AltburgfestivalD-55626 BundenbachD-55626 BundenbachAltburgfestivalAltburgfestival
14.8.202616.8.2026großer MittelaltermarktD-66117 SaarbrückenDeutsch-Französischer Garten Anmeldung
14.8.202616.8.202613. Dobler SpectaculumD-75335 DobelHöhenstr. am WasserturmSpectaculum 
14.8.202616.8.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-79576 Weil am RheinDrei Länder GartenSpectaculum Weil am RheinSpectaculum
14.8.202616.8.2026Historische Burgtage TittmoningD-84529 TittmoningBurg TittmoningHistorische BurgtageHistorische Burgtage
14.8.202616.8.2026Cave GladiumD-93437 Furth im Waldnähe FestwieseCave GladiumCave Gladium
15.8.202616.8.2026Wikingerfest Kloster MalchowD-17213 MalchowKulturzentrum Kloster MalchowVisit MalchowVisit Malchow
15.8.202616.8.2026Schlacht bei WallsbüllD-24980 WallsbüllValsgaardValsgaardValsgaard
15.8.202616.8.20263. Coppenbrügger SpectaculumD-31863 CoppenbrüggeBurg Coppenbrügge   
15.8.202616.8.2026MettelaltermoartL-7610 Larochette / Fels MettelaltermoartLorraine Medievale
KW 34
19.8.202621.8.2026Mittelalterspectaculum zu VetzbergD-35444 VetzbergBurg VetzbergVPZ EventsVPZ Events
20.8.202623.8.2026Mittelalter am MeerD-26316 Varel Dangastauf der Gast 40Friesische Marktgilde Friesische Marktgilde
21.8.202623.8.20262. Mittelalterliche Treybe zu HambührenD-29313 HambührenSchützenplatzmittelalterliches TreibenHeimatverein
Robert Kresse
21.8.202623.8.2026MittelaltermarktD-82256 Fürstenfeldbruck TurbaeventsTurbaevents
21.8.202623.8.2026Horto HistoricoD-93158 TeublitzStadtparkHorto HistoricoHorto Historico
21.8.202623.8.2026Südtiroler Ritterspiele
Giochi Medievali dell Alto Adige
I-39020 SchludernsFlugplatzstrasseRitterspiele 
22.8.202623.8.202614. historisches Burgfest mit RitterturnierD-06268 QuerfurtBurgCOEX GmbHCOEX GmbH
22.8.202623.8.2026Mittelaltermarkt zu TydalD-24852 Eggebek ZeytreyseZeytreyse
22.8.202623.8.20263. Mittelalterliches Treyben auf dem Graner BergD-34466 Wolfhagen mittelalterliches Treybenmittelalterliches Treyben
22.8.202623.8.202613. Mittelaltermarkt SteinauD-36396 Steinau an der StraßeSchloss Rathausplatz KinzigwiesenMittelaltermarkt SteinauTrimburger Ritterschaft
E-Mail an die Orga
22.8.202623.8.202621. Gigantisches Mittelalterliches SpektakelD-64579 GernsheimSchäferwiese - Wormser Straße - Fischerfest ParkplatzHeimdalls ErbenHeimdalls Erben
22.8.202623.8.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-67346 SpeyerUnterer DomgartenSpectaculum SpeyerSpectaculum
KW 35
28.8.202630.8.2026Gaudium zur PurgA-3251 Purgstall /    ErlaufSchloßpark PurgstallGaudium zur Purg 
28.8.202630.8.2026Fantasy Spektakulum zu UsterCH-8610 UsterRiedikonFantasy Spektakulum zu UsterTurnei
28.8.202630.8.2026Lichterfest Halle im MittelalterD-06108 Halle SaaleInnenstadtDas nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
28.8.202630.8.2026MittelaltermarktD-06667 WeißenfelsBadeanlagenMittelaltermarkt 
28.8.202630.8.20269. WikingertageD-18586 Göhren / Insel RügenSeebrücke Zum Germanen
28.8.202630.8.2026Piratenwelt Brake(Unterweser)D-26931 Brake (Unterweser)Kaje E-Mail an die Orga
28.8.202630.8.2026Mittelalterfest
am Vörder See
D-27432 BremervördeVörder SeeMittelalterfest 
28.8.202630.8.2026ANNO 1320 - Das zwölfte FestD-32369 Rahden-KleinendorfMuseumshofANNO 1320Anno Events
28.8.202630.8.2026MittelaltermarktD-35619 BraunfelsKurparkLorraine MedievaleLorraine Medievale
28.8.202630.8.2026MittelaltermarktD-82256 Fürstenfeldbruck TurbaeventsTurbaevents
28.8.202630.8.2026Dager av UlverD-84149 Velden VilsVolksfestplatzDager av UlverDager av Ulver
28.8.202629.8.2026Medieval Festival HorsensDK-8700 HorsensFængslet Fussingsvej 8Middelalderfestival 
29.8.202630.8.2026HunnenfestA-2151 Asparn/Zaya Schloss AsparnAsparn/Zaya
29.8.202630.8.2026MittelaltermarktCh-3629 KiesenRingstrasseMittelaltermarkt KiesenMittelaltermarkt Kiesen
29.8.202630.8.2026Mittelalterliches Markttreiben
Hannover - Misburg
D-30629 HannoverAm blauen See Hannover-MisburgMittelalterliches MarkttreibenNaturfreunde Misburg
Draconis Events
29.8.202630.8.20261. Borchener Mittelaltertage D-33178 BorchenMalickrod HofZum fröhlichen NormannenZum fröhlichen Normannen
29.8.202630.8.2026Borchener MittelaltertageD-33178 BorchenMallinckrodthof BorchenBorchener MittelaltertageZum froehlichen Normannen
29.8.202630.8.2026Zunftmarkt Bad WimpfenD-74206 Bad WimpfenAltstadtZunftmarktZunftmarkt
29.8.202630.8.2026Mittelalterliches Sommerfest
des Gnadenhof für Tiere e.V.
D-76149 Karlsruhe - NeureutAm BaufeldDas Löwenbanner zu Hagenbach und BergDas Löwenbanner zu Hagenbach und Berg
Gnadenhof für Tiere
29.8.202630.8.2026Fantasy- & MittelaltermarktD-92690 PressathKiesi Beachall for you eventsall for you events
29.8.202630.8.202632. MittelalterstadtfestD-99947 Bad LangensalzaInnenstadtBad Langensalza Bad Langensalza
E-Mail an die Orga

Termine im September

Werbung
hierwerben
1.9.202613.9.2026Fantasy- & MittelaltermarktD-85467 NeuchingKiesi Beachall for you eventsall for you events
4.9.20266.9.2026MittelaltermarktA-6971 HardStedepark - FestwieseMittelalter VeranstaltungenMittelalter Veranstaltungen
4.9.20266.9.2026Mittelalterlich Phantasie Spectaculum
das größte reisende Mittelalter Kultur Festival der Welt
D-21376 LuhmühlenTurnierplatzSpectaculum LuhmühlenSpectaculum
4.9.20266.9.2026SehuasfestD-38723 Seesen SehusafestSehusafest
4.9.20266.9.2026MittelalterspektakelD-44629 HerneSchloss StrünkedeSündenfreiSündenfrei
4.9.20266.9.2026MittelalterspektakelD-89522 Heidenheim / BrenzSchloss HellensteinTurbaeventsTurbaevents
5.9.20266.9.2026Fantasy- & MittelaltermarktD- 98663    Heldburg all for you eventsall for you events
5.9.20266.9.202615.Ritter SpektakelD-01458 Ottendorf-OkrillaSchloß HermsdorfMittelalter-SpektakelMittelalter-Spektakel
5.9.20266.9.2026Mittelalter an der SilberschmelzeD-08289 SchneebergSilberschmelzhütte St. Georgen E-Mail an die Orga
5.9.20266.9.202627. Mittelalterliches BurgfestD-08499 MylauBurgCOEX GmbHCOEX GmbH
5.9.20266.9.2026RitterfestD-15831 DiedersdorfSchloss DiedersdorfCarnica SpectaculiCarnica Spectaculi
5.9.20266.9.2026DisibodmarktD-55568 StaudernheimSchulstraße 12Kolping StaudernheimKolping Staudernheim
E-Mail an die Orga
5.9.20266.9.2026Schinderhannes RäuberfestD-55756 Herrsteinhistorischer OrtskernSchinderhannes-RäuberfestHerrstein
E-Mail an die Orga
5.9.20266.9.20266. Gondsrother RitterlagertD-63594 HasselrothWaldsportplatzRitterlagerGeschichtsverein
5.9.20266.9.2026Colosseum HasalahaD-67454 HaßlochPferderennbahnMittelaltermarkt FreisenMittelaltermarkt Freisen
5.9.20267.9.2026MittelaltermarktD-78120 Furtwangen / LinachHof zum wilden Michel - Linach 9Zum wilden Michel 
5.9.20266.9.2026Mittelalter Spektakulum
Konzert Trollfaust 4.9.
D-89561 DischingenBurg KatzensteinBurg Katzenstein
5.9.20266.9.2026Mittelalter SpektakelD-96149 BreitengüßbachGut LeimershofMittelalter SpektakelE-Mail an die Orga
E-Mail an Gut Leimershof
6.9.20266.9.2026Kinderfest im GeschichtsparkD-95671 BärnauGeschichtspark Bärnau-TachovGeschichtspark BärnauGeschichtspark Bärnau
KW 37
11.9.202613.9.2026ANNO 1225 - Das 1. FestD-30980 BarsinghausenRittergut Eckerde IANNO 1225Anno Events
11.9.202613.9.2026Steinfurt TingD-36358 HerbsteinSchwarzastraße 8Steinfrut TingSteinfrut Ting
11.9.202613.9.2026Historischer Korn- und HansemarktD-49749 HaselünneStadtkernKorn und Hansemarkt 
11.9.202613.9.2026Hayner Burgfest
Ritter Recken Turmlegenden
D-63303 DreieichenhainReichsburg Hayn in der DreieichHayner BurgfestHayner Burgfest
11.9.202613.9.2026MittelaltermarktD-72488 Sigmaringen TurbaeventsTurbaevents
11.9.202613.9.2026Festival-Mediaval XVIID-95100 SelbGoldbergFestival MediavalFestival Mediaval
12.9.202613.9.2026Zeitreise ins MittelalterA-3730 Eggenburg  Zeitreise ins MittelalterMittelalter.co
12.9.202613.9.2026Historisches Markttreiben
Zwickau im Wandel der Zeit
D-08056 ZwickauHauptmarkt Kornmarkt - DomhofMarkttreiben
12.9.202613.9.2026Josef MarktD-33609 BielefeldHalhofJosef MarktDie Falken -Events
12.9.202613.9.2026MittelaltermarktD-45289 Essen-Burgaltendord  Fabulix
12.9.202613.9.2026Mittelalter im Park D-49326 Melle Wellingholzhausen Mittelalter im Park 
KW 38
18.9.202620.9.2026MittelalterspektakelCH-2500 Biel NidauStrandbadCaligatus FeleusCaligatus Feleus
18.9.202620.9.2026Spektakulum MulneD-23879 MöllnKurpark MöllnSpektakulum MulneMölln Tourismus
18.9.202620.9.2026MittelaltermarktD-33175 Bad LippspringeArminiuspark  
18.9.202620.9.20264. großer Mittelaltermarkt
rund um die Bettinger
D-66839 SchmelzHüttersdorfer Straße 29Mittelaltermarkt SchmelzHistorische Märkte und Feste
Bewerbungen
18.9.202620.9.2026Mittelaltermeile
Altstadtfest
D-76593 GernsbachAltstadtProHistoryProHistory
18.9.202620.9.2026Mittelaltermarkt Alte ZiegeleiD-84032 AltdorfAlte Ziegelei - Dekan-Wagner-Str. 13 Taverne im Vitztumb
18.9.202620.9.2026SpektakulumD-94548 Innernzell  E-Mail Anmeldung Händler
E-Mail Anmeldung Lager
19.9.202620.9.2026Katharienmarkt zuD-27318 HoyaBürgerparkKatharinenmarkt HoyaArs Vivendi
19.9.202620.9.2026Mittelalter auf dem Alten LunehofD-27612 Loxstedt  E-Mail an die Orga
19.9.202620.9.2026MittelalterfestD-29328 Müden/Örtze Wildpark MüdenWildpark Müden
19.9.202620.9.2026RoswithafestD-37581 Bad Gandersheim COEX GmbHE-Mail an die Orga
19.9.202620.9.2026RitterfestD-39319 JerichowKlosterwiese JerichowCarnica SpectaculiCarnica Spectaculi
19.9.202620.9.2026Die Wikinger kommen!D-46509 XantenSchloss HorstSündenfreiSündenfrei
19.9.202620.9.2026Mittelaltermarkt
mit Turnier
D-67705 TrippstadtSchlossgartenMittelaltermarkt TrippstadtLorraine Medievale
19.9.202620.9.2026Burgfest StettenfelsD-74199 UntergruppenbachBurg StettenfelsMittelalterfestAnmeldung
19.9.202620.9.2026Mittelaltermarkt
zu Bietigheim ( Baden )
D-76467 Bietigheim / BadenFestplatz Bietigheim - Stöckwiese 7Bickesheimer Spiegelfechter
19.9.202620.9.2026MittelalterspektakelD-93186 Adlersberg / Pettendorf SündenfreiSündenfrei
19.9.202620.9.2026Tempus MedievaleL-8386 KoerichGréiweschlassTempus MedievaleTempus Mediaevale FB
Tempus Mediaevale
KW 39
25.9.202627.9.2026Mittelalter SpektakelA-8750 JudenburgSchloss LiechtensteinSchloss Liechtenstein 
25.9.202627.9.2026Mittelalter Handwerkermarkt
im Ritterhaus zu Bubikon
CH-8608 BubikonRitterhaus BubikonMittelalter HandwerkermarktTurnei
25.9.202627.9.2026Arnsberger Mittelalter HerbstD-59821 ArnsbergAlt Stadt + Schlossruine Suerlänner Tross
Verkehrsverein Arnsberg
25.9.202627.9.2026Eberfinger RitterspieleD-79780 Stühlingen/EberfingenBolzplatz / FestplatzEberfinger RitterspieleFabula Corvinus
25.9.202627.9.2026Fantasy- & MittelaltermarktD-85591 Vaterstetten all for you eventsall for you events
25.9.202627.9.2026Zwiebelmarkt ApoldaD-99510 Apolda SündenfreiSündenfrei
26.9.202627.9.202616. Historisches SchlossspektakelD-04600 AltenburgSchlossCOEX GmbHCOEX GmbH
26.9.202627.9.2026Mittelalter Erlebnis MarktD-24855 JübekStadion Jübek - StadionstraßeMittelalter Erlebnis MarktAnmeldung
26.9.202627.9.2026MichaelismarktD-46236 BottropCyriakusplatzHistory & EventHistory & Event
26.9.202627.9.2026Retro-MPSD-46325 BorkenWasserschloss PröbstingRetro-MPS EmkendorfSpectaculum
26.9.202627.9.2026BurgmannentageD-49377 VechtaZitadellenparkBurgmannentageArs Vivendi
26.9.202627.9.202655. Heidelberger HerbstD-69120 Heidelberg SündenfreiSündenfrei
26.9.202627.9.2026MittelaltermarktD-75378 Bad LiebenzellKurparkMarkt Bad LiebenzellLorraine Medievale
26.9.202627.9.2026BurgbelebungD-88422 KanzachBachritterburg KanzachBachritterburgBachritterburg

Termine im Oktober

Werbung
+SEErenade
2.10.20264.10.2026Torgau leuchtet!D-04860 TorgauSchloss HartenfelsSündenfreiSündenfrei
2.10.20264.10.2026MittelaltermarktD-31848 Bad MünderFreizeitbadDas nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
2.10.20264.10.2026ANNO 1290
Kaisermarkt
D-38690 GoslarHistorische AltstadtANNO 1290Anno Events
2.10.20264.10.202611. SEErenade
Wikinger-Markt- und Lagertage
D-45721 HalternSilbersee II - zum VogelsbergSEErenadeE-Mail an sunrise03@gmx.net
2.10.20264.10.2026MittelaltermarktD-67661 KaiserslauternGelterswoogMittelaltermarktGelterswoog
2.10.20264.10.2026Ritterspiele zu DornstettenD-72280 Dornstetten/Hallwangen Ritterspiele zu DornstettenFabula Corvinus
2.10.20264.10.2026Mittelaltermarkt Grafing bei Münchend-85567 Grafing bei MünchenVolksfestplatzMittelaltermarkt Familia Canem Nignum
3.10.20264.10.20262. RitterfestD-06502 ThaleKurparkCarnica SpectaculiCarnica Spectaculi
3.10.20264.10.202623. Wikinger auf Burg RabensteinD-09106 ChemnitzBurg RabensteinCOEX GmbHCOEX GmbH
3.10.20264.10.2026GauklerfestD-13599 Berlin SpandauZitadelle SpandauCarnica SpectaculiCarnica Spectaculi
3.10.20264.10.2026Mittelalter & Fantasytage mit DrachenD-56346 St. Goarshausenauf der LoreleySündenfreiSündenfrei
3.10.20264.10.2026BurgfestspieleD-63549 RonneburgAuf der Burg 1OstermarktBurg Ronneburg
3.10.20264.10.202613. Mittelalterliches SpektakelD-66679 Losheim am SeeFestplatz - Zum Stausee - Heimdalls ErbenHeimdalls Erben
3.10.20265.10.2026BurgbelebungD-88422 KanzachBachritterburg KanzachBachritterburgBachritterburg
3.10.20264.10.2026Thüringer RitterturnierD-99448 KranichfeldNiederburgSündenfreiSündenfrei
KW 41
9.10.202611.10.2026Halloween Mittelalterspektakel zu WinterthurCH-8400 WinterthurTeuchelweiherplatzHalloween MittelalterspektakelTurnei
9.10.202611.10.2026Mittelaltermarkt mit großem RitterturnierD-40229 Düsseldorf-EllerSchützenplatz Heidelberger Str.Ritterturnier
Markteintritt
Das Ritterturnier
Kontakt Bewerbungen
9.10.202611.10.2026Mittelaltermarkt an der DrachenbrückeD-45661 Recklinghausen HochlarmarkKlalstraße 53bLiberum FantasyLiberum Fantasy
9.10.202611.10.2026Glanz der RitterzeitD-72459 Albstadt LautlingenSchlossGlanz der RitterzeitFabula Corvinus
9.10.202611.10.20265. Historisches Stadtfest
zu Meersburg
D-88709 MeersburgAltstadtMittelalter MeersburgMittelalter Meersburg
9.10.202611.10.2026373. ZwiebelmarktD-99423 Weimar SündenfreiSündenfrei
10.10.202611.10.2026Mittelalterlicher Markt D-34497 KorbachFußgängerzone - Prof.-Kümmel-Str. bis RathausMittelalterlicher Markt Korbacher Hanse
10.10.202611.10.2026ResidenzmarktD-35781 WeilburgBarck SchlossplatzProHistoryProHistory
10.10.202611.10.2026MittelaltermarktD-63150 HeusenstammlSchlossLorraine MedievaleLorraine Medievale
10.10.202611.10.2026Fantasy- & MittelaltermarktD-91334 HemhofenFreifläche Hauptstraße
neben Hotel goldener Schwan
all for you eventsall for you events
KW 42
16.10.202618.10.20264. Mittelalterlicher Katharinen Markt SteinauD-36396 Steinau an der StraßeRathausplatz & Brüder-Grimm-StraßeTrimburger RitterschaftTrimburger Ritterschaft
E-Mail an die Orga
16.10.202618.10.2026MittelaltermarktD-91555 FeuchtwangenSulzachparkTurbaeventsTurbaevents
17.10.202618.10.2026Internationale Reenactor-MesseD-34121 KasselMesse Kassel
Damaschkestraße 55
Reenactor-MesseE-Mail an die Orga
17.10.202618.10.20265. Mittelalter-Markttage D-44575 Castrop-Rauxel SündenfreiSündenfrei
17.10.202618.10.202620.Mittelaltermarkt FreudenbergD-57258 FreudenbergKurparkMM ProduktionMM Produktion
17.10.202618.10.2026Steampunk MarktD-66578 Schiffweiler-Redengroße WerkshalleLorraine MedievaleLorraine Medievale
KW 43
23.10.202625.10.2026Die kuriose altertümliche KirmesD-85540 HaarGroße Wiese am WieselwegEvent MittelalterEvent Mittelalter
24.10.202624.10.2026Bal Renaissance - Danse MedievaleD-81825 MünchenGasthof Obermaier - Truderinger Str. 306Bal RenaissanceBlack Flag Records
24.10.202625.10.2026Steampunk MarktL-5501 RemichPlace Dr. Fernand KonsLorraine MedievaleLorraine Medievale
KW 44
30.10.20261.11.2026Halloween Mittelalterspektakel ZürichCH-8005 ZürichHardturmMittelalterspektakelTurnei
E-Mail an die Orga
30.10.20261.11.2026ANNO HALLOWEEN / SAMHAIND-33333 GüterslohRittergut KruseANNO HALLOWEEN / SAMHAINAnno Events

Termine im November

Werbung
hierwerben
2.11.202630.12.2026Felsenkeller- WeihnachtsmarktD-04229 Leipzig / Plagwitz SündenfreiSündenfrei
6.11.20268.11.2026ANNO 1292D-29221 CelleCeller Schloss ANNO 1292Anno Events
7.11.20268.11.2026MittelaltermarktD-74821 Mosbach Mittelalter- und KunsthandwerkermarktHistorische Märkte und Feste
Bewerbungen
KW 46
12.11.202622.12.2026Historische WeihnachtD-10245 Berlin FriedrichshainRAW-GeländeCarnica SpectaculiCarnica Spectaculi
13.11.202623.12.2026MittelaltermarktD-45127 EssenKopstadtplatzHistory & EventHistory & Event
KW 47
20.11.202622.11.2026Mittelalter Weihnachtsmarkt
auf Schloss Laufen am Rheinfall
CH-8447 DachsenSchloss Laufen am RheinfallTurneiTurnei
KW 48
23.11.202623.12.2026Freibeuterdorf
Schlachtezauber
D-28195 BremenSchlachte / WeseruferFogelvreiFogelvrei
25.11.202623.12.2026Mittelalter- WeihnachtD-01067 DresdenStallhofSündenfreiSündenfrei
27.11.202623.12.2026Kloster- WeihnachtD-09111 Chemnitz SündenfreiSündenfrei
27.11.202629.11.2026Mittelalter- Advent (Fr.- So.)D-44629 HerneSchloss StrünkedeSündenfreiSündenfrei
28.11.202629.11.202612. mittelalterlicher WeihnachtsmarktD-42553 VelbertVorburg Schloss HardenbergMarktgilde zu HardenbergMarktgilde zu Hardenberg
28.11.202629.11.2026Historischer WeihnachtsmarktD-63549 RonneburgAuf der Burg 1OstermarktBurg Ronneburg
29.11.202629.11.2026Advent im GeschichtsparkD-95671 BärnauGeschichtspark Bärnau-TachovGeschichtspark BärnauGeschichtspark Bärnau

Termine im Dezember

Werbung
hierwerben
4.12.20266.12.2026Mittelalter- Advent (Fr.- So.)D-44629 HerneSchloss StrünkedeSündenfreiSündenfrei
4.12.20266.12.2026Mittelalter im AdventD-71263 Weil der StadtCarlo-Schmid-PlatzLorraine MedievaleLorraine Medievale
4.12.20266.12.2026Mittelalter im AdventD-71263 Weil der StadtCarlo-Schmid-PlatzLorraine MedievaleLorraine Medievale
5.12.20266.12.2026Broicher SchlossweihnachtD-45479 Mülheim a.d. RuhrSchloss BroichHistory & EventHistory & Event
5.12.20266.12.2026Historischer WeihnachtsmarktD-63549 RonneburgAuf der Burg 1OstermarktBurg Ronneburg
KW 50
11.12.202613.12.2026Mittelalterliche WeihnachtsmarktD-27793 WildeshausenAlexanderkircheDas nordische MarktvolkDas nordische Marktvolk
E-Mail an die Orga
11.12.202613.12.2026ANNO 1280 WYHNACHT VII.D-33333 GüterslohRittergut KruseANNO 1280 WYHNACHTAnno Events
11.12.202613.12.2026Mittelalter- Advent (Fr.- So.)D-44629 HerneSchloss StrünkedeSündenfreiSündenfrei
11.12.202613.12.2026MittelaltermarktD-78056 Villingen-SchwenningenMünsterplatz VillingenLorraine MedievaleLorraine Medievale
12.12.202613.12.2026Broicher SchlossweihnachtD-45479 Mülheim a.d. RuhrSchloss BroichHistory & EventHistory & Event
12.12.202613.12.2026Historischer WeihnachtsmarktD-63549 RonneburgAuf der Burg 1OstermarktBurg Ronneburg
12.12.202613.12.2026mittelalterlicher WeihnachtsmarktD-72401 HaigerlochKarlstalmittelalterlicher WeihnachtsmarktFabula Corvinus
KW 51
18.12.202620.12.2026Mittelalter- Advent (Fr.- So.)D-44629 HerneSchloss StrünkedeSündenfreiSündenfrei
19.12.202620.12.2026Broicher SchlossweihnachtD-45479 Mülheim a.d. RuhrSchloss BroichHistory & EventHistory & Event
19.12.202620.12.202618. Romantischer Mittelalterlicher Weihnachtsmarkt D-64759 GernsheimSchäferwiese - Wormser Straße - Fischerfest Parkplatz Heimdalls ErbenHeimdalls Erben
KW 52
27.12.202630.12.2026RauhnächteD-01067 DresdenStallhofSündenfreiSündenfrei
27.12.202630.12.2026RauhnächteD-01067 DresdenStallhofSündenfreiSündenfrei
Es wurden 494 von 557 Einträgen ausgegeben.
+ Das Marktkalendarium ist nach § 87a ff. des Urheberrechtsgesetzes (UrHG) urheberrechtlich geschützt. + Ich bitte dies zu beachten.
+ Links auf das Marktkalendarium dürfen gerne gesetzt werden. Eine kurze Info wenn ein Link gesetzt wird würde mich freuen. +
+
+ + + +Alle Angaben ohne Gewähr.
+ +Da es mir nicht möglich ist regelmäßig alle Termine auf Absagen und +Verschiebungen zu prüfen kann ich eventuelle Fehler nicht ausschließen. +Sollten ihnen fehlerhafte Angaben auffallen dann teilen sie mir dies bitte mit, +damit ich diese korrigieren kann.
+ +Termin melden über Formular oder E-Mail +
+
+ +

+ + nach oben + +

+ + +
+ Startseite  + Werben beim Marktkalendarium  + Termin melden  + Impressum +
+
+ + 



+ + + +
Wichtiger Hinweis:
+Alle Angaben ohne Gewähr. Bitte überprüfen Sie die Angaben vor einem Besuch nochmals.
+ + + diff --git a/backend/internal/domain/discovery/crawler/testdata/mittelalterkalender.html b/backend/internal/domain/discovery/crawler/testdata/mittelalterkalender.html new file mode 100644 index 0000000..054e394 --- /dev/null +++ b/backend/internal/domain/discovery/crawler/testdata/mittelalterkalender.html @@ -0,0 +1,8035 @@ + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + 0 0 0 0 0 0 0 0 0 + + + +Mittelalterkalender | Mittelaltertermine und historische Feste in Deutschland und Europa - Historische Feste, Mittelaltermärkte und Fantasy-Festivals 2026 nach Datum + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+

Markt-Termine

+ + + + + +

2026

+ + + + + +

2027

+ + + + + +
+
+
+

Historische Feste, Mittelaltermärkte und Fantasy-Festivals 2026 nach Datum

+ +

Januar

+
+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
02.01.2026 bis 06.01.202601067Dresden
02.01.2026 bis 04.01.202663785Obernburg am Main
03.01.2026 bis 04.01.202647608Geldern
05.01.2026 bis 05.01.20265710Kaprun
30.01.2026 bis 30.01.202638836Huy-Dedeleben
31.01.2026 bis 01.02.202642489Wülfrath
+
+
+
+ +

+ +
+
 
+
+ +

Februar

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
01.02.2026 bis 01.02.202658455Witten
07.02.2026 bis 07.02.202638889Elbingerode (Harz)
20.02.2026 bis 20.02.202638836Huy-Dedeleben
26.02.2026 bis 01.03.202624103Kiel
27.02.2026 bis 01.03.202674321Bietigheim-Bissingen
27.02.2026 bis 01.03.202665239Hochheim-Massenheim
+
+
+
+ +

+

+
 
+
+ +

März

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
06.03.2026 bis 07.03.202625596Wacken
06.03.2026 bis 09.03.202663571Gelnhausen
06.03.2026 bis 08.03.202631785Hameln
07.03.2026 bis 08.03.202604107Leipzig
07.03.2026 bis 08.03.202666839Schmelz
08.03.2026 bis 08.03.202653894Mechernich-Satzvey
13.03.2026 bis 15.03.202668165Mannheim
13.03.2026 bis 13.03.202638836Huy-Dedeleben
13.03.2026 bis 15.03.202683131Nußdorf am Inn
14.03.2026 bis 15.03.202697424Schweinfurt
14.03.2026 bis 15.03.202607570Weida
14.03.2026 bis 15.03.202689561Dischingen-Katzenstein
14.03.2026 bis 15.03.202638667Bad Harzburg
14.03.2026 bis 14.03.202681825München
15.03.2026 bis 15.03.202627283Verden (Aller)
20.03.2026 bis 22.03.202672351Geislingen
20.03.2026 bis 22.03.20269630Wattwil
20.03.2026 bis 22.03.202617328Penkun
21.03.2026 bis 22.03.202659063Hamm
21.03.2026 bis 22.03.202699448Kranichfeld
21.03.2026 bis 21.03.202614641Nauen
21.03.2026 bis 22.03.202609405Zschopau
21.03.2026 bis 22.03.202664579Gernsheim
21.03.2026 bis 22.03.202663549Ronneburg
21.03.2026 bis 22.03.202697877Wertheim
27.03.2026 bis 29.03.202631020Salzhemmendorf
28.03.2026 bis 29.03.202614550Groß Kreutz (Havel)
28.03.2026 bis 29.03.202698631Römhild
28.03.2026 bis 29.03.202639435Egeln
28.03.2026 bis 28.03.202676185Karlsruhe
28.03.2026 bis 28.03.202604319Leipzig
28.03.2026 bis 29.03.202663549Ronneburg
28.03.2026 bis 29.03.202601683Nossen
28.03.2026 bis 29.03.202601979Lauchhammer
29.03.2026 bis 29.03.202691183Abenberg
+
+
+
+ +

+
+ + + +
+
+
 
+
+ +

April

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
01.04.2026 bis 06.04.202604109Leipzig
02.04.2026 bis 05.04.202649419Wagenfeld
03.04.2026 bis 06.04.202614823Rabenstein/Fläming
03.04.2026 bis 06.04.202630966Hemmingen
03.04.2026 bis 06.04.202624866Busdorf
03.04.2026 bis 06.04.202618249Warnow
03.04.2026 bis 06.04.202649186Bad Iburg
04.04.2026 bis 05.04.202617358Torgelow
04.04.2026 bis 06.04.202671263Weil der Stadt
04.04.2026 bis 06.04.202699448Kranichfeld
04.04.2026 bis 06.04.202667663Kaiserslautern
04.04.2026 bis 06.04.202647574Goch-Asperden
04.04.2026 bis 06.04.202635781Weilburg
04.04.2026 bis 06.04.202646286Dorsten
04.04.2026 bis 06.04.202656346St. Goarshausen
04.04.2026 bis 04.04.202638836Huy-Dedeleben
04.04.2026 bis 06.04.202617235Neustrelitz
04.04.2026 bis 06.04.202615859Storkow
04.04.2026 bis 06.04.202613599Berlin
04.04.2026 bis 06.04.202609106Chemnitz
04.04.2026 bis 06.04.202638889Blankenburg (Harz)
04.04.2026 bis 06.04.202601705Freital
04.04.2026 bis 06.04.2026 32423Minden
04.04.2026 bis 06.04.202636287Breitenbach am Herzberg
04.04.2026 bis 06.04.202623966Wismar
05.04.2026 bis 06.04.202689407Dillingen an der Donau
05.04.2026 bis 06.04.202656332Brodenbach
05.04.2026 bis 06.04.202653894Mechernich-Satzvey
06.04.2026 bis 06.04.202695671Bärnau
09.04.2026 bis 12.04.202634117Kassel
10.04.2026 bis 12.04.20263177Laupen
11.04.2026 bis 12.04.202621382Brietlingen
11.04.2026 bis 12.04.202634513Waldeck
11.04.2026 bis 12.04.202666953Pirmasens
11.04.2026 bis 12.04.202696178Pommersfelden
11.04.2026 bis 12.04.202635394Gießen
11.04.2026 bis 12.04.202658706Menden
11.04.2026 bis 12.04.202606258Schkopau
11.04.2026 bis 12.04.202697253Gaukönigshofen
11.04.2026 bis 12.04.202607929Saalburg-Ebersdorf
11.04.2026 bis 12.04.202604849Bad Düben
11.04.2026 bis 12.04.20262640Gloggnitz
12.04.2026 bis 12.04.202656332Brodenbach
18.04.2026 bis 19.04.202602829Königshain
18.04.2026 bis 19.04.202638122Braunschweig
18.04.2026 bis 19.04.202637218Witzenhausen
18.04.2026 bis 19.04.202616552Mühlenbecker Land
18.04.2026 bis 19.04.202674541Vellberg
18.04.2026 bis 19.04.202641844Wegberg
18.04.2026 bis 19.04.202614979Großbeeren-Diedersdorf
18.04.2026 bis 19.04.202695505Immenreuth
19.04.2026 bis 19.04.202656332Brodenbach
24.04.2026 bis 26.04.202678628Rottweil
24.04.2026 bis 26.06.202631303Burgdorf
24.04.2026 bis 25.04.202639343 Hohe Börde - Bebertal
24.04.2026 bis 26.04.20268610Uster
24.04.2026 bis 26.04.202601744Dippoldiswalde-Seifersdorf
24.04.2026 bis 25.04.202638378Warberg
24.04.2026 bis 26.04.202606869Coswig (Anhalt)
24.04.2026 bis 26.04.202695671Bärnau
25.04.2026 bis 26.04.202625355Barmstedt
25.04.2026 bis 26.04.202696268Mitwitz
25.04.2026 bis 26.04.202679713Bad Säckingen
25.04.2026 bis 26.04.202634414Warburg
25.04.2026 bis 26.04.202699880Hörsel-Neufrankenroda
25.04.2026 bis 25.04.202680687München
25.04.2026 bis 26.04.202649179Ostercappeln-Venne
25.04.2026 bis 26.04.202666333Völklingen-Geislautern
25.04.2026 bis 26.04.20264982Kirchdorf am Inn
25.04.2026 bis 26.04.202690513Zirndorf
25.04.2026 bis 26.04.202699631Weißensee
25.04.2026 bis 26.04.202613053Berlin
25.04.2026 bis 26.04.202604703Leisnig
26.04.2026 bis 26.04.202656332Brodenbach
30.04.2026 bis 02.05.202638879Wernigerode-Schierke
30.04.2026 bis 03.05.202648455Bad Bentheim
30.04.2026 bis 02.05.202615848Beeskow
30.04.2026 bis 03.05.202651588Nümbrecht
30.04.2026 bis 30.04.202629389Bad Bodenteich
30.04.2026 bis 30.04.202617217Penzlin
30.04.2026 bis 30.04.20263242Texingtal-Plankenstein
30.04.2026 bis 30.04.20265710Kaprun
30.04.2026 bis 30.04.202653894Mechernich-Satzvey
30.04.2026 bis 03.05.202699867Gotha
30.04.2026 bis 03.05.202615374Müncheberg
30.04.2026 bis 03.05.202627308Kirchlinteln
30.04.2026 bis 03.05.202663549Ronneburg
30.04.2026 bis 03.05.202606502Thale
+
+
+
+ +

+

Römer- und Germanentage im Museum und Park Kalkriese / Bramsche-Kalkriese

+
+
 
+
+ +

Mai

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
01.05.2026 bis 03.05.202678166Donaueschingen
01.05.2026 bis 03.05.20266240Rattenberg
01.05.2026 bis 03.05.202692543Guteneck
01.05.2026 bis 03.05.202631134Hildesheim
01.05.2026 bis 03.05.202622880Wedel
01.05.2026 bis 01.05.202629389Bad Bodenteich
01.05.2026 bis 03.05.202666629Freisen
01.05.2026 bis 03.05.202614641Nauen
01.05.2026 bis 03.05.202667659Kaiserslautern
01.05.2026 bis 03.05.202686476Neuburg a.d. Kammel
01.05.2026 bis 03.05.202698597Breitungen
01.05.2026 bis 03.05.202618106Rostock
01.05.2026 bis 03.05.202606632Freyburg (Unstrut)
01.05.2026 bis 03.05.202647447Moers
01.05.2026 bis 03.05.202649419Wagenfeld
01.05.2026 bis 03.05.202639387 Oschersleben (Bode)
01.05.2026 bis 01.05.202653894Mechernich-Satzvey
01.05.2026 bis 03.05.202697980Bad Mergentheim
01.05.2026 bis 01.05.202656332Brodenbach
01.05.2026 bis 03.05.202666849Landstuhl
01.05.2026 bis 03.05.202676593Gernsbach
01.05.2026 bis 03.05.202650769Köln
01.05.2026 bis 03.05.202606862Dessau-Roßlau
02.05.2026 bis 03.05.202629389Bad Bodenteich
02.05.2026 bis 03.05.202603096Dissen-Striesow
02.05.2026 bis 02.05.202681825München
02.05.2026 bis 03.05.20261220Wien
02.05.2026 bis 03.05.20263400Klosterneuburg
03.05.2026 bis 03.05.202691217Hersbruck
03.05.2026 bis 03.05.202656332Brodenbach
06.05.2026 bis 10.05.202656479Westernohe
08.05.2026 bis 10.05.20267251 AZVorden
08.05.2026 bis 10.05.20263112Allmendingen
08.05.2026 bis 10.05.202683233Bernau am Chiemsee
08.05.2026 bis 10.05.202684577Tüßling
08.05.2026 bis 10.05.202682024Taufkirchen
08.05.2026 bis 08.05.202638836Huy-Dedeleben
08.05.2026 bis 10.05.202691161Hilpoltstein
08.05.2026 bis 10.05.202626629Großefehn-Spetzerfehn
08.05.2026 bis 10.05.202669226Nußloch
08.05.2026 bis 10.05.202664678Lindenfels
08.05.2026 bis 10.05.202641849Wassenberg
09.05.2026 bis 10.05.202637574Einbeck-Salzderhelden
09.05.2026 bis 10.05.202601996Senftenberg
09.05.2026 bis 10.05.202618246Rühn
09.05.2026 bis 10.05.202609456Annaberg-Buchholz
09.05.2026 bis 10.05.202647546Kalkar
09.05.2026 bis 09.05.202652074Aachen
09.05.2026 bis 10.05.202604519Rackwitz
09.05.2026 bis 10.05.202665203Wiesbaden-Biebrich
09.05.2026 bis 10.05.202609669Frankenberg-Sachsenburg
09.05.2026 bis 10.05.202618442Niepars
09.05.2026 bis 10.05.202631535Neustadt am Rübenberge
09.05.2026 bis 10.05.202658455Witten
13.05.2026 bis 13.05.202602763Zittau
14.05.2026 bis 17.05.202626180Rastede
14.05.2026 bis 17.05.202624646Warder
14.05.2026 bis 17.05.202617248Rechlin
14.05.2026 bis 14.05.202656332Brodenbach
14.05.2026 bis 17.05.202603096Dissen-Striesow
14.05.2026 bis 14.05.202656077Koblenz
14.05.2026 bis 14.05.202642659Solingen
14.05.2026 bis 17.05.202624598Heidmühlen
14.05.2026 bis 17.05.20263242Texingtal-Plankenstein
14.05.2026 bis 17.05.202607545Gera
14.05.2026 bis 17.05.202639624Kalbe (Milde)
14.05.2026 bis 17.05.202689079Ulm
14.05.2026 bis 17.05.202635796 Weinbach
14.05.2026 bis 17.05.202646509Xanten
14.05.2026 bis 16.05.202651147Köln
14.05.2026 bis 17.05.202615345Altlandsberg
14.05.2026 bis 17.05.202649214Bad Rothenfelde
14.05.2026 bis 14.05.2026 34388Trendelburg
14.05.2026 bis 17.05.202609429Wolkenstein
14.05.2026 bis 17.05.202609648Kriebstein
14.05.2026 bis 17.05.202606543Falkenstein/Harz
14.05.2026 bis 17.05.202631008Elze
15.05.2026 bis 17.05.202663549Ronneburg
15.05.2026 bis 17.05.202633142 Büren
15.05.2026 bis 17.05.20268340Hinwil
15.05.2026 bis 17.05.202667549Worms
15.05.2026 bis 17.05.202692269Fensterbach
15.05.2026 bis 17.05.202686732Oettingen
16.05.2026 bis 17.05.202691459Markt Erlbach
16.05.2026 bis 17.05.202653894Mechernich-Satzvey
16.05.2026 bis 17.05.20262500Baden
16.05.2026 bis 17.05.202671297Mönsheim
16.05.2026 bis 17.05.202656077Koblenz
16.05.2026 bis 17.05.20262151Asparn an der Zaya
16.05.2026 bis 17.05.202646485Wesel
16.05.2026 bis 17.05.202666578Schiffweiler-Reden
21.05.2026 bis 25.05.202649419Wagenfeld
22.05.2026 bis 25.05.202689264Weißenhorn
22.05.2026 bis 25.05.202666346 Püttlingen
22.05.2026 bis 24.05.202699610Sömmerda
22.05.2026 bis 31.05.202626810Westoverledingen-Ihrhove
22.05.2026 bis 25.05.202604279Leipzig
22.05.2026 bis 25.05.202691541Rothenburg ob der Tauber
22.05.2026 bis 25.05.202677787Nordrach
23.05.2026 bis 25.05.202624594Hohenwestedt
23.05.2026 bis 25.05.202624802Emkendorf
23.05.2026 bis 25.05.202653894Mechernich-Satzvey
23.05.2026 bis 25.05.2026 88289Waldburg
23.05.2026 bis 25.05.202614469Potsdam
23.05.2026 bis 25.05.202619322Weisen-Schilde
23.05.2026 bis 25.05.202645479Mülheim an der Ruhr
23.05.2026 bis 25.05.202699885Ohrdruf
23.05.2026 bis 25.05.202627356Rotenburg (Wümme)
23.05.2026 bis 25.05.202686508Rehling
23.05.2026 bis 25.05.202695671Bärnau
23.05.2026 bis 25.05.202642659Solingen
23.05.2026 bis 25.05.202653557Bad Hönningen
23.05.2026 bis 25.05.202604626Posterstein
23.05.2026 bis 25.05.202638889Blankenburg (Harz)
23.05.2026 bis 25.05.202676661Philippsburg
23.05.2026 bis 25.05.202699831Amt Creuzburg
23.05.2026 bis 25.05.202604687Trebsen
23.05.2026 bis 25.05.202647809Krefeld
23.05.2026 bis 25.05.202663549Ronneburg
23.05.2026 bis 25.05.202685399Hallbergmoos
23.05.2026 bis 25.05.202659348Pattensen
23.05.2026 bis 25.05.20264794Kopfing
23.05.2026 bis 25.05.202616515Oranienburg
23.05.2026 bis 24.05.202624321Lütjenburg
23.05.2026 bis 25.05.202689561Dischingen-Katzenstein
24.05.2026 bis 25.05.202656332Brodenbach
24.05.2026 bis 25.05.202624980Wallsbüll
24.05.2026 bis 25.05.20268301Kainbach-Hönigtal
24.05.2026 bis 25.05.202649565Bramsche-Kalkriese
29.05.2026 bis 31.05.202694327Bogen
29.05.2026 bis 31.05.202691583Schillingsfürst
29.05.2026 bis 31.05.202619306Neustadt-Glewe
29.05.2026 bis 31.05.202627777Ganderkesee
29.05.2026 bis 31.05.202684424Isen
30.05.2026 bis 31.05.20268064Volketswil
30.05.2026 bis 31.05.202640878Ratingen
30.05.2026 bis 31.05.20262000Neuchâtel
30.05.2026 bis 31.05.202664354Reinheim
30.05.2026 bis 31.05.202614806Bad Belzig
30.05.2026 bis 31.05.202632791Lage-Heiden
30.05.2026 bis 31.05.202634346Hann. Münden
30.05.2026 bis 31.05.202663500Seligenstadt
31.05.2026 bis 31.05.202667466Esthal-Erfenstein
31.05.2026 bis 31.05.202656332Brodenbach
+
+
+
+ +

+

Römer- und Germanentage im Museum und Park Kalkriese / Bramsche-Kalkriese

+
+
 
+
+ +

Juni

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
04.06.2026 bis 07.06.202691586Lichtenau
04.06.2026 bis 07.06.202686655Harburg
04.06.2026 bis 04.06.202656332Brodenbach
04.06.2026 bis 07.06.202659071Hamm
04.06.2026 bis 07.06.202645731Waltrop
04.06.2026 bis 07.06.202649419Wagenfeld
04.06.2026 bis 06.06.202687509Immenstadt i.Allgäu
04.06.2026 bis 07.06.202668766Hockenheim
04.06.2026 bis 07.06.202695491Ahorntal
04.06.2026 bis 07.06.202633334Gütersloh-Isselhorst
04.06.2026 bis 07.06.202694496Ortenburg
05.06.2026 bis 07.06.202606406Bernburg (Saale)
06.06.2026 bis 07.06.202691239Henfenfeld
06.06.2026 bis 07.06.202675323Bad Wildbad
06.06.2026 bis 07.06.202656459Rotenhain
06.06.2026 bis 07.06.20262151Asparn an der Zaya
06.06.2026 bis 07.06.20269545 PABourtange
06.06.2026 bis 07.06.202624326Ascheberg (Holstein)
06.06.2026 bis 07.06.202655232Alzey
06.06.2026 bis 07.06.20262344Maria Enzersdorf
07.06.2026 bis 07.06.202656332Brodenbach
11.06.2026 bis 13.06.20263800 Interlaken
12.06.2026 bis 14.06.202678532Tuttlingen
12.06.2026 bis 14.06.2026524 31Herrljunga
12.06.2026 bis 14.06.202686551Aichach
12.06.2026 bis 14.06.202697215Uffenheim
12.06.2026 bis 14.06.202696528Schalkau
12.06.2026 bis 14.06.202632805Horn-Bad Meinberg
12.06.2026 bis 14.06.202609573Augustusburg
12.06.2026 bis 14.06.202606886Lutherstadt Wittenberg
12.06.2026 bis 14.06.202622119Hamburg
12.06.2026 bis 13.06.202691183Abenberg
12.06.2026 bis 14.06.202639576Stendal
13.06.2026 bis 14.06.202638531 Rötgesbüttel
13.06.2026 bis 14.06.202631629Estorf
13.06.2026 bis 14.06.202656410Montabaur
13.06.2026 bis 14.06.202639340Haldensleben
13.06.2026 bis 14.06.202677160Provins
13.06.2026 bis 14.06.20268706Useldange
13.06.2026 bis 14.06.202621224Rosengarten
14.06.2026 bis 14.06.202656332Brodenbach
19.06.2026 bis 21.06.202601069Dresden
19.06.2026 bis 21.06.20264434Hölstein
19.06.2026 bis 21.06.202695671Bärnau
19.06.2026 bis 21.06.202649504Lotte-Büren
19.06.2026 bis 21.06.202609471Bärenstein
19.06.2026 bis 21.06.202685567Grafing
19.06.2026 bis 20.09.20262361Laxenburg
19.06.2026 bis 21.06.202635759Driedorf
19.06.2026 bis 21.06.202699084Erfurt
20.06.2026 bis 21.06.202638229Salzgitter-Gebhardshagen
20.06.2026 bis 21.06.202656368Katzenelnbogen
20.06.2026 bis 21.06.202645899Gelsenkirchen
20.06.2026 bis 21.06.202638229Salzgitter-Gebhardshagen
20.06.2026 bis 21.06.20263812Groß-Siegharts
20.06.2026 bis 20.06.202613467Berlin
20.06.2026 bis 21.06.202619336Plattenburg
20.06.2026 bis 21.06.20268605Kapfenberg
20.06.2026 bis 21.06.202625889Witzwort
20.06.2026 bis 21.06.202617493Greifswald
21.06.2026 bis 21.06.202664739Höchst im Odenwald
21.06.2026 bis 21.06.202658091Hagen
24.06.2026 bis 28.06.202649419Wagenfeld
26.06.2026 bis 28.06.202671334Waiblingen
26.06.2026 bis 28.06.202691550Dinkelsbühl
26.06.2026 bis 27.06.202639356Walbeck
26.06.2026 bis 28.06.202683410Laufen an der Salzach
27.06.2026 bis 28.06.202640764Langenfeld
27.06.2026 bis 28.06.20264982Kirchdorf am Inn
27.06.2026 bis 28.06.202635279Neustadt (Hessen)
27.06.2026 bis 28.06.202648149Münster
27.06.2026 bis 28.06.202673450Neresheim
27.06.2026 bis 28.06.202659192Bergkamen
27.06.2026 bis 28.06.202655276Oppenheim
28.06.2026 bis 28.06.202656332Brodenbach
28.06.2026 bis 28.06.202653894Mechernich-Satzvey
+
+
+
+ +

+ +
+
 
+
+ +

Juli

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
02.07.2026 bis 05.07.202626382Wilhelmshaven
03.07.2026 bis 05.07.202604509Delitzsch
03.07.2026 bis 05.07.20265800Nyborg
03.07.2026 bis 05.07.202691438Bad Windsheim
03.07.2026 bis 05.07.202687629Füssen
03.07.2026 bis 05.07.202688499Riedlingen
03.07.2026 bis 05.07.202615517Fürstenwalde/Spree
03.07.2026 bis 05.07.202683536 Gars am Inn
03.07.2026 bis 05.07.202616792Zehdenick
03.07.2026 bis 06.07.202675015Bretten
03.07.2026 bis 05.07.202667655Kaiserslautern
04.07.2026 bis 05.07.202617235Neustrelitz
04.07.2026 bis 05.07.20263592Röhrenbach
04.07.2026 bis 05.07.202603096Dissen-Striesow
04.07.2026 bis 04.07.202614641Nauen
04.07.2026 bis 05.07.20265000Namur
04.07.2026 bis 05.07.202666333Völklingen
04.07.2026 bis 12.07.202609669Frankenberg-Sachsenburg
04.07.2026 bis 05.07.202615848Beeskow
04.07.2026 bis 05.07.202623812Wahlstedt
04.07.2026 bis 05.07.202624848 Kropp
04.07.2026 bis 05.07.202615569Woltersdorf
04.07.2026 bis 05.07.202633154Salzkotten
04.07.2026 bis 05.07.202695671Bärnau
05.07.2026 bis 05.07.202656332Brodenbach
05.07.2026 bis 05.07.202678662Bösingen
09.07.2026 bis 12.07.202624866Busdorf
10.07.2026 bis 12.07.202672762Reutlingen
10.07.2026 bis 12.07.202682269Geltendorf-Kaltenberg
10.07.2026 bis 12.07.202663868Großwallstadt
10.07.2026 bis 12.07.202618556Putgarten
10.07.2026 bis 12.07.202638364Schöningen
10.07.2026 bis 12.07.202617192Waren (Müritz)
10.07.2026 bis 12.07.202637671 Höxter
10.07.2026 bis 12.07.202666399Mandelbachtal
11.07.2026 bis 12.07.202699897Tambach-Dietharz
11.07.2026 bis 12.07.202621423Drage-Stove
11.07.2026 bis 12.07.202624972Steinberg-Norgaardholz
11.07.2026 bis 12.07.202677704 Oberkirch
11.07.2026 bis 12.07.202665589Hadamar
11.07.2026 bis 12.07.202624619Bornhöved
12.07.2026 bis 12.07.202656332Brodenbach
12.07.2026 bis 13.07.202639393Völpke
17.07.2026 bis 19.07.202682269Geltendorf-Kaltenberg
17.07.2026 bis 19.07.202687477Sulzberg
17.07.2026 bis 19.07.202694065Waldkirchen
17.07.2026 bis 19.07.202618574Garz/Rügen
17.07.2026 bis 19.07.202624369Waabs
17.07.2026 bis 19.07.202623999Insel Poel
17.07.2026 bis 19.07.20265710Kaprun
18.07.2026 bis 19.07.202674245Löwenstein
18.07.2026 bis 19.07.202666625Nohfelden
18.07.2026 bis 19.07.202666954Pirmasens
18.07.2026 bis 19.07.202698669Veilsdorf
18.07.2026 bis 19.07.202623758Oldenburg in Holstein
18.07.2026 bis 19.07.202667141Neuhofen
18.07.2026 bis 19.07.202634454Bad Arolsen
18.07.2026 bis 19.07.202607985Elsterberg
18.07.2026 bis 20.07.20266065 AXMontfort
19.07.2026 bis 19.07.202656332Brodenbach
23.07.2026 bis 26.07.202618439Stralsund
24.07.2026 bis 26.07.202682269Geltendorf-Kaltenberg
24.07.2026 bis 26.07.202633602Bielefeld
24.07.2026 bis 26.07.202686989Steingaden
24.07.2026 bis 26.07.202627211Bassum
24.07.2026 bis 26.07.202682065Baierbrunn
25.07.2026 bis 02.08.20269401Vianden
25.07.2026 bis 26.07.202631675Bückeburg
25.07.2026 bis 26.07.20269360Friesach
25.07.2026 bis 26.07.202696231Bad Staffelstein
25.07.2026 bis 26.07.202624321Lütjenburg
25.07.2026 bis 25.07.202626629Großefehn-Spetzerfehn
26.07.2026 bis 26.07.202656332Brodenbach
28.07.2026 bis 02.08.202634513Waldeck
30.07.2026 bis 02.08.20262161 ANLisse
30.07.2026 bis 02.08.202626506Norden-Norddeich
31.07.2026 bis 02.08.202648653 Coesfeld
31.07.2026 bis 02.08.202688630Pfullendorf
31.07.2026 bis 02.08.20269450Altstätten
31.07.2026 bis 02.08.202687629Füssen
31.07.2026 bis 03.08.202691161Hilpoltstein
31.07.2026 bis 02.08.202695491Ahorntal
31.07.2026 bis 02.08.202684416Taufkirchen (Vils)
31.07.2026 bis 02.08.202692431Neunburg vorm Wald
31.07.2026 bis 02.08.202689168 Niederstotzingen-Stetten
+
+
+
+ +

+ +
+
 
+
+
+ + + +

August

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
01.08.2026 bis 02.08.202661440Oberursel (Taunus)
01.08.2026 bis 02.08.202631675Bückeburg
01.08.2026 bis 02.08.20263532Rastenfeld
01.08.2026 bis 02.08.202679871Eisenbach
01.08.2026 bis 02.08.202666706Perl
01.08.2026 bis 02.08.202676332Bad Herrenalb
01.08.2026 bis 09.08.202626553Dornum
01.08.2026 bis 01.08.202616303Schwedt OT Passow
02.08.2026 bis 02.08.202607768Orlamünde
02.08.2026 bis 02.08.202656332Brodenbach
06.08.2026 bis 08.08.202631628 Landesbergen
07.08.2026 bis 09.08.202692224Amberg
07.08.2026 bis 09.08.202638376Süpplingenburg
07.08.2026 bis 09.08.202648291Telgte
07.08.2026 bis 09.08.202683714Miesbach
07.08.2026 bis 09.08.202634613Schwalmstadt-Treysa
08.08.2026 bis 09.08.20264204Reichenau im Mühlkreis
08.08.2026 bis 09.08.202641189Mönchengladbach
08.08.2026 bis 09.08.202601833Stolpen
08.08.2026 bis 09.08.202695671Bärnau
08.08.2026 bis 09.08.202674918Angelbachtal
08.08.2026 bis 09.08.202608115Lichtentanne-Schönfels
08.08.2026 bis 09.08.202617094Burg Stargard
08.08.2026 bis 09.08.202653840Troisdorf
08.08.2026 bis 09.08.202637115Duderstadt
09.08.2026 bis 09.08.202656332Brodenbach
12.08.2026 bis 16.08.202627568Bremerhaven
14.08.2026 bis 16.08.202634560Fritzlar
14.08.2026 bis 16.08.202634560Fritzlar
14.08.2026 bis 16.08.202649082Osnabrück
14.08.2026 bis 16.08.202679576Weil am Rhein
14.08.2026 bis 16.08.20269675 HHWinschoten
14.08.2026 bis 16.08.202693437Furth im Wald
15.08.2026 bis 16.08.202624980Wallsbüll
15.08.2026 bis 16.08.202619243Wittenburg
15.08.2026 bis 16.08.202696145Seßlach
16.08.2026 bis 16.08.202656332Brodenbach
16.08.2026 bis 17.08.202642659Solingen
20.08.2026 bis 23.08.202623966Wismar
21.08.2026 bis 23.08.202682256Fürstenfeldbruck
21.08.2026 bis 23.08.202617217Penzlin
21.08.2026 bis 23.08.202693158Teublitz
21.08.2026 bis 23.08.202635390Grand-Fougeray
21.08.2026 bis 23.08.20261230Wien
21.08.2026 bis 23.08.202659348Lüdinghausen
21.08.2026 bis 23.08.202629313Hambühren
21.08.2026 bis 23.08.202649152Bad Essen
21.08.2026 bis 23.08.202667167Erpolzheim
21.08.2026 bis 23.08.202639020Schluderns
22.08.2026 bis 23.08.202664579Gernsheim
22.08.2026 bis 23.08.202606268Querfurt
22.08.2026 bis 23.08.202667346Speyer
22.08.2026 bis 23.08.202634466Wolfhagen
23.08.2026 bis 23.08.202656332Brodenbach
27.08.2026 bis 30.08.202649419Wagenfeld
28.08.2026 bis 30.08.202635619Braunfels
28.08.2026 bis 30.08.202627432Bremervörde
28.08.2026 bis 30.08.20268610Uster
28.08.2026 bis 30.08.202689423Gundelfingen
28.08.2026 bis 29.08.20268700Horsens
28.08.2026 bis 30.08.202682256Fürstenfeldbruck
28.08.2026 bis 30.08.202632369Rahden
29.08.2026 bis 29.08.202699768Harztor-Neustadt
29.08.2026 bis 30.08.202645721Haltern am See
29.08.2026 bis 30.08.202692690Pressath
29.08.2026 bis 29.08.202614641Nauen
29.08.2026 bis 30.08.202609669Frankenberg-Sachsenburg
29.08.2026 bis 30.08.202630629Hannover
29.08.2026 bis 30.08.20262151Asparn an der Zaya
29.08.2026 bis 30.08.20263629Kiesen
30.08.2026 bis 30.08.202656332Brodenbach
31.08.2026 bis 02.08.202666629Freisen
+
+
+
+ +

+

+
 
+
+ +

September

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
04.09.2026 bis 05.09.202604523Pegau
04.09.2026 bis 06.09.202691541Rothenburg ob der Tauber
04.09.2026 bis 06.09.202644629Herne
04.09.2026 bis 06.09.202636199Rotenburg a. d. Fulda
04.09.2026 bis 06.09.202663549Ronneburg
04.09.2026 bis 06.09.202621394Salzhausen-Luhmühlen
04.09.2026 bis 06.09.20265613Hilfikon
04.09.2026 bis 06.09.202689522Heidenheim an der Brenz
04.09.2026 bis 06.09.202606679Hohenmölsen
04.09.2026 bis 06.09.202689561Dischingen-Katzenstein
04.09.2026 bis 06.09.202689075Ulm
05.09.2026 bis 06.09.202667454Haßloch
05.09.2026 bis 06.09.202683236Übersee
05.09.2026 bis 07.09.202668152Ribeauvillé
05.09.2026 bis 06.09.202655568Staudernheim
05.09.2026 bis 06.09.202601458Ottendorf-Okrilla
05.09.2026 bis 05.09.20269470Werdenberg
05.09.2026 bis 06.09.202621335Lüneburg
05.09.2026 bis 06.09.20264352Sperken
05.09.2026 bis 06.09.202626632Ihlow
05.09.2026 bis 06.09.202608499Reichenbach-Mylau
05.09.2026 bis 06.09.202663594Hasselroth
05.09.2026 bis 06.09.202655756Herrstein
05.09.2026 bis 06.09.202657537Wissen
06.09.2026 bis 06.09.202695671Bärnau
06.09.2026 bis 06.09.202679312Emmendingen
06.09.2026 bis 06.09.202656332Brodenbach
11.09.2026 bis 13.09.202601945Hohenbocka
11.09.2026 bis 13.09.202695100Selb
11.09.2026 bis 13.09.202636358Herbstein
11.09.2026 bis 11.09.202638836Huy-Dedeleben
11.09.2026 bis 13.09.202672488Sigmaringen
11.09.2026 bis 13.09.202630890Barsinghausen
11.09.2026 bis 13.09.202649740Haselünne
11.09.2026 bis 13.09.202663303Dreieich-Dreieichenhain
11.09.2026 bis 13.09.202629649Wietzendorf
12.09.2026 bis 13.09.202685467Neuching
12.09.2026 bis 13.09.202633609Bielefeld
12.09.2026 bis 13.09.202653894Mechernich-Satzvey
12.09.2026 bis 13.09.20263730Eggenburg
12.09.2026 bis 13.09.20263428Düdelingen
12.09.2026 bis 13.09.202614979Großbeeren-Diedersdorf
12.09.2026 bis 13.09.202601848Hohnstein
12.09.2026 bis 13.09.202649326Melle
12.09.2026 bis 13.09.202608056Zwickau
12.09.2026 bis 13.09.202638871Ilsenburg (Harz)
13.09.2026 bis 13.09.202656332Brodenbach
13.09.2026 bis 13.09.202645731Waltrop
18.09.2026 bis 20.09.202653489Sinzig
18.09.2026 bis 20.09.20262560Nidau
18.09.2026 bis 20.09.202655583Bad Münster am Stein-Ebernburg
18.09.2026 bis 20.09.202666839Schmelz
18.09.2026 bis 20.09.202688483Burgrieden
18.09.2026 bis 20.09.202676593Gernsbach
19.09.2026 bis 20.09.202667705Trippstadt
19.09.2026 bis 20.09.20266300Zug
19.09.2026 bis 20.09.202646509Xanten
19.09.2026 bis 20.09.202637154Northeim
19.09.2026 bis 20.09.202693186Pettendorf-Adlersberg
19.09.2026 bis 20.09.202697688Bad Kissingen
19.09.2026 bis 20.09.202653894Mechernich-Satzvey
19.09.2026 bis 20.09.202627318Hoya
19.09.2026 bis 20.09.202674199Untergruppenbach
19.09.2026 bis 20.09.202627612Loxstedt
19.09.2026 bis 20.09.20268385Koerich
19.09.2026 bis 20.09.202639319Jerichow
19.09.2026 bis 20.09.202676467Bietigheim
19.09.2026 bis 20.09.202647574Goch-Asperden
19.09.2026 bis 20.09.202659192Bergkamen
19.09.2026 bis 20.09.202633175Bad Lippspringe
20.09.2026 bis 20.09.202656332Brodenbach
20.09.2026 bis 20.09.202601809Müglitztal
25.09.2026 bis 04.10.202604109Leipzig
25.09.2026 bis 27.09.202679780Stühlingen
25.09.2026 bis 27.09.202699510Apolda
25.09.2026 bis 27.09.20268608Bubikon
26.09.2026 bis 27.09.202649377Vechta
26.09.2026 bis 27.09.202675378Bad Liebenzell
26.09.2026 bis 27.09.202636214Nenterhausen
26.09.2026 bis 27.09.202669120Heidelberg
26.09.2026 bis 27.09.202663549Ronneburg
26.09.2026 bis 27.09.202612359Berlin
26.09.2026 bis 27.09.202671723Großbottwar
26.09.2026 bis 27.09.202646325Borken
26.09.2026 bis 27.09.20262361Laxenburg
26.09.2026 bis 27.09.202604600Altenburg
26.09.2026 bis 27.09.202621643 Beckdorf
26.09.2026 bis 27.09.202637218Witzenhausen
26.09.2026 bis 27.09.202646236Bottrop
27.09.2026 bis 27.09.202656332Brodenbach
+
+
+
+ +

+
+ + + + +
+
 
+
+ +

Oktober

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
01.10.2026 bis 04.10.202649419Wagenfeld
02.10.2026 bis 04.10.202667663Kaiserslautern
02.10.2026 bis 04.10.202672280Dornstetten
02.10.2026 bis 04.10.202604860Torgau
02.10.2026 bis 04.10.202645721Haltern am See
02.10.2026 bis 04.10.202691438Bad Windsheim
02.10.2026 bis 04.10.202638690Goslar
02.10.2026 bis 04.10.20266060Hall in Tirol
02.10.2026 bis 04.10.202649536Lienen
02.10.2026 bis 04.10.202685567Grafing
02.10.2026 bis 04.10.20263014Bern
03.10.2026 bis 04.10202656346St. Goarshausen
03.10.2026 bis 04.10.202697239Aub
03.10.2026 bis 04.10.202603096Dissen-Striesow
03.10.2026 bis 04.10.202699448Kranichfeld
03.10.2026 bis 04.10.202663549Ronneburg
03.10.2026 bis 04.10.202614469Potsdam
03.10.2026 bis 04.10.20263573Rosenburg
03.10.2026 bis 04.10.202656332Brodenbach
03.10.2026 bis 04.10.202613599Berlin
03.10.2026 bis 04.10.202609106Chemnitz
09.10.2026 bis 11.10.202699423Weimar
09.10.2026 bis 11.10.20268400Winterthur
09.10.2026 bis 11.10.202672459Albstadt
09.10.2026 bis 11.10.202645661Recklinghausen
09.10.2026 bis 11.10.202688709Meersburg
10.10.2026 bis 11.10.202663150Heusenstamm
10.10.2026 bis 11.10.20263573Rosenburg
10.10.2026 bis 11.10.202691334Hemhofen
10.10.2026 bis 10.10.20262151Asparn an der Zaya
10.10.2026 bis 11.10.202635781Weilburg
10.10.2026 bis 11.10.202615848 Beeskow
11.10.2026 bis 11.10.202656332Brodenbach
16.10.2026 bis 18.10.202636396Steinau an der Straße
16.10.2026 bis 18.10.202691555Feuchtwagen
16.10.2026 bis 16.10.202638836Huy-Dedeleben
16.10.2026 bis 01.11.202628195Bremen
17.10.2026 bis 18.10.202666578Schiffweiler-Reden
17.10.2026 bis 18.10.202644575Castrop-Rauxel
17.10.2026 bis 18.10.202617235Neustrelitz
17.10.2026 bis 18.10.202657258Freudenberg
18.10.2026 bis 18.10.202656332Brodenbach
23.10.2026 bis 25.10.202646483Wesel
23.10.2026 bis 25.10.202690513Zirndorf
23.10.2026 bis 25.10.20265620Bremgarten
23.10.2026 bis 23.10.202685540Haar
24.10.2026 bis 25.10.20265533Remich
24.10.2026 bis 25.10.202624866Busdorf
24.10.2026 bis 24.10.202653894Mechernich-Satzvey
24.10.2026 bis 24.10.2026 81825München
24.10.2026 bis 25.10.202685540Haar
25.10.2026 bis 25.10.202656332Brodenbach
29.10.2026 bis 01.11.202649419Wagenfeld
30.10.2026 bis 01.11.202633129Delbrück-Schöning
30.10.2026 bis 1.11.20268005Zürich
30.10.2026 bis 01.11.202699448Kranichfeld
31.10.2026 bis 31.10.202606886Lutherstadt Wittenberg
31.10.2026 bis 31.10.202653894Mechernich-Satzvey
31.10.2026 bis 01.11.202656332Brodenbach
31.10.2026 bis 01.11.20269545 PABourtange
+
+
+
+ +

+

+
 
+
+ +

November

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
06.11.2026 bis 08.11.202619055Schwerin
06.11.2026 bis 08.11.202629221Celle
07.11.2026 bis 08.11.202674821Mosbach
07.11.2026 bis 07.11.202653894Mechernich-Satzvey
07.11.2026 bis 08.11.20262151Asparn an der Zaya
08.11.2026 bis 08.11.202653894Mechernich-Satzvey
12.11.2026 bis 22.12.202610245Berlin
13.11.2026 bis 13.11.202638836Huy-Dedeleben
13.11.2026 bis 23.12.202645127Essen
13.11.2026 bis 13.11.202653424Remagen
14.11.2026 bis 15.11.20264352Sperken
20.11.2026 bis 22.11.20268248Laufen am Rheinfall
20.11.2026 bis 22.11.20268447Dachsen
21.11.2026 bis 21.11.202689168 Niederstotzingen-Stetten
23.11.2026 bis 30.11.202675175Pforzheim
23.11.2026 bis 23.12.202628199Bremen
24.11.2026 bis 22.12.202604109Leipzig
25.11.2026 bis 23.12.202601067Dresden
26.11.2026 bis 30.12.202604229Leipzig
27.11.2026 bis 29.11.202644629Herne
27.11.2026 bis 23.12.202609111Chemnitz
27.11.2026 bis 29.11.202612359Berlin
28.11.2026 bis 29.11.202653894Mechernich-Satzvey
28.11.2026 bis 29.11.202663549Ronneburg
28.11.2026 bis 29.11.202604687Trebsen
28.11.2026 bis 28.11.202624321Lütjenburg
29.11.2026 bis 29.11.202695671Bärnau
+
+
+
+ +

+EXKURSIA ist ein kostenloser Dienst, der Ausflugsziele und Freizeittipps in Deutschland auflistet.
+
 
+
+ +

Dezember

+ +

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BeginnEndeTitelPLZOrtDetails
04.12.2026 bis 06.12.202671263Weil der Stadt
04.12.2026 bis 06.12.202683301Traunreut
04.12.2026 bis 06.12.202612359Berlin
04.12.2026 bis 06.12.202644629Herne
05.12.2026 bis 06.12.202604687Trebsen
05.12.2026 bis 06.12.202645479Mülheim an der Ruhr
05.12.2026 bis 06.12.202653894Mechernich-Satzvey
05.12.2026 bis 06.12.202663549Ronneburg
05.12.2026 bis 06.12.202621335Lüneburg
11.12.2026 bis 13.12.202678050Villingen-Schwenningen
11.12.2026 bis 13.12.202633334Gütersloh-Isselhorst
11.12.2026 bis 11.12.202638836Huy-Dedeleben
11.12.2026 bis 13.12.202612359Berlin
11.12.2026 bis 13.12.202644629Herne
12.12.2026 bis 13.12.202604687Trebsen
12.12.2026 bis 13.12.202672401Haigerloch
12.12.2026 bis 13.12.202645479Mülheim an der Ruhr
12.12.2026 bis 13.12.202653894Mechernich-Satzvey
12.12.2026 bis 13.12.202663549Ronneburg
12.12.2026 bis 12.12.202604279Leipzig
12.12.2026 bis 13.12.202629413Diesdorf
12.12.2026 bis 13.12.20262344Maria Enzersdorf
12.12.2026 bis 13.12.202699831Amt Creuzburg
18.12.2026 bis 20.12.202649525Lengerich
18.12.2026 bis 20.12.202612359Berlin
18.12.2026 bis 20.12.202644629Herne
19.12.2026 bis 20.12.202601996Senftenberg
19.12.2026 bis 20.12.202604687Trebsen
19.12.2026 bis 20.12.202634414Warburg
19.12.2026 bis 20.12.202645479Mülheim an der Ruhr
19.12.2026 bis 20.12.202653894Mechernich-Satzvey
19.12.2026 bis 20.12.202664579Gernsheim
27.12.2026 bis 05.01.202788289Waldburg
27.12.2026 bis 30.12.202601067Dresden
30.12.2026 bis 31.12.202618586Ostseebad Göhren
+
+
+
+ +

+
+ +
+
+ + + + + + + + + + + diff --git a/backend/internal/domain/discovery/crawler/testdata/mittelaltermarkt_online_page1.json b/backend/internal/domain/discovery/crawler/testdata/mittelaltermarkt_online_page1.json new file mode 100644 index 0000000..944229e --- /dev/null +++ b/backend/internal/domain/discovery/crawler/testdata/mittelaltermarkt_online_page1.json @@ -0,0 +1 @@ +{"events":[{"id":9562,"global_id":"mittelaltermarkt.online?id=9562","global_id_lineage":["mittelaltermarkt.online?id=9562"],"author":"1","status":"publish","date":"2025-11-13 11:01:49","date_utc":"2025-11-13 10:01:49","modified":"2025-12-11 00:24:39","modified_utc":"2025-12-10 23:24:39","url":"https:\/\/mittelaltermarkt.online\/event\/3-eisenbacher-rauhnachtsmarkt-in-der-knechtsmuehle-by-2026\/","rest_url":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/events\/9562","title":"3. Eisenbacher Rauhnachtsmarkt in der Knechtsm\u00fchle (BY) 2026","description":"

Vom 02. bis 04. Januar 2026<\/strong> verwandelt sich die Knechtsm\u00fchle<\/strong> in Obernburg am Main<\/strong>, Bayern, erneut in einen besonderen Ort voller Mystik und Winterzauber: Der 3. Eisenbacher Rauhnachtsmarkt<\/strong> l\u00e4dt Besucher ein, das neue Jahr mit einer Prise Magie und viel Gem\u00fctlichkeit zu begr\u00fc\u00dfen.<\/p>\n


\n

\u2728 Was erwartet dich?<\/h3>\n

Direkt nach den Feiertagen bietet der Markt in und um die alte Getreidem\u00fchle eine zauberhafte Auszeit. Das Ambiente der Knechtsm\u00fchle<\/strong>, mit ihren historischen Mauern, schafft die perfekte Kulisse f\u00fcr die stimmungsvollen Rauhn\u00e4chte<\/strong> \u2013 jene sagenumwobene Zeit zwischen den Jahren, in der die Grenzen zur Anderswelt d\u00fcnner erscheinen.<\/p>\n

\ud83c\udf2c\ufe0f Gl\u00fchwein, Met und Punsch<\/strong> sorgen f\u00fcr wohlige W\u00e4rme, w\u00e4hrend mittelalterliche Kl\u00e4nge und Folk-Rock-Musik<\/strong> eine einzigartige Atmosph\u00e4re schaffen. Traditionelle Br\u00e4uche<\/strong> runden das Erlebnis ab.<\/p>\n

An den Marktst\u00e4nden findest du handgemachte Sch\u00e4tze<\/strong>, winterliche Spezialit\u00e4ten und vielleicht sogar den ein oder anderen Gl\u00fccksbringer f\u00fcrs neue Jahr.<\/p>\n


\n

\ud83d\udccd Ort des Geschehens: Die Knechtsm\u00fchle<\/h3>\n

Die Knechtsm\u00fchle<\/strong>, eine ehemalige Getreidem\u00fchle, liegt idyllisch zwischen Feldern und Wald nahe Eisenbach, einem Ortsteil von Obernburg. Der Ort bietet genau das richtige Ma\u00df an Abgeschiedenheit und Charme f\u00fcr eine Veranstaltung dieser Art. Hier scheint die Zeit ein wenig langsamer zu laufen \u2013 ideal, um die Rauhn\u00e4chte bewusst zu erleben.<\/p>\n


\n

\ud83c\udfd9\ufe0f \u00dcber Obernburg am Main<\/h3>\n

Obernburg am Main<\/strong>, gelegen im unterfr\u00e4nkischen Landkreis Miltenberg, hat rund 8.500 Einwohner<\/strong>. Die Stadt blickt auf eine r\u00f6mische Vergangenheit zur\u00fcck \u2013 das R\u00f6mermuseum zeugt noch heute davon. Dank der N\u00e4he zur A3 und B469 ist Obernburg gut erreichbar, ob aus Richtung W\u00fcrzburg, Aschaffenburg oder Frankfurt.<\/p>\n


\n

\ud83d\udcdc Programm<\/h3>\n

\"3.<\/a><\/p>\n

\"3.<\/a><\/p>\n

\"3.<\/a><\/p>\n

\"3.<\/a><\/p>\n


\n

\ud83d\udd17 Weitere Infos und \u00e4hnliche Events:<\/h3>\n

\ud83d\udc49 Veranstaltungen im Januar 2026<\/a>
\n\ud83d\udc49
Mittelalterm\u00e4rkte in Bayern<\/a>
\n\ud83d\udc49
Mittelalterm\u00e4rkte in Deutschland<\/a><\/p>","excerpt":"","slug":"3-eisenbacher-rauhnachtsmarkt-in-der-knechtsmuehle-by-2026","image":{"url":"https:\/\/mittelaltermarkt.online\/wp-content\/uploads\/2025\/11\/3.-Eisenbacher-Rauhnachtsmarkt-in-der-Knechtsmuehle-BY-2026.jpeg","id":9563,"extension":"jpeg","width":1024,"height":1024,"filesize":243455,"sizes":{"medium":{"width":300,"height":300,"mime-type":"image\/jpeg","filesize":29596,"url":"https:\/\/mittelaltermarkt.online\/wp-content\/uploads\/2025\/11\/3.-Eisenbacher-Rauhnachtsmarkt-in-der-Knechtsmuehle-BY-2026-300x300.jpeg"},"thumbnail":{"width":150,"height":150,"mime-type":"image\/jpeg","filesize":9844,"url":"https:\/\/mittelaltermarkt.online\/wp-content\/uploads\/2025\/11\/3.-Eisenbacher-Rauhnachtsmarkt-in-der-Knechtsmuehle-BY-2026-150x150.jpeg"},"medium_large":{"width":768,"height":768,"mime-type":"image\/jpeg","filesize":130564,"url":"https:\/\/mittelaltermarkt.online\/wp-content\/uploads\/2025\/11\/3.-Eisenbacher-Rauhnachtsmarkt-in-der-Knechtsmuehle-BY-2026-768x768.jpeg"}}},"all_day":true,"start_date":"2026-01-02 00:00:00","start_date_details":{"year":"2026","month":"01","day":"02","hour":"00","minutes":"00","seconds":"00"},"end_date":"2026-01-04 23:59:59","end_date_details":{"year":"2026","month":"01","day":"04","hour":"23","minutes":"59","seconds":"59"},"utc_start_date":"2026-01-01 23:00:00","utc_start_date_details":{"year":"2026","month":"01","day":"01","hour":"23","minutes":"00","seconds":"00"},"utc_end_date":"2026-01-04 22:59:59","utc_end_date_details":{"year":"2026","month":"01","day":"04","hour":"22","minutes":"59","seconds":"59"},"timezone":"Europe\/Berlin","timezone_abbr":"CET","cost":"","cost_details":{"currency_symbol":"$","currency_code":"USD","currency_position":"prefix","values":[]},"website":"","show_map":false,"show_map_link":true,"hide_from_listings":false,"sticky":false,"featured":false,"categories":[{"name":"Weihnachtsm\u00e4rkte","slug":"weihnachtsmaerkte","term_group":0,"term_taxonomy_id":973,"taxonomy":"tribe_events_cat","description":"Liste der n\u00e4chstgelegenen Weihnachtsm\u00e4rkte. Hier finden Sie aktuelle Informationen, einschlie\u00dflich Daten, Uhrzeiten, Standorte, Karten, Kontaktinformationen und andere interessante Details, die Sie ben\u00f6tigen.","parent":0,"count":200,"filter":"raw","id":973,"urls":{"self":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/categories\/973","collection":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/categories"}}],"tags":[{"name":"Bayern","slug":"bayern","term_group":0,"term_taxonomy_id":33,"taxonomy":"post_tag","description":"","parent":0,"count":245,"filter":"raw","id":33,"urls":{"self":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags\/33","collection":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags"}},{"name":"Deutschland","slug":"deutschland","term_group":0,"term_taxonomy_id":6,"taxonomy":"post_tag","description":"","parent":0,"count":1602,"filter":"raw","id":6,"urls":{"self":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags\/6","collection":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags"}},{"name":"Eisenbacher Rauhnachtsmarkt in der Knechtsm\u00fchle","slug":"eisenbacher-rauhnachtsmarkt-in-der-knechtsmuehle","term_group":0,"term_taxonomy_id":1144,"taxonomy":"post_tag","description":"","parent":0,"count":2,"filter":"raw","id":1144,"urls":{"self":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags\/1144","collection":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags"}},{"name":"Obernburg am Main","slug":"obernburg-am-main","term_group":0,"term_taxonomy_id":1143,"taxonomy":"post_tag","description":"","parent":0,"count":2,"filter":"raw","id":1143,"urls":{"self":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags\/1143","collection":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/tags"}}],"venue":{"id":5136,"author":"1","status":"publish","date":"2024-11-12 00:53:47","date_utc":"2024-11-11 23:53:47","modified":"2024-11-12 00:53:47","modified_utc":"2024-11-11 23:53:47","url":"https:\/\/mittelaltermarkt.online\/veranstaltungsort\/getreidemuehle-knecht\/","venue":"Getreidem\u00fchle Knecht","slug":"getreidemuehle-knecht","address":"M\u00fchlstra\u00dfe 7","city":"Obernburg am Main","country":"Germany","province":"Unterfranken","state":"Bayern","zip":"63785","website":"https:\/\/www.bio-mit-gesicht.de\/","stateprovince":"Bayern","geo_lat":49.8350054,"geo_lng":9.1108948,"show_map":false,"show_map_link":false,"global_id":"mittelaltermarkt.online?id=5136","global_id_lineage":["mittelaltermarkt.online?id=5136"]},"organizer":[{"id":5137,"author":"1","status":"publish","date":"2024-11-12 00:53:48","date_utc":"2024-11-11 23:53:48","modified":"2024-11-12 00:53:48","modified_utc":"2024-11-11 23:53:48","url":"https:\/\/mittelaltermarkt.online\/veranstalter\/knechts-muehlenladen\/","organizer":"Knecht’s M\u00fchlenladen","slug":"knechts-muehlenladen","website":"https:\/\/www.facebook.com\/profile.php?id=100057437889485","global_id":"mittelaltermarkt.online?id=5137","global_id_lineage":["mittelaltermarkt.online?id=5137"]}],"custom_fields":[],"is_virtual":false,"virtual_url":null,"virtual_video_source":""},{"id":10001458,"global_id":"mittelaltermarkt.online?id=10001458","global_id_lineage":["mittelaltermarkt.online?id=10001458"],"author":"1","status":"publish","date":"2025-10-11 17:39:25","date_utc":"2025-10-11 15:39:25","modified":"2025-12-26 09:19:07","modified_utc":"2025-12-26 08:19:07","url":"https:\/\/mittelaltermarkt.online\/event\/dresdner-rauhnaechte-im-stallhof-2025-2026\/2026-01-02\/","rest_url":"https:\/\/mittelaltermarkt.online\/wp-json\/tribe\/events\/v1\/events\/10001458","title":"Dresdner Rauhn\u00e4chte im Stallhof 2025-2026","description":"

Wenn die Zeit stillzustehen scheint und der Duft von Weihrauch durch alte Gem\u00e4uer zieht, beginnen in Dresden<\/strong> die geheimnisvollen Rauhn\u00e4chte<\/strong>. Vom 27. bis 30. Dezember 2025<\/strong> sowie vom 2. bis 6. Januar 2026<\/strong> verwandelt sich der historische Stallhof<\/strong> der s\u00e4chsischen Landeshauptstadt in einen mittelalterlichen Jahrmarkt voller Magie, Musik und Mystik<\/strong>.<\/p>\n

\ud83e\uddd9\u200d\u2640\ufe0f Die Magie der Rauhn\u00e4chte erleben<\/h2>\n

Die Rauhn\u00e4chte<\/strong> \u2013 jene sagenumwobenen zw\u00f6lf N\u00e4chte zwischen Weihnachten<\/strong> und Dreik\u00f6nig<\/strong> \u2013 gelten seit Jahrhunderten als Zeit der Rituale, der Reinigung und des Neuanfangs. Die Dresdner Rauhn\u00e4chte<\/strong> greifen diese Tradition auf und lassen sie im Herzen der Altstadt lebendig werden.<\/p>\n

\ud83c\udfad Wahrsagerinnen, magische Kreaturen, geheimnisvolle Gestalten und Geschichten aus einer anderen Zeit<\/strong> begegnen den G\u00e4sten auf Schritt und Tritt. Dazu gibt es:<\/p>\n