From a6b974f3c62ccad610e162fc2dd4d546c514164b Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:51:25 -0500 Subject: [PATCH 1/9] Set SITE_URL for smoke container --- .github/workflows/pages.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 8ebb74a..f393095 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -99,6 +99,7 @@ jobs: - name: Start Unfold container run: | docker run -d --rm -p 3000:3000 \ + -e SITE_URL=http://localhost:3000 \ -e VAULT_PATH=vault \ -v "$PWD/vault:/workspaces/schema-nodes/vault" \ --name unfold-smoke \ From 31f233e07b1868b8c81e9e4a01a95ea0c5ce4379 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:53:37 -0500 Subject: [PATCH 2/9] Remove duplicate getSiteDest --- src/unfold/site/site.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/unfold/site/site.ts b/src/unfold/site/site.ts index e513502..fe0dc7f 100644 --- a/src/unfold/site/site.ts +++ b/src/unfold/site/site.ts @@ -56,9 +56,6 @@ const getVaultPath = (): string => { return override; }; -const getSiteDest = (): string => - Deno.env.get("SITE_OUTPUT_DIR")?.trim() || ".unfold/site"; - const getSiteDest = (): string => Deno.env.get("SITE_OUTPUT_DIR")?.trim() || ".unfold/site"; From f7f410f0967d500d837300d8b81093bc08c1d72b Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:00:13 -0500 Subject: [PATCH 3/9] Skip JSON-LD validation when vault is empty --- src/unfold/pipeline/validate.ts | 13 +++++++++++++ src/unfold/pipeline/validate_test.ts | 27 +++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/unfold/pipeline/validate.ts b/src/unfold/pipeline/validate.ts index 1717f83..ded7a0b 100644 --- a/src/unfold/pipeline/validate.ts +++ b/src/unfold/pipeline/validate.ts @@ -111,6 +111,19 @@ export const runValidate = async ( await deps.writeError(errorLines.join("\n")); throw new Error("JSON-LD load failed"); } + if (nodes.length === 0) { + const lines = [ + `JSON-LD validation skipped (mode: ${mode}):`, + ` - no JSON-LD nodes found in ${vaultPath}`, + "Hint: export JSON-LD into the vault or set VAULT_BASE_URL.", + ]; + if (mode === "strict") { + await deps.writeError(lines.join("\n")); + throw new Error("JSON-LD validation failed"); + } + await deps.writeWarning(lines.join("\n")); + return; + } if (mode === "strict") { const jsonSchemaErrors = await deps.validateNodesJsonSchema(nodes); diff --git a/src/unfold/pipeline/validate_test.ts b/src/unfold/pipeline/validate_test.ts index bf6dacf..273c874 100644 --- a/src/unfold/pipeline/validate_test.ts +++ b/src/unfold/pipeline/validate_test.ts @@ -1,14 +1,22 @@ import { assert, assertRejects } from "@std/assert"; -import { ValidationError } from "../inputs/jsonld/types.ts"; +import { ValidationError, type VaultNode } from "../inputs/jsonld/types.ts"; import { ORPHAN_REACHABILITY_PREFIX } from "../inputs/jsonld/validator_reachability.ts"; import { runValidate, type ValidationDeps } from "./validate.ts"; +const SAMPLE_NODES: VaultNode[] = [ + { + "@type": "Catalog", + "@id": "https://example.org/catalog", + _source: { file: "vault/catalog.jsonld", path: "vault/catalog.jsonld" }, + }, +]; + const createBaseDeps = ( overrides: Partial = {}, ): Partial => ({ prepareVault: async () => {}, getVaultPath: () => "/vault", - loadVault: async () => ({ nodes: [], errors: [] }), + loadVault: async () => ({ nodes: SAMPLE_NODES, errors: [] }), validateNodesJsonSchema: async () => [], validateNodes: async () => [], loadShapes: async () => ({ shapes: [] }), @@ -22,6 +30,21 @@ const createBaseDeps = ( ...overrides, }); +Deno.test("runValidate - dev mode warns when no JSON-LD nodes found", async () => { + const warnings: string[] = []; + const deps = createBaseDeps({ + loadVault: async () => ({ nodes: [], errors: [] }), + writeWarning: async (text: string) => { + warnings.push(text); + }, + }); + + await runValidate({ mode: "dev", deps }); + + assert(warnings.length === 1); + assert(warnings[0].includes("no JSON-LD nodes found")); +}); + Deno.test("runValidate - dev mode warns on orphan reachability", async () => { const warnings: string[] = []; const deps = createBaseDeps({ From 50a646def990436be194b00ccc4161b2042978fe Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:01:08 -0500 Subject: [PATCH 4/9] Gate smoke job on build --- .github/workflows/pages.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index f393095..4cd550d 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -78,6 +78,7 @@ jobs: container-smoke: runs-on: ubuntu-latest + needs: build steps: - name: Checkout uses: actions/checkout@v4 @@ -117,7 +118,7 @@ jobs: publish-image: runs-on: ubuntu-latest - needs: container-smoke + needs: [build, container-smoke] steps: - name: Checkout uses: actions/checkout@v4 From f9eb928369c24f6dac64e3a784b13e8b6c7cf180 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:02:18 -0500 Subject: [PATCH 5/9] Preserve smoke container for logs --- .github/workflows/pages.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4cd550d..80cdd2f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -99,22 +99,28 @@ jobs: - name: Start Unfold container run: | - docker run -d --rm -p 3000:3000 \ + docker run -d -p 3000:3000 \ -e SITE_URL=http://localhost:3000 \ -e VAULT_PATH=vault \ -v "$PWD/vault:/workspaces/schema-nodes/vault" \ --name unfold-smoke \ unfold-dev-ci:latest for i in $(seq 1 30); do + running=$(docker inspect -f '{{.State.Running}}' unfold-smoke 2>/dev/null || echo "false") + if [ "$running" != "true" ]; then + echo "Container exited before health check passed." + docker logs unfold-smoke || true + exit 1 + fi curl -fsS http://localhost:3000/healthz >/dev/null 2>&1 && exit 0 sleep 1 done - docker logs unfold-smoke + docker logs unfold-smoke || true exit 1 - name: Stop Unfold container if: always() - run: docker stop unfold-smoke + run: docker rm -f unfold-smoke || true publish-image: runs-on: ubuntu-latest From 9f3f3f9628fc7cff12c9967d8ac9c78fd8bf0e19 Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:06:17 -0500 Subject: [PATCH 6/9] Fix AI exporters test path --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 80cdd2f..183736c 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -65,7 +65,7 @@ jobs: VAULT_PATH: vault - name: Validate AI exports - run: deno test -A src/unfold/tests/exporters_ai_test.ts + run: deno test -A src/unfold/exporters/exporters_ai_test.ts env: SITE_URL: ${{ steps.site.outputs.site_url }} SITE_OUTPUT_DIR: .unfold/site From 60afe82890eb7022c9591fb2f4852b3b856d938e Mon Sep 17 00:00:00 2001 From: Robert DeLanghe <1240090+bdelanghe@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:30:14 -0500 Subject: [PATCH 7/9] Fix JSON-LD site build pipeline --- src/unfold/exporters/llms.ts | 2 + src/unfold/exporters/mcp.ts | 4 ++ src/unfold/inputs/vault/load_vault.ts | 2 +- src/unfold/renderers/lume/jsonld_loader.ts | 38 +++++++++------ src/unfold/site/site.ts | 56 ++++++++++++++++++---- 5 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/unfold/exporters/llms.ts b/src/unfold/exporters/llms.ts index 381ebdf..251f4c6 100644 --- a/src/unfold/exporters/llms.ts +++ b/src/unfold/exporters/llms.ts @@ -79,6 +79,8 @@ export const buildLlmsTxt = async ( const url = resolveCanonical(page, siteUrl); const title = normalizeText( page.document?.querySelector("title")?.textContent, + ) || normalizeText( + typeof page.data?.title === "string" ? page.data.title : "", ); if (!url) { return null; diff --git a/src/unfold/exporters/mcp.ts b/src/unfold/exporters/mcp.ts index 6816387..ba0ba24 100644 --- a/src/unfold/exporters/mcp.ts +++ b/src/unfold/exporters/mcp.ts @@ -108,11 +108,15 @@ export const buildMcpBundle = async ( const url = resolveCanonical(page, siteUrl); const title = normalizeText( page.document?.querySelector("title")?.textContent, + ) || normalizeText( + typeof page.data?.title === "string" ? page.data.title : "", ); const description = normalizeText( page.document?.querySelector('meta[name="description"]')?.getAttribute( "content", ), + ) || normalizeText( + typeof page.data?.description === "string" ? page.data.description : "", ); if (!url || !title) { return null; diff --git a/src/unfold/inputs/vault/load_vault.ts b/src/unfold/inputs/vault/load_vault.ts index 3eadfe9..91d33ce 100644 --- a/src/unfold/inputs/vault/load_vault.ts +++ b/src/unfold/inputs/vault/load_vault.ts @@ -1,5 +1,5 @@ import { walk } from "@std/fs"; -import { relative } from "@std/path"; +import { isAbsolute, join, relative, toFileUrl } from "@std/path"; import { vaultConfig, DEFAULT_VAULT_DIRS } from "./vault_config.ts"; export const loadVaultRoot = (): URL => { diff --git a/src/unfold/renderers/lume/jsonld_loader.ts b/src/unfold/renderers/lume/jsonld_loader.ts index bde885f..c6e8dd4 100644 --- a/src/unfold/renderers/lume/jsonld_loader.ts +++ b/src/unfold/renderers/lume/jsonld_loader.ts @@ -19,20 +19,20 @@ type LoaderFunction = (path: string) => Promise; * Extract page data from a JSON-LD node * Maps JSON-LD properties to Lume page data */ -function extractPageData(node: JsonLdNode): PageData { +function extractPageData(node: JsonLdNode, sourcePath: string): PageData { const extractedDate = extractDate(node); const data: PageData = { // Required Lume fields - url: extractUrl(node), - title: extractTitle(node), + url: extractUrl(node, sourcePath), + title: extractTitle(node, sourcePath), date: extractedDate || new Date(), // Lume requires Date, not undefined // Optional fields description: extractString(node, "description"), // Content - content: extractContent(node), + content: extractContent(node, sourcePath), // JSON-LD metadata (full node for