diff --git a/.gitignore b/.gitignore index 76996131..b8e1f1a3 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ Gemfile.lock # Ignore objects directory to avoid committing binary files (other than demo files) # if you would like to commit your objects to github, delete the line below! -objects/ \ No newline at end of file +objects/ + +# Ignore offline site build +offline_site/ diff --git a/_config.yml b/_config.yml index 81dbc5a2..eb818470 100644 --- a/_config.yml +++ b/_config.yml @@ -73,7 +73,7 @@ organization-logo-nav: https://www.lib.uidaho.edu/collectionbuilder/cdil-white.p # provide a relative path in this repository or full url lib-assets: /assets/lib # ignore stuff -exclude: [docs/, Rakefile, rakelib/, README.md, LICENSE, CITATION.cff, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, AGENTS.md, CLAUDE.md, HUMANS.md] +exclude: [docs/, Rakefile, rakelib/, README.md, LICENSE, CITATION.cff, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, AGENTS.md, CLAUDE.md, HUMANS.md, offline_site/] # compress CSS output sass: style: compressed diff --git a/_data/demo-compoundobjects-allmedia.csv b/_data/demo-compoundobjects-allmedia.csv index 719eace4..4c2664a4 100644 --- a/_data/demo-compoundobjects-allmedia.csv +++ b/_data/demo-compoundobjects-allmedia.csv @@ -9,8 +9,8 @@ As an army terrible with batters sweeping before it the black shades of night, r Why do you suffer the indignities of life, when you can control life? Your fate is in your hands now, for at last, the way is open, if you will but take it. When Dr. Frank B Robinson first realized the dynamic implications of Psychiana, he was in debt, a humble drug clerk with no future, no hope. Now he possesses all that he wants of wealth, his mind is at peace, and he is accorded praise and renown as an outstanding and famous psychologist. The same forces that raised him out of the depths, to stand on a high plane among men, can be put to work for you." -demo_004,demo_008,"University of Idaho vs. University of Southern California (Football), 10/30/1925",Vandal Athletics,1925-10-30,,"Example YouTube video item. Idaho Football vs. University of Southern California 10/30/1925 at Neale Stadium in Moscow, ID. Score: 7 - 51 (L). Silent film footage.",American Football,"Moscow, Idaho",46.726113,-117.015671,"Vandal Video Collection, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/vandalvideo/","MG 23, Item 29",Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://youtu.be/CVXQ3X6Q8oU,https://img.youtube.com/vi/CVXQ3X6Q8oU/hqdefault.jpg,https://img.youtube.com/vi/CVXQ3X6Q8oU/mqdefault.jpg,grainy image from historic film depicting fans heading to football game, -demo_005,demo_008,Interview with K. Silem Mohammad,"Becker, Devin",2014-06-16,,Example Vimeo video item. Oral history interview with poet K. Silem Mohammad discussing recent changes in writing practices that occurred due to the advent of the computer and the arrival of the digital age.,Poetry,"Ashland, OR",42.1916714,-122.728533,"CTRL+Shift, Center for Digital Inquiry and Learning, University of Idaho Library, https://ctrl-shift.org/",,Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://vimeo.com/464555587,,,,"/objects/demo_005.txt" +demo_yt,demo_008,"University of Idaho vs. University of Southern California (Football), 10/30/1925",Vandal Athletics,1925-10-30,,"Example YouTube video item. Idaho Football vs. University of Southern California 10/30/1925 at Neale Stadium in Moscow, ID. Score: 7 - 51 (L). Silent film footage.",American Football,"Moscow, Idaho",46.726113,-117.015671,"Vandal Video Collection, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/vandalvideo/","MG 23, Item 29",Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://youtu.be/CVXQ3X6Q8oU,https://img.youtube.com/vi/CVXQ3X6Q8oU/hqdefault.jpg,https://img.youtube.com/vi/CVXQ3X6Q8oU/mqdefault.jpg,grainy image from historic film depicting fans heading to football game, +demo_vimeo,demo_008,Interview with K. Silem Mohammad,"Becker, Devin",2014-06-16,,Example Vimeo video item. Oral history interview with poet K. Silem Mohammad discussing recent changes in writing practices that occurred due to the advent of the computer and the arrival of the digital age.,Poetry,"Ashland, OR",42.1916714,-122.728533,"CTRL+Shift, Center for Digital Inquiry and Learning, University of Idaho Library, https://ctrl-shift.org/",,Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://vimeo.com/464555587,,,,"/objects/demo_005.txt" demo_006,demo_008,Ford pumper used for slash burning control,"Thompson, J. B.",1932-09-01,,Example image item externally hosted. Ford pumper used for slash burning control. September 1932. From the photo series depicting the broadcast burn at the mouth of Benton Creek.,forestry,Bonner County;Priest River Experimental Forest;Benton Creek,48.34472222,-116.8483333,"Experimental Forest and Savenac Nursery Archive, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/expforest/",FirePump1,Image;StillImage,image/jpeg,eng,"Material produced by the United States Forest Service and is in Public Domain. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",https://creativecommons.org/publicdomain/mark/1.0/,image,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/demo_006.jpg,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/small/demo_006_sm.jpg,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/thumbs/demo_006_th.jpg,man standing with pump machinery next to a river with trees and mountains in the distance, demo_007,demo_008,Influence of Fishway Placement on Fallback of Adult Salmon at the Bonneville Dam on the Columbia River,"Reischel, T.S.; Bjornn, T.C.",2003,,Example metadata-only record with link to external source. Journal article.,fisheries management,Bonneville Dam,45.6442837,-121.9428256,"North American Journal of Fisheries Management, vol. 23, issue 4, p. 1215-1224. DOI: 10.1577/M02-113",DOI:10.1577/M02-113,Text,application/pdf,eng,"Metadata-only record, please check publication for rights",,record,https://www.doi.org/10.1577/M02-113,,,, demo_008,,Hell's Half Acre,Keeping Watch,,,"Example compound object. Hells Half Acre is an R-6 cabin that is easily accessible by road in the Selway-Bitterroot Wilderness. Originally a platform on top of a forty-foot-tall tower, the original Hell's Half Acre cabin was an L-4. Now one of the most popular lookouts in Idaho or Montana, it sits above a snag forest of mostly lodgepole pine. A sign at the base of the summit informs drivers that the tower opens at 9 am. Unsure if this refers to mountain or pacific time, we wait at the sign and drink our coffee.",,,45.64579,-114.62838,Keeping Watch Fire Tower Oral HIstory Project,,record,compound_object,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,compound_object,,https://cdil.lib.uidaho.edu/keeping-watch/objects/small/outside_shot_hells_half_sm.jpg,https://cdil.lib.uidaho.edu/keeping-watch/objects/thumbs/outside_shot_hells_half_th.jpg,fire lookout tower with a flag pole and American flag, diff --git a/_data/demo-compoundobjects-metadata.csv b/_data/demo-compoundobjects-metadata.csv index 9053cf7a..ffbbfca2 100644 --- a/_data/demo-compoundobjects-metadata.csv +++ b/_data/demo-compoundobjects-metadata.csv @@ -9,8 +9,8 @@ As an army terrible with batters sweeping before it the black shades of night, r Why do you suffer the indignities of life, when you can control life? Your fate is in your hands now, for at last, the way is open, if you will but take it. When Dr. Frank B Robinson first realized the dynamic implications of Psychiana, he was in debt, a humble drug clerk with no future, no hope. Now he possesses all that he wants of wealth, his mind is at peace, and he is accorded praise and renown as an outstanding and famous psychologist. The same forces that raised him out of the depths, to stand on a high plane among men, can be put to work for you." -demo_004,,"University of Idaho vs. University of Southern California (Football), 10/30/1925",Vandal Athletics,1925-10-30,,"Example YouTube video item. Idaho Football vs. University of Southern California 10/30/1925 at Neale Stadium in Moscow, ID. Score: 7 - 51 (L). Silent film footage.",American Football,"Moscow, Idaho",46.726113,-117.015671,"Vandal Video Collection, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/vandalvideo/","MG 23, Item 29",Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://youtu.be/CVXQ3X6Q8oU,https://img.youtube.com/vi/CVXQ3X6Q8oU/hqdefault.jpg,https://img.youtube.com/vi/CVXQ3X6Q8oU/mqdefault.jpg,grainy image from historic film depicting fans heading to football game, -demo_005,,Interview with K. Silem Mohammad,"Becker, Devin",2014-06-16,,Example Vimeo video item. Oral history interview with poet K. Silem Mohammad discussing recent changes in writing practices that occurred due to the advent of the computer and the arrival of the digital age.,Poetry,"Ashland, OR",42.1916714,-122.728533,"CTRL+Shift, Center for Digital Inquiry and Learning, University of Idaho Library, https://ctrl-shift.org/",,Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://vimeo.com/464555587,,,,"/objects/demo_005.txt" +demo_yt,,"University of Idaho vs. University of Southern California (Football), 10/30/1925",Vandal Athletics,1925-10-30,,"Example YouTube video item. Idaho Football vs. University of Southern California 10/30/1925 at Neale Stadium in Moscow, ID. Score: 7 - 51 (L). Silent film footage.",American Football,"Moscow, Idaho",46.726113,-117.015671,"Vandal Video Collection, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/vandalvideo/","MG 23, Item 29",Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://youtu.be/CVXQ3X6Q8oU,https://img.youtube.com/vi/CVXQ3X6Q8oU/hqdefault.jpg,https://img.youtube.com/vi/CVXQ3X6Q8oU/mqdefault.jpg,grainy image from historic film depicting fans heading to football game, +demo_vimeo,,Interview with K. Silem Mohammad,"Becker, Devin",2014-06-16,,Example Vimeo video item. Oral history interview with poet K. Silem Mohammad discussing recent changes in writing practices that occurred due to the advent of the computer and the arrival of the digital age.,Poetry,"Ashland, OR",42.1916714,-122.728533,"CTRL+Shift, Center for Digital Inquiry and Learning, University of Idaho Library, https://ctrl-shift.org/",,Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://vimeo.com/464555587,,,,"/objects/demo_005.txt" demo_006,,Ford pumper used for slash burning control,"Thompson, J. B.",1932-09-01,,Example image item externally hosted. Ford pumper used for slash burning control. September 1932. From the photo series depicting the broadcast burn at the mouth of Benton Creek.,forestry,Bonner County;Priest River Experimental Forest;Benton Creek,48.34472222,-116.8483333,"Experimental Forest and Savenac Nursery Archive, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/expforest/",FirePump1,Image;StillImage,image/jpeg,eng,"Material produced by the United States Forest Service and is in Public Domain. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",https://creativecommons.org/publicdomain/mark/1.0/,image,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/demo_006.jpg,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/small/demo_006_sm.jpg,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/thumbs/demo_006_th.jpg,man standing with pump machinery next to a river with trees and mountains in the distance, demo_007,,Influence of Fishway Placement on Fallback of Adult Salmon at the Bonneville Dam on the Columbia River,"Reischel, T.S.; Bjornn, T.C.",2003,,Example metadata-only record with link to external source. Journal article.,fisheries management,Bonneville Dam,45.6442837,-121.9428256,"North American Journal of Fisheries Management, vol. 23, issue 4, p. 1215-1224. DOI: 10.1577/M02-113",DOI:10.1577/M02-113,Text,application/pdf,eng,"Metadata-only record, please check publication for rights",,record,https://www.doi.org/10.1577/M02-113,,,, demo_008,,Hell's Half Acre,Keeping Watch,,,"Example compound object. Hells Half Acre is an R-6 cabin that is easily accessible by road in the Selway-Bitterroot Wilderness. Originally a platform on top of a forty-foot-tall tower, the original Hell's Half Acre cabin was an L-4. Now one of the most popular lookouts in Idaho or Montana, it sits above a snag forest of mostly lodgepole pine. A sign at the base of the summit informs drivers that the tower opens at 9 am. Unsure if this refers to mountain or pacific time, we wait at the sign and drink our coffee.",,,45.64579,-114.62838,Keeping Watch Fire Tower Oral HIstory Project,,record,compound_object,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,compound_object,,https://cdil.lib.uidaho.edu/keeping-watch/objects/small/outside_shot_hells_half_sm.jpg,https://cdil.lib.uidaho.edu/keeping-watch/objects/thumbs/outside_shot_hells_half_th.jpg,fire lookout tower with a flag pole and American flag, diff --git a/_data/demo-metadata.csv b/_data/demo-metadata.csv index 9d323885..1e5c5661 100644 --- a/_data/demo-metadata.csv +++ b/_data/demo-metadata.csv @@ -9,7 +9,7 @@ As an army terrible with batters sweeping before it the black shades of night, r Why do you suffer the indignities of life, when you can control life? Your fate is in your hands now, for at last, the way is open, if you will but take it. When Dr. Frank B Robinson first realized the dynamic implications of Psychiana, he was in debt, a humble drug clerk with no future, no hope. Now he possesses all that he wants of wealth, his mind is at peace, and he is accorded praise and renown as an outstanding and famous psychologist. The same forces that raised him out of the depths, to stand on a high plane among men, can be put to work for you." -demo_004,"University of Idaho vs. University of Southern California (Football), 10/30/1925",Vandal Athletics,1925-10-30,"Example YouTube video item. Idaho Football vs. University of Southern California 10/30/1925 at Neale Stadium in Moscow, ID. Score: 7 - 51 (L).",American Football,"Moscow, Idaho",46.726113,-117.015671,"Vandal Video Collection, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/vandalvideo/","MG 23, Item 29",Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://youtu.be/CVXQ3X6Q8oU,https://img.youtube.com/vi/CVXQ3X6Q8oU/hqdefault.jpg,https://img.youtube.com/vi/CVXQ3X6Q8oU/mqdefault.jpg,grainy image from historic film depicting fans heading to football game, -demo_005,Interview with K. Silem Mohammad,Devin Becker,2014-06-16,Example Vimeo video item. Oral history interview with poet K. Silem Mohammad discussing recent changes in writing practices that occurred due to the advent of the computer and the arrival of the digital age.,Poetry,"Ashland, OR",42.1916714,-122.728533,"CTRL+Shift, Center for Digital Inquiry and Learning, University of Idaho Library, https://ctrl-shift.org/",,Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://vimeo.com/464555587,,,,/objects/demo_005.txt +demo_yt,"University of Idaho vs. University of Southern California (Football), 10/30/1925",Vandal Athletics,1925-10-30,"Example YouTube video item. Idaho Football vs. University of Southern California 10/30/1925 at Neale Stadium in Moscow, ID. Score: 7 - 51 (L).",American Football,"Moscow, Idaho",46.726113,-117.015671,"Vandal Video Collection, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/vandalvideo/","MG 23, Item 29",Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://youtu.be/CVXQ3X6Q8oU,https://img.youtube.com/vi/CVXQ3X6Q8oU/hqdefault.jpg,https://img.youtube.com/vi/CVXQ3X6Q8oU/mqdefault.jpg,grainy image from historic film depicting fans heading to football game, +demo_vimeo,Interview with K. Silem Mohammad,Devin Becker,2014-06-16,Example Vimeo video item. Oral history interview with poet K. Silem Mohammad discussing recent changes in writing practices that occurred due to the advent of the computer and the arrival of the digital age.,Poetry,"Ashland, OR",42.1916714,-122.728533,"CTRL+Shift, Center for Digital Inquiry and Learning, University of Idaho Library, https://ctrl-shift.org/",,Image;MovingImage,video/mp4,eng,"In Copyright - Educational Use Permitted. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/InC-EDU/1.0/,video,https://vimeo.com/464555587,,,,/objects/demo_005.txt demo_006,Ford pumper used for slash burning control,"Thompson, J. B.",1932-09-01,Example image item externally hosted. Ford pumper used for slash burning control. September 1932. From the photo series depicting the broadcast burn at the mouth of Benton Creek.,forestry,Bonner County;Priest River Experimental Forest;Benton Creek,48.34472222,-116.8483333,"Experimental Forest and Savenac Nursery Archive, Digital Initiatives, University of Idaho Library, https://www.lib.uidaho.edu/digital/expforest/",FirePump1,Image;StillImage,image/jpeg,eng,"Material produced by the United States Forest Service and is in Public Domain. For more information, please contact University of Idaho Library Special Collections and Archives Department at libspec@uidaho.edu.",http://rightsstatements.org/vocab/NoC-US/1.0/,image,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/demo_006.jpg,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/small/demo_006_sm.jpg,https://www.lib.uidaho.edu/collectionbuilder/demo-objects/thumbs/demo_006_th.jpg,man standing with pump machinery next to a river with trees and mountains in the distance, demo_007,Influence of Fishway Placement on Fallback of Adult Salmon at the Bonneville Dam on the Columbia River,"Reischel, T.S.; Bjornn, T.C.",2003,Example metadata-only record with link to external source. Journal article.,fisheries management,Bonneville Dam,45.6442837,-121.9428256,"North American Journal of Fisheries Management, vol. 23, issue 4, p. 1215-1224. DOI: 10.1577/M02-113",DOI:10.1577/M02-113,Text,application/pdf,eng,"metadata-only record, please check publication for rights",,record,https://www.doi.org/10.1577/M02-113,,,, diff --git a/_includes/feature/video-modal.html b/_includes/feature/video-modal.html index 9dfc5a5c..76d355b2 100644 --- a/_includes/feature/video-modal.html +++ b/_includes/feature/video-modal.html @@ -3,7 +3,7 @@ Video modal feature from an item's objectid or external link. This include adds a card featuring the thumb image, title, and description of a video--when clicked it opens a modal containing the video embed. The modal displays video information, transcript, link to item. This is especially useful feature if you would like multiple videos on one page. - E.G. --> {% include feature/video-modal.html objectid="demo_004" %} + E.G. --> {% include feature/video-modal.html objectid="demo_yt" %} It requires an "objectid" with the include, which is used to find the video details. Alternatively, a URL to an external video can be used in "objectid". YouTube and Vimeo items will use iframe embeds, video files use html video element. diff --git a/_includes/head/head.html b/_includes/head/head.html index 5fd397ac..63400552 100644 --- a/_includes/head/head.html +++ b/_includes/head/head.html @@ -15,8 +15,9 @@ -{% if site.data.theme.font-cdn %}{{ site.data.theme.font-cdn }}{%- endif -%} - +{% comment %} offline build always uses local bootstrap and skips CDN fonts, since CDN resources are unavailable offline {% endcomment %} +{% if site.data.theme.font-cdn and jekyll.environment != "offline" %}{{ site.data.theme.font-cdn }}{%- endif -%} + {% if page.layout == 'data' %}{%- endif -%} {% if page.layout == 'map' %} @@ -29,8 +30,10 @@ {% comment %} Meta tags and analytics are added during production build ONLY {%- endcomment -%} -{% if jekyll.environment == "production" %} +{% if jekyll.environment == "production" or jekyll.environment == "offline" %} {% if layout.item-meta %}{% include head/item-meta.html %}{% else %}{% include head/page-meta.html %}{% endif %} +{% endif %} +{% if jekyll.environment == "production" %} {% include head/analytics.html %} {%- endif -%} {% if site.noindex == true or page.noindex == true or layout.noindex == true %}{% endif %} diff --git a/_includes/js/table-js.html b/_includes/js/table-js.html index 1ed1ecc4..f5574203 100644 --- a/_includes/js/table-js.html +++ b/_includes/js/table-js.html @@ -1,6 +1,15 @@ +{% comment %} + + Javascript for DataTables interactive table feature, https://datatables.net/ + Used with the "data.html" layout. + By default it loads table data from "assets/js/metadata.min.json" for better performance with large collections. + For offline build, it provides an alternative basic table version. + +{%- endcomment -%} {% assign fcount = site.data.config-table | size %} - + +{% unless jekyll.environment == "offline" or page.simple-table == true %} +{% else %} + +{% endunless %} diff --git a/_layouts/data.html b/_layouts/data.html index 2d833c7d..47f6a904 100644 --- a/_layouts/data.html +++ b/_layouts/data.html @@ -15,9 +15,22 @@ {% for f in fields %} {{ f }} {% endfor %} - Link + {% unless jekyll.environment == "offline" %}Link{% endunless %} + {% if jekyll.environment == "offline" or page.simple-table == true %} + + {% if site.data.theme.data-child-objects == true %} + {%- assign items = site.data[site.metadata] | where_exp: 'item','item.objectid' -%} + {% else %} + {%- assign items = site.data[site.metadata] | where_exp: 'item','item.objectid and item.parentid == nil' -%} + {% endif %} + {%- assign fields = site.data.config-table | map: 'field' -%} + {%- for item in items -%} + {% for f in fields %}{% if forloop.first %}{{ item[f] }}{% else %}{{ item[f] }}{% endif %}{% endfor %} + {%- endfor -%} + + {% endif %} \ No newline at end of file diff --git a/docs/rake_tasks/build_offline.md b/docs/rake_tasks/build_offline.md new file mode 100644 index 00000000..9208e2ed --- /dev/null +++ b/docs/rake_tasks/build_offline.md @@ -0,0 +1,46 @@ +# build_offline + +`rake build_offline` generates your CB site, downloads all external media, and then rewrites all internal links to create a copy of your project that can be used fully offline in the local filesystem. + +The fully static offline file version is intended to serve as an artifact for digital preservation that maintains the functionality of a project in a minimal environment. +It can also be useful for sharing the site (via a thumb drive) in a location with no internet, or for content that needs to remain offline for security or privacy reasons. + +The task will: + +1. Complete a fresh build of the site into the output directory, using the "offline" JEKYLL_ENV which swaps out some parts of the site specific to building offline. +2. Download external media (images, PDFs, audio) referenced in the "object_location", "image_small", and "image_thumb" fields of your metadata CSV into the "objects" folder of the output directory (not your project's "objects" folder). Downloaded files are named by objectid following CB conventions, e.g. "objects/small/demo_001_sm.jpg". Any download that fails is left pointing to the original external link, and a summary is written to "offline_build_log.txt" in the output directory. +3. Rewrite all internal links to relative file paths so that pages can load and link correctly from the local filesystem. +4. Inline the SVG icon sprite into each page, since browsers block loading external SVG symbol files from the local filesystem. + +When the task completes, you can browse the offline version by clicking "offline_site/index.html" to open it in your web browser. + +| option | description | default value | +| --- | --- | --- | +| download_external | attempt to download all external media linked in the project including items, true/false | true | +| output_dir | directory name for output offline version | "offline_site" | +| skip_rewrite | comma separated list of directories to skip rewriting, useful for external libraries containing HTML that should not be modified. Pass an empty value (e.g. `rake build_offline[true,offline_site,]`) to rewrite everything. | the `lib-assets` value from "_config.yml", usually "assets/lib" | +| download_video | also download directly hosted video files (mp4, webm, ogv, mov) linked in metadata. These can be very large, so they are off by default. | false | + +Pass options as rake arguments: + +`rake build_offline[false,"my_offline_copy","assets/lib",true]` + +## Notes + +The output directory is excluded from the Jekyll build automatically. If you use a custom output_dir, you may still want to add it to `exclude` in "_config.yml" and to ".gitignore" so it is not picked up by `jekyll serve` or committed to your repository. + +The build uses a placeholder `baseurl`, so every path that passes through Jekyll's `relative_url` or `absolute_url` filter is marked and can be rewritten reliably in HTML, CSS, JS, and data files. Links to the site root or to directory-style permalinks (e.g. "/search/") are pointed at the "index.html" file, since browsers do not resolve directories without a server. As a fallback, hardcoded root-relative paths in HTML attributes (e.g. a markdown link written as "/browse.html") are also rewritten when they point to a file or folder in the build. + +If you have external downloads fail, you may want to manually download those images and add them to your project and metadata, then re-run the task. +Manually prepping your "objects" folder may be more failsafe than relying on the builtin download functionality in some cases. + +## Limitations + +- Streaming video (YouTube, Vimeo, etc.) is not downloaded and will not play offline. Item pages for video objects will display without the video, although their thumbnail images are downloaded. Directly hosted video files are only downloaded when `download_video` is true. +- Bootswatch themes and CDN fonts (`bootswatch` and `font-cdn` in "_data/theme.yml") are not available offline. The offline build always uses the local Bootstrap CSS and browser default fonts instead. +- Map tiles (Leaflet/OpenStreetMap) require internet access. The map page will show markers but no background tiles when offline. +- External images not in metadata (e.g., organization logos in the banner) remain as external links and require internet to display. Please manually adjust those images in your project. +- Paths in standalone JavaScript files (e.g. "assets/js") and in the data exports in "assets/data" are rewritten as root-relative paths (e.g. "/items/demo_001.html") rather than relative ones, because a relative path inside a script resolves against the page that loads it rather than the script itself. Keep any site links in page-specific JS includes (as CB does in "_includes/js") rather than in standalone ".js" files. +- Paths that do not pass through a Liquid url filter are only rewritten when they appear in an HTML attribute and point into the site (i.e. begin with a top-level file or folder of the build). Absolute links to other resources on the site's own domain are left as external links. +- The rewrite script is idiosyncratic to CB projects, so may not work correctly for other websites or highly customized projects. A unit test for the rewrite rules can be run with `ruby rakelib/test/offline_rewrite_test.rb`. + diff --git a/rakelib/build_offline.rake b/rakelib/build_offline.rake new file mode 100644 index 00000000..bdfbc3e4 --- /dev/null +++ b/rakelib/build_offline.rake @@ -0,0 +1,375 @@ +############################################################################### +# TASK: build_offline +# +# build a CollectionBuilder site and replace links for offline use +# +# generates the Jekyll site, downloads external media (images, pdfs), +# and rewrites all internal links so the site works from the local filesystem without a server. +# +# options (passed as rake arguments, e.g. rake build_offline[true,offline_site,assets/lib,false]): +# download_external - download external media linked in metadata, true/false (default: true) +# output_dir - directory name for the offline output (default: "offline_site") +# skip_rewrite - comma separated directories to skip rewriting, useful for external libraries that should not be modified (default: "lib-assets" value from _config.yml, usually "assets/lib") +# download_video - also download directly hosted video files (mp4, webm, etc.), true/false (default: false) +# +# see docs/rake_tasks/build_offline.md for full documentation +############################################################################### + +require 'csv' +require 'digest' +require 'net/http' +require 'open-uri' +require 'pathname' +require 'set' +require 'tempfile' +require 'uri' +require 'yaml' + +# file types to download for offline use by default (images, pdfs, audio) +OFFLINE_MEDIA_EXTENSIONS = %w[.jpg .jpeg .png .gif .tif .tiff .pdf .mp3 .wav .ogg .m4a].freeze + +# video file types, only downloaded when the download_video option is true. +# streaming platforms (YouTube, Vimeo, etc.) have no file extension so are never downloaded. +OFFLINE_VIDEO_EXTENSIONS = %w[.mp4 .webm .ogv .mov].freeze + +# metadata field => [objects subdirectory, filename suffix] for downloaded files. +# downloads are named by objectid following CB conventions, e.g. objects/small/demo_001_sm.jpg +OFFLINE_MEDIA_FIELDS = { + 'object_location' => ['objects', ''], + 'image_small' => ['objects/small', '_sm'], + 'image_thumb' => ['objects/thumbs', '_th'] +}.freeze + +# check whether the URL points to a file type eligible for offline download +def offline_downloadable?(url, extensions) + ext = File.extname(URI.parse(url).path).downcase + extensions.include?(ext) +rescue URI::InvalidURIError + false +end + +# build a local filename for a downloaded file. +# uses objectid + suffix + original extension; rows without objectid fall back to the +# original basename plus a short hash of the url to avoid collisions. +def offline_dest_filename(objectid, url, suffix) + path = URI.parse(url).path + ext = File.extname(path).downcase + id = objectid.to_s.strip + if id.empty? + "#{File.basename(path, '.*')}_#{Digest::MD5.hexdigest(url)[0, 8]}#{suffix}#{ext}" + else + "#{id}#{suffix}#{ext}" + end +end + +# download a file from url and save to dest_path; returns true on success +def offline_download(url, dest_path) + puts "Downloading: #{url}" + URI.open(url, 'rb', open_timeout: 30, read_timeout: 60) do |remote| + IO.copy_stream(remote, dest_path) + end + puts " -> #{dest_path}" + true +rescue OpenURI::HTTPError, SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT, + Net::OpenTimeout, Net::ReadTimeout, RuntimeError => e + puts " -> download failed: #{e.message}" + FileUtils.rm_f(dest_path) + false +end + +# normalize a root-relative site path for the local filesystem. +# returns [site_path, tail] where site_path has no leading slash and tail is any +# fragment/query string. browsers do not resolve directories to index.html from file://, +# so "/" and "/dir/" become "index.html" and "dir/index.html". +def offline_site_path(path) + m = path.delete_prefix('/').match(/\A([^#?]*)(.*)\z/m) + base = m[1] + base = 'index.html' if base.empty? + base += 'index.html' if base.end_with?('/') + [base, m[2]] +end + +# convert a root-relative site path into a path relative to the current file. +# returns nil when the path does not point into the offline site, i.e. its first segment is +# not a top-level file or directory of the build output (top_level is a Set of names, or nil +# to skip the check). callers leave such strings untouched, which avoids rewriting quoted text +# or attribute values that merely begin with a slash. +# directory_index - apply the "/" and "/dir/" => index.html rules. only safe when the path is +# known to be complete; JS code often builds URLs from a prefix string such as +# '/items/' + id, where appending index.html would corrupt the result. +def offline_localize(path, prefix, top_level, directory_index: true) + if directory_index + base, tail = offline_site_path(path) + else + base, tail = path.delete_prefix('/'), '' + end + first = base.split(%r{[/#?]}, 2).first.to_s + return nil if top_level && !top_level.include?(first) + "#{prefix}#{base}#{tail}" +end + +# read a text file for rewriting; returns nil (with a warning) if it is not valid UTF-8, +# so that binary or oddly encoded files are never silently mangled +def offline_read(filepath) + content = File.binread(filepath).force_encoding('utf-8') + return content if content.valid_encoding? + + puts " Warning: '#{filepath}' is not valid UTF-8, skipping" + nil +end + +# placeholder baseurl used for the offline build. every path that passes through Jekyll's +# relative_url / absolute_url filters is prefixed with it, so the rewrite only has to replace +# this one token rather than guess which strings in the rendered output are site paths. +OFFLINE_SENTINEL = '/__CB_OFFLINE_ROOT__'.freeze + +# file types that may contain the sentinel (anything Jekyll renders through Liquid) +OFFLINE_REWRITE_EXTENSIONS = %w[html js css json xml csv txt svg webmanifest].freeze + +# rewrite all internal links in a file's content for local filesystem use. +# depth - number of directory levels below the offline root (0 = root-level files) +# url_map - hash of { external_url => root_relative_local_path } for downloaded media +# top_level - Set of top-level names in the build output, used to gate the fallback rewrite of +# hardcoded root-relative attribute paths (nil disables the fallback) +# type - file extension as a symbol; controls how the sentinel is resolved. :html and :css +# get a prefix relative to the file's own location (browsers resolve both against +# the file itself). every other type gets a root-relative path: a standalone JS file +# resolves against whichever page loads it, and data exports are not navigated. +def offline_rewrite_links(content, depth, url_map, top_level = nil, type: :html) + prefix = '../' * depth + # absolute_url output carries the site host in front of the sentinel when url is set + sentinel = %r{(?:https?://[^/"'\s]+)?#{Regexp.escape(OFFLINE_SENTINEL)}} + + # 1. replace downloaded external media URLs with relative local paths + url_map.each do |external_url, local_path| + content = content.gsub(external_url, "#{prefix}#{local_path.delete_prefix('/')}") + end + + # 2. sentinel paths in complete HTML attribute values get the directory => index.html rules, + # since "/" and "/search/" do not resolve without a server. the value is bounded by its own + # opening quote so the other quote type may appear inside it. a closing quote followed by + # "+" marks a JS string prefix such as href="/items/" + id, which must keep its slash. + if type == :html + content = content.gsub(/((?:href|src|action|content|xlink:href|data-src|data|poster)=)(["'])#{sentinel}(\/.*?)\2(?=(\s*\+)?)/) do + attr, quote, path, concat = $1, $2, $3, $4 + "#{attr}#{quote}#{offline_localize(path, prefix, nil, directory_index: concat.nil?)}#{quote}" + end + end + + # 3. every remaining sentinel becomes a plain prefix: JS strings and template literals, + # inline JSON, CSS url(), meta content, and data files + root = %i[html css].include?(type) ? prefix : '/' + content = content.gsub(%r{#{sentinel}/}, root) + content = content.gsub(sentinel, root.chomp('/')) + + # 4. fallback for hardcoded root-relative paths in HTML attributes that did not pass through + # a Liquid url filter (e.g. a markdown link written as /browse.html). only rewritten when + # the first segment is a top-level file or directory of the build, which leaves quoted text + # or attribute values that merely begin with a slash untouched. + if type == :html && top_level + content = content.gsub(/((?:href|src|action|content|xlink:href|data-src|data|poster)=)(["'])(\/(?!\/).*?)\2(?=(\s*\+)?)/) do + attr, quote, path, concat = $1, $2, $3, $4 + local = offline_localize(path, prefix, top_level, directory_index: concat.nil?) + "#{attr}#{quote}#{local || path}#{quote}" + end + end + + content +end + +desc 'Build jekyll site and rewrite links for offline use' +task :build_offline, [:download_external, :output_dir, :skip_rewrite, :download_video] do |_t, args| + args.with_defaults( + download_external: 'true', + output_dir: 'offline_site', + download_video: 'false' + ) + + download_external = args.download_external.to_s.strip.downcase != 'false' + offline_dir = args.output_dir.to_s.strip.chomp('/') + abort 'output_dir cannot be empty' if offline_dir.empty? + download_video = args.download_video.to_s.strip.downcase == 'true' + media_extensions = download_video ? OFFLINE_MEDIA_EXTENSIONS + OFFLINE_VIDEO_EXTENSIONS : OFFLINE_MEDIA_EXTENSIONS + + # load site configuration for metadata filename, library path, and exclude list + config = YAML.load_file('_config.yml') + metadata_name = config['metadata'] + + # directories to leave untouched by the link rewrite, comma separated. + # defaults to the lib-assets directory from _config.yml (third-party libraries). + # an explicit empty value (e.g. rake build_offline[true,offline_site,]) means rewrite everything. + skip_rewrite_dirs = (args.skip_rewrite || config['lib-assets'] || 'assets/lib').to_s + .split(',').map { |d| d.strip.delete('"\'').delete_prefix('/').chomp('/') }.reject(&:empty?) + + # build jekyll site with the offline environment directly into the output directory. + # a temporary config override: + # - sets baseurl to the sentinel token (and blanks url) so every path produced by the + # relative_url / absolute_url filters is marked for rewriting + # - adds the output directory to the exclude list so that a custom output_dir is never + # read back in as site content on later builds + ENV['JEKYLL_ENV'] = 'offline' + excludes = Array(config['exclude']).map(&:to_s) + excludes << "#{offline_dir}/" unless excludes.include?(offline_dir) || excludes.include?("#{offline_dir}/") + override = Tempfile.new(['offline_config', '.yml']) + override.write({ 'baseurl' => OFFLINE_SENTINEL, 'url' => '', 'exclude' => excludes }.to_yaml) + override.close + begin + system('bundle', 'exec', 'jekyll', 'build', + '--destination', offline_dir, + '--config', "_config.yml,#{override.path}") or abort 'Jekyll build failed' + ensure + override.unlink + end + + # track { external_url => root_relative_local_path } for successfully downloaded files only, + # so that a failed download leaves the original external link in place rather than a broken local path + url_map = {} + downloaded = [] # urls fetched in this run + reused = [] # urls whose local file already existed in the build output + failed = [] # urls that could not be downloaded + skipped = [] # external urls in media fields left as-is (streaming, unsupported types, invalid) + + if download_external + if metadata_name.nil? || metadata_name.strip.empty? + puts "No 'metadata' key found in _config.yml, skipping external media download." + else + metadata_file = File.join('_data', "#{metadata_name}.csv") + unless File.exist?(metadata_file) + puts "Metadata file '#{metadata_file}' not found, skipping external media download." + else + puts "Scanning '#{metadata_file}' for external media to download..." + csv_data = CSV.read(metadata_file, headers: true, encoding: 'utf-8') + + OFFLINE_MEDIA_FIELDS.each do |field, (subdir, suffix)| + next unless csv_data.headers.include?(field) + + dest_dir = File.join(offline_dir, subdir) + FileUtils.mkdir_p(dest_dir) + + csv_data.each do |row| + url = row[field].to_s.strip + next if url.empty? + next unless url.start_with?('http') + next if url_map.key?(url) || failed.include?(url) # already handled from another row/field + + unless offline_downloadable?(url, media_extensions) + skipped << url unless skipped.include?(url) + next + end + + filename = offline_dest_filename(row['objectid'], url, suffix) + dest_path = File.join(dest_dir, filename) + # root-relative path uses forward slashes regardless of OS + root_relative = "/#{subdir}/#{filename}" + + if File.exist?(dest_path) + # a local object with the same name was copied from the project; reuse it rather than overwrite + puts "Warning: '#{dest_path}' already exists, using it for #{url}" + reused << url + url_map[url] = root_relative + elsif offline_download(url, dest_path) + downloaded << url + url_map[url] = root_relative + else + failed << url + end + end + end + + # summary of download results, also written to the output directory for reference + summary = [] + summary << "External media download summary (#{Time.now.strftime('%Y-%m-%d %H:%M')})" + summary << " downloaded: #{downloaded.size}" + summary << " reused existing local files: #{reused.size}" + summary << " failed: #{failed.size}" + summary << " left as external links: #{skipped.size}" + unless failed.empty? + summary << '' + summary << 'Failed downloads (links left pointing to the external URL):' + failed.each { |u| summary << " #{u}" } + end + unless skipped.empty? + summary << '' + summary << 'External links not downloaded (streaming platforms or unsupported file types):' + skipped.each { |u| summary << " #{u}" } + end + puts + puts summary + File.write(File.join(offline_dir, 'offline_build_log.txt'), summary.join("\n") + "\n") + end + end + end + + # rewrite all links for local filesystem use by resolving the sentinel baseurl in every + # Liquid-rendered text file (see offline_rewrite_links for how each file type is handled) + puts "Rewriting links for offline use..." + updated = 0 + top_level = Dir.children(offline_dir).to_set + Dir.glob(File.join(offline_dir, '**', "*.{#{OFFLINE_REWRITE_EXTENSIONS.join(',')}}")).each do |filepath| + rel = Pathname.new(filepath).relative_path_from(Pathname.new(offline_dir)).to_s + # skip files inside the skip_rewrite directories (e.g. third-party libraries), matching on directory boundary + next if skip_rewrite_dirs.any? { |d| rel == d || rel.start_with?("#{d}/") } + depth = rel.count('/') + type = File.extname(filepath).delete_prefix('.').downcase.to_sym + content = offline_read(filepath) + next if content.nil? + new_content = offline_rewrite_links(content, depth, url_map, top_level, type: type) + if new_content != content + File.binwrite(filepath, new_content) + updated += 1 + end + end + puts " #{updated} file(s) updated.#{skip_rewrite_dirs.empty? ? '' : " (skipped '#{skip_rewrite_dirs.join("', '")}')"}" + + # any sentinel left behind means a file type or location the rewrite did not cover + leftovers = Dir.glob(File.join(offline_dir, '**', '*')).select do |f| + File.file?(f) && File.size(f) < 50_000_000 && File.binread(f).include?(OFFLINE_SENTINEL) + end + unless leftovers.empty? + puts " Warning: sentinel '#{OFFLINE_SENTINEL}' still present in #{leftovers.size} file(s):" + leftovers.first(20).each { |f| puts " #{f}" } + end + + # inline SVG icon sprite: browsers block loading external SVG files in local file:// mode, + # so we embed the full sprite as a hidden in each HTML page and rewrite all + # href="PATH/cb-icons.svg#id" references to fragment-only href="#id". + # this handles both static elements in HTML and dynamically-built icon strings in JS. + puts "Inlining SVG icon sprite for offline use..." + svg_sprite_path = File.join(offline_dir, 'assets', 'css', 'cb-icons.svg') + if File.exist?(svg_sprite_path) + sprite_svg = File.read(svg_sprite_path, encoding: 'utf-8') + # strip XML declaration — not valid inside HTML documents + sprite_svg = sprite_svg.sub(/\A<\?xml[^>]*\?>\s*/, '') + # mark the injected sprite as hidden; it is a symbol library, not visible content + inline_sprite = sprite_svg.sub(/