From a1036390a9eb78f9e7002c79cd83248507f5868f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Jul 2026 19:00:44 +0000 Subject: [PATCH] Scaffold museomics/methods site (Jekyll + client-side filtering) Implements the revised spec: a GitHub Actions-built Jekyll site serving a filterable Methods page and a curated Bibliography, with issue-form intake. - Two-level taxonomy: Kingdom + Phylum/Division facets replace a flat taxon tag - GitHub Actions build/deploy (deploy.yml) + PR CI (ci.yml), not stock Pages - Single controlled vocabulary in _data/tags.yml, enforced by scripts/validate_entries.rb (required fields + valid tags) in CI - Liquid-generated methods.json / bibliography.json (front-matter + rendered body) consumed by framework-free assets/js/app.js - Free-text search, sort control, live results count, empty state, and URL query-param sync for shareable filtered views - Accessible chips: colour + facet badge + aria-label (not colour alone) - Optional DOI so unpublished/in-house methods aren't excluded; optional controlled outcome signal - Contribute page + two GitHub Issue Forms (dropdowns mirror the vocabulary) - Content licensed CC BY 4.0 (LICENSE-CONTENT.md); code stays MIT - Seed example entries (clearly marked) and revised SPEC.md Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01HYAEmscFHjiQFP1sqNEBEF --- .github/ISSUE_TEMPLATE/config.yml | 5 + .../ISSUE_TEMPLATE/suggest-bibliography.yml | 73 ++++ .github/ISSUE_TEMPLATE/suggest-method.yml | 148 ++++++++ .github/workflows/ci.yml | 24 ++ .github/workflows/deploy.yml | 47 +++ .gitignore | 5 + CONTRIBUTING.md | 85 +++++ Gemfile | 5 + Gemfile.lock | 236 ++++++++++++ LICENSE-CONTENT.md | 15 + README.md | 43 ++- SPEC.md | 175 +++++++++ .../example-adna-extraction-paper.md | 9 + _bibliography/example-single-stranded-sop.md | 9 + _bibliography/example-target-capture-paper.md | 9 + _config.yml | 42 +++ _data/facets.yml | 41 ++ _data/tags.yml | 81 ++++ _includes/nav.html | 10 + _layouts/default.html | 41 ++ _layouts/method.html | 28 ++ _methods/genome-skimming-pinned-beetle.md | 35 ++ _methods/herbarium-plant-target-capture.md | 33 ++ _methods/study-skin-bird-mitogenome.md | 34 ++ assets/css/style.css | 177 +++++++++ assets/js/app.js | 349 ++++++++++++++++++ bibliography.json | 18 + bibliography/index.html | 42 +++ contribute/index.md | 47 +++ index.html | 55 +++ methods.json | 25 ++ scripts/validate_entries.rb | 99 +++++ 32 files changed, 2044 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/suggest-bibliography.yml create mode 100644 .github/ISSUE_TEMPLATE/suggest-method.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE-CONTENT.md create mode 100644 SPEC.md create mode 100644 _bibliography/example-adna-extraction-paper.md create mode 100644 _bibliography/example-single-stranded-sop.md create mode 100644 _bibliography/example-target-capture-paper.md create mode 100644 _config.yml create mode 100644 _data/facets.yml create mode 100644 _data/tags.yml create mode 100644 _includes/nav.html create mode 100644 _layouts/default.html create mode 100644 _layouts/method.html create mode 100644 _methods/genome-skimming-pinned-beetle.md create mode 100644 _methods/herbarium-plant-target-capture.md create mode 100644 _methods/study-skin-bird-mitogenome.md create mode 100644 assets/css/style.css create mode 100644 assets/js/app.js create mode 100644 bibliography.json create mode 100644 bibliography/index.html create mode 100644 contribute/index.md create mode 100644 index.html create mode 100644 methods.json create mode 100644 scripts/validate_entries.rb diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ef23e1f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Contribute directly (pull request) + url: https://github.com/museomics/methods/blob/main/CONTRIBUTING.md + about: Comfortable with GitHub? Add an entry by PR instead. diff --git a/.github/ISSUE_TEMPLATE/suggest-bibliography.yml b/.github/ISSUE_TEMPLATE/suggest-bibliography.yml new file mode 100644 index 0000000..bfd4206 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggest-bibliography.yml @@ -0,0 +1,73 @@ +name: Suggest a bibliography entry +description: Propose a paper or SOP to add to the Bibliography page. +title: "[Bibliography] " +labels: ["type: bibliography-suggestion"] +# NOTE: tag options mirror _data/tags.yml — update both together. +body: + - type: markdown + attributes: + value: "Fields marked * are required." + - type: dropdown + id: type + attributes: + label: Type* + options: [paper, sop] + validations: + required: true + - type: input + id: title + attributes: + label: Title* + validations: + required: true + - type: input + id: authors + attributes: + label: Authors or institution* + validations: + required: true + - type: input + id: year + attributes: + label: Year / date* + placeholder: e.g. 2024 + validations: + required: true + - type: input + id: link + attributes: + label: Link or DOI* + placeholder: https://doi.org/10.xxxx/xxxxx + validations: + required: true + - type: dropdown + id: tags + attributes: + label: Relevant method tags + multiple: true + options: + - silica-column + - magnetic-bead + - phenol-chloroform + - ctab + - chelex + - tagmentation + - ligation + - single-stranded + - pcr-free + - hybridization-capture + - genome-skimming + - shotgun-wgs + - target-capture + - metabarcoding + - rad-seq + - sanger + validations: + required: false + - type: input + id: notes + attributes: + label: Relevance note + description: Optional one-line note on why it's relevant. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/suggest-method.yml b/.github/ISSUE_TEMPLATE/suggest-method.yml new file mode 100644 index 0000000..8a6515c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/suggest-method.yml @@ -0,0 +1,148 @@ +name: Suggest a method +description: Propose a working museomics method to add to the Methods page. +title: "[Method] " +labels: ["type: method-suggestion"] +# NOTE: the dropdown options below mirror _data/tags.yml. GitHub Issue Forms +# cannot read that file, so if you add a vocabulary term there, add it here too. +body: + - type: markdown + attributes: + value: | + Thanks for contributing! Fill in what you can. A maintainer will review + and add it to the site. Fields marked * are required. + - type: input + id: title + attributes: + label: Title* + placeholder: e.g. Genome skimming from a 40-year-old pinned beetle + validations: + required: true + - type: input + id: summary + attributes: + label: One-line summary* + description: Shown on the collapsed card. + validations: + required: true + - type: dropdown + id: kingdom + attributes: + label: Kingdom* + multiple: true + options: [Animalia, Plantae, Fungi, Chromista, Bacteria, Archaea] + validations: + required: true + - type: dropdown + id: phylum + attributes: + label: Phylum / Division* + multiple: true + options: + - Chordata + - Arthropoda + - Mollusca + - Annelida + - Echinodermata + - Cnidaria + - Nematoda + - Platyhelminthes + - Porifera + - Tracheophyta + - Bryophyta + - Ascomycota + - Basidiomycota + - "Other (describe in detail box)" + validations: + required: true + - type: dropdown + id: preservation + attributes: + label: Preservation type* + multiple: true + options: + - dry-pinned + - study-skin + - herbarium-pressed + - fluid-ethanol + - fluid-formalin + - frozen + - dried-tissue + - bone-subfossil + - "Other (describe in detail box)" + validations: + required: true + - type: dropdown + id: extraction + attributes: + label: Extraction method* + multiple: true + options: + - silica-column + - magnetic-bead + - phenol-chloroform + - ctab + - chelex + - "Other (describe in detail box)" + validations: + required: true + - type: dropdown + id: library + attributes: + label: Library prep method* + multiple: true + options: + - tagmentation + - ligation + - single-stranded + - pcr-free + - hybridization-capture + - "Other (describe in detail box)" + validations: + required: true + - type: dropdown + id: sequencing + attributes: + label: Sequencing / downstream method* + multiple: true + options: + - genome-skimming + - shotgun-wgs + - target-capture + - metabarcoding + - rad-seq + - sanger + - "Other (describe in detail box)" + validations: + required: true + - type: dropdown + id: outcome + attributes: + label: Outcome + description: How well did it work? + options: + - worked-well + - worked-with-caveats + - exploratory + validations: + required: false + - type: input + id: doi + attributes: + label: DOI or link to paper + description: Optional — leave blank for unpublished / in-house methods. + placeholder: 10.xxxx/xxxxx + validations: + required: false + - type: textarea + id: detail + attributes: + label: Detail / notes + description: Protocol notes, yields, caveats. Becomes the expanded card body. Markdown OK. + validations: + required: false + - type: input + id: contributor + attributes: + label: Your name (for attribution) + validations: + required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0b41723 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + validate-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + - name: Validate entries against controlled vocabulary + run: ruby scripts/validate_entries.rb + - name: Build with Jekyll (catches broken front-matter / Liquid) + run: bundle exec jekyll build + env: + JEKYLL_ENV: production diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..674436a --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,47 @@ +name: Build & deploy to GitHub Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; don't cancel an in-progress production deploy. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + bundler-cache: true + - name: Validate entries against controlled vocabulary + run: ruby scripts/validate_entries.rb + - id: pages + uses: actions/configure-pages@v5 + - name: Build with Jekyll + run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + env: + JEKYLL_ENV: production + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99d4b36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +_site/ +.jekyll-cache/ +.jekyll-metadata +vendor/ +.sass-cache/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dcfd8cf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,85 @@ +# Contributing to museomics/methods + +Two ways to contribute: a **GitHub issue form** (no coding), or a **pull +request** (add a Markdown file directly). Either way, a maintainer reviews +before it goes live. + +## Option A — Issue form (easiest) + +- [Suggest a method](https://github.com/museomics/methods/issues/new?template=suggest-method.yml) +- [Suggest a bibliography entry](https://github.com/museomics/methods/issues/new?template=suggest-bibliography.yml) + +A maintainer transcribes accepted issues into entry files. + +## Option B — Pull request + +Add one Markdown file per entry, then open a PR. CI validates it automatically. + +### Method entry — `_methods/.md` + +```yaml +--- +title: "Genome skimming from a 40-year-old pinned beetle" +kingdom: [Animalia] # one or more, from _data/tags.yml +phylum: [Arthropoda] # "Phylum / Division", from _data/tags.yml +preservation: [dry-pinned] +extraction: [silica-column] +library: [tagmentation] +sequencing: [genome-skimming] +outcome: worked-well # optional: worked-well | worked-with-caveats | exploratory +contributor: "Your Name" +date: 2026-07-19 +summary: "One-line outcome, shown on the collapsed card." +doi: "10.xxxx/xxxxx" # optional — omit for unpublished methods +--- + +Full detail in Markdown. Renders when the card is expanded: protocol notes, +yields, caveats, links to SOPs in the bibliography, etc. +``` + +Required: `title`, `kingdom`, `phylum`, `preservation`, `extraction`, +`library`, `sequencing`, `summary`, `contributor`, `date`. +Optional: `outcome`, `doi`, body. + +### Bibliography entry — `_bibliography/.md` + +```yaml +--- +type: paper # paper | sop +title: "..." +authors_or_institution: "..." +year_or_date: 2025 +link: "https://doi.org/10.xxxx/xxxxx" +tags: [extraction, silica-column] # optional, from _data/tags.yml +notes: "Optional one-line relevance note" +--- +``` + +Required: `type`, `title`, `authors_or_institution`, `year_or_date`, `link`. + +## The controlled vocabulary + +All tag values live in [`_data/tags.yml`](_data/tags.yml) — the single source of +truth. CI (`scripts/validate_entries.rb`) fails the build if an entry uses a +value that isn't listed there, which keeps filtering consistent and stops typos +fragmenting the taxonomy. + +**Need a new tag value?** Add it to `_data/tags.yml` in the same PR. If that +value is also offered as a dropdown in an issue form, add it to the matching +list in `.github/ISSUE_TEMPLATE/*.yml` too — the forms can't read `tags.yml` +automatically. The maintainer curates this vocabulary. + +## Local preview + +```sh +bundle install +bundle exec jekyll serve +# then open http://localhost:4000/methods/ +``` + +Run `ruby scripts/validate_entries.rb` before pushing to catch schema errors. + +## Licence + +Entry content is contributed under [CC BY 4.0](LICENSE-CONTENT.md). Site code is +MIT ([LICENSE](LICENSE)). diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9b1c89d --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" +gem "jekyll", "~> 4.3" +group :jekyll_plugins do + gem "jekyll-sitemap" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..bf54525 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,236 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + base64 (0.3.0) + bigdecimal (4.1.2) + colorator (1.1.0) + concurrent-ruby (1.3.8) + csv (3.3.5) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.17.4) + ffi (1.17.4-aarch64-linux-gnu) + ffi (1.17.4-aarch64-linux-musl) + ffi (1.17.4-arm-linux-gnu) + ffi (1.17.4-arm-linux-musl) + ffi (1.17.4-arm64-darwin) + ffi (1.17.4-x86-linux-gnu) + ffi (1.17.4-x86-linux-musl) + ffi (1.17.4-x86_64-darwin) + ffi (1.17.4-x86_64-linux-gnu) + ffi (1.17.4-x86_64-linux-musl) + forwardable-extended (2.6.0) + google-protobuf (4.35.1) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.35.1-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) + http_parser.rb (0.8.1) + i18n (1.15.2) + concurrent-ruby (~> 1.0) + jekyll (4.4.1) + addressable (~> 2.4) + base64 (~> 0.2) + colorator (~> 1.0) + csv (~> 3.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + json (~> 2.6) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (~> 0.3, >= 0.3.6) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-sass-converter (3.1.0) + sass-embedded (~> 1.75) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + json (2.21.1) + kramdown (2.5.2) + rexml (>= 3.4.4) + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.10.0) + logger + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (7.0.5) + rake (13.4.2) + rb-fsevent (0.11.2) + rb-inotify (0.11.1) + ffi (~> 1.0) + rexml (3.4.4) + rouge (4.7.0) + safe_yaml (1.0.5) + sass-embedded (1.101.0) + google-protobuf (~> 4.31) + rake (>= 13) + sass-embedded (1.101.0-aarch64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-aarch64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-aarch64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-arm-linux-androideabi) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-arm-linux-gnueabihf) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-arm-linux-musleabihf) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-riscv64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-riscv64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-riscv64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-x86_64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-x86_64-linux-android) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-x86_64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.101.0-x86_64-linux-musl) + google-protobuf (~> 4.31) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.6.0) + webrick (1.9.2) + +PLATFORMS + aarch64-linux-android + aarch64-linux-gnu + aarch64-linux-musl + arm-linux-androideabi + arm-linux-gnu + arm-linux-gnueabihf + arm-linux-musl + arm-linux-musleabihf + arm64-darwin + riscv64-linux-android + riscv64-linux-gnu + riscv64-linux-musl + ruby + x86-linux-gnu + x86-linux-musl + x86_64-darwin + x86_64-linux-android + x86_64-linux-gnu + x86_64-linux-musl + +DEPENDENCIES + jekyll (~> 4.3) + jekyll-sitemap + +CHECKSUMS + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + colorator (1.1.0) sha256=e2f85daf57af47d740db2a32191d1bdfb0f6503a0dfbc8327d0c9154d5ddfc38 + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 + csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + em-websocket (0.5.3) sha256=f56a92bde4e6cb879256d58ee31f124181f68f8887bd14d53d5d9a292758c6a8 + eventmachine (1.2.7) sha256=994016e42aa041477ba9cff45cbe50de2047f25dd418eba003e84f0d16560972 + ffi (1.17.4) sha256=bcd1642e06f0d16fc9e09ac6d49c3a7298b9789bcb58127302f934e437d60acf + ffi (1.17.4-aarch64-linux-gnu) sha256=b208f06f91ffd8f5e1193da3cae3d2ccfc27fc36fba577baf698d26d91c080df + ffi (1.17.4-aarch64-linux-musl) sha256=9286b7a615f2676245283aef0a0a3b475ae3aae2bb5448baace630bb77b91f39 + ffi (1.17.4-arm-linux-gnu) sha256=d6dbddf7cb77bf955411af5f187a65b8cd378cb003c15c05697f5feee1cb1564 + ffi (1.17.4-arm-linux-musl) sha256=9d4838ded0465bef6e2426935f6bcc93134b6616785a84ffd2a3d82bc3cf6f95 + ffi (1.17.4-arm64-darwin) sha256=19071aaf1419251b0a46852abf960e77330a3b334d13a4ab51d58b31a937001b + ffi (1.17.4-x86-linux-gnu) sha256=38e150df5f4ca555e25beca4090823ae09657bceded154e3c52f8631c1ed72cf + ffi (1.17.4-x86-linux-musl) sha256=fbeec0fc7c795bcf86f623bb18d31ea1820f7bd580e1703a3d3740d527437809 + ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496 + ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d + ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e + forwardable-extended (2.6.0) sha256=1bec948c469bbddfadeb3bd90eb8c85f6e627a412a3e852acfd7eaedbac3ec97 + google-protobuf (4.35.1) sha256=a3a6471331d918f58dfa4d014a8f6286f0af2cf4840216bde52fcf2ea3fe3726 + google-protobuf (4.35.1-aarch64-linux-gnu) sha256=50ca44d0eeff3f8475e630a1accdd974256f3510694d574e2c9d6119ea8bc9e1 + google-protobuf (4.35.1-aarch64-linux-musl) sha256=d5c65cef6bd6498a9e5ed5f88cf6cf7e341c10b0a005e32137d5d1a2b6e8c18a + google-protobuf (4.35.1-arm64-darwin) sha256=d9c957df04fa89c749fa9a72a7b383eb4296efc9b2303dc6fd6fbe39c698ad6b + google-protobuf (4.35.1-x86-linux-gnu) sha256=cc7492566b27ad8b5dfa3a7b6f9b11e905050cc53c2fa8ff22de375a9e7a70fb + google-protobuf (4.35.1-x86-linux-musl) sha256=ab403790b59e4dc588ffbed1eaacf05d4ca2f0d12ac9c13d6c64e69380d8c99e + google-protobuf (4.35.1-x86_64-darwin) sha256=66b62b4df00931018a692806df66393efa960d6d2b7da69735187249f950d3ee + google-protobuf (4.35.1-x86_64-linux-gnu) sha256=c786439087512a3fbd199e9897d265b855f951d4027e218ea55e858d45969edd + google-protobuf (4.35.1-x86_64-linux-musl) sha256=91890eb0002934a339fdb7d77a147c46b7474b6799db27872b747b905837f744 + http_parser.rb (0.8.1) sha256=9ae8df145b39aa5398b2f90090d651c67bd8e2ebfe4507c966579f641e11097a + i18n (1.15.2) sha256=00f9eb62412fe593b2a65a97daa75300d37abb8f7202ec748e94b6d46a9dd1b5 + jekyll (4.4.1) sha256=4c1144d857a5b2b80d45b8cf5138289579a9f8136aadfa6dd684b31fe2bc18c1 + jekyll-sass-converter (3.1.0) sha256=83925d84f1d134410c11d0c6643b0093e82e3a3cf127e90757a85294a3862443 + jekyll-sitemap (1.4.0) sha256=0de08c5debc185ea5a8f980e1025c7cd3f8e0c35c8b6ef592f15c46235cf4218 + jekyll-watch (2.2.1) sha256=bc44ed43f5e0a552836245a54dbff3ea7421ecc2856707e8a1ee203a8387a7e1 + json (2.21.1) sha256=13a43df75d95641443f5702dff350f237164a9d811ff0f2c2800d4d980220583 + kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa + kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729 + liquid (4.0.4) sha256=4fcfebb1a045e47918388dbb7a0925e7c3893e58d2bd6c3b3c73ec17a2d8fdb3 + listen (3.10.0) sha256=c6e182db62143aeccc2e1960033bebe7445309c7272061979bb098d03760c9d2 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + mercenary (0.4.0) sha256=b25a1e4a59adca88665e08e24acf0af30da5b5d859f7d8f38fba52c28f405138 + pathutil (0.16.2) sha256=e43b74365631cab4f6d5e4228f812927efc9cb2c71e62976edcb252ee948d589 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rb-fsevent (0.11.2) sha256=43900b972e7301d6570f64b850a5aa67833ee7d87b458ee92805d56b7318aefe + rb-inotify (0.11.1) sha256=a0a700441239b0ff18eb65e3866236cd78613d6b9f78fea1f9ac47a85e47be6e + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rouge (4.7.0) sha256=dba5896715c0325c362e895460a6d350803dbf6427454f49a47500f3193ea739 + safe_yaml (1.0.5) sha256=a6ac2d64b7eb027bdeeca1851fe7e7af0d668e133e8a88066a0c6f7087d9f848 + sass-embedded (1.101.0) sha256=57dbc3409e2c0a2c581a4c9945c2bd72ec88e71ec98017bd02dd1da8a76b22f4 + sass-embedded (1.101.0-aarch64-linux-android) sha256=ad0a35c6ff5cdc4e31c5d8261a768ef460c6c7d1458081183e42170e1474c4b5 + sass-embedded (1.101.0-aarch64-linux-gnu) sha256=8f6926349e880dbb4fda75fb182086fb60aa821b756bcbfc8e5b2b23b1f269d6 + sass-embedded (1.101.0-aarch64-linux-musl) sha256=0a02b4b75db305160db1b0512f040762242128a9b5c01b0fa7a504f2b24557f2 + sass-embedded (1.101.0-arm-linux-androideabi) sha256=88e762531f90d4a2eef55ffafeba337585e1bb24b22307ebd7fe5505de9a4476 + sass-embedded (1.101.0-arm-linux-gnueabihf) sha256=358df7f98d13ff53739ebc45f5c5acd4dd96833bf87f39c74d11f798081d4158 + sass-embedded (1.101.0-arm-linux-musleabihf) sha256=d993a3ad017250d46d1312bc10cfded5ab4585906d37a60a56494bd662182d3a + sass-embedded (1.101.0-arm64-darwin) sha256=9fed684380b49499dfc856aba0d026a0748f924bd78044ffff2bae1537aea73e + sass-embedded (1.101.0-riscv64-linux-android) sha256=1bd35527e1ca24af6aa2ba9db797bc9b72331ebbec9ff6e99ea02b24f83b3da1 + sass-embedded (1.101.0-riscv64-linux-gnu) sha256=7805a249ed4c31b58b2f7d054d14f0ed24c61a836f579cd76ed2367088142bcf + sass-embedded (1.101.0-riscv64-linux-musl) sha256=8bd60eb9ebe6f9f2740a4e9ddb792a3db1fbba282e3375103844b4979eb7c731 + sass-embedded (1.101.0-x86_64-darwin) sha256=20ff4afd7c052b3f8d7b4abe511e8114985a07dff7616750a43ee1fc2759fb28 + sass-embedded (1.101.0-x86_64-linux-android) sha256=472b72114ededa00a22e13553dd31f98d1a92d52bdbe2e738a4dbac2dd50c779 + sass-embedded (1.101.0-x86_64-linux-gnu) sha256=ff48452b2351eaaf4e6e02f59de0e9e35f6f163e8456d520db49b4d87cf73c27 + sass-embedded (1.101.0-x86_64-linux-musl) sha256=2286feef3c7a8d9bbb075aa1651e1a81942aca1213f7c2d30cc1d6f4eb5b4104 + terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131 + +BUNDLED WITH + 4.0.9 diff --git a/LICENSE-CONTENT.md b/LICENSE-CONTENT.md new file mode 100644 index 0000000..3f50224 --- /dev/null +++ b/LICENSE-CONTENT.md @@ -0,0 +1,15 @@ +# Content licence + +The **content** of entries in this repository — method write-ups, summaries, +notes, and bibliography records under `_methods/` and `_bibliography/` — is +licensed under the **Creative Commons Attribution 4.0 International licence +(CC BY 4.0)**. + +You are free to share and adapt this content for any purpose, provided you give +appropriate credit. Contributors are credited by name on each entry. + +Full licence text: https://creativecommons.org/licenses/by/4.0/legalcode +Human-readable summary: https://creativecommons.org/licenses/by/4.0/ + +The site **code** (layouts, styles, scripts, build configuration) is licensed +separately under the MIT Licence — see [LICENSE](LICENSE). diff --git a/README.md b/README.md index 212a490..a3b821a 100644 --- a/README.md +++ b/README.md @@ -1 +1,42 @@ -# methods \ No newline at end of file +# museomics / methods + +A community resource for **museomics** — DNA-based methods applied to museum +specimens. It showcases working methods across taxonomic groups and preservation +types, backed by a curated bibliography of supporting papers and SOPs. + +🌐 **Site:** https://museomics.github.io/methods/ + +- **Methods** (`/`) — filter working methods by kingdom, phylum/division, + preservation, extraction, library prep, and sequencing. Cards expand to full + protocol notes. +- **Bibliography** (`/bibliography/`) — papers and SOPs, filterable by type and + method tag. +- **Contribute** (`/contribute/`) — suggest an entry via issue form, or by PR. + +## How it works + +- **Jekyll** static site, built and deployed to GitHub Pages by GitHub Actions + (`.github/workflows/deploy.yml`) — not the stock Pages build, so plugins and a + validation step are available. +- Entries are Markdown files with YAML front-matter in `_methods/` and + `_bibliography/`. A Liquid template compiles them to `methods.json` / + `bibliography.json` at build time. +- Filtering, search, sorting, and card expansion are plain client-side + JavaScript (`assets/js/app.js`) — no framework. Active filters are reflected + in the URL so views are shareable. +- Tags come from a controlled vocabulary in `_data/tags.yml`, enforced in CI by + `scripts/validate_entries.rb`. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md). The seed `example-*` / `Seed example` +entries are placeholders — replace them with real content. + +## Licences + +- Site code: [MIT](LICENSE) +- Entry content: [CC BY 4.0](LICENSE-CONTENT.md) + +## Citation + +Please cite as *museomics/methods* (https://museomics.github.io/methods/). diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..0326ad7 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,175 @@ +# museomics/methods — Build Spec & Implementation Plan (revised) + +> Revision note: this supersedes the original draft. Changes from the review: +> two-level taxonomy (Kingdom + Phylum/Division) replacing a single taxon +> facet; GitHub Actions build instead of stock GitHub Pages; a single +> canonical controlled vocabulary enforced by CI; free-text search; sort +> control; empty state; accessible chips (not colour-alone); optional DOI; +> optional outcome signal; separate content licence (CC BY 4.0). + +## Purpose + +A community resource for museomics (DNA-based methods applied to museum +specimens) showcasing working methods across taxonomic groups and +preservation types, with a subordinate bibliography of supporting papers and +SOPs. The Methods page is the primary deliverable; the Bibliography is a +secondary, curated reference list. + +## Assumptions + +- Repo `museomics/methods` under the `museomics` GitHub org. +- Served via GitHub Pages at `museomics.github.io/methods/`, built and deployed + by a **GitHub Actions workflow** (not the stock Pages Jekyll build). This + frees plugin choice and lets CI run a validation step. +- No separate landing page — root loads directly into the Methods page. +- Static site, client-side filtering, no backend/database. +- Community contributions arrive via GitHub Issue Forms; a maintainer reviews + and merges them into content (PRs also accepted from technical contributors). +- The controlled vocabulary is curated by the maintainer. + +## Site structure + +``` +/ → Methods page (index) — primary deliverable +/bibliography/ → Bibliography page — papers + SOPs, secondary +/contribute/ → Links to both issue forms + PR instructions +_methods/*.md → method entries +_bibliography/*.md → bibliography entries +_data/tags.yml → controlled vocabulary (single source of truth) +_data/facets.yml → facet display metadata (order, label, colour, abbr) +methods.json → Liquid-generated at build time from _methods +bibliography.json → Liquid-generated at build time from _bibliography +assets/js/app.js → shared client-side filter/search/sort/render logic +scripts/validate_entries.rb → CI vocabulary + schema validation +.github/workflows/deploy.yml → build + deploy to Pages +.github/workflows/ci.yml → PR validation + build check +.github/ISSUE_TEMPLATE/suggest-method.yml +.github/ISSUE_TEMPLATE/suggest-bibliography.yml +``` + +Nav bar on every page: **Methods | Bibliography | Contribute** + +--- + +## 1. Methods page (`/`) + +Let visitors find and filter working methods by **kingdom**, **phylum/division**, +preservation, extraction, library prep, and sequencing/downstream method. +Multi-tagging per entry is supported on every facet. + +### Layout (top to bottom) +1. Header — title + one-line purpose. +2. Tag legend — collapsible explainer of the six facets and their chip + colours/badges. +3. Controls — free-text search box + sort control (newest / oldest / A–Z), then + a filter bar of six collapsible multi-select facets, populated from + `_data/tags.yml`. OR within a facet, AND across facets. +4. Results count (live) + "Clear all filters". +5. Card grid. +6. Empty state (shown when no entry matches). +7. Footer contribute prompt. + +### Card behaviour +- **Collapsed:** title, optional outcome badge, one-line summary, tag chips. + Chips are colour-coded **and** carry a short facet badge + `aria-label` so + they're distinguishable without relying on colour (accessibility). +- **Expanded (on click):** full Markdown body, DOI link (if present, new tab), + contributor + date, collapse control. + +### Entry data model — `_methods/.md` +```yaml +--- +title: "Genome skimming from a 40-year-old pinned beetle" +kingdom: [Animalia] +phylum: [Arthropoda] +preservation: [dry-pinned] +extraction: [silica-column] +library: [tagmentation] +sequencing: [genome-skimming] +outcome: worked-well # optional controlled value +contributor: "Contributor Name" +date: 2026-07-19 +summary: "One-line outcome, shown on the collapsed card." +doi: "10.xxxx/xxxxx" # OPTIONAL — omit for unpublished methods +--- +Full detail body as Markdown (renders on expand). +``` +Required: title, kingdom, phylum, preservation, extraction, library, +sequencing, summary, contributor, date. DOI is optional so unpublished / +in-house methods are not excluded. + +### Filtering mechanics +- Build compiles all entries (front-matter **and** rendered body) into + `methods.json`. Body is inlined because the corpus is small; this also lets + search cover the detail text. Split to lazy-loaded per-entry files only if + load time becomes noticeable. +- Client JS fetches once on load. All filtering/search/sort is client-side. +- The base path and vocabulary are injected into the page by the Jekyll layout + (`window.SITE`) so JS never guesses the baseurl and filter options come from + the config, not just from values present in the data. +- Active filters, search text and non-default sort are reflected in the URL + query string (`?kingdom=Animalia&preservation=dry-pinned&q=beetle`) and + restored on load. + +--- + +## 2. Bibliography page (`/bibliography/`) + +A curated list of papers and SOPs, typed and tag-filterable. Simpler than +Methods — no expansion; each row links out to its source. + +### Entry data model — `_bibliography/.md` +```yaml +--- +type: paper # paper | sop +title: "..." +authors_or_institution: "..." +year_or_date: 2025 +link: "https://doi.org/10.xxxx/xxxxx" +tags: [extraction, silica-column] # reuse shared vocabulary +notes: "Optional one-line relevance note" +--- +``` + +### Layout +- Search box + sort + filter bar (a `type` filter — Paper/SOP — plus the method + facets that apply to references: extraction, library, sequencing). +- List of rows with a type badge, linking out directly. No inline expansion. +- Contribute prompt. + +--- + +## 3. Contribute page (`/contribute/`) +Short explainer + links to both issue forms + PR instructions + a note on the +controlled vocabulary and content licence. + +--- + +## 4. GitHub Issue Forms +`suggest-method.yml` and `suggest-bibliography.yml` with fields matching the +schemas, `labels` set for triage. Dropdowns mirror `_data/tags.yml` (Issue +Forms can't read data files — the mirror is documented in CONTRIBUTING.md). +Method form: DOI optional; a "Other (describe in detail box)" escape hatch on +each dropdown so contributors aren't blocked by a missing vocab term. + +Maintainer workflow: review issue → create/edit the Markdown file → close on +merge. Auto-conversion of issues to Markdown is a possible v2 enhancement. + +--- + +## 5. Implementation stack +- **Jekyll** built by GitHub Actions; deployed with `actions/deploy-pages`. +- Collections `_methods`, `_bibliography`. +- `_data/tags.yml` (vocabulary) + `_data/facets.yml` (display metadata). +- Liquid-generated `methods.json` / `bibliography.json` (no custom Ruby plugin + needed, so it would even run on stock Pages as a fallback). +- Plain JS for all interactivity. +- CI: `scripts/validate_entries.rb` checks required fields + vocabulary on every + PR and before every deploy; `jekyll build` runs as a second check. + +## Deferred / v2 +- Clicking a card chip to apply it as a filter. +- GitHub Action to auto-convert merged issues into Markdown files. +- Lazy-loading per-entry bodies if the corpus grows large. +- Scheduled dead-link checking of DOIs/links. +- Structured cross-links from methods to bibliography SOPs. diff --git a/_bibliography/example-adna-extraction-paper.md b/_bibliography/example-adna-extraction-paper.md new file mode 100644 index 0000000..cbf5ecb --- /dev/null +++ b/_bibliography/example-adna-extraction-paper.md @@ -0,0 +1,9 @@ +--- +type: paper +title: "Example: a silica-based extraction method for highly degraded DNA" +authors_or_institution: "Author A, Author B, et al." +year_or_date: 2013 +link: "https://doi.org/10.0000/example-extraction" +tags: [silica-column] +notes: "Seed example — replace with a real reference. Widely used ancient-DNA extraction approach." +--- diff --git a/_bibliography/example-single-stranded-sop.md b/_bibliography/example-single-stranded-sop.md new file mode 100644 index 0000000..acb715a --- /dev/null +++ b/_bibliography/example-single-stranded-sop.md @@ -0,0 +1,9 @@ +--- +type: sop +title: "Example SOP: single-stranded library preparation for degraded DNA" +authors_or_institution: "Example Lab / Institution" +year_or_date: 2024 +link: "https://doi.org/10.0000/example-sop" +tags: [single-stranded, shotgun-wgs] +notes: "Seed example — replace with a real SOP. Bench protocol for ssDNA libraries." +--- diff --git a/_bibliography/example-target-capture-paper.md b/_bibliography/example-target-capture-paper.md new file mode 100644 index 0000000..c8a3397 --- /dev/null +++ b/_bibliography/example-target-capture-paper.md @@ -0,0 +1,9 @@ +--- +type: paper +title: "Example: universal target-capture bait set for flowering plants" +authors_or_institution: "Author C, Author D, et al." +year_or_date: 2019 +link: "https://doi.org/10.0000/example-capture" +tags: [hybridization-capture, target-capture] +notes: "Seed example — replace with a real reference." +--- diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..5c7db15 --- /dev/null +++ b/_config.yml @@ -0,0 +1,42 @@ +title: "museomics / methods" +description: >- + A community resource for museomics — DNA-based methods applied to museum + specimens — showcasing working methods across taxonomic groups and + preservation types, with a curated bibliography of supporting papers and SOPs. +# Served as a GitHub Pages project site at museomics.github.io/methods/ +url: "https://museomics.github.io" +baseurl: "/methods" + +collections: + methods: + output: true + permalink: /method/:name/ + bibliography: + output: false + +# Per-entry method pages are JSON-driven on the index; the standalone rendered +# page exists mainly to give each method a stable permalink. +defaults: + - scope: + path: "" + type: methods + values: + layout: method + +plugins: + - jekyll-sitemap + +exclude: + - Gemfile + - Gemfile.lock + - vendor + - scripts + - SPEC.md + - CONTRIBUTING.md + - README.md + - LICENSE + - LICENSE-CONTENT.md + - "*.gemspec" + +sass: + style: compressed diff --git a/_data/facets.yml b/_data/facets.yml new file mode 100644 index 0000000..acc0413 --- /dev/null +++ b/_data/facets.yml @@ -0,0 +1,41 @@ +# Facet display metadata. Order here = order in the filter bar and legend. +# `key` matches a list in tags.yml and a field in entry front-matter. +# `abbr` is shown as a small badge on each chip so facets are distinguishable +# without relying on colour alone (accessibility). +# `methods` / `bibliography` flag which page a facet applies to. +- key: kingdom + label: Kingdom + abbr: K + color: green + methods: true + bibliography: false +- key: phylum + label: Phylum / Division + abbr: P + color: teal + methods: true + bibliography: false +- key: preservation + label: Preservation + abbr: Pr + color: blue + methods: true + bibliography: false +- key: extraction + label: Extraction + abbr: Ex + color: orange + methods: true + bibliography: true +- key: library + label: Library prep + abbr: Lb + color: purple + methods: true + bibliography: true +- key: sequencing + label: Sequencing + abbr: Sq + color: red + methods: true + bibliography: true diff --git a/_data/tags.yml b/_data/tags.yml new file mode 100644 index 0000000..d458220 --- /dev/null +++ b/_data/tags.yml @@ -0,0 +1,81 @@ +# ----------------------------------------------------------------------------- +# Controlled vocabulary (single source of truth for filter options) +# ----------------------------------------------------------------------------- +# This file drives the filter UI on the Methods and Bibliography pages AND is +# enforced by CI: every tag used in an entry's front-matter must appear here, +# or the build fails (see scripts/validate_entries.rb). +# +# To add a term: add it to the relevant list below, in a PR. If it is a term +# that also appears as a dropdown in a GitHub Issue Form, update the matching +# option list in .github/ISSUE_TEMPLATE/*.yml too (the forms cannot read this +# file automatically). CONTRIBUTING.md explains this. +# +# Values are the canonical stored form (lower-case, hyphenated where multi-word) +# except taxonomy, which uses conventional capitalised names. +# ----------------------------------------------------------------------------- + +# Taxonomy is split into two levels so visitors can filter broadly (Kingdom) or +# more specifically (Phylum / Division). Curated by the maintainer; extend as +# real contributions arrive rather than pre-populating exhaustively. +kingdom: + - Animalia + - Plantae + - Fungi + - Chromista + - Bacteria + - Archaea + +phylum: # "Phylum / Division" — divisions are the botanical equivalent + - Chordata + - Arthropoda + - Mollusca + - Annelida + - Echinodermata + - Cnidaria + - Nematoda + - Platyhelminthes + - Porifera + - Tracheophyta # vascular plants + - Bryophyta # mosses + - Ascomycota + - Basidiomycota + +preservation: + - dry-pinned + - study-skin + - herbarium-pressed + - fluid-ethanol + - fluid-formalin + - frozen + - dried-tissue + - bone-subfossil + +extraction: + - silica-column + - magnetic-bead + - phenol-chloroform + - ctab + - chelex + +library: + - tagmentation + - ligation + - single-stranded + - pcr-free + - hybridization-capture + +sequencing: + - genome-skimming + - shotgun-wgs + - target-capture + - metabarcoding + - rad-seq + - sanger + +# Optional, controlled outcome signal for method entries (see facets.yml). +# Lets visitors gauge how well a method worked at a glance. Not a filter facet +# by default, but validated when present. +outcome: + - worked-well + - worked-with-caveats + - exploratory diff --git a/_includes/nav.html b/_includes/nav.html new file mode 100644 index 0000000..e09275d --- /dev/null +++ b/_includes/nav.html @@ -0,0 +1,10 @@ + diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 0000000..cb026bb --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,41 @@ + + + + + + {% if page.title %}{{ page.title }} · {% endif %}{{ site.title }} + + + + + + + {% include nav.html %} +
+ {{ content }} +
+
+
+

+ A community museomics resource · + Source on GitHub +

+

+ Code licensed MIT · entry content licensed + CC BY 4.0 +

+
+
+ {% if page.scripts %}{% for s in page.scripts %} + + {% endfor %}{% endif %} + + diff --git a/_layouts/method.html b/_layouts/method.html new file mode 100644 index 0000000..b07323c --- /dev/null +++ b/_layouts/method.html @@ -0,0 +1,28 @@ +--- +layout: default +page_id: method +--- +
+ +

{{ page.title }}

+ {% if page.summary %}

{{ page.summary }}

{% endif %} + +
    + {% for f in site.data.facets %} + {% assign vals = page[f.key] %} + {% if vals %}{% for v in vals %} +
  • {{ f.abbr }}{{ v }}
  • + {% endfor %}{% endif %} + {% endfor %} +
+ +
+ {{ content }} +
+ +
+ {% if page.doi %}
Source
doi.org/{{ page.doi }}
{% endif %} + {% if page.contributor %}
Contributor
{{ page.contributor }}
{% endif %} + {% if page.date %}
Added
{{ page.date | date: "%Y-%m-%d" }}
{% endif %} +
+
diff --git a/_methods/genome-skimming-pinned-beetle.md b/_methods/genome-skimming-pinned-beetle.md new file mode 100644 index 0000000..06aaee2 --- /dev/null +++ b/_methods/genome-skimming-pinned-beetle.md @@ -0,0 +1,35 @@ +--- +title: "Genome skimming from a 40-year-old pinned beetle" +kingdom: [Animalia] +phylum: [Arthropoda] +preservation: [dry-pinned] +extraction: [silica-column] +library: [tagmentation] +sequencing: [genome-skimming] +outcome: worked-well +contributor: "Seed example — replace with real entry" +date: 2026-07-01 +summary: "Low-coverage shotgun recovered a full mitogenome and nuclear rRNA from a dry-pinned specimen." +doi: "10.0000/example-beetle" +--- + +**This is a seed example entry** to demonstrate the schema and the site. Replace +or delete it once real contributions arrive. + +## Approach + +A single leg was removed from a dry-pinned beetle collected in the 1980s. DNA +was extracted with a silica-column protocol adapted for degraded material +(extended proteinase K digestion, low-volume elution). A tagmentation-based +library was sequenced to ~2× nuclear coverage. + +## Outcome + +- Complete mitochondrial genome assembled. +- Nuclear ribosomal cluster (18S/28S) recovered at usable depth. +- Fragment lengths peaked ~90 bp, consistent with dry storage degradation. + +## Caveats + +Endogenous yield varies strongly with specimen handling history. Non-destructive +sampling of a single leg was sufficient here but may not be for smaller taxa. diff --git a/_methods/herbarium-plant-target-capture.md b/_methods/herbarium-plant-target-capture.md new file mode 100644 index 0000000..1870eb6 --- /dev/null +++ b/_methods/herbarium-plant-target-capture.md @@ -0,0 +1,33 @@ +--- +title: "Target capture from herbarium sheets using CTAB extraction" +kingdom: [Plantae] +phylum: [Tracheophyta] +preservation: [herbarium-pressed] +extraction: [ctab] +library: [hybridization-capture] +sequencing: [target-capture] +outcome: worked-with-caveats +contributor: "Seed example — replace with real entry" +date: 2026-06-15 +summary: "Angiosperms353 target capture from pressed herbarium tissue up to ~120 years old." +doi: "10.0000/example-herbarium" +--- + +**Seed example entry.** Replace with a real method. + +## Approach + +Leaf fragments (~20 mg) from herbarium sheets were extracted with a modified +CTAB protocol including a sorbitol pre-wash to remove polysaccharides. Libraries +were enriched with the Angiosperms353 universal bait set before sequencing. + +## Outcome + +Locus recovery correlated with specimen age and original drying speed. Recently +collected, fast-dried material gave near-complete gene sets; older sheets +recovered a usable but reduced subset. + +## Caveats + +Polysaccharide and polyphenol carry-over from some families inhibited library +prep; an additional clean-up step was needed for Malvaceae material. diff --git a/_methods/study-skin-bird-mitogenome.md b/_methods/study-skin-bird-mitogenome.md new file mode 100644 index 0000000..72b2fd6 --- /dev/null +++ b/_methods/study-skin-bird-mitogenome.md @@ -0,0 +1,34 @@ +--- +title: "Mitogenome from a historical bird study skin toe pad" +kingdom: [Animalia] +phylum: [Chordata] +preservation: [study-skin] +extraction: [magnetic-bead] +library: [single-stranded] +sequencing: [shotgun-wgs] +outcome: worked-well +contributor: "Seed example — replace with real entry" +date: 2026-05-20 +summary: "Single-stranded library from a toe-pad clip yielded a complete mitogenome from a ~100-year-old skin." +doi: "10.0000/example-bird" +--- + +**Seed example entry.** Replace with a real method. + +## Approach + +A small toe-pad clip was taken from a study skin. DNA was extracted with a +magnetic-bead protocol optimised for short fragments, and a single-stranded +library was built to capture ultra-short and damaged molecules. Shallow shotgun +sequencing followed. + +## Outcome + +- Complete mitogenome at high coverage. +- Sufficient nuclear data for SNP-based identification. +- Deamination profile consistent with historical DNA, useful for authentication. + +## Caveats + +Single-stranded library prep is more costly and laborious than double-stranded +approaches; reserve it for the most degraded material where it pays off. diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..607da6b --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,177 @@ +/* museomics/methods — styles. Light theme with a dark-mode fallback. */ +:root { + --bg: #ffffff; + --surface: #f7f8f9; + --surface-2: #eef0f2; + --text: #1c2024; + --muted: #5c6570; + --border: #d9dde1; + --accent: #1f6f5c; + --accent-text: #ffffff; + --radius: 10px; + --maxw: 1000px; + + /* facet palette: soft background + readable text */ + --green-bg:#e3f3ea; --green-fg:#1c6b47; + --teal-bg:#e0f2f1; --teal-fg:#116a63; + --blue-bg:#e5eefc; --blue-fg:#1f5bb5; + --orange-bg:#fdeede; --orange-fg:#a85c11; + --purple-bg:#efe6fa; --purple-fg:#6b3aa8; + --red-bg:#fce8e6; --red-fg:#b23028; + --grey-bg:#e9ecef; --grey-fg:#495057; +} +@media (prefers-color-scheme: dark) { + :root { + --bg:#15181b; --surface:#1d2125; --surface-2:#262b30; + --text:#e6e9ec; --muted:#9aa4ae; --border:#333a41; --accent:#3fae90; + --green-bg:#173a2c; --green-fg:#7fd3ad; + --teal-bg:#123a37; --teal-fg:#6fd0c7; + --blue-bg:#172c4d; --blue-fg:#8fb6f0; + --orange-bg:#3d2a14; --orange-fg:#e0a465; + --purple-bg:#2c1f45; --purple-fg:#bd9bec; + --red-bg:#3d1e1b; --red-fg:#e89a92; + --grey-bg:#2a2f34; --grey-fg:#aab2ba; + } +} + +* { box-sizing: border-box; } +html { -webkit-text-size-adjust: 100%; } +body { + margin: 0; + font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: var(--text); + background: var(--bg); +} +.container { max-width: var(--maxw); margin: 0 auto; padding: 0 1.1rem; } +a { color: var(--accent); } +h1, h2, h3 { line-height: 1.2; } +.muted { color: var(--muted); } +.lead { font-size: 1.08rem; color: var(--muted); max-width: 60ch; } +.visually-hidden { + position: absolute; width: 1px; height: 1px; overflow: hidden; + clip: rect(0 0 0 0); white-space: nowrap; +} + +/* nav */ +.site-nav { border-bottom: 1px solid var(--border); background: var(--surface); } +.nav-inner { display: flex; align-items: center; justify-content: space-between; padding: .7rem 1.1rem; } +.brand { font-weight: 700; text-decoration: none; color: var(--text); } +.brand-slash { color: var(--accent); } +.site-nav nav a { margin-left: 1.1rem; text-decoration: none; color: var(--muted); font-weight: 600; } +.site-nav nav a[aria-current="page"] { color: var(--text); border-bottom: 2px solid var(--accent); } + +main.container { padding-top: 1.6rem; padding-bottom: 3rem; } +.page-head h1 { margin: 0 0 .3rem; } + +/* legend */ +.legend { margin: 1.2rem 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: .6rem .9rem; } +.legend summary { cursor: pointer; font-weight: 600; } +.legend-body { padding-top: .6rem; } +.legend-list { list-style: none; display: flex; flex-wrap: wrap; gap: .5rem; padding: 0; margin: .4rem 0 0; } + +/* controls */ +.controls { margin: 1.2rem 0 1.4rem; } +.searchbar { display: flex; gap: .6rem; margin-bottom: .8rem; } +.searchbar input[type="search"] { + flex: 1; padding: .6rem .8rem; font-size: 1rem; + border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg); color: var(--text); +} +.searchbar select { + padding: .6rem .7rem; border: 1px solid var(--border); border-radius: var(--radius); + background: var(--bg); color: var(--text); +} +.filterbar { display: flex; flex-wrap: wrap; gap: .6rem; } +.facet { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); min-width: 160px; } +.facet > summary { + cursor: pointer; padding: .5rem .7rem; font-weight: 600; list-style: none; + border-left: 4px solid var(--border); border-radius: var(--radius); +} +.facet > summary::-webkit-details-marker { display: none; } +.facet[open] > summary { border-bottom: 1px solid var(--border); border-radius: var(--radius) var(--radius) 0 0; } +.facet--green > summary { border-left-color: var(--green-fg); } +.facet--teal > summary { border-left-color: var(--teal-fg); } +.facet--blue > summary { border-left-color: var(--blue-fg); } +.facet--orange > summary { border-left-color: var(--orange-fg); } +.facet--purple > summary { border-left-color: var(--purple-fg); } +.facet--red > summary { border-left-color: var(--red-fg); } +.facet--grey > summary { border-left-color: var(--grey-fg); } +.facet-opts { padding: .5rem .7rem; max-height: 240px; overflow-y: auto; } +.opt { display: flex; align-items: center; gap: .45rem; padding: .15rem 0; cursor: pointer; } +.opt input { margin: 0; } + +.controls-foot { display: flex; align-items: center; gap: 1rem; margin-top: .9rem; } +.results-count { margin: 0; font-weight: 600; } +.btn-clear, .link-btn { + background: none; border: 1px solid var(--border); color: var(--accent); + padding: .35rem .7rem; border-radius: var(--radius); cursor: pointer; font: inherit; +} +.link-btn { border: none; text-decoration: underline; padding: 0; } + +/* chips */ +.chip-row { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .5rem; } +.chip { + display: inline-flex; align-items: center; gap: .35rem; + font-size: .82rem; padding: .18rem .55rem; border-radius: 999px; white-space: nowrap; + background: var(--surface-2); color: var(--text); +} +.chip-abbr { font-size: .66rem; font-weight: 700; padding: .05rem .3rem; border-radius: 4px; background: rgba(0,0,0,.08); } +@media (prefers-color-scheme: dark) { .chip-abbr { background: rgba(255,255,255,.14); } } +.chip--green { background: var(--green-bg); color: var(--green-fg); } +.chip--teal { background: var(--teal-bg); color: var(--teal-fg); } +.chip--blue { background: var(--blue-bg); color: var(--blue-fg); } +.chip--orange { background: var(--orange-bg); color: var(--orange-fg); } +.chip--purple { background: var(--purple-bg); color: var(--purple-fg); } +.chip--red { background: var(--red-bg); color: var(--red-fg); } +.chip--grey { background: var(--grey-bg); color: var(--grey-fg); } + +/* method cards */ +.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; } +.card { border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); overflow: hidden; } +.card--open { border-color: var(--accent); } +.card-head { + display: block; width: 100%; text-align: left; cursor: pointer; + background: none; border: none; padding: .9rem 1rem; color: inherit; font: inherit; +} +.card-head:hover { background: var(--surface-2); } +.card-title { margin: 0 0 .2rem; font-size: 1.05rem; } +.card-summary { margin: .3rem 0 0; color: var(--muted); } +.outcome { display: inline-block; font-size: .72rem; font-weight: 700; padding: .1rem .45rem; border-radius: 4px; vertical-align: middle; } +.outcome--worked-well { background: var(--green-bg); color: var(--green-fg); } +.outcome--worked-with-caveats { background: var(--orange-bg); color: var(--orange-fg); } +.outcome--exploratory { background: var(--grey-bg); color: var(--grey-fg); } +.card-body { padding: 0 1rem 1rem; border-top: 1px solid var(--border); } +.card-detail { padding-top: .8rem; } +.card-detail h2 { font-size: 1.02rem; } +.card-detail h3 { font-size: .95rem; } +.card-detail img { max-width: 100%; height: auto; } +.card-doi { margin: .8rem 0 .2rem; word-break: break-word; } +.card-byline { margin: .2rem 0 0; font-size: .85rem; } + +/* bibliography */ +.biblio-list { display: flex; flex-direction: column; gap: .7rem; } +.biblio-row { display: flex; gap: .8rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); padding: .8rem 1rem; } +.type-badge { flex: 0 0 auto; align-self: flex-start; font-size: .68rem; font-weight: 700; padding: .15rem .45rem; border-radius: 4px; } +.type-badge--paper { background: var(--blue-bg); color: var(--blue-fg); } +.type-badge--sop { background: var(--purple-bg); color: var(--purple-fg); } +.biblio-title { margin: 0; font-size: 1rem; } +.biblio-meta { margin: .15rem 0 0; font-size: .88rem; } +.biblio-notes { margin: .35rem 0 0; } + +/* misc */ +.empty-state { padding: 1.5rem; text-align: center; color: var(--muted); } +.load-error { color: var(--red-fg); } +.contribute-prompt { margin-top: 2rem; padding: 1rem; background: var(--surface); border: 1px dashed var(--border); border-radius: var(--radius); text-align: center; } +.site-footer { border-top: 1px solid var(--border); padding: 1.4rem 0; color: var(--muted); font-size: .9rem; } +.site-footer p { margin: .2rem 0; } + +/* method standalone page */ +.method-page { max-width: 70ch; } +.back-link { margin-bottom: .5rem; } +.method-meta dt { font-weight: 700; margin-top: .6rem; } +.method-meta dd { margin: 0; } + +@media (max-width: 640px) { + .searchbar { flex-direction: column; } + .facet { min-width: 0; width: 100%; } + .filterbar { flex-direction: column; } +} diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..b75f93a --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,349 @@ +/* museomics/methods — client-side filtering, search, sort and rendering. + * No framework. Reads config injected by the Jekyll layout in window.SITE: + * { baseurl, facets:[{key,label,abbr,color,methods,bibliography}], vocab:{key:[...]} } + * Runs on both the Methods page (data-page="methods") and the Bibliography + * page (data-page="bibliography"); a small per-page config object switches + * behaviour where they differ. + */ +(function () { + "use strict"; + + var SITE = window.SITE || { baseurl: "", facets: [], vocab: {} }; + var PAGE = document.body.getAttribute("data-page"); + if (PAGE !== "methods" && PAGE !== "bibliography") return; + + // --- per-page configuration ------------------------------------------- + var CONFIG = { + methods: { + dataUrl: SITE.baseurl + "/methods.json", + facets: SITE.facets.filter(function (f) { return f.methods; }), + hasType: false, + searchFields: ["title", "summary", "body"], + sortDefault: "date-desc", + valuesFor: function (entry, key) { return entry[key] || []; } + }, + bibliography: { + dataUrl: SITE.baseurl + "/bibliography.json", + facets: SITE.facets.filter(function (f) { return f.bibliography; }), + hasType: true, + searchFields: ["title", "authors_or_institution", "notes"], + sortDefault: "year-desc", + // Bibliography stores one flat `tags` array; a facet's values are the + // subset of that array present in the facet's vocabulary. + valuesFor: function (entry, key) { + var vocab = SITE.vocab[key] || []; + return (entry.tags || []).filter(function (t) { return vocab.indexOf(t) !== -1; }); + } + } + }[PAGE]; + + // --- state ------------------------------------------------------------ + var ALL = []; // all entries + var filters = {}; // facetKey -> array of selected values + CONFIG.facets.forEach(function (f) { filters[f.key] = []; }); + var typeFilter = []; // bibliography type (paper/sop) + var query = ""; + var sort = CONFIG.sortDefault; + + // --- helpers ---------------------------------------------------------- + function el(tag, attrs, children) { + var node = document.createElement(tag); + if (attrs) Object.keys(attrs).forEach(function (k) { + if (k === "class") node.className = attrs[k]; + else if (k === "html") node.innerHTML = attrs[k]; + else if (k === "text") node.textContent = attrs[k]; + else node.setAttribute(k, attrs[k]); + }); + (children || []).forEach(function (c) { if (c) node.appendChild(c); }); + return node; + } + + function facetByKey(key) { + for (var i = 0; i < SITE.facets.length; i++) if (SITE.facets[i].key === key) return SITE.facets[i]; + return null; + } + + // --- URL <-> state ---------------------------------------------------- + function readUrl() { + var p = new URLSearchParams(window.location.search); + CONFIG.facets.forEach(function (f) { + var v = p.get(f.key); + filters[f.key] = v ? v.split(",").filter(Boolean) : []; + }); + if (CONFIG.hasType) { + var t = p.get("type"); + typeFilter = t ? t.split(",").filter(Boolean) : []; + } + query = p.get("q") || ""; + sort = p.get("sort") || CONFIG.sortDefault; + } + + function writeUrl() { + var p = new URLSearchParams(); + CONFIG.facets.forEach(function (f) { + if (filters[f.key].length) p.set(f.key, filters[f.key].join(",")); + }); + if (CONFIG.hasType && typeFilter.length) p.set("type", typeFilter.join(",")); + if (query) p.set("q", query); + if (sort !== CONFIG.sortDefault) p.set("sort", sort); + var qs = p.toString(); + var url = window.location.pathname + (qs ? "?" + qs : ""); + window.history.replaceState(null, "", url); + } + + // --- filtering & sorting --------------------------------------------- + function anyFilterActive() { + if (query) return true; + if (CONFIG.hasType && typeFilter.length) return true; + return CONFIG.facets.some(function (f) { return filters[f.key].length; }); + } + + function matches(entry) { + // AND across facets, OR within a facet. + for (var i = 0; i < CONFIG.facets.length; i++) { + var key = CONFIG.facets[i].key; + var sel = filters[key]; + if (!sel.length) continue; + var vals = CONFIG.valuesFor(entry, key); + var ok = sel.some(function (s) { return vals.indexOf(s) !== -1; }); + if (!ok) return false; + } + if (CONFIG.hasType && typeFilter.length && typeFilter.indexOf(entry.type) === -1) return false; + if (query) { + var q = query.toLowerCase(); + var hit = CONFIG.searchFields.some(function (field) { + var val = entry[field]; + return val && String(val).toLowerCase().indexOf(q) !== -1; + }); + if (!hit) return false; + } + return true; + } + + function sortEntries(list) { + var arr = list.slice(); + arr.sort(function (a, b) { + switch (sort) { + case "title-asc": return (a.title || "").localeCompare(b.title || ""); + case "date-asc": return String(a.date || "").localeCompare(String(b.date || "")); + case "date-desc": return String(b.date || "").localeCompare(String(a.date || "")); + case "year-asc": return String(a.year_or_date || "").localeCompare(String(b.year_or_date || "")); + case "year-desc": return String(b.year_or_date || "").localeCompare(String(a.year_or_date || "")); + default: return 0; + } + }); + return arr; + } + + // --- rendering: chips ------------------------------------------------- + function chip(facet, value) { + return el("span", { class: "chip chip--" + facet.color, "aria-label": facet.label + ": " + value }, [ + el("span", { class: "chip-abbr", title: facet.label, "aria-hidden": "true", text: facet.abbr }), + document.createTextNode(value) + ]); + } + + // --- rendering: methods cards ---------------------------------------- + function renderCard(entry) { + var chips = []; + CONFIG.facets.forEach(function (f) { + (entry[f.key] || []).forEach(function (v) { chips.push(chip(f, v)); }); + }); + + var summary = el("button", { + class: "card-head", + type: "button", + "aria-expanded": "false" + }, [ + el("h3", { class: "card-title", text: entry.title || "Untitled" }), + entry.outcome ? el("span", { class: "outcome outcome--" + entry.outcome, text: outcomeLabel(entry.outcome) }) : null, + el("p", { class: "card-summary", text: entry.summary || "" }), + el("div", { class: "chip-row" }, chips) + ]); + + var metaChildren = []; + if (entry.doi) metaChildren.push(el("p", { class: "card-doi" }, [ + el("a", { href: "https://doi.org/" + entry.doi, target: "_blank", rel: "noopener", text: "doi.org/" + entry.doi }) + ])); + var byline = []; + if (entry.contributor) byline.push(entry.contributor); + if (entry.date) byline.push(entry.date); + if (byline.length) metaChildren.push(el("p", { class: "card-byline muted", text: byline.join(" · ") })); + + var body = el("div", { class: "card-body", hidden: "hidden" }, [ + el("div", { class: "card-detail", html: entry.body || "" }) + ].concat(metaChildren)); + + var card = el("article", { class: "card" }, [summary, body]); + + summary.addEventListener("click", function () { + var open = summary.getAttribute("aria-expanded") === "true"; + summary.setAttribute("aria-expanded", open ? "false" : "true"); + if (open) body.setAttribute("hidden", "hidden"); else body.removeAttribute("hidden"); + card.classList.toggle("card--open", !open); + }); + return card; + } + + function outcomeLabel(v) { + return ({ + "worked-well": "Worked well", + "worked-with-caveats": "Worked with caveats", + "exploratory": "Exploratory" + })[v] || v; + } + + // --- rendering: bibliography rows ------------------------------------ + function renderRow(entry) { + var chips = []; + CONFIG.facets.forEach(function (f) { + CONFIG.valuesFor(entry, f.key).forEach(function (v) { chips.push(chip(f, v)); }); + }); + var titleNode = entry.link + ? el("a", { href: entry.link, target: "_blank", rel: "noopener", text: entry.title || "Untitled" }) + : el("span", { text: entry.title || "Untitled" }); + + var meta = [entry.authors_or_institution, entry.year_or_date].filter(Boolean).join(" · "); + + return el("article", { class: "biblio-row" }, [ + el("span", { class: "type-badge type-badge--" + (entry.type || "paper"), text: (entry.type || "paper").toUpperCase() }), + el("div", { class: "biblio-main" }, [ + el("h3", { class: "biblio-title" }, [titleNode]), + meta ? el("p", { class: "biblio-meta muted", text: meta }) : null, + entry.notes ? el("p", { class: "biblio-notes", text: entry.notes }) : null, + chips.length ? el("div", { class: "chip-row" }, chips) : null + ]) + ]); + } + + // --- rendering: filter UI -------------------------------------------- + function buildFilterBar() { + var bar = document.getElementById("filterbar"); + bar.innerHTML = ""; + + if (CONFIG.hasType) { + bar.appendChild(facetGroup({ key: "__type", label: "Type", abbr: "T", color: "grey" }, + ["paper", "sop"], typeFilter, function (val, on) { + toggleIn(typeFilter, val, on); update(); + }, function (v) { return v.toUpperCase(); })); + } + + CONFIG.facets.forEach(function (f) { + var vocab = SITE.vocab[f.key] || []; + bar.appendChild(facetGroup(f, vocab, filters[f.key], function (val, on) { + toggleIn(filters[f.key], val, on); update(); + })); + }); + } + + function facetGroup(facet, values, selectedRef, onToggle, labelFn) { + var options = values.map(function (v) { + var checked = selectedRef.indexOf(v) !== -1; + var input = el("input", { type: "checkbox", value: v }); + if (checked) input.checked = true; + input.addEventListener("change", function () { onToggle(v, input.checked); }); + return el("label", { class: "opt" }, [input, el("span", { text: labelFn ? labelFn(v) : v })]); + }); + var details = el("details", { class: "facet facet--" + facet.color }); + if (selectedRef.length) details.open = true; + var count = selectedRef.length ? " (" + selectedRef.length + ")" : ""; + details.appendChild(el("summary", { text: facet.label + count })); + details.appendChild(el("div", { class: "facet-opts" }, options)); + return details; + } + + function toggleIn(arr, val, on) { + var i = arr.indexOf(val); + if (on && i === -1) arr.push(val); + if (!on && i !== -1) arr.splice(i, 1); + } + + // --- legend (methods only) ------------------------------------------- + function buildLegend() { + var ul = document.getElementById("legend-list"); + if (!ul) return; + CONFIG.facets.forEach(function (f) { + ul.appendChild(el("li", {}, [ + el("span", { class: "chip chip--" + f.color }, [ + el("span", { class: "chip-abbr", "aria-hidden": "true", text: f.abbr }), + document.createTextNode(f.label) + ]) + ])); + }); + } + + // --- main update loop ------------------------------------------------- + function update() { + writeUrl(); + var shown = sortEntries(ALL.filter(matches)); + + var container = document.getElementById(PAGE === "methods" ? "cards" : "biblio-list"); + container.innerHTML = ""; + var render = PAGE === "methods" ? renderCard : renderRow; + shown.forEach(function (e) { container.appendChild(render(e)); }); + + var count = document.getElementById("results-count"); + count.textContent = shown.length + " of " + ALL.length + + (PAGE === "methods" ? " methods" : " entries") + " shown"; + + document.getElementById("empty-state").hidden = shown.length !== 0; + var clearBtn = document.getElementById("clear-filters"); + if (clearBtn) clearBtn.hidden = !anyFilterActive(); + + // Keep summary counts in the filter bar fresh. + buildFilterBar(); + } + + function clearAll() { + CONFIG.facets.forEach(function (f) { filters[f.key] = []; }); + typeFilter = []; + query = ""; + var qEl = document.getElementById("q"); + if (qEl) qEl.value = ""; + update(); + } + + // --- init ------------------------------------------------------------- + function init() { + readUrl(); + buildLegend(); + + var qEl = document.getElementById("q"); + if (qEl) { + qEl.value = query; + var t; + qEl.addEventListener("input", function () { + clearTimeout(t); + t = setTimeout(function () { query = qEl.value.trim(); update(); }, 150); + }); + } + var sortEl = document.getElementById("sort"); + if (sortEl) { + sortEl.value = sort; + sortEl.addEventListener("change", function () { sort = sortEl.value; update(); }); + } + ["clear-filters", "empty-clear"].forEach(function (id) { + var b = document.getElementById(id); + if (b) b.addEventListener("click", clearAll); + }); + + fetch(CONFIG.dataUrl) + .then(function (r) { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.json(); + }) + .then(function (data) { ALL = data; update(); }) + .catch(function (err) { + var container = document.getElementById(PAGE === "methods" ? "cards" : "biblio-list"); + container.innerHTML = ""; + container.appendChild(el("p", { class: "load-error", + text: "Could not load data (" + err.message + ")." })); + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/bibliography.json b/bibliography.json new file mode 100644 index 0000000..995d84c --- /dev/null +++ b/bibliography.json @@ -0,0 +1,18 @@ +--- +layout: null +sitemap: false +--- +[ +{% for b in site.bibliography %} + { + "id": {{ b.slug | jsonify }}, + "type": {{ b.type | jsonify }}, + "title": {{ b.title | jsonify }}, + "authors_or_institution": {{ b.authors_or_institution | jsonify }}, + "year_or_date": {{ b.year_or_date | jsonify }}, + "link": {{ b.link | jsonify }}, + "tags": {{ b.tags | jsonify }}, + "notes": {{ b.notes | jsonify }} + }{% unless forloop.last %},{% endunless %} +{% endfor %} +] diff --git a/bibliography/index.html b/bibliography/index.html new file mode 100644 index 0000000..9672948 --- /dev/null +++ b/bibliography/index.html @@ -0,0 +1,42 @@ +--- +layout: default +page_id: bibliography +title: Bibliography +scripts: + - /assets/js/app.js +--- +
+

Bibliography

+

+ Papers and standard operating procedures (SOPs) relevant to museomics, + curated from community suggestions. Filter by type or by method tag. +

+
+ +
+ +
+
+

+ +
+
+ +
+ + + + diff --git a/contribute/index.md b/contribute/index.md new file mode 100644 index 0000000..8f6e1f1 --- /dev/null +++ b/contribute/index.md @@ -0,0 +1,47 @@ +--- +layout: default +page_id: contribute +title: Contribute +--- + +# Contribute + +This resource grows from the museomics community. There are two ways to add to +it — no coding required for the first. + +## 1. Suggest via a form (recommended) + +Fill in a short GitHub issue form. A maintainer reviews it and adds it to the +site. You'll need a free GitHub account. + +- **[Suggest a method →](https://github.com/museomics/methods/issues/new?template=suggest-method.yml)** + — a working method you've used on museum material. +- **[Suggest a bibliography entry →](https://github.com/museomics/methods/issues/new?template=suggest-bibliography.yml)** + — a paper or SOP worth listing. + +## 2. Contribute directly by pull request + +If you're comfortable with GitHub: + +1. Fork the [repository](https://github.com/museomics/methods). +2. Add one Markdown file to `_methods/` (or `_bibliography/`) following the + front-matter schema in + [`CONTRIBUTING.md`](https://github.com/museomics/methods/blob/main/CONTRIBUTING.md). +3. Use only tag values from the controlled vocabulary in + [`_data/tags.yml`](https://github.com/museomics/methods/blob/main/_data/tags.yml). + CI checks this automatically — if you need a new tag, add it to that file in + the same PR. +4. Open a pull request. A maintainer will review and merge. + +## A note on tags + +Tags are a curated, controlled vocabulary so filtering stays consistent and +typos don't fragment the taxonomy. Proposing a new value is welcome — just edit +`_data/tags.yml` in your PR, or mention it in your issue. + +## Licence & citation + +Entry content is published under +[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): reuse freely with +attribution. Contributors are credited by name on each entry. If you use this +resource, please cite it as *museomics/methods* (museomics.github.io/methods). diff --git a/index.html b/index.html new file mode 100644 index 0000000..d0fae13 --- /dev/null +++ b/index.html @@ -0,0 +1,55 @@ +--- +layout: default +page_id: methods +title: Methods +scripts: + - /assets/js/app.js +--- +
+

Museomics methods

+

+ Working DNA-based methods for museum specimens, filterable by taxon, + preservation type, and lab approach. Each entry is a real, tried method + contributed by the community. +

+
+ +
+ What do the tags mean? +
+

+ Every method is tagged across up to six facets. Filters combine with + OR within a facet and AND across facets. + Chips are colour-coded and carry a short facet badge: +

+
+
+
+ +
+ +
+
+

+ +
+
+ +
+ + + + diff --git a/methods.json b/methods.json new file mode 100644 index 0000000..c697b08 --- /dev/null +++ b/methods.json @@ -0,0 +1,25 @@ +--- +layout: null +sitemap: false +--- +[ +{% for m in site.methods %} + { + "id": {{ m.slug | jsonify }}, + "url": {{ m.url | relative_url | jsonify }}, + "title": {{ m.title | jsonify }}, + "summary": {{ m.summary | jsonify }}, + "doi": {{ m.doi | jsonify }}, + "contributor": {{ m.contributor | jsonify }}, + "date": {{ m.date | date: "%Y-%m-%d" | jsonify }}, + "outcome": {{ m.outcome | jsonify }}, + "kingdom": {{ m.kingdom | jsonify }}, + "phylum": {{ m.phylum | jsonify }}, + "preservation": {{ m.preservation | jsonify }}, + "extraction": {{ m.extraction | jsonify }}, + "library": {{ m.library | jsonify }}, + "sequencing": {{ m.sequencing | jsonify }}, + "body": {{ m.content | jsonify }} + }{% unless forloop.last %},{% endunless %} +{% endfor %} +] diff --git a/scripts/validate_entries.rb b/scripts/validate_entries.rb new file mode 100644 index 0000000..c6a088e --- /dev/null +++ b/scripts/validate_entries.rb @@ -0,0 +1,99 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# Validates every method and bibliography entry against the controlled +# vocabulary in _data/tags.yml and a small required-field schema. +# Exits non-zero (failing CI) if any entry is invalid. Run: +# ruby scripts/validate_entries.rb + +require "yaml" +require "date" + +ROOT = File.expand_path("..", __dir__) +TAGS = YAML.safe_load_file(File.join(ROOT, "_data", "tags.yml")) + +errors = [] + +# Front-matter fields on method entries that must be drawn from a vocab list. +METHOD_FACETS = %w[kingdom phylum preservation extraction library sequencing].freeze +METHOD_REQUIRED = (METHOD_FACETS + %w[title summary contributor date]).freeze + +BIB_REQUIRED = %w[type title authors_or_institution year_or_date link].freeze +# Any tag on a bibliography entry must exist in one of these facet lists. +BIB_TAG_VOCAB = (TAGS["extraction"] + TAGS["library"] + TAGS["sequencing"] + + TAGS["preservation"] + TAGS["kingdom"] + TAGS["phylum"]).uniq.freeze + +def front_matter(path) + raw = File.read(path, encoding: "UTF-8") + unless raw.start_with?("---") + return [nil, "missing YAML front-matter"] + end + # Split on the closing --- of the front-matter block. + _, fm, = raw.split(/^---\s*$\n?/, 3) + [YAML.safe_load(fm, permitted_classes: [Date, Time]), nil] +rescue => e + [nil, "unparseable front-matter (#{e.message})"] +end + +def as_array(v) + v.nil? ? [] : Array(v) +end + +# --- methods --------------------------------------------------------------- +Dir.glob(File.join(ROOT, "_methods", "*.md")).sort.each do |path| + rel = path.sub("#{ROOT}/", "") + fm, err = front_matter(path) + if err + errors << "#{rel}: #{err}" + next + end + + METHOD_REQUIRED.each do |field| + val = fm[field] + errors << "#{rel}: missing required field `#{field}`" if val.nil? || (val.respond_to?(:empty?) && val.empty?) + end + + METHOD_FACETS.each do |facet| + allowed = TAGS[facet] || [] + as_array(fm[facet]).each do |v| + errors << "#{rel}: `#{facet}` value \"#{v}\" not in _data/tags.yml" unless allowed.include?(v) + end + end + + if fm["outcome"] && !(TAGS["outcome"] || []).include?(fm["outcome"]) + errors << "#{rel}: `outcome` value \"#{fm['outcome']}\" not in _data/tags.yml" + end +end + +# --- bibliography ---------------------------------------------------------- +Dir.glob(File.join(ROOT, "_bibliography", "*.md")).sort.each do |path| + rel = path.sub("#{ROOT}/", "") + fm, err = front_matter(path) + if err + errors << "#{rel}: #{err}" + next + end + + BIB_REQUIRED.each do |field| + val = fm[field] + errors << "#{rel}: missing required field `#{field}`" if val.nil? || (val.respond_to?(:empty?) && val.empty?) + end + + unless %w[paper sop].include?(fm["type"]) + errors << "#{rel}: `type` must be `paper` or `sop` (got #{fm['type'].inspect})" + end + + as_array(fm["tags"]).each do |t| + errors << "#{rel}: tag \"#{t}\" not in _data/tags.yml" unless BIB_TAG_VOCAB.include?(t) + end +end + +# --- report ---------------------------------------------------------------- +if errors.empty? + puts "✓ All entries valid." + exit 0 +else + warn "✗ #{errors.length} validation error(s):" + errors.each { |e| warn " - #{e}" } + exit 1 +end