Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .sampo/changesets/radiant-flamebinder-ukko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
npm/graphgarden-web: minor
cargo/graphgarden-core: minor
---

**⚠️ breaking change:** Added `friends` field to the public file. `fetchFriendGraphs` now takes a `friends` parameter, and fetches all declared friends unconditionally.
5 changes: 5 additions & 0 deletions .sampo/changesets/verdant-stormweaver-vainamoinen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
cargo/graphgarden-protocol: minor
---

**⚠️ breaking change:** Added required `friends` field, an array of declared friend site base URLs.
2 changes: 2 additions & 0 deletions crates/graphgarden-core/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub fn build(config: &Config) -> Result<PublicFile> {
description: config.site.description.clone(),
language: config.site.language.clone(),
},
friends: config.friends.clone(),
nodes,
edges,
})
Expand Down Expand Up @@ -175,6 +176,7 @@ mod tests {
assert_eq!(result.version, crate::model::PROTOCOL_VERSION);
assert_eq!(result.base_url, "https://alice.dev/");
assert_eq!(result.site.title, "Alice's Garden");
assert_eq!(result.friends, vec!["https://bob.dev/"]);
assert_eq!(result.nodes.len(), 2);
assert!(
result
Expand Down
4 changes: 4 additions & 0 deletions crates/graphgarden-core/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub struct PublicFile {
pub generated_at: String,
pub base_url: String,
pub site: SiteMetadata,
pub friends: Vec<String>,
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
}
Expand Down Expand Up @@ -69,6 +70,7 @@ mod tests {
description: Some(String::from("A blog about gardening")),
language: Some(String::from("en")),
},
friends: vec![String::from("https://bob.dev/")],
nodes: vec![
Node {
url: String::from("/"),
Expand Down Expand Up @@ -148,6 +150,7 @@ mod tests {
"description": "A blog about …",
"language": "en"
},
"friends": ["https://bob.dev/"],
"nodes": [
{ "url": "/", "title": "Home" },
{ "url": "/about", "title": "About" },
Expand Down Expand Up @@ -199,6 +202,7 @@ mod tests {
"generated_at": "2026-02-17T12:00:00Z",
"base_url": "https://alice.dev/",
"site": { "title": "Test" },
"friends": [],
"nodes": "not_an_array",
"edges": []
}"#;
Expand Down
2 changes: 2 additions & 0 deletions crates/graphgarden-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Served at `BASE_URL/.well-known/graphgarden.json`.
"description": "A blog about …", // optional
"language": "en" // optional, BCP 47
},
"friends": ["https://bob.dev/"],
"nodes": [
{ "url": "/", "title": "Home" },
{ "url": "/about", "title": "About" },
Expand All @@ -40,6 +41,7 @@ Served at `BASE_URL/.well-known/graphgarden.json`.
- **`edges[].source`** — relative path (must match a node).
- **`edges[].target`** — relative path for `internal`, absolute URL for `friend`.
- **`edges[].type`** — `"internal"` (same site) or `"friend"` (site declared in config). Other external links are ignored during crawl.
- **`friends`** — array of declared friend site base URLs. All listed origins are fetched unconditionally by the web component, regardless of whether any edges reference them.

## Caching

Expand Down
23 changes: 23 additions & 0 deletions crates/graphgarden/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ fn build_happy_path() {
assert_eq!(value["base_url"], "https://test.dev/");
assert_eq!(value["site"]["title"], "Test Site");
assert!(!value["nodes"].as_array().unwrap().is_empty());

let friends = value["friends"].as_array().unwrap();
assert!(friends.is_empty());
}

#[test]
Expand Down Expand Up @@ -298,6 +301,10 @@ fn build_friend_links() {
"external non-friend links should be dropped"
);

let friends = value["friends"].as_array().unwrap();
assert_eq!(friends.len(), 1);
assert_eq!(friends[0], "https://bob.dev/");

assert!(
edges
.iter()
Expand Down Expand Up @@ -523,6 +530,10 @@ fn build_full_config() {
.iter()
.any(|e| e["source"] == "/about/" && e["target"] == "/" && e["type"] == "internal")
);

let friends = value["friends"].as_array().unwrap();
assert_eq!(friends.len(), 1);
assert_eq!(friends[0], "https://bob.dev/");
}

#[test]
Expand Down Expand Up @@ -627,6 +638,15 @@ fn build_protocol_invariants() {
);
assert!(edge_pairs.insert(pair), "duplicate edge found: {:?}", pair);
}

let friends = value["friends"].as_array().unwrap();
for friend in friends {
let url = friend.as_str().unwrap();
assert!(
url.starts_with("http://") || url.starts_with("https://"),
"friend URL should be absolute HTTP(S), got: {url}"
);
}
}

/// Validates that a string matches the `YYYY-MM-DDTHH:MM:SSZ` pattern.
Expand Down Expand Up @@ -677,6 +697,9 @@ fn build_empty_site() {
assert!(value["generated_at"].as_str().is_some());
assert_eq!(value["base_url"], "https://test.dev/");
assert_eq!(value["site"]["title"], "Test Site");

let friends = value["friends"].as_array().unwrap();
assert!(friends.is_empty());
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions fixtures/bob/graphgarden.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"description": "Bob's your uncle for all things rabbits in England",
"language": "en"
},
"friends": ["https://alice.test/"],
"nodes": [
{ "url": "/", "title": "Home" },
{ "url": "/about/", "title": "About" },
Expand Down
7 changes: 6 additions & 1 deletion fixtures/tests/pipeline.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ describe("build pipeline", () => {
}
});

test("friends lists Bob as a friend", () => {
expect(aliceGraph.friends).toEqual(["https://bob.test/"]);
});

test("Alice's friend edges target URLs that exist in Bob's graph", () => {
const bobGraph: GraphGardenFile = JSON.parse(readFileSync(BOB_JSON_PATH, "utf-8"));

Expand Down Expand Up @@ -239,12 +243,13 @@ describe("bob mock server", () => {
generated_at: "2025-01-01T00:00:00Z",
base_url: "https://local.test/",
site: { title: "Local" },
friends: [`${bobUrl}/`],
nodes: [{ url: "/", title: "Home" }],
edges: [{ source: "/", target: `${bobUrl}/`, type: "friend" }],
};
const graph = buildGraph(localFile, DEFAULT_CONFIG);

await fetchFriendGraphs(graph, DEFAULT_CONFIG);
await fetchFriendGraphs(graph, DEFAULT_CONFIG, localFile.friends);

for (const node of bobGraph.nodes) {
const absoluteUrl = new URL(node.url, bobGraph.base_url).href;
Expand Down
Loading