Skip to content

Commit f652113

Browse files
Merge pull request #30 from OpenElementsLabs/feat/015-additional-web-sources
Additional website sources — hiero.org/blog + Support & Care (config-only)
2 parents 738e75b + c080436 commit f652113

5 files changed

Lines changed: 118 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ Current state: **Phase 1 complete** (specs 001–014). The app crawls configured
1414
(sitemap discovery + bounded fallback, robots.txt-aware), fetches politely (conditional GET, per-host
1515
rate limit, retries), extracts content/metadata with jsoup, indexes into Meilisearch (startup
1616
bootstrap + scheduled incremental refresh), and exposes four MCP tools on `/mcp` (`search_content`,
17-
`list_posts`, `get_post`, `list_categories`) backed by a scoped Meilisearch key. Phase 2+ (see
18-
[`docs/roadmap.md`](docs/roadmap.md)): additional website sources, Git/Markdown sources, and search
19-
enhancements.
17+
`list_posts`, `get_post`, `list_categories`) backed by a scoped Meilisearch key. Three website sources are
18+
configured (spec 015): `open-elements` (`/posts/**`), `hiero` (`/blog/**`), and `support-and-care`
19+
(`/en/support-care*`). Phase 3+ (see [`docs/roadmap.md`](docs/roadmap.md)): Git/Markdown sources and
20+
search enhancements.
2021

2122
### Tech Stack
2223

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Implementation Steps: Additional Website Sources
2+
3+
## Step 1: Resolve open questions (live checks)
4+
5+
- [x] **hiero.org sitemap:** `https://hiero.org/sitemap.xml` exists — a flat `<urlset>` with 66 `<loc>`s, 62 of them `/blog/<post>`. So `/sitemap.xml` + `url-include: [/blog/**]` covers all posts; the no-sitemap fallback crawl is not needed.
6+
- [x] **hiero.org robots.txt:** served as a Next.js HTML catch-all (no `User-agent`/`Disallow` rules) → treated as allow-all by `HttpRobotsPolicy`.
7+
- [x] **hiero post markup:** posts are wrapped in `<main><article>` → selector `main article`. No OpenGraph/Article meta, so `title` falls back to `<title>`/`<h1>` and `publishedDate` may be null (metadata is limited for hiero).
8+
- [x] **Support & Care:** two pages — `/en/support-care` and `/en/support-care-maven` — listed in `/en/sitemap.xml` (under `/en/`, not `/support-care` as the design assumed). OE `robots.txt` is `Allow: /`.
9+
10+
## Step 2: Configuration (no code)
11+
12+
- [x] Add `hiero` source: `base-url https://hiero.org`, `sitemaps [/sitemap.xml]`, `url-include [/blog/**]`, `content-selector "main article"`
13+
- [x] Add `support-and-care` source: `base-url https://open-elements.com`, `sitemaps [/en/sitemap.xml]`, `url-include ["/en/support-care*", "/en/support-care/**"]` (matches both current pages), `content-selector "body"` + `content-exclude [nav, header, footer, .cookie-banner, aside]`
14+
15+
## Step 3: Tests
16+
17+
- [x] `ConfiguredSourcesTest` — the three sources bind from `application.yaml` with the expected type/URLs/selectors (no code)
18+
19+
**Acceptance criteria:**
20+
- [x] All tests pass (`mvn test`); build green
21+
22+
## Behavior Coverage
23+
24+
| Scenario | Layer | Covered in Step |
25+
|----------|-------|-----------------|
26+
| New source needs no code | Backend | Step 3 (`ConfiguredSourcesTest.allSourcesAreConfigured`) |
27+
| Blog posts are indexed (/blog/** only) | Backend | Step 3 (hiero `urlInclude` `[/blog/**]`); live index verified manually |
28+
| Sitemap-absent fallback | Backend | N/A for hiero — the sitemap exists and lists posts (finding); generic fallback is covered by spec 004 |
29+
| Clean extraction with the article selector | Backend | Step 1 finding (`main article`); live extraction verified manually |
30+
| robots.txt honored for hiero.org | Backend | Covered by spec 014 (`HttpRobotsPolicy`); hiero has no rules → allow-all (finding) |
31+
| Body-selector with excludes yields clean text | Backend | Step 2 config; live extraction verified manually |
32+
| URL scope matches single vs. multi page | Backend | Step 3 (`supportAndCareSourceIsConfigured`) — multi-page `/en/support-care*` |
33+
| Source filter isolates results | Backend | Covered by spec 011 (`ContentSearchServiceTest` source filter) |
34+
35+
Config-only spec: binding is unit-tested; live crawl/extraction against the third-party sites is verified manually against a dev index (not run in CI, which must not hit external sites).

docs/specs/INDEX.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ roadmap step, to be implemented sequentially (each builds on the previous).
2020
| 012 | 012-content-mcp-tools | Content MCP tools | backend, mcp, api | `ContentMcpToolProvider` — the 4 MCP tools | #23 | done |
2121
| 013 | 013-scoped-search-key | Scoped search key | backend, search, security | Read-only scoped Meilisearch key for the content index | #25 | done |
2222
| 014 | 014-ops-robustness | Ops & robustness | backend, infrastructure, observability | robots.txt handling, fault tolerance, logging/metrics | #27 | done |
23-
| 015 | 015-additional-web-sources | Additional web sources | backend, configuration | hiero.org/blog + Support & Care as config-only sources | | open |
23+
| 015 | 015-additional-web-sources | Additional web sources | backend, configuration | hiero.org/blog + Support & Care as config-only sources | #29 | done |
2424
| 016 | 016-git-markdown-source | Git markdown source | backend, architecture, security | `type: git` source + `GitSourceStrategy` (GitHub Markdown) || open |
2525
| 017 | 017-search-enhancements | Search enhancements | backend, search | Facets, synonyms/stop words, optional semantic search || open |

src/main/resources/application.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,22 @@ open-elements:
6363
url-include: [ "/posts/**", "/de/posts/**" ]
6464
content-selector: "article"
6565
enabled: true
66+
# hiero.org has a real /sitemap.xml (flat urlset) listing every /blog/<post>; robots.txt is a
67+
# Next.js HTML catch-all (no rules -> allow-all). Posts are wrapped in <main><article>.
68+
- id: hiero
69+
type: website
70+
base-url: https://hiero.org
71+
sitemaps: [ /sitemap.xml ]
72+
url-include: [ "/blog/**" ]
73+
content-selector: "main article"
74+
enabled: true
75+
# Support & Care is two pages under /en/ (support-care, support-care-maven), listed in
76+
# /en/sitemap.xml. They lack a clean article container, so take the <body> minus boilerplate.
77+
- id: support-and-care
78+
type: website
79+
base-url: https://open-elements.com
80+
sitemaps: [ /en/sitemap.xml ]
81+
url-include: [ "/en/support-care*", "/en/support-care/**" ]
82+
content-selector: "body"
83+
content-exclude: [ "nav", "header", "footer", ".cookie-banner", "aside" ]
84+
enabled: true
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.openelements.content;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.Map;
6+
import java.util.function.Function;
7+
import java.util.stream.Collectors;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
13+
/**
14+
* Verifies that the website sources are bound from {@code application.yaml} with no code — the point
15+
* of spec 015 (hiero.org/blog and Support & Care are added purely as configuration).
16+
*
17+
* <p>Shares the application context of the other {@code api-key.enabled=false} tests. The actual crawl
18+
* and extraction against the live sites are verified manually against a dev index.
19+
*/
20+
@SpringBootTest(properties = "openelements.mcp.auth.api-key.enabled=false")
21+
@DisplayName("Configured website sources")
22+
class ConfiguredSourcesTest {
23+
24+
@Autowired
25+
private ContentSourceProperties properties;
26+
27+
private Map<String, ContentSource> sourcesById() {
28+
return properties.sources().stream()
29+
.collect(Collectors.toMap(ContentSource::id, Function.identity()));
30+
}
31+
32+
@Test
33+
@DisplayName("open-elements, hiero and support-and-care are all configured")
34+
void allSourcesAreConfigured() {
35+
assertThat(sourcesById()).containsKeys("open-elements", "hiero", "support-and-care");
36+
}
37+
38+
@Test
39+
@DisplayName("the hiero source targets /blog via the sitemap with the article selector")
40+
void hieroSourceIsConfigured() {
41+
ContentSource hiero = sourcesById().get("hiero");
42+
assertThat(hiero.type()).isEqualTo(SourceType.WEBSITE);
43+
assertThat(hiero.baseUrl()).isEqualTo("https://hiero.org");
44+
assertThat(hiero.sitemaps()).containsExactly("/sitemap.xml");
45+
assertThat(hiero.urlInclude()).containsExactly("/blog/**");
46+
assertThat(hiero.contentSelector()).isEqualTo("main article");
47+
assertThat(hiero.enabled()).isTrue();
48+
}
49+
50+
@Test
51+
@DisplayName("the support-and-care source scopes to the /en/support-care pages with body + excludes")
52+
void supportAndCareSourceIsConfigured() {
53+
ContentSource support = sourcesById().get("support-and-care");
54+
assertThat(support.baseUrl()).isEqualTo("https://open-elements.com");
55+
assertThat(support.urlInclude()).contains("/en/support-care*");
56+
assertThat(support.contentSelector()).isEqualTo("body");
57+
assertThat(support.contentExclude()).contains("nav", "header", "footer", ".cookie-banner");
58+
}
59+
}

0 commit comments

Comments
 (0)