paths,
+ String token
+) {
+
+ public GitConfig {
+ paths = paths == null ? List.of() : List.copyOf(paths);
+ }
+
+ /** @return {@code true} if an access token is configured (private repo) */
+ public boolean hasToken() {
+ return token != null && !token.isBlank();
+ }
+}
diff --git a/src/main/java/com/openelements/content/GitSourceStrategy.java b/src/main/java/com/openelements/content/GitSourceStrategy.java
new file mode 100644
index 0000000..6de351d
--- /dev/null
+++ b/src/main/java/com/openelements/content/GitSourceStrategy.java
@@ -0,0 +1,269 @@
+package com.openelements.content;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.web.client.RestClient;
+
+/**
+ * {@link ContentSourceStrategy} for {@link SourceType#GIT}: indexes Markdown files directly from a
+ * GitHub repository.
+ *
+ * Discovery lists the repo tree (GitHub Git Trees API) and keeps blob paths matching the source's
+ * {@code paths} globs, using each file's blob SHA as the change marker. Fetch reads the raw file,
+ * splits YAML frontmatter from the Markdown body (cleaning Hugo shortcodes), maps the file path to a
+ * canonical website URL, and derives the locale from the filename suffix. Private repos are accessed
+ * with a bearer token that is used server-side only and never logged.
+ *
+ *
A hard discovery failure (e.g. auth error) is surfaced as an exception so the caller (bootstrap
+ * step / refresh scheduler) isolates the source rather than treating an empty listing as "everything
+ * was deleted".
+ */
+@Component
+public class GitSourceStrategy implements ContentSourceStrategy {
+
+ private static final Logger log = LoggerFactory.getLogger(GitSourceStrategy.class);
+ private static final Pattern SHORTCODE = Pattern.compile("\\{\\{[<%].*?[%>]\\}\\}", Pattern.DOTALL);
+ private static final Pattern DATED_FILENAME = Pattern.compile("^(\\d{4})-(\\d{2})-(\\d{2})-(.+)$");
+
+ private final RestClient restClient;
+ private final YAMLMapper yamlMapper = new YAMLMapper();
+ private final AntPathMatcher pathMatcher = new AntPathMatcher();
+
+ public GitSourceStrategy(RestClient.Builder restClientBuilder) {
+ this.restClient = restClientBuilder.build();
+ }
+
+ @Override
+ public SourceType type() {
+ return SourceType.GIT;
+ }
+
+ @Override
+ public List discover(ContentSource source) {
+ GitConfig git = source.git();
+ if (git == null || git.repo() == null || git.repo().isBlank()) {
+ throw new IllegalStateException("Git source " + source.id() + " is missing its git configuration");
+ }
+ JsonNode tree;
+ try {
+ tree = restClient.get()
+ .uri(URI.create("https://api.github.com/repos/" + git.repo() + "/git/trees/" + git.ref() + "?recursive=1"))
+ .headers(headers -> applyGitHubHeaders(headers, git))
+ .retrieve()
+ .body(JsonNode.class);
+ } catch (Exception e) {
+ // Surface as a failure so the source is isolated (not mass-deleted). Never log the token.
+ log.warn("Git discovery failed for source {} (repo {}): {}", source.id(), git.repo(), e.toString());
+ throw new IllegalStateException("Git discovery failed for " + git.repo(), e);
+ }
+
+ List items = new ArrayList<>();
+ if (tree != null) {
+ for (JsonNode node : tree.path("tree")) {
+ if (!"blob".equals(node.path("type").asText())) {
+ continue;
+ }
+ String path = node.path("path").asText("");
+ if (!path.isEmpty() && matchesAnyGlob(path, git.paths())) {
+ items.add(new DiscoveredItem(path, node.path("sha").asText(null)));
+ }
+ }
+ }
+ return items;
+ }
+
+ @Override
+ public FetchOutcome fetch(ContentSource source, DiscoveredItem item) {
+ GitConfig git = source.git();
+ String content;
+ try {
+ content = restClient.get()
+ .uri(URI.create("https://raw.githubusercontent.com/" + git.repo() + "/" + git.ref() + "/" + item.url()))
+ .headers(headers -> applyGitHubHeaders(headers, git))
+ .retrieve()
+ .body(String.class);
+ } catch (Exception e) {
+ log.warn("Skipping {} in {}: fetch failed ({})", item.url(), git.repo(), e.toString());
+ return FetchOutcome.skip();
+ }
+ if (content == null || content.isBlank()) {
+ return FetchOutcome.skip();
+ }
+
+ ParsedMarkdown parsed = parseMarkdown(content);
+ if (parsed.body().isBlank()) {
+ log.warn("Skipping {} in {}: no content after frontmatter", item.url(), git.repo());
+ return FetchOutcome.skip();
+ }
+ Map frontmatter = parsed.frontmatter();
+ ContentDocument document = new ContentDocument(
+ ContentDocument.id(source.id(), item.url()),
+ source.id(),
+ localeFromPath(item.url()),
+ mapToUrl(source, item.url(), frontmatter),
+ firstNonBlank(string(frontmatter, "title"), slugOf(item.url())),
+ firstNonBlank(string(frontmatter, "excerpt"), string(frontmatter, "description")),
+ parsed.body(),
+ string(frontmatter, "author"),
+ categories(frontmatter),
+ firstNonBlank(string(frontmatter, "date"), string(frontmatter, "publishedDate")),
+ item.lastmod(),
+ firstNonBlank(string(frontmatter, "previewImage"), string(frontmatter, "image")));
+ return FetchOutcome.index(document);
+ }
+
+ private static void applyGitHubHeaders(HttpHeaders headers, GitConfig git) {
+ headers.set(HttpHeaders.ACCEPT, "application/vnd.github+json");
+ if (git.hasToken()) {
+ headers.setBearerAuth(git.token());
+ }
+ }
+
+ private boolean matchesAnyGlob(String path, List globs) {
+ return globs.stream().anyMatch(glob -> pathMatcher.match(glob, path));
+ }
+
+ // ---- markdown / frontmatter ----
+
+ ParsedMarkdown parseMarkdown(String content) {
+ Map frontmatter = Map.of();
+ String body = content;
+ if (content.startsWith("---")) {
+ int firstNewline = content.indexOf('\n');
+ int closing = firstNewline < 0 ? -1 : content.indexOf("\n---", firstNewline);
+ if (firstNewline > 0 && closing > firstNewline) {
+ String block = content.substring(firstNewline + 1, closing);
+ int bodyStart = content.indexOf('\n', closing + 1);
+ body = bodyStart >= 0 ? content.substring(bodyStart + 1) : "";
+ frontmatter = parseYaml(block);
+ }
+ }
+ return new ParsedMarkdown(frontmatter, SHORTCODE.matcher(body).replaceAll("").strip());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map parseYaml(String block) {
+ try {
+ Map map = yamlMapper.readValue(block, Map.class);
+ return map == null ? Map.of() : map;
+ } catch (Exception e) {
+ log.debug("Ignoring unparseable frontmatter: {}", e.toString());
+ return Map.of();
+ }
+ }
+
+ // ---- path -> url / locale ----
+
+ String mapToUrl(ContentSource source, String path, Map frontmatter) {
+ String base = trimTrailingSlash(source.baseUrl());
+ String fileName = stripMarkdownExtension(lastSegment(path));
+ String withoutLocale = stripLocaleSuffix(fileName);
+ var matcher = DATED_FILENAME.matcher(withoutLocale);
+ if (matcher.matches()) {
+ return base + "/posts/" + matcher.group(1) + "/" + matcher.group(2) + "/" + matcher.group(3)
+ + "/" + matcher.group(4);
+ }
+ String slug = string(frontmatter, "slug");
+ if (slug != null) {
+ return base + "/posts/" + slug;
+ }
+ return base + "/" + stripMarkdownExtension(path);
+ }
+
+ String localeFromPath(String path) {
+ String fileName = stripMarkdownExtension(lastSegment(path));
+ int dot = fileName.lastIndexOf('.');
+ if (dot > 0) {
+ String suffix = fileName.substring(dot + 1);
+ if (suffix.length() == 2 && suffix.chars().allMatch(Character::isLetter)) {
+ return suffix.toLowerCase(Locale.ROOT);
+ }
+ }
+ return "en";
+ }
+
+ private static String slugOf(String path) {
+ return stripLocaleSuffix(stripMarkdownExtension(lastSegment(path)));
+ }
+
+ private static String lastSegment(String path) {
+ int slash = path.lastIndexOf('/');
+ return slash >= 0 ? path.substring(slash + 1) : path;
+ }
+
+ private static String stripMarkdownExtension(String value) {
+ if (value.endsWith(".md")) {
+ return value.substring(0, value.length() - 3);
+ }
+ if (value.endsWith(".mdx")) {
+ return value.substring(0, value.length() - 4);
+ }
+ return value;
+ }
+
+ private static String stripLocaleSuffix(String fileName) {
+ int dot = fileName.lastIndexOf('.');
+ if (dot > 0) {
+ String suffix = fileName.substring(dot + 1);
+ if (suffix.length() == 2 && suffix.chars().allMatch(Character::isLetter)) {
+ return fileName.substring(0, dot);
+ }
+ }
+ return fileName;
+ }
+
+ private static String trimTrailingSlash(String url) {
+ return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
+ }
+
+ // ---- frontmatter helpers ----
+
+ @SuppressWarnings("unchecked")
+ private static List categories(Map frontmatter) {
+ Object value = frontmatter.containsKey("categories") ? frontmatter.get("categories") : frontmatter.get("tags");
+ List categories = new ArrayList<>();
+ if (value instanceof List> list) {
+ list.forEach(item -> categories.add(String.valueOf(item)));
+ } else if (value instanceof String text) {
+ for (String part : text.split(",")) {
+ if (!part.isBlank()) {
+ categories.add(part.strip());
+ }
+ }
+ }
+ return categories;
+ }
+
+ private static String string(Map frontmatter, String key) {
+ Object value = frontmatter.get(key);
+ if (value == null) {
+ return null;
+ }
+ String text = String.valueOf(value).strip();
+ return text.isEmpty() ? null : text;
+ }
+
+ private static String firstNonBlank(String... values) {
+ for (String value : values) {
+ if (value != null && !value.isBlank()) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ /** The frontmatter map and the cleaned Markdown body. */
+ record ParsedMarkdown(Map frontmatter, String body) {
+ }
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index e41c4d4..1844658 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -82,3 +82,16 @@ open-elements:
content-selector: "body"
content-exclude: [ "nav", "header", "footer", ".cookie-banner", "aside" ]
enabled: true
+ # Example Git/Markdown source (spec 016). Disabled by default: enable it and supply a read-only,
+ # repo-scoped token via GITHUB_TOKEN_OE_WEBSITE (never commit a token). Higher-fidelity content
+ # than HTML scraping for repos we own.
+ - id: oe-website-markdown
+ type: git
+ base-url: https://open-elements.com
+ enabled: false
+ git:
+ provider: github
+ repo: OpenElements/open-elements-website
+ ref: main
+ paths: [ "content/posts/**/*.md", "content/posts/**/*.mdx" ]
+ token: ${GITHUB_TOKEN_OE_WEBSITE:}
diff --git a/src/test/java/com/openelements/content/ConfiguredSourcesTest.java b/src/test/java/com/openelements/content/ConfiguredSourcesTest.java
index 2dbbeda..28183ad 100644
--- a/src/test/java/com/openelements/content/ConfiguredSourcesTest.java
+++ b/src/test/java/com/openelements/content/ConfiguredSourcesTest.java
@@ -56,4 +56,16 @@ void supportAndCareSourceIsConfigured() {
assertThat(support.contentSelector()).isEqualTo("body");
assertThat(support.contentExclude()).contains("nav", "header", "footer", ".cookie-banner");
}
+
+ @Test
+ @DisplayName("the example git source binds as a GIT source (disabled by default)")
+ void gitSourceIsConfigured() {
+ ContentSource git = sourcesById().get("oe-website-markdown");
+ assertThat(git.type()).isEqualTo(SourceType.GIT);
+ assertThat(git.enabled()).isFalse();
+ assertThat(git.git()).isNotNull();
+ assertThat(git.git().repo()).isEqualTo("OpenElements/open-elements-website");
+ assertThat(git.git().ref()).isEqualTo("main");
+ assertThat(git.git().paths()).contains("content/posts/**/*.md");
+ }
}
diff --git a/src/test/java/com/openelements/content/ContentBootstrapStepTest.java b/src/test/java/com/openelements/content/ContentBootstrapStepTest.java
index 175d4d8..53b0f99 100644
--- a/src/test/java/com/openelements/content/ContentBootstrapStepTest.java
+++ b/src/test/java/com/openelements/content/ContentBootstrapStepTest.java
@@ -32,7 +32,7 @@ class ContentBootstrapStepTest {
private static ContentSource source(String id, boolean enabled) {
return new ContentSource(
- id, SourceType.WEBSITE, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), enabled);
+ id, SourceType.WEBSITE, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), enabled, null);
}
private static ContentSourceProperties properties(ContentSource... sources) {
diff --git a/src/test/java/com/openelements/content/ContentExtractorTest.java b/src/test/java/com/openelements/content/ContentExtractorTest.java
index 010b89e..3763e04 100644
--- a/src/test/java/com/openelements/content/ContentExtractorTest.java
+++ b/src/test/java/com/openelements/content/ContentExtractorTest.java
@@ -19,7 +19,7 @@ class ContentExtractorTest {
private static ContentSource source(String contentSelector, List contentExclude) {
return new ContentSource(
"test", SourceType.WEBSITE, "https://ex.com",
- List.of(), List.of("/**"), List.of(), contentSelector, contentExclude, true);
+ List.of(), List.of("/**"), List.of(), contentSelector, contentExclude, true, null);
}
private ExtractedContent extract(String contentSelector, List exclude, String html) {
diff --git a/src/test/java/com/openelements/content/ContentIndexerTest.java b/src/test/java/com/openelements/content/ContentIndexerTest.java
index a33b645..91a4509 100644
--- a/src/test/java/com/openelements/content/ContentIndexerTest.java
+++ b/src/test/java/com/openelements/content/ContentIndexerTest.java
@@ -19,7 +19,7 @@
class ContentIndexerTest {
private static final ContentSource SOURCE = new ContentSource(
- "oe", SourceType.WEBSITE, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), true);
+ "oe", SourceType.WEBSITE, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), true, null);
private final InMemoryContentIndexStore store = new InMemoryContentIndexStore();
private final StubStrategy strategy = new StubStrategy();
diff --git a/src/test/java/com/openelements/content/ContentRefreshSchedulerTest.java b/src/test/java/com/openelements/content/ContentRefreshSchedulerTest.java
index df01fb9..4deb9b1 100644
--- a/src/test/java/com/openelements/content/ContentRefreshSchedulerTest.java
+++ b/src/test/java/com/openelements/content/ContentRefreshSchedulerTest.java
@@ -41,7 +41,7 @@ void markBootstrapComplete() {
private static ContentSource source(String id, boolean enabled) {
return new ContentSource(
- id, SourceType.WEBSITE, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), enabled);
+ id, SourceType.WEBSITE, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), enabled, null);
}
private static ContentSourceProperties properties(boolean enabled, ContentSource... sources) {
diff --git a/src/test/java/com/openelements/content/GitSourceStrategyTest.java b/src/test/java/com/openelements/content/GitSourceStrategyTest.java
new file mode 100644
index 0000000..f7733c1
--- /dev/null
+++ b/src/test/java/com/openelements/content/GitSourceStrategyTest.java
@@ -0,0 +1,160 @@
+package com.openelements.content;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.web.client.RestClient;
+
+/**
+ * Tests for {@link GitSourceStrategy} using {@link MockRestServiceServer} to stub the GitHub trees
+ * and raw-content endpoints (no real network), plus direct unit tests of the pure mapping helpers.
+ */
+@DisplayName("Git source strategy")
+class GitSourceStrategyTest {
+
+ private static final String TREES_URL =
+ "https://api.github.com/repos/OpenElements/website/git/trees/main?recursive=1";
+
+ private MockRestServiceServer server;
+ private GitSourceStrategy strategy;
+
+ @BeforeEach
+ void setUp() {
+ RestClient.Builder builder = RestClient.builder();
+ server = MockRestServiceServer.bindTo(builder).ignoreExpectOrder(true).build();
+ strategy = new GitSourceStrategy(builder);
+ }
+
+ private static ContentSource gitSource(String token, String... paths) {
+ return new ContentSource("oe-md", SourceType.GIT, "https://open-elements.com",
+ List.of(), List.of("/**"), List.of(), null, List.of(), true,
+ new GitConfig("github", "OpenElements/website", "main", List.of(paths), token));
+ }
+
+ private static String rawUrl(String path) {
+ return "https://raw.githubusercontent.com/OpenElements/website/main/" + path;
+ }
+
+ @Test
+ @DisplayName("handles the GIT source type")
+ void handlesGitType() {
+ assertThat(strategy.type()).isEqualTo(SourceType.GIT);
+ }
+
+ @Test
+ @DisplayName("discovery lists tree blobs filtered by the paths glob, with their SHAs")
+ void discoveryFiltersByPaths() {
+ server.expect(requestTo(TREES_URL)).andRespond(withSuccess("""
+ {"tree":[
+ {"path":"README.md","type":"blob","sha":"r1"},
+ {"path":"content/posts/2026-03-12-slug.md","type":"blob","sha":"s1"},
+ {"path":"content/posts/image.png","type":"blob","sha":"i1"},
+ {"path":"content/posts","type":"tree","sha":"t1"}
+ ]}""", MediaType.APPLICATION_JSON));
+
+ List items = strategy.discover(gitSource(null, "content/posts/**/*.md"));
+
+ assertThat(items).containsExactly(new DiscoveredItem("content/posts/2026-03-12-slug.md", "s1"));
+ }
+
+ @Test
+ @DisplayName("a configured token is sent as a bearer credential")
+ void tokenIsSentForPrivateRepo() {
+ server.expect(requestTo(TREES_URL))
+ .andExpect(header("Authorization", "Bearer secret-token"))
+ .andRespond(withSuccess("{\"tree\":[]}", MediaType.APPLICATION_JSON));
+
+ strategy.discover(gitSource("secret-token", "content/posts/**/*.md"));
+
+ server.verify();
+ }
+
+ @Test
+ @DisplayName("a discovery auth failure is surfaced so the source is isolated")
+ void missingTokenFailsClearly() {
+ server.expect(requestTo(TREES_URL)).andRespond(withStatus(HttpStatus.NOT_FOUND));
+
+ assertThatThrownBy(() -> strategy.discover(gitSource(null, "content/posts/**/*.md")))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("OpenElements/website");
+ }
+
+ @Test
+ @DisplayName("fetch parses frontmatter, cleans shortcodes, maps the URL and derives the locale")
+ void fetchExtractsFrontmatterAndBody() {
+ String path = "content/posts/2026-03-12-slug.md";
+ server.expect(requestTo(rawUrl(path))).andRespond(withSuccess("""
+ ---
+ title: "Agentic Wallets"
+ date: "2026-03-12"
+ author: "hendrik"
+ categories: ["ai", "web3"]
+ ---
+ Body text about {{< figure src="x.png" >}} wallets.
+ """, MediaType.TEXT_PLAIN));
+
+ FetchOutcome outcome = strategy.fetch(gitSource(null, "content/posts/**/*.md"),
+ new DiscoveredItem(path, "s1"));
+
+ assertThat(outcome.result()).isEqualTo(FetchOutcome.Result.INDEX);
+ ContentDocument document = outcome.document();
+ assertThat(document.title()).isEqualTo("Agentic Wallets");
+ assertThat(document.author()).isEqualTo("hendrik");
+ assertThat(document.categories()).containsExactly("ai", "web3");
+ assertThat(document.publishedDate()).isEqualTo("2026-03-12");
+ assertThat(document.url()).isEqualTo("https://open-elements.com/posts/2026/03/12/slug");
+ assertThat(document.locale()).isEqualTo("en");
+ assertThat(document.lastmod()).isEqualTo("s1");
+ assertThat(document.body()).contains("Body text about", "wallets.");
+ assertThat(document.body()).doesNotContain("{{<", "title:");
+ }
+
+ @Test
+ @DisplayName("a fetch failure skips the file without failing the batch")
+ void fetchFailureIsSkipped() {
+ String path = "content/posts/gone.md";
+ server.expect(requestTo(rawUrl(path))).andRespond(withStatus(HttpStatus.NOT_FOUND));
+
+ FetchOutcome outcome = strategy.fetch(gitSource(null, "content/posts/**/*.md"),
+ new DiscoveredItem(path, "s1"));
+
+ assertThat(outcome.result()).isEqualTo(FetchOutcome.Result.SKIP);
+ }
+
+ // ---- pure mapping helpers ----
+
+ @Test
+ @DisplayName("a dated filename maps to the canonical /posts/YYYY/MM/DD/slug URL")
+ void datedFilenameMapsToCanonicalUrl() {
+ String url = strategy.mapToUrl(
+ gitSource(null, "content/posts/**/*.md"), "content/posts/2026-03-12-my-post.md", Map.of());
+ assertThat(url).isEqualTo("https://open-elements.com/posts/2026/03/12/my-post");
+ }
+
+ @Test
+ @DisplayName("the locale is derived from a filename language suffix")
+ void localeFromFilenameSuffix() {
+ assertThat(strategy.localeFromPath("content/posts/2026-03-12-slug.de.md")).isEqualTo("de");
+ assertThat(strategy.localeFromPath("content/posts/2026-03-12-slug.md")).isEqualTo("en");
+ }
+
+ @Test
+ @DisplayName("a file without frontmatter yields the whole content as the body")
+ void parsesBodyWithoutFrontmatter() {
+ GitSourceStrategy.ParsedMarkdown parsed = strategy.parseMarkdown("Just body, no frontmatter.");
+ assertThat(parsed.frontmatter()).isEmpty();
+ assertThat(parsed.body()).isEqualTo("Just body, no frontmatter.");
+ }
+}
diff --git a/src/test/java/com/openelements/content/SitemapCrawlerTest.java b/src/test/java/com/openelements/content/SitemapCrawlerTest.java
index 1efeb6d..29c0a07 100644
--- a/src/test/java/com/openelements/content/SitemapCrawlerTest.java
+++ b/src/test/java/com/openelements/content/SitemapCrawlerTest.java
@@ -41,7 +41,7 @@ private static ContentSource source(List sitemaps, List include,
private static ContentSource source(String baseUrl, List sitemaps,
List include, List exclude) {
return new ContentSource(
- "test", SourceType.WEBSITE, baseUrl, sitemaps, include, exclude, "article", List.of(), true);
+ "test", SourceType.WEBSITE, baseUrl, sitemaps, include, exclude, "article", List.of(), true, null);
}
private void stubXml(String url, String body) {
diff --git a/src/test/java/com/openelements/content/SourceStrategyRegistryTest.java b/src/test/java/com/openelements/content/SourceStrategyRegistryTest.java
index 262e155..856b4a6 100644
--- a/src/test/java/com/openelements/content/SourceStrategyRegistryTest.java
+++ b/src/test/java/com/openelements/content/SourceStrategyRegistryTest.java
@@ -15,7 +15,7 @@ class SourceStrategyRegistryTest {
private static ContentSource source(SourceType type) {
return new ContentSource(
- "s", type, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), true);
+ "s", type, "https://ex.com", List.of(), List.of("/**"), List.of(), "article", List.of(), true, null);
}
private static ContentSourceStrategy strategyFor(SourceType type) {
diff --git a/src/test/java/com/openelements/content/UrlMatcherTest.java b/src/test/java/com/openelements/content/UrlMatcherTest.java
index 2d7aeea..315497d 100644
--- a/src/test/java/com/openelements/content/UrlMatcherTest.java
+++ b/src/test/java/com/openelements/content/UrlMatcherTest.java
@@ -18,7 +18,7 @@ class UrlMatcherTest {
private static ContentSource source(List include, List exclude) {
return new ContentSource(
"test", SourceType.WEBSITE, "https://example.com",
- List.of(), include, exclude, "article", List.of(), true);
+ List.of(), include, exclude, "article", List.of(), true, null);
}
@Test
@@ -57,7 +57,7 @@ void defaultIncludeMatchesEverything() {
// urlInclude omitted -> ContentSource defaults it to ["/**"].
ContentSource source = new ContentSource(
"test", SourceType.WEBSITE, "https://example.com",
- List.of(), null, List.of("/private/**"), "article", List.of(), true);
+ List.of(), null, List.of("/private/**"), "article", List.of(), true, null);
assertThat(matcher.matches(source, "/anything/at/all")).isTrue();
assertThat(matcher.matches(source, "/private/secret")).isFalse();
diff --git a/src/test/java/com/openelements/content/WebsiteSourceStrategyTest.java b/src/test/java/com/openelements/content/WebsiteSourceStrategyTest.java
index d5eaa0e..08b68bc 100644
--- a/src/test/java/com/openelements/content/WebsiteSourceStrategyTest.java
+++ b/src/test/java/com/openelements/content/WebsiteSourceStrategyTest.java
@@ -53,7 +53,7 @@ void setUp() {
private static ContentSource source(List sitemaps) {
return new ContentSource(
"open-elements", SourceType.WEBSITE, "https://ex.com",
- sitemaps, List.of("/**"), List.of(), "article", List.of(), true);
+ sitemaps, List.of("/**"), List.of(), "article", List.of(), true, null);
}
private static DiscoveredItem item(String lastmod) {