Repository navigation
Conversation
RoCrateExport::DetachedCrate builds an RO-Crate 1.3 metadata document for a single resource from its Bioschemas representation: the TeSS page URL is the Root Data Entity (typed Dataset plus the Bioschemas type), nested objects are flattened into the @graph, the Bioschemas profile is declared with conformsTo, and properties not defined by the RO-Crate context are left out. RoCrateExport::Identifier normalises DOIs (bare, doi:, http(s)://doi.org/, dx.doi.org/) to https://doi.org/ URLs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Material and event pages now advertise FAIR Signposting links, in the HTTP Link header and as <link> elements in the HTML head: describedby for the Bioschemas JSON-LD, the Detached RO-Crate and the JSON-API; cite-as for the DOI; author for ORCIDs; license; and type. The Detached RO-Crate is served at /materials/:id/ro-crate-metadata.json and /events/:id/ro-crate-metadata.json, and on the page URL by content negotiation for application/ld+json with the RO-Crate profile. Rails' own asset preload links in the Link header have unquoted attribute values (base64 integrity hashes) and a valueless nopush, which make strict Link header parsers reject the whole header. These values are now quoted and nopush is dropped where signposting is added. Modelled on SEEK's FairSignposting concern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python tests in test/signposting use the signposting library as an independent client against a running TeSS: they check the Link header and HTML <link> elements of material and event pages, follow describedby to the Detached RO-Crate, check content negotiation and DOI rewriting, and validate the crates with rocrate-validator. A GitHub Actions workflow loads the test fixtures, starts TeSS and runs them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the Python signposting tests as an opt-in suite run against a running TeSS, rather than adding a second CI job that duplicates the setup of the main test workflow. CI integration can be discussed with the maintainers separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Signposting Sniffing browser extension only looks at the first rel of a Link header and ignored the whole header when it started with Rails' asset preload links. Emit the signposting links first and keep the preload links after them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stain
marked this pull request as draft
October 22, 2026 22:24
The Root Data Entity is now a plain Dataset for the TeSS record, with TeSS as publisher, instead of the resource's Bioschemas type mixed into the Dataset. The Material or Event is the root's mainEntity, with its Bioschemas properties, profile and sdPublisher, identified by its external URL (or a local identifier if it has no valid URL) rather than a plain url string. A Material is also a data entity of the crate (hasPart); an Event is not. Give SPDX licences as SPDX license URIs (http://spdx.org/licenses/<id>), which match SPDX's RDF, in the crate and the Signposting license link, instead of the HTML page from the licence dictionary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…page Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved metadata exposure and unsafe Link serialization block approval, alongside licence handling, negotiation and test assertion defects.
6 open issues
Validate and encode DOI-derived Link header targets · New Honor Accept quality and profile parameters during content negotiation · New Prevent forbidden record metadata from appearing in error-page links · New Use normalized licence values for choices without reference URLs · New Fall back to licence dictionary URL when reference is absent · New Preserve singleton Hash relationships in exporter assertions · New
What changed in this PR
Adds machine-readable discovery and Detached RO-Crate exports for TeSS materials and events, building on the existing Bioschemas generators.
Changes:
- Publishes FAIR Signposting links in HTTP headers and HTML, while preserving asset preload hints.
- Adds RO-Crate endpoints, profile-based content negotiation, and identifier normalization.
- Documents the interfaces and adds Rails tests plus opt-in Python interoperability checks.
| File | Description |
|---|---|
test/unit/ro_crate_export_identifier_test.rb |
Tests DOI and licence URL normalization. |
test/signposting/test_signposting.py |
Checks discovery and crate interoperability against a live server. |
test/signposting/requirements.txt |
Defines Python test dependencies. |
test/integration/fair_signposting_test.rb |
Tests signposting, exports, negotiation and access control. |
test/controllers/about_controller_test.rb |
Checks developer documentation content. |
lib/ro_crate_export/identifier.rb |
Normalizes DOI and licence identifiers. |
lib/ro_crate_export/detached_crate.rb |
Builds flattened Detached RO-Crate graphs. |
docs/api.md |
Documents discovery and export interfaces. |
config/routes.rb |
Adds material and event crate routes. |
app/views/layouts/_head.html.erb |
Renders HTML signposting links. |
app/views/about/developers.erb |
Adds discovery and export examples. |
app/controllers/materials_controller.rb |
Integrates material signposting and crate negotiation. |
app/controllers/events_controller.rb |
Integrates event signposting and crate negotiation. |
app/controllers/concerns/fair_signposting.rb |
Shares link generation, header handling and crate rendering. |
app/controllers/application_controller.rb |
Allows anonymous crate requests, retaining action-level authorization. |
.gitignore |
Ignores Python bytecode artifacts. |
Review effort: Balanced
| return if links.empty? | ||
|
|
||
| signposting = links.map do |url, attributes| | ||
| (["<#{url}>"] + attributes.map { |key, value| %(#{key}="#{value}") }).join(' ; ') |
| # "Accept: application/ld+json; profile="https://w3id.org/ro/crate"". | ||
| def ro_crate_requested? | ||
| profile = Regexp.escape(RoCrateExport::DetachedCrate::PROFILE) | ||
| request.headers['Accept'].to_s.match?(%r{application/ld\+json\s*;[^,]*profile="?#{profile}(?=["\s,;]|\z)}i) |
| # An array of [url, { attribute => value }] pairs. | ||
| def fair_signposting_links | ||
| resource = fair_signposting_resource | ||
| return [] unless resource&.persisted? |
| 'name' => main['name'], | ||
| 'description' => main['description'], | ||
| 'datePublished' => date_published, | ||
| 'license' => licence || NO_LICENCE, |
| def self.licence_url(licence) | ||
| return nil if licence.blank? | ||
|
|
||
| reference = LicenceDictionary.instance.lookup_value(licence, 'reference').presence |
| crate['@graph'].each do |entity| | ||
| assert entity['@type'].present?, "#{entity['@id']} has no @type" | ||
| entity.except('@id', '@type').each_value do |value| | ||
| Array(value).grep(Hash).each do |ref| |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary of changes
Linkheader and as<link>elements in the HTML<head>:describedby→ Bioschemas JSON-LD (.jsonld, with the Bioschemas profile), Detached RO-Crate (withprofile="https://w3id.org/ro/crate") and JSON-APIcite-as→ the DOI, normalised tohttps://doi.org/…(also fromdoi:, bare10.…,http://doi.org/,http://dx.doi.org/)author→ authors' ORCIDs,license→ SPDX licence URL,type→ schema.org type +AboutPagelib/ro_crate_export/):GET /materials/:id/ro-crate-metadata.jsonandGET /events/:id/ro-crate-metadata.jsonAccept: application/ld+json; profile="https://w3id.org/ro/crate"(plainapplication/ld+jsonstill returns Bioschemas as before)Dataset+ the Bioschemas type; nested objects are flattened; the Bioschemas profile is declared viaconformsTo; properties not defined by the RO-Crate context (contact,eligibility,event_types,hostInstitution) are left out of the crate.Linkentries, which have unquoted values (integrity=sha256-…=) and a valuelessnopush. Strict parsers (e.g.httplink, used by the Pythonsignpostinglibrary) reject the whole header because of these, including any signposting links. On pages with signposting the values are now quoted andnopushdropped; the preload hints are kept.test/integration/fair_signposting_test.rb,test/unit/ro_crate_export_identifier_test.rb(new code has 100% line coverage)test/signposting/using the signposting library as an independent client, plus rocrate-validator (no REQUIRED issues). These run against a live TeSS server, so they are opt-in and not part of CI. Happy to discuss adding them totest.ymlif wanted.docs/api.md.The design follows SEEK's
FairSignpostingconcern.Motivation and context
TeSS already publishes Bioschemas markup, but machine clients have no standard way to discover it or other representations from a record's URL. FAIR Signposting provides that discovery through typed links, and it is what tools such as the RO-Crate retrieval algorithm, F-UJI and other FAIR assessment tools look for. A Detached RO-Crate makes each TeSS record consumable by RO-Crate tooling (WorkflowHub, rocrate-validator, ro-crate-py/js/ruby). It is also a first step towards packaging learning paths and collections as RO-Crates.
Points for review:
cite-asuses the material's DOI. That DOI usually resolves to the material itself (e.g. Zenodo), not to the TeSS record, so it is used for Signposting but deliberately not ascite-ason the crate's Root Data Entity. RO-Crate 1.3 requires a rootcite-asto resolve to the crate itself.ro_crateaction. This avoids it being picked when Rails generates URLs foraction: :show.Linkheaders and.jsonld. Signposting will only be useful to machines if the challenge allows these requests, which is a deployment question.Screenshots
Example
Linkheader (test fixtures, preload links omitted):Example RO-Crate
Checklist
Notes from Claude
What it does
Linkheader and as<link>elements in the HTML head:cite-asfor the DOI, rewritten to https://doi.org/ (this covers doi:, a bare 10.…, http://doi.org/ and http://dx.doi.org/)authorfor ORCIDs,license, andtype/materials/:id/ro-crate-metadata.jsonand/events/:id/ro-crate-metadata.json. It's also served on the page URL to requests whoseAcceptheader hasapplication/ld+json; profile="https://w3id.org/ro/crate". The crate is built from the existing Bioschemas output.a. the crate generator and DOI helper
b. the Signposting concern and routes
c. the Python tests and a CI workflow
d. the docs in docs/api.md
Tests
Things you should know