From 1049a58d70f77e6578b09c74e41536803aad932c Mon Sep 17 00:00:00 2001 From: "@s.roertgen" Date: Tue, 16 Apr 2024 14:11:07 +0200 Subject: [PATCH 1/3] 284-other-attr-for-cs --- gatsby-node.js | 2 + shapes/skohub.shacl.ttl | 36 ++++++-- src/components/ConceptScheme.jsx | 7 +- src/components/header.jsx | 2 + src/context.js | 4 + src/pages/index.js | 8 +- src/queries.js | 6 ++ src/types.js | 2 + test/conceptScheme.test.jsx | 117 ++++++++++++++++++++++++ test/data/ttl/slashURIConceptScheme.ttl | 2 +- 10 files changed, 174 insertions(+), 12 deletions(-) create mode 100644 test/conceptScheme.test.jsx diff --git a/gatsby-node.js b/gatsby-node.js index 04fa0d7..dd869fc 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -402,6 +402,8 @@ exports.createPages = async ({ graphql, actions: { createPage } }) => { conceptSchemes.data.allConceptScheme.edges.map(({ node: cs }) => ({ id: cs.id, title: cs.title, + dctitle: cs.dctitle, + prefLabel: cs.prefLabel, description: cs.description, languages: Array.from(languagesByCS[cs.id]), })) diff --git a/shapes/skohub.shacl.ttl b/shapes/skohub.shacl.ttl index eb148af..ecb16f9 100644 --- a/shapes/skohub.shacl.ttl +++ b/shapes/skohub.shacl.ttl @@ -7,6 +7,7 @@ @prefix rdfs: . @prefix skos: . @prefix dct: . +@prefix dc: . @prefix sdo: . @prefix vann: . @@ -17,14 +18,33 @@ :ConceptSchemeShape a sh:NodeShape ; sh:targetClass skos:ConceptScheme ; - sh:property [ - sh:path dct:title ; - sh:minCount 1; - sh:datatype rdf:langString ; - sh:message "Concept Scheme has no dct:title with a language tag!" ; - sh:severity sh:Violation ; - rdfs:comment "Tested with 01_cs_no_title.ttl" ; - ] ; + sh:or ( + [ + sh:path dct:title ; + sh:minCount 1; + sh:datatype rdf:langString ; + sh:message "Concept Scheme has no dct:title with a language tag!" ; + sh:severity sh:Violation ; + rdfs:comment "Tested with 01_cs_no_title.ttl" ; + ] + [ + sh:path skos:prefLabel ; + sh:minCount 1; + sh:datatype rdf:langString ; + sh:message "Concept Scheme has no skos:prefLabel with a language tag!" ; + sh:severity sh:Violation ; + rdfs:comment "Tested with 01_cs_no_title.ttl" ; + ] + [ + sh:path dc:title ; + sh:minCount 1; + sh:datatype rdf:langString ; + sh:message "Concept Scheme has no dc:title with a language tag!" ; + sh:severity sh:Violation ; + rdfs:comment "Tested with 01_cs_no_title.ttl" ; + ] + + ); sh:property [ sh:path skos:hasTopConcept ; sh:minCount 1 ; diff --git a/src/components/ConceptScheme.jsx b/src/components/ConceptScheme.jsx index 801f4d3..10bddd8 100644 --- a/src/components/ConceptScheme.jsx +++ b/src/components/ConceptScheme.jsx @@ -36,7 +36,12 @@ const ConceptScheme = ({ id={getDomId(conceptScheme.id)} >
-

{i18n(language)(conceptScheme.title)}

+

+ {(conceptScheme?.title && i18n(language)(conceptScheme.title)) || + (conceptScheme?.prefLabel && + i18n(language)(conceptScheme.prefLabel)) || + (conceptScheme?.dctitle && i18n(language)(conceptScheme.dctitle))} +

{conceptScheme.description && ( diff --git a/src/components/header.jsx b/src/components/header.jsx index 9f84a72..b57aec2 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -159,6 +159,8 @@ const Header = ({ siteTitle }) => { )} > {data.currentScheme?.title?.[data.selectedLanguage] || + data.currentScheme?.prefLabel?.[data.selectedLanguage] || + data.currentScheme?.dctitle?.[data.selectedLanguage] || data.currentScheme.id}
diff --git a/src/context.js b/src/context.js index 0ae32b3..27ca751 100644 --- a/src/context.js +++ b/src/context.js @@ -14,6 +14,10 @@ const jsonld = { "@id": "dct:title", "@container": "@language", }, + dctitle: { + "@id": "http://purl.org/dc/elements/1.1/title", + "@container": "@language", + }, description: { "@id": "dct:description", "@container": "@language", diff --git a/src/pages/index.js b/src/pages/index.js index f659a6b..14f5ebd 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -58,7 +58,6 @@ const IndexPage = ({ location }) => { }) } }, [data?.languages, data?.selectedLanguage]) - return ( @@ -79,7 +78,12 @@ const IndexPage = ({ location }) => { } to={getFilePath(conceptScheme.id, `html`, customDomain)} > - {(conceptScheme.title && i18n(language)(conceptScheme.title)) || + {(conceptScheme?.title && + i18n(language)(conceptScheme.title)) || + (conceptScheme?.prefLabel && + i18n(language)(conceptScheme.prefLabel)) || + (conceptScheme?.dctitle && + i18n(language)(conceptScheme.dctitle)) || conceptScheme.id} diff --git a/src/queries.js b/src/queries.js index da97cb4..9f6dcce 100644 --- a/src/queries.js +++ b/src/queries.js @@ -139,6 +139,12 @@ module.exports.allConceptScheme = (languages) => ` title { ${[...languages].join(" ")} } + prefLabel { + ${[...languages].join(" ")} + } + dctitle { + ${[...languages].join(" ")} + } description { ${[...languages].join(" ")} } diff --git a/src/types.js b/src/types.js index 911dc60..517e642 100644 --- a/src/types.js +++ b/src/types.js @@ -9,6 +9,8 @@ module.exports = (languages) => ` type ConceptScheme implements Node { type: String, title: LanguageMap, + dctitle: LanguageMap, + prefLabel: LanguageMap, description: LanguageMap, hasTopConcept: [Concept] @link(from: "hasTopConcept___NODE"), languages: [String] diff --git a/test/conceptScheme.test.jsx b/test/conceptScheme.test.jsx new file mode 100644 index 0000000..30a5be4 --- /dev/null +++ b/test/conceptScheme.test.jsx @@ -0,0 +1,117 @@ +import { describe, expect, it, vi } from "vitest" +import { render, screen, within } from "@testing-library/react" +import * as Gatsby from "gatsby" + +import React from "react" +import ConceptScheme from "../src/components/ConceptScheme.jsx" +import { ConceptSchemePC } from "./data/pageContext" +import mockFetch from "./mocks/mockFetch" +import { mockConfig } from "./mocks/mockConfig" +import { + createHistory, + createMemorySource, + LocationProvider, +} from "@gatsbyjs/reach-router" +import { ContextProvider, useSkoHubContext } from "../src/context/Context" + +const useStaticQuery = vi.spyOn(Gatsby, `useStaticQuery`) + +function renderConceptScheme(history, pageContext, location, children = null) { + return render( + + + + + + ) +} +describe.concurrent("Concept", () => { + afterEach(() => { + vi.clearAllMocks() + }) + vi.spyOn(window, "fetch").mockImplementation(mockFetch) + useStaticQuery.mockImplementation(() => mockConfig) + vi.mock("../src/context/Context.jsx", async () => { + const actual = await vi.importActual("../src/context/Context.jsx") + return { + ...actual, + useSkoHubContext: vi.fn(), + } + }) + + it("renders conceptScheme component", () => { + useSkoHubContext.mockReturnValue({ + data: { + currentScheme: {}, + selectedLanguage: "de", + }, + updateState: vi.fn(), + }) + + const route = "/w3id.org/index.html" + const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } + renderConceptScheme(history, ConceptSchemePC, location) + expect( + screen.getByRole("heading", { name: /Test Vokabular/i }) + ).toBeInTheDocument() + }) + + it("renders conceptScheme component with skos:prefLabel", () => { + useSkoHubContext.mockReturnValue({ + data: { + currentScheme: {}, + selectedLanguage: "de", + }, + updateState: vi.fn(), + }) + const ConceptSchemePCprefLabel = { + ...ConceptSchemePC, + node: { + ...ConceptSchemePC.node, + prefLabel: { + de: "PrefLabel DE", + }, + }, + } + delete ConceptSchemePCprefLabel["node"]["title"] + const route = "/w3id.org/index.html" + const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } + renderConceptScheme(history, ConceptSchemePCprefLabel, location) + expect( + screen.getByRole("heading", { name: /PrefLabel DE/i }) + ).toBeInTheDocument() + }) + + it("renders conceptScheme component with dctitle", () => { + useSkoHubContext.mockReturnValue({ + data: { + currentScheme: {}, + selectedLanguage: "de", + }, + updateState: vi.fn(), + }) + const ConceptSchemePCprefLabel = { + ...ConceptSchemePC, + node: { + ...ConceptSchemePC.node, + dctitle: { + de: "dctitle DE", + }, + }, + } + delete ConceptSchemePCprefLabel["node"]["title"] + const route = "/w3id.org/index.html" + const history = createHistory(createMemorySource(route)) + const location = { search: "?lang=de" } + renderConceptScheme(history, ConceptSchemePCprefLabel, location) + expect( + screen.getByRole("heading", { name: /dctitle DE/i }) + ).toBeInTheDocument() + }) +}) diff --git a/test/data/ttl/slashURIConceptScheme.ttl b/test/data/ttl/slashURIConceptScheme.ttl index c04ff56..e2512b7 100644 --- a/test/data/ttl/slashURIConceptScheme.ttl +++ b/test/data/ttl/slashURIConceptScheme.ttl @@ -27,7 +27,7 @@ a skos:Concept ; skos:prefLabel "Konzept 2"@de, "Concept 2"@en ; skos:narrower ; - skos:broader ; + skos:broader ; skos:inScheme <> . a skos:Concept ; From 0b8a809c98a57284ba5ee48df589c3779523084a Mon Sep 17 00:00:00 2001 From: "@s.roertgen" Date: Wed, 24 Apr 2024 12:02:26 +0200 Subject: [PATCH 2/3] Add dc:description, add tests for meta tags in head, fix attrs passed to SEO #284 --- cypress/e2e/head.cy.js | 15 +++++++++++++++ src/components/Concept.jsx | 10 ++++++---- src/components/ConceptScheme.jsx | 19 ++++++++----------- src/context.js | 4 ++++ src/queries.js | 3 +++ src/templates/App.jsx | 15 ++++++--------- src/types.js | 1 + 7 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 cypress/e2e/head.cy.js diff --git a/cypress/e2e/head.cy.js b/cypress/e2e/head.cy.js new file mode 100644 index 0000000..2695be3 --- /dev/null +++ b/cypress/e2e/head.cy.js @@ -0,0 +1,15 @@ +describe("Concept Scheme has correct head tags", () => { + it("has correct title", () => { + cy.visit("/w3id.org/index.html", { + onBeforeLoad(win) { + Object.defineProperty(win.navigator, "language", { value: "de-DE" }) + }, + }) + cy.title().should("eq", "Test Vokabular | SkoHub Vocabs") + cy.get(`head > meta[name="keywords"]`).should( + "have.attr", + "content", + "Concept, Test Vokabular" + ) + }) +}) diff --git a/src/components/Concept.jsx b/src/components/Concept.jsx index b148571..e2522fd 100644 --- a/src/components/Concept.jsx +++ b/src/components/Concept.jsx @@ -13,6 +13,9 @@ const Concept = ({ const { config, conceptSchemes } = getConfigAndConceptSchemes() const { data } = useSkoHubContext() const [language, setLanguage] = useState("") + const definition = + concept?.definition || concept?.description || concept?.dcdescription + const title = concept?.prefLabel || concept?.title || concept?.dctitle useEffect(() => { setLanguage(data.selectedLanguage) @@ -25,8 +28,7 @@ const Concept = ({

{concept.notation && {concept.notation.join(",")} } - {(concept?.prefLabel && i18n(language)(concept.prefLabel)) || - (concept?.title && i18n(language)(concept.title))} + {title && i18n(language)(title)}

@@ -44,11 +46,11 @@ const Concept = ({ )} - {concept.definition && ( + {definition && (

Definition

- {i18n(language)(concept.definition) || + {i18n(language)(definition) || `*No definition in language "${language}" provided.*`}
diff --git a/src/components/ConceptScheme.jsx b/src/components/ConceptScheme.jsx index 8029cb4..76ebf33 100644 --- a/src/components/ConceptScheme.jsx +++ b/src/components/ConceptScheme.jsx @@ -17,30 +17,27 @@ const ConceptScheme = ({ }, [data?.selectedLanguage]) const pathname = useLocation() - + const description = conceptScheme?.description || conceptScheme?.dcdescription + const title = + conceptScheme?.title || conceptScheme?.dctitle || conceptScheme?.prefLabel // got some hash uri to show if (pathname.hash) { - const filtered = embed.filter((c) => c.json.id.endsWith(pathname.hash)) + const filtered = embed.find((c) => c.json.id.endsWith(pathname.hash)) return (
- +
) } else { return (
-

- {(conceptScheme?.title && i18n(language)(conceptScheme.title)) || - (conceptScheme?.prefLabel && - i18n(language)(conceptScheme.prefLabel)) || - (conceptScheme?.dctitle && i18n(language)(conceptScheme.dctitle))} -

+

{title && i18n(language)(title)}

- {conceptScheme.description && ( + {description && (
- {i18n(language)(conceptScheme.description)} + {i18n(language)(description)}
)}
diff --git a/src/context.js b/src/context.js index 50829d6..ad93f9b 100644 --- a/src/context.js +++ b/src/context.js @@ -18,6 +18,10 @@ const jsonld = { "@id": "http://purl.org/dc/elements/1.1/title", "@container": "@language", }, + dcdescription: { + "@id": "http://purl.org/dc/elements/1.1/description", + "@container": "@language", + }, description: { "@id": "dct:description", "@container": "@language", diff --git a/src/queries.js b/src/queries.js index 7fe8b12..01fc881 100644 --- a/src/queries.js +++ b/src/queries.js @@ -157,6 +157,9 @@ module.exports.allConceptScheme = (languages) => ` description { ${[...languages].join(" ")} } + dcdescription { + ${[...languages].join(" ")} + } hasTopConcept { ...ConceptFields narrower { diff --git a/src/templates/App.jsx b/src/templates/App.jsx index b9a6c93..f30936d 100644 --- a/src/templates/App.jsx +++ b/src/templates/App.jsx @@ -160,19 +160,16 @@ const App = ({ pageContext, children, location }) => { }) const toggleClick = (e) => setLabels({ ...labels, [e]: !labels[e] }) + const title = + pageContext.node?.prefLabel || + pageContext.node?.title || + pageContext.node?.dctitle return (
diff --git a/src/context.js b/src/context.js index ad93f9b..843a3cd 100644 --- a/src/context.js +++ b/src/context.js @@ -6,6 +6,7 @@ const jsonld = { "@vocab": "http://www.w3.org/2004/02/skos/core#", xsd: "http://www.w3.org/2001/XMLSchema#", dct: "http://purl.org/dc/terms/", + dc: "http://purl.org/dc/elements/1.1/", schema: "https://schema.org/", vann: "http://purl.org/vocab/vann/", ldp: "http://www.w3.org/ns/ldp#", @@ -14,12 +15,12 @@ const jsonld = { "@id": "dct:title", "@container": "@language", }, - dctitle: { - "@id": "http://purl.org/dc/elements/1.1/title", + "dc:title": { + "@id": "dc:title", "@container": "@language", }, - dcdescription: { - "@id": "http://purl.org/dc/elements/1.1/description", + "dc:description": { + "@id": "dc:description", "@container": "@language", }, description: { diff --git a/src/pages/index.js b/src/pages/index.js index 14f5ebd..bcec5c3 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -58,6 +58,20 @@ const IndexPage = ({ location }) => { }) } }, [data?.languages, data?.selectedLanguage]) + + const getTitle = (conceptScheme) => { + const title = + i18n(language)( + conceptScheme?.title || + conceptScheme?.prefLabel || + conceptScheme?.dc_title + ) || conceptScheme.id + if (title) { + return title + } + return conceptScheme.id + } + return ( @@ -78,13 +92,7 @@ const IndexPage = ({ location }) => { } to={getFilePath(conceptScheme.id, `html`, customDomain)} > - {(conceptScheme?.title && - i18n(language)(conceptScheme.title)) || - (conceptScheme?.prefLabel && - i18n(language)(conceptScheme.prefLabel)) || - (conceptScheme?.dctitle && - i18n(language)(conceptScheme.dctitle)) || - conceptScheme.id} + {getTitle(conceptScheme)} ))} diff --git a/src/queries.js b/src/queries.js index 01fc881..9270664 100644 --- a/src/queries.js +++ b/src/queries.js @@ -151,13 +151,13 @@ module.exports.allConceptScheme = (languages) => ` prefLabel { ${[...languages].join(" ")} } - dctitle { + dc_title { ${[...languages].join(" ")} } description { ${[...languages].join(" ")} } - dcdescription { + dc_description { ${[...languages].join(" ")} } hasTopConcept { diff --git a/src/templates/App.jsx b/src/templates/App.jsx index f30936d..3923cf5 100644 --- a/src/templates/App.jsx +++ b/src/templates/App.jsx @@ -158,12 +158,11 @@ const App = ({ pageContext, children, location }) => { inline: "nearest", }) }) - const toggleClick = (e) => setLabels({ ...labels, [e]: !labels[e] }) const title = pageContext.node?.prefLabel || pageContext.node?.title || - pageContext.node?.dctitle + pageContext.node?.dc_title return ( diff --git a/src/types.js b/src/types.js index b90001d..48bcd1e 100644 --- a/src/types.js +++ b/src/types.js @@ -9,10 +9,10 @@ module.exports = (languages) => ` type ConceptScheme implements Node { type: String, title: LanguageMap, - dctitle: LanguageMap, + dc_title: LanguageMap, prefLabel: LanguageMap, description: LanguageMap, - dcdescription: LanguageMap, + dc_description: LanguageMap, hasTopConcept: [Concept] @link(from: "hasTopConcept___NODE"), languages: [String] } diff --git a/test/common.test.js b/test/common.test.js index 01ec492..b637ef2 100644 --- a/test/common.test.js +++ b/test/common.test.js @@ -6,6 +6,8 @@ const { replaceFilePathInUrl, getLinkPath, getLanguageFromUrl, + replaceKeyInObject, + replaceMultipleKeysInObject, } = require("../src/common") describe("Translate", () => { @@ -91,3 +93,28 @@ describe("getLanguageFromUrl", () => { expect(getLanguageFromUrl(location)).toBeNull() }) }) + +describe("replaceKeysInObject", () => { + it("replaces key in an object", () => { + const obj = { a: 1, b: 2 } + const newObj = replaceKeyInObject(obj, "a", "c") + expect(newObj).toStrictEqual({ c: 1, b: 2 }) + }) + + it("also works if the key is not present", () => { + const obj = { a: 1, b: 2 } + const newObj = replaceKeyInObject(obj, "x", "c") + expect(newObj).toStrictEqual({ a: 1, b: 2 }) + }) +}) + +describe("replaceMultipleKeysInObject", () => { + it("replaces multiple keys in an object", () => { + const obj = { a: 1, b: 2 } + const newObj = replaceMultipleKeysInObject(obj, [ + ["a", "c"], + ["b", "d"], + ]) + expect(newObj).toStrictEqual({ c: 1, d: 2 }) + }) +}) diff --git a/test/conceptScheme.test.jsx b/test/conceptScheme.test.jsx index 30a5be4..6514dab 100644 --- a/test/conceptScheme.test.jsx +++ b/test/conceptScheme.test.jsx @@ -100,7 +100,7 @@ describe.concurrent("Concept", () => { ...ConceptSchemePC, node: { ...ConceptSchemePC.node, - dctitle: { + dc_title: { de: "dctitle DE", }, }, diff --git a/test/data/ttl/slashURIConceptSchemeDCproperties.ttl b/test/data/ttl/slashURIConceptSchemeDCproperties.ttl new file mode 100644 index 0000000..b6fdbaa --- /dev/null +++ b/test/data/ttl/slashURIConceptSchemeDCproperties.ttl @@ -0,0 +1,46 @@ +@base . +@prefix dct: . +@prefix dc: . +@prefix skos: . +@prefix schema: . +@prefix vann: . + +<> a skos:ConceptScheme ; + dc:title "Test Vokabular DC"@de, "Test Vocabulary DC"@en ; + dc:description "Test Beschreibung DC"@de, "Test Description DC"@en ; + dct:issued "2019-12-11" ; + dct:publisher ; + skos:hasTopConcept . + + a skos:Concept ; + skos:prefLabel "Konzept 1"@de, "Concept 1"@en ; + skos:altLabel "Alternativbezeichnung 1"@de, "Alternativbezeichnung 2"@de, "Alt Label 1"@en ; + skos:hiddenLabel "Verstecktes Label 1"@de, "Verstecktes Label 2"@de ; + skos:definition "Meine Definition"@de ; + skos:example "Ein Beispiel"@de ; + skos:scopeNote "Meine Scope Note"@de ; + skos:note "Meine Anmerkung"@de ; + skos:relatedMatch ; + skos:narrower ; + skos:notation "1", "notation" ; + skos:topConceptOf <> . + + a skos:Concept ; + skos:prefLabel "Konzept 2"@de, "Concept 2"@en ; + skos:narrower ; + skos:broader ; + skos:inScheme <> . + + a skos:Concept ; + skos:prefLabel "Konzept 3"@de ; + skos:broader ; + skos:inScheme <> . + + a skos:Concept ; + skos:prefLabel "Konzept 4"@de ; + skos:broader ; + skos:inScheme <> . + + a skos:Collection ; + skos:prefLabel "Meine Collection"@de, "My Collection"@en ; + skos:member , .