module Jekyll::Filters::JsonldFilter

Generate JSON-LD metadata for the GTN.

Constants

A11Y
EDU_ROLES
GTN

Public Instance Methods

Generate the Dublin Core metadata for a material. Parmaeters:

material

The material to generate the metadata for.

site

The site object.

Returns: A string containing the metadata.

Example:

{{ material | generate_dublin_core: site }}
=> <meta name="DC.identifier" content="..." />
# File _plugins/jekyll-jsonld.rb, line 68
def generate_dublin_core(material, site)
  return if material.key?('data') && material['data'].fetch('type', 'none') != 'tutorial_hands_on'

  attributes = [
    ['DC.identifier', site['github_repository']],
    ['DC.type', 'text'],
    ['DC.title', material['title']],
    ['DC.publisher', 'Galaxy Training Network'],
    ['DC.date', Gtn::ModificationTimes.obtain_time(material['path'])]
  ]

  attributes += Gtn::Contributors.get_authors(material).map do |user|
    ['DC.creator', Gtn::Contributors.fetch_name(site, user)]
  end

  attributes.map { |a, b| "<meta name=\"#{a}\" content=\"#{b}\">" }.join("\n")
end

Generate the JSON-LD metadata for an event Parameters:

page

The page object.

site

The Jekyll::Site site object.

Returns:

Hash

The JSON-LD metadata.

Examples:

{{ page | generate_event_jsonld: site }}
# File _plugins/jekyll-jsonld.rb, line 299
def generate_event_jsonld(page, site)
  organisers = Gtn::Contributors.get_organisers(page.to_h).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end
  instructors = Gtn::Contributors.get_instructors(page.to_h).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end
  funders = Gtn::Contributors.get_funders(site, page.to_h).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end
  funding = Gtn::Contributors.get_grants(site, page.to_h).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end

  materials = []
  if page['program']
    page['program'].each do |section|
      if !section.key? 'tutorials'
        next
      end

      section['tutorials'].each do |tutorial|
        if tutorial.key?('custom')
          next
        end

        material = Gtn::TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name'])
        materials.push(material)
      end
    end
  end
  materials.compact!

  # Extract EDAM terms from all materials
  edam_terms = materials.map do |material|
    material.fetch('edam_ontology', []).map do |term|
      {
        '@type': 'DefinedTerm',
        '@id': "http://edamontology.org/#{term}",
        inDefinedTermSet: 'http://edamontology.org',
        termCode: term,
      }
    end
  end.flatten.uniq

  learning_objectives = materials.map do |material|
    material.fetch('objectives', [])
  end.flatten.compact

  # TODO: add topic edam terms too? Not sure.
  parts = []
  materials.each do |material|
    mat = generate_material_jsonld(material, site['data'][material['topic_name']], site)
    if !mat.nil? && !mat.empty?
      parts.push(mat)
    end
  end

  if page['program']
    syllab = page['program'].reject { |s| s['section'].nil? }.map do |section|
      {
        '@type': 'Syllabus',
        name: section['section'],
        description: section.fetch('description', nil),
      }
    end
  end

  data = {
    '@context': 'https://schema.org',
    '@type': 'Course',
    url: "#{site['url']}#{site['baseurl']}#{page['url']}",
    name: page['title'],
    keywords: page['tags'] || [],
    description: page['description'],

    about: edam_terms, # TeSS, "scientific topics".
    audience: page['audience'], # TeSS: target audience
    # If 'online' is present in the mode, the course is online.
    # Will fail on "this is NOT an online course"
    # Acceptable.
    courseMode: page['mode'],
    startDate: page['date_start'],
    endDate: page['date_end'],
    organizer: organisers, # TeSS only, US spelling, non-standard

    location: page['location'], # TODO, TeSS location
    teaches: learning_objectives, # TeSS, "learning objectives"
    # timeRequired: 'P1D', # TeSS, "duration", TODO: calculate from start/end date, not implemented in scraper currently.

    availableLanguage: ['en'], # TODO: support other languages
    inLanguage: ['en'], # TODO: support other languages
    # courseCode
    # coursePrerequisites
    # educationalCredentialAwarded
    # financialAidEligible
    # hasCourseInstance
    # numberOfCredits
    # occupationalCredentialAwarded
    # syllabusSections
    # totalHistoricalEnrollment

    # assesses
    # competencyRequired
    # educationalAlignment
    # educationalLevel
    # educationalUse
    # learningResourceType
    # teaches

    funder: funders, # Org or person
    funding: funding, # Grant
    publisher: GTN,
    provider: GTN,
    syllabusSections: syllab,
    # Session materials
    # TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out.
    # hasPart: parts,
  }

  begin
    data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path)
    data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path)
  rescue StandardError
    data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path'])
    data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path'])
  end

  if page['cover']
    data['image'] = if page['cover'] =~ /^http/
                      [page['cover']]
                    else
                      ["#{site['url']}#{site['baseurl']}#{page['cover']}"]
                    end
  end

  # We CANNOT guarantee A11Y
  # data.update(A11Y)
  if page['cost'] and page['cost'].downcase == 'free'
    data['isAccessibleForFree'] = true
    offer = {
      '@type': 'Offer',
      price: 0,
      priceCurrency: 'EUR',
      category: 'Free',
      isAccessibleForFree: true,
    }
  elsif page['cost']
    data['isAccessibleForFree'] = false
    offer = {
      '@type': 'Offer',
      price: page['cost'].split[0],
      priceCurrency: page['cost'].split[1],
      isAccessibleForFree: false,
      category: 'Paid',
      # TODO: this can be more advanced but we need to collect start/end times, and timezone.
    }
  end

  # TODO: this is wrong in a whole host of scenarios like incl weekends.
  course_days = (page.fetch('date_end', page['date_start']) - page['date_start']).to_i
  if course_days < 1
    course_days = 1
  end
  data['hasCourseInstance'] = [
    {
      '@type': 'CourseInstance',
      courseMode: page['mode'],
      # courseWorkload: "A daily course running from #{page['date_start']} to #{page['date_end']}",
      offers: offer,
      instructor: instructors,
      isAccessibleForFree: data['isAccessibleForFree'],
      courseSchedule: {
        '@type': 'Schedule',
        startDate: page['date_start'],
        endDate: page.fetch('date_end', page['date_start']),
        repeatCount: course_days,
        repeatFrequency: 'daily', # Contrary to schema.org spec, this is what Google wants.
      },
      courseWorkload: "P#{course_days}D",
    }
  ]

  data['offers'] = [offer]

  if page.key?('location') && page['location'].keys.length > 1
    data['location'] = {
      '@type': 'Place',
      name: page['location']['name'],
      address: {
        '@type': 'PostalAddress',
        streetAddress: page['location'].fetch('address', nil),
        addressLocality: page['location'].fetch('city', nil),
        addressRegion: page['location'].fetch('region', nil),
        postalCode: page['location'].fetch('postcode', nil),
        addressCountry: page['location'].fetch('country', nil)
      }
    }
  end

  JSON.pretty_generate(data)
end

Generate the JSON-LD metadata for a funding organisation Parameters:

id

The id of the person.

contributor

The contributor object from ORGANISATIONS.yaml.

site

The site object.

Returns:

Hash

The JSON-LD metadata.

# File _plugins/jekyll-jsonld.rb, line 185
def generate_funder_jsonld(id, contributor, site)
  {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    'http://purl.org/dc/terms/conformsTo': {
      '@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT',
      '@type': 'CreativeWork'
    },
    name: Gtn::Contributors.fetch_name(site, id),
    description: contributor.fetch('funding_statement', 'An organization supporting the Galaxy Training Network'),
    url: contributor.fetch('url', "https://training.galaxyproject.org/training-material/hall-of-fame/#{id}/"),
    logo: contributor.fetch('avatar', "https://github.com/#{id}.png"),
  }
end

Generate the JSON-LD metadata for a grant Parameters:

id

The id of the grant.

contributor

The contributor object from GRANTS.yaml.

site

The site object.

Returns:

Hash

The JSON-LD metadata.

# File _plugins/jekyll-jsonld.rb, line 208
def generate_grant_jsonld(id, contributor, site)
  organization = {
    '@context': 'https://schema.org',
    '@type': 'Grant',
    identifier: contributor['funding_id'],
    url: Gtn::Contributors.fetch_funding_url(contributor) || contributor['url'],
    funder: generate_funder_jsonld(id, contributor, site)
  }

  organization['startDate'] = contributor['start_date'] if contributor.key?('start_date')
  organization['endDate'] = contributor['end_date'] if contributor.key?('end_date')

  organization
end

Generate the JSON-LD metadata for a learning pathway Parameters:

page

The page object.

site

The Jekyll::Site site object.

Returns:

Hash

The JSON-LD metadata.

Examples:

{{ page | generate_learning_pathway_jsonld: site }}
# File _plugins/jekyll-jsonld.rb, line 512
def generate_learning_pathway_jsonld(page, site)
  materials = []
  page['pathway'].each do |section|
    if !section.key? 'tutorials'
      next
    end

    section['tutorials'].each do |tutorial|
      if tutorial.key?('custom')
        next
      end

      material = Gtn::TopicFilter.fetch_tutorial_material(site, tutorial['topic'], tutorial['name'])
      materials.push(material)
    end
  end
  materials.compact!

  # Extract EDAM terms from all materials
  edam_terms = materials.map do |material|
    material.fetch('edam_ontology', []).map do |term|
      {
        '@type': 'DefinedTerm',
        '@id': "http://edamontology.org/#{term}",
        inDefinedTermSet: 'http://edamontology.org',
        termCode: term,
      }
    end
  end.flatten.uniq

  learning_objectives = materials.map do |material|
    material.fetch('objectives', [])
  end.flatten.compact

  funders = materials.map do |material|
    Gtn::Contributors.get_funders(site, material).map do |x|
      to_pfo_jsonld(x, site, json: false)
    end
  end.flatten.uniq.compact

  funding = materials.map do |material|
    Gtn::Contributors.get_grants(site, material).map do |x|
      to_pfo_jsonld(x, site, json: false)
    end
  end.flatten.uniq.compact

  # TODO: add topic edam terms too? Not sure.
  parts = []
  materials.each do |material|
    mat = generate_material_jsonld(material, site['data'][material['topic_name']], site)
    if !mat.nil? && !mat.empty?
      parts.push(mat)
    end
  end

  syllab = page['pathway'].reject { |s| s['section'].nil? }.map do |section|
    {
      '@type': 'Syllabus',
      name: section['section'],
      description: section.fetch('description', nil),
    }
  end

  data = {
    '@context': 'https://schema.org',
    '@type': 'Course',
    url: "#{site['url']}#{site['baseurl']}#{page['url']}",
    name: "Learning Pathway #{page['title']}",
    keywords: page['tags'] || [],
    description: page['description'],
    about: edam_terms, # TeSS, "scientific topics".
    audience: page['audience'], # TeSS: target audience
    teaches: learning_objectives, # TeSS, "learning objectives"
    availableLanguage: ['en'], # TODO: support other languages
    inLanguage: ['en'], # TODO: support other languages
    # courseCode
    # coursePrerequisites
    # educationalCredentialAwarded
    # financialAidEligible
    # hasCourseInstance
    # numberOfCredits
    # occupationalCredentialAwarded
    # syllabusSections
    # totalHistoricalEnrollment

    # assesses
    # competencyRequired
    # educationalAlignment
    # educationalLevel
    # educationalUse
    # learningResourceType
    # teaches

    funder: funders, # Org or person
    funding: funding, # Grant
    publisher: GTN,
    provider: GTN,
    syllabusSections: syllab,
    # Session materials
    # TODO: not currently parsed by TeSS, google just complains about it, so we're leaving it out.
    # hasPart: parts,
  }

  begin
    data['dateModified'] = Gtn::ModificationTimes.obtain_time(page.path)
    data['datePublished'] = Gtn::PublicationTimes.obtain_time(page.path)
  rescue StandardError
    data['dateModified'] = Gtn::ModificationTimes.obtain_time(page['path'])
    data['datePublished'] = Gtn::PublicationTimes.obtain_time(page['path'])
  end

  if page['cover']
    data['image'] = if page['cover'] =~ /^http/
                      [page['cover']]
                    else
                      ["#{site['url']}#{site['baseurl']}#{page['cover']}"]
                    end
  end

  # We CANNOT guarantee A11Y
  # data.update(A11Y)
  data['isAccessibleForFree'] = true
  offer = {
    '@type': 'Offer',
    price: 0,
    priceCurrency: 'EUR',
    category: 'Free',
    isAccessibleForFree: true,
  }
  data['offers'] = [offer]

  # TODO: this is basically just wrong.
  data['hasCourseInstance'] = [
    {
      '@type': 'CourseInstance',
      courseMode: 'online',
      offers: offer,
      isAccessibleForFree: data['isAccessibleForFree'],
    }
  ]

  JSON.pretty_generate(data)
end

Convert a material to JSON-LD. Intended to be used by the filters which you should call in templates.

Parameters:

material

The material object.

topic

The topic object.

site

The Jekyll::Site site object.

Returns:

Hash

The JSON-LD metadata.

# File _plugins/jekyll-jsonld.rb, line 682
def generate_material_jsonld(material, topic, site)
  langCodeMap = {
    "en" => 'English',
    "es" => 'Español',
    "fr" => 'Français',
  }

  eduLevel = {
    'Introductory' => 'Beginner',
    'Intermediate' => 'Intermediate',
    'Advanced' => 'Advanced'
  }
  return '{}' if !topic

  topic_desc = {
    '@type': 'CreativeWork',
    name: (topic['title']).to_s,
    description: (topic['summary']).to_s,
    url: "#{site['url']}#{site['baseurl']}/topics/#{topic['name']}/"
  }

  # aggregate everything
  data = {
    # Properties from Course
    '@context': 'http://schema.org',
    '@type': 'LearningResource',

    # Required for BioSchemas
    'http://purl.org/dc/terms/conformsTo': {
      '@id': 'https://bioschemas.org/profiles/TrainingMaterial/1.0-RELEASE',
      '@type': 'CreativeWork'
    },

    # Properties from CreativeWork
    # "about" described below
    #
    # "accountablePerson":,
    # "aggregateRating":,
    # "alternativeHeadline":,
    # "associatedMedia":,
    audience: {
      '@type': 'EducationalAudience',
      educationalRole: EDU_ROLES[topic['type']]
    },
    # "audio":,
    # "award":,
    # "author" described below
    # "character":,
    citation: [
      {
        '@type': 'CreativeWork',
        name: 'Galaxy Training: A Powerful Framework for Teaching!',
        url: 'https://doi.org/10.1371/journal.pcbi.1010752'
      },
      {
        '@type': 'CreativeWork',
        name: 'Community-Driven Data Analysis Training for Biology',
        url: 'https://doi.org/10.1016/j.cels.2018.05.012'
      }
    ],
    # "comment":,
    # "commentCount":,
    # "contentLocation":,
    # "contentRating":,
    # "contentReferenceTime":,
    # "contributor" described below
    # copyrightHolder: GTN,
    # copyrightNotice: m
    # "copyrightYear":,
    # "correction":,
    # "creator":,
    # "dateCreated":,
    # "datePublished":,
    discussionUrl: site['gitter_url'],
    # "editor":,
    # "educationalAlignment":,
    # "educationalUse":,
    # "encoding":,
    # "encodingFormat":,
    # "exampleOfWork":,
    # "expires":,
    # "funder": funding,
    # "genre":,
    # "hasPart" described below
    headline: (material['title']).to_s,
    # "interactionStatistic":,
    interactivityType: 'mixed',
    isAccessibleForFree: true,
    # "isBasedOn":,
    isFamilyFriendly: true,
    # "isPartOf" described below
    # "keywords": described below
    # "learningResourceType" described below
    license: 'https://spdx.org/licenses/CC-BY-4.0.html',
    # "locationCreated":,
    # "mainEntity":,
    # "material":,
    # "mentions" described below
    # "offers":,
    # "position":,
    producer: GTN,
    provider: GTN,
    # "publication":,
    # "publisher":,
    # "publisherImprint":,
    # "publishingPrinciples":,
    # "recordedAt":,
    # "releasedEvent":,
    # "review":,
    # "schemaVersion":,
    # "sdDatePublished":,
    # "sdLicense":,
    # "sdPublisher":,
    sourceOrganization: GTN,
    # "spatialCoverage":,
    # "sponsor":,
    # "temporalCoverage":,
    # "text":,
    # "thumbnailUrl":,
    # "timeRequired" described below
    # "translationOfWork":,
    # "translator": Google Translate???,
    # "typicalAgeRange":,
    # "version":,
    # "video":,
    # "workExample":,
    # "workTranslation":,

    # Properties from Thing
    # "additionalType":,
    # "alternateName":,
    # "description" described below
    # "disambiguatingDescription":,
    # "image":,
    # "mainEntityOfPage":,
    # "name" described below
    # "potentialAction":,
    # "sameAs":,
    # "subjectOf":,
    # "url" described below
    workTranslation: [],
    creativeWorkStatus: material['draft'] ? 'Draft' : 'Active',
  }

  if material.key?('pub_date')
    data['dateModified'] = material['mod_date']
    data['datePublished'] = material['pub_date']
  else
    begin
      data['dateModified'] = Gtn::ModificationTimes.obtain_time(material.path)
      data['datePublished'] = Gtn::PublicationTimes.obtain_time(material.path)
    rescue StandardError
      data['dateModified'] = Gtn::ModificationTimes.obtain_time(material['path'])
      data['datePublished'] = Gtn::PublicationTimes.obtain_time(material['path'])
    end
  end

  if material.key?('copyright')
    # copyrightHolder: GTN,
    data['copyrightNotice'] = material['copyright']
  else
    # I'm not sure this is accurate.
    data['copyrightHolder'] = GTN
  end

  funders = Gtn::Contributors.get_funders(site, material).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end
  grants = Gtn::Contributors.get_grants(site, material).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end

  data['funder'] = funders
  data['funding'] = grants

  data['identifier'] = "https://gxy.io/GTN:#{material['short_id']}" if material.key?('short_id')

  data.update(A11Y)

  actual_material = Gtn::TopicFilter.fetch_tutorial_material(site, material['topic_name'], material['tutorial_name'])

  # info depending if tutorial, hands-on or slide level
  # parts = []
  # data['hasPart'] = parts

  mentions = []
  description = []

  data['isPartOf'] = topic_desc

  data['abstract'] = material
    .fetch('content', '')
    .strip
    .split("\n")
    .first

  if ! data['abstract'].nil?
    data['abstract'] = data['abstract']
      .gsub(/\{\{\s*site.baseurl\s*\}\}/, url_prefix(site))
      .gsub(/\[{{\s*site.url\s*}}/, '[' + url_prefix(site))
      .gsub(/{% link (topics[^%]*).md %}/, url_prefix(site) + '\1.html')
      .gsub(/{% link (topics[^%]*).html %}/, url_prefix(site) + '\1.html')
      .gsub(/\s*\(?{%\s*cite [^}]+\s*%}\)?/, '')
      .gsub('{{ site.github_repository }}', safe_site_config(site, 'github_repository', 'https://example.com'))
      .gsub(/{% snippet ([^%]*) %}/, '')
      .gsub(/{% include ([^%]*) %}/, '')
  end

  description.push("## Abstract\n\n#{data['abstract']}\n\n")

  if (material['name'] == 'tutorial.md') || (material['name'] == 'slides.html')

    if material['name'] == 'tutorial.md'
      data['learningResourceType'] = 'e-learning'
      description.push("## About This Material\n\nThis is a Hands-on Tutorial from the GTN which is usable either for individual self-study, or as a teaching material in a classroom.\n\n")
    else
      data['learningResourceType'] = 'slides'
    end

    data['name'] = material['title']
    data['url'] = "#{site['url']}#{site['baseurl']}#{material['url']}"

    # Requires https://github.com/galaxyproject/training-material/pull/4271
    data['version'] = Gtn::ModificationTimes.obtain_modification_count(material['path'])

    # Time required
    if material.key?('time_estimation') && !material['time_estimation'].nil?
      data['timeRequired'] = "PT#{material['time_estimation'].upcase}"
    end

    # Description with questions, objectives and keypoints
    if material.key?('questions') && !material['questions'].nil? && material['questions'].length.positive?
      questions = material['questions'].join("\n - ")
      description.push("## Questions this #{material['type']} will address\n\n - #{questions}\n\n")
    end
    if material.key?('objectives') && !material['objectives'].nil? && material['objectives'].length.positive?
      objectives = material['objectives'].map{|x| "- #{x}"}.join("\n")
      description.push("## Learning Objectives\n\n#{objectives}\n\n")
      data['teaches'] = objectives
    end
    if material.key?('keypoints') && !material['keypoints'].nil? && material['keypoints'].length.positive?
      keypoints = material['keypoints'].join("\n - ")
      description.push("## Key Points\n\n - #{keypoints}\n\n")
    end

    # Keywords
    data['keywords'] = [topic['title']] + (material['tags'] || [])
    # Zenodo links
  end

  # Mentions are 'external resources' in TeSS.
  # This could be expanded with
  # - supported servers
  # - tools and resources used (e.g. Galaxy) or tools linked to the TS.
  # - slides (if tutorial) and tutorial (if slides)
  # - other materials in the same topic?
  if actual_material.key?('workflows')
    mentions.push({
                    '@type': 'Thing',
                    url: "#{site['url']}#{site['baseurl']}#{material['dir']}workflows/",
                    name: "Associated Workflows"
                  })
  end

  # Notebooks
  if actual_material.key?('notebook')
    if actual_material['notebook']['language'] != 'r'
      # Python, Bash, SQL (all via jupyter)
      url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.ipynb"
      mentions.push({
                      '@type': 'Thing',
                      url: url,
                      name: "Jupyter Notebook (with Solutions)"
                    })
      mentions.push({
                      '@type': 'Thing',
                      url: url.gsub(/\.ipynb$/, '-course.ipynb'),
                      name: "Jupyter Notebook (without Solutions)"
                    })
    elsif actual_material['notebook']['language'] == 'r' # Actual R
      url = "#{site['url']}#{site['baseurl']}#{material['dir']}#{material['topic_name']}-#{material['tutorial_name']}.Rmd"
      mentions.push({
                      '@type': 'Thing',
                      url: url,
                      name: "Quarto/RMarkdown Notebook"
                    })
      mentions.push({
                      '@type': 'Thing',
                      url: "https://bio.tools/tool/rstudio",
                      name: "RStudio"
                    })
    end
  end

  # Tools
  uses_tools = false
  (actual_material['tools'] || []).each do |tool|
    if site.data['tool-meta'].nil?
      next
    end

    toolmeta = site.data['tool-meta'][tool]
    if toolmeta.nil?
      next
    end

    if toolmeta['bio.tools'].length.positive?
      mentions.push({
                      '@type': 'Thing',
                      url: "https://bio.tools/tool/#{toolmeta['bio.tools']}",
                      name: toolmeta.fetch('bio.tools_name', toolmeta['name'])
                    })
    end
    uses_tools = true
  end
  if uses_tools
    mentions.push({
                    '@type': 'Thing',
                    url: "https://bio.tools/tool/galaxy",
                    name: "Galaxy"
                  })
  end

  # Zenodo link out
  if actual_material.key?('zenodo_link') && ! actual_material['zenodo_link'].nil?
    if actual_material['zenodo_link'].length.positive?
      mentions.push({
                      '@type': 'Thing',
                      url: (actual_material['zenodo_link']).to_s,
                      name: "Associated Training Datasets"
                    })
    end
  end

  if description.empty?
    description.push(material.fetch('content', '').strip.split("\n").first)
  end
  data['description'] = description.join("\n")

  data['inLanguage'] = if material.key?('lang')
                         {
                           '@type': 'Language',
                           name: langCodeMap[material['lang']],
                           alternateName: material['lang']
                         }
                       else
                         {
                           '@type': 'Language',
                           name: 'English',
                           alternateName: 'en'
                         }
                       end

  # Course requirements (material + topic)
  reqs = []
  reqs.push(*topic['requirements']) if topic.key?('requirements')
  reqs.push(*material['requirements']) if material.key?('requirements')
  if !reqs.empty?
    coursePrerequisites = []
    reqs.each do |req|
      if req['type'] == 'internal'
        if req.key?('tutorials')
          (req['tutorials']).each do |tuto|
            (site['pages']).each do |page|
              if ((page['name'] == 'tutorial.md') || (page['name'] == 'slides.html')) &&
                 ((page['topic_name'] == req['topic_name']) && (page['tutorial_name'] == tuto))
                # slides
                if page['name'] == 'slides.html'
                  coursePrerequisites.push(
                    {
                      '@context': 'http://schema.org',
                      '@type': 'LearningResource',
                      url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/" \
                           "tutorials/#{tuto}/slides.html",
                      name: (page['title']).to_s,
                      description: "Slides for '#{page['title']}' tutorial",
                      learningResourceType: 'slides',
                      interactivityType: 'expositive',
                      provider: GTN
                    }
                  )
                  if page['hands_on_url']
                    coursePrerequisites.push(
                      {
                        '@context': 'http://schema.org',
                        '@type': 'LearningResource',
                        url: (page['hands_on_url']).to_s,
                        learningResourceType: 'e-learning',
                        interactivityType: 'expositive',
                      }
                    )
                  end
                end
                # hands-on
                if page['name'] == 'tutorial.md'
                  coursePrerequisites.push(
                    {
                      '@context': 'http://schema.org',
                      '@type': 'LearningResource',
                      url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/tutorials" \
                           "/#{tuto}/tutorial.html",
                      name: (page['title']).to_s,
                      description: "Hands-on for '#{page['title']}' tutorial",
                      learningResourceType: 'e-learning',
                      interactivityType: 'expositive',
                      provider: GTN
                    }
                  )
                end
              end
            end
          end
        else
          coursePrerequisites.push(
            {
              '@context': 'http://schema.org',
              '@type': 'LearningResource',
              url: "#{site['url']}#{site['baseurl']}/topics/#{req['topic_name']}/",
              name: (site['data'][req['topic_name']]['title']).to_s,
              description: (site['data'][req['topic_name']]['title']).to_s,
              provider: GTN
            }
          )
        end
      elsif req['type'] == 'external'
        coursePrerequisites.push({
                                   '@type': 'CreativeWork',
                                   url: (req['link']).to_s,
                                   name: (req['title']).to_s
                                 })
      else
        coursePrerequisites.push((req['title']).to_s)
      end
    end
    data['competencyRequired'] = coursePrerequisites.uniq
  end

  # Add contributors/authors
  if material.key?('contributors') || material.key?('contributions')
    authors = Gtn::Contributors.get_authors(material).map do |x|
      generate_person_jsonld(x, Gtn::Contributors.fetch_contributor(site, x), site)
    end

    data['author'] = authors
  end

  # Add non-author contributors
  if material.key?('contributions')
    data['contributor'] = Gtn::Contributors.get_non_authors(material).map do |x|
      generate_person_jsonld(x, site['data']['contributors'][x], site)
    end
  end

  about = []
  about.push(topic_desc)
  edam_terms = topic.fetch('edam_ontology', []) | material.fetch('edam_ontology', [])

  about += edam_terms.map do |term|
    {
      '@type': 'DefinedTerm',
      '@id': "http://edamontology.org/#{term}",
      inDefinedTermSet: 'http://edamontology.org',
      termCode: term,
      # "name": ,
      url: 'https://bioportal.bioontology.org/ontologies/EDAM/?p=classes&conceptid=' \
           "http%3A%2F%2Fedamontology.org%2F#{term}"
    }
  end

  data['about'] = about

  data['educationalLevel'] = material.key?('level') ? eduLevel[material['level']] : 'Beginner'
  data['mentions'] = mentions

  data
end

Generate the JSON-LD metadata for a news article (blog) Parameters:

page

The page object.

site

The Jekyll::Site site object.

Returns:

Hash

The JSON-LD metadata.

# File _plugins/jekyll-jsonld.rb, line 255
def generate_news_jsonld(page, site)
  authors = Gtn::Contributors.get_authors(page.to_h).map do |x|
    to_pfo_jsonld(x, site, json: false)
  end

  data = {
    '@context': 'https://schema.org',
    '@type': 'BlogPosting',
    url: "#{site['url']}#{site['baseurl']}#{page['url']}",
    name: page['title'],
    headline: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags.
    keywords: page['tags'] || [],
    description: page.excerpt[0..100].gsub(/\n/, ' '), # TODO: remove html tags
    articleBody: page.content, # TODO: remove html tags
    datePublished: page.date,
    dateModified: Gtn::ModificationTimes.obtain_time(page.path),
    author: authors,
    publisher: GTN,
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': "#{site['url']}#{page['url']}"
    },
    image: {
      '@type': 'ImageObject',
      width: 60,
      height: 60,
      url: "#{site['baseurl']}/assets/images/GTN-60px.png"
    }
  }
  data.update(A11Y)

  JSON.pretty_generate(data)
end

Generate the JSON-LD metadata for an organisation Parameters:

id

The id of the org.

contributor

The contributor object from ORGANISATIONS.yaml.

site

The site object.

Returns:

Hash

The JSON-LD metadata.

# File _plugins/jekyll-jsonld.rb, line 159
def generate_org_jsonld(id, contributor, site)
  organization = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    'http://purl.org/dc/terms/conformsTo': {
      '@id': 'https://bioschemas.org/profiles/Organization/0.3-DRAFT',
      '@type': 'CreativeWork'
    },
    id: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/",
    name: Gtn::Contributors.fetch_name(site, id),
    description: 'An organization supporting the Galaxy Training Network',
  }

  organization['url'] = contributor['url'] if contributor.key?('url') && contributor['url']

  organization
end

Generate the JSON-LD metadata for a person Parameters:

id

The id of the person.

contributor

The contributor object from CONTRIBUTORS.yaml.

site

The site object.

Returns:

Hash

The JSON-LD metadata.

Example:

generate_person_jsonld("hexylena", site['data']['contributors']['hexylena'], site)
=> {
  "@context": "https://schema.org",
  "@type": "Person",
  "http://purl.org/dc/terms/conformsTo": {
    # Bioschemas profile
    "@id": "https://bioschemas.org/profiles/Person/0.2-DRAFT-2019_07_19",
    "@type": "Person"
  },
  "url": "https://training.galaxyproject.org/hall-of-fame/hexylena/",
  "mainEntityOfPage": "https://training.galaxyproject.org/hall-of-fame/hexylena/",
  "name": "hexylena",
  "image": "https://avatars.githubusercontent.com/hexylena",
  "description": "A contributor to the GTN project.",
  "memberOf": [...],
  "identifier": "https://orcid.org/0000-0002-6601-2165",
  "orcid": "https://orcid.org/0000-0002-6601-2165"
}
# File _plugins/jekyll-jsonld.rb, line 115
def generate_person_jsonld(id, contributor, site)
  member_of = Gtn::Contributors.fetch_contributor(site, id)['affiliations'] || []
  member_of = member_of.map do |org_id|
    org = Gtn::Contributors.fetch_contributor(site, org_id)
    generate_org_jsonld(org_id, org, site)
  end

  person = {
    '@context': 'https://schema.org',
    '@type': 'Person',
    'http://purl.org/dc/terms/conformsTo': {
      '@id': 'https://bioschemas.org/profiles/Person/0.3-DRAFT',
      '@type': 'CreativeWork'
    },
    # I guess these are identical?
    url: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/",
    mainEntityOfPage: "#{site['url']}#{site['baseurl']}/hall-of-fame/#{id}/",
    name: Gtn::Contributors.fetch_name(site, id),
    image: "https://avatars.githubusercontent.com/#{id}",
    # No clue what to put here it's a person.
    description: if contributor.nil?
                   'A contributor to the GTN project.'
                 else
                   contributor.fetch('bio',
                                     'A contributor to the GTN project.')
                 end,
    memberOf: [GTN] + member_of,
  }
  if !contributor.nil? && contributor.key?('orcid') && contributor['orcid']
    person['identifier'] = "https://orcid.org/#{contributor['orcid']}"
    person['orcid'] = "https://orcid.org/#{contributor['orcid']}"
  end

  person
end

Convert a material to JSON-LD, intended to be used in Jekyll Liquid templates. Parameters:

material

The material object.

topic

The topic object.

site

The Jekyll::Site site object.

Returns:

String

The JSON-LD metadata.

Examples:

{{ material | to_jsonld: topic, site }}
# File _plugins/jekyll-jsonld.rb, line 668
def to_jsonld(material, topic, site)
  JSON.pretty_generate(generate_material_jsonld(material, topic, site))
end

Generate the JSON-LD metadata for a person, funder, or organisation as JSON. Parameters:

id

The id of the person.

site

The site object.

json

Should the output be rendered as JSON (only really used in contributor page.)

Returns:

String

The JSON-LD metadata.

# File _plugins/jekyll-jsonld.rb, line 231
def to_pfo_jsonld(id, site, json: true)
  contributor = Gtn::Contributors.fetch_contributor(site, id)
  d = if Gtn::Contributors.person?(site, id)
        generate_person_jsonld(id, contributor, site)
      elsif Gtn::Contributors.grant?(site, id)
        generate_grant_jsonld(id, contributor, site)
      else
        generate_org_jsonld(id, contributor, site)
      end

  if json
    JSON.pretty_generate(d)
  else
    d
  end
end