Overriding a context register in a Jekyll Block plugin - ruby

I'm trying to get more control over my post excerpts in Jekyll. Using the default excerpt code doesn't seem to work well in my case, as it (a) needs to be the first paragraph, and (b) is still rendered on the page. For most of my blog posts, I want either custom text, which isn't meant to be part of the actual blog post, or some text later on in the blog post. I was hoping to do this with a block, that can optionally take a parameter to render the content (although I'll add in that specific feature later). I've currently got this proof-of-concept plugin:
require "jekyll"
module Jekyll
class RenderExcerptBlock < Liquid::Block
def render(context)
page = context.registers[:page]
content = super
page["excerpt"] = content.lines[0..50]
content
end
end
end
Liquid::Template.register_tag "excerpt", Jekyll::RenderExcerptBlock
Which would theoretically let me do this:
{% excerpt %}
A snippet of this post
{% endexcerpt %}
However, this gives the error when running jekyll build:
Liquid Exception: Key excerpt cannot be set in the drop. in /Users/nick.chambers/blog/_posts/2020-01-21-unix-shell-scripting-with-bash.md
ERROR: YOUR SITE COULD NOT BE BUILT:
------------------------------------
Key excerpt cannot be set in the drop.
Is it possible to override this register's value in some way? Alternatively, is there a more Jekyll way to achieve this?

The Jekyll way to have a custom excerpt for your post (or any document in a collection), is to simply add it to the front matter:
---
title: Test Doc
excerpt: This is a custom excerpt
---
The opening paragraph of this document.
Another paragraph in this document.
Then reference it in your layout to render it:
<div class="post-excerpt">
{{ page.excerpt }}
</div>
or
<div>
{% for post in site.posts %}
<h2>{{ post.title }}</h2>
<div>{{ post.excerpt }}</div>
{% endfor %}
</div>

Related

Errors with nesting macros and passing a collection through them? Using Eleventy with Nunjucks

Somewhat of a more advanced question here... is it possible to pass a collection down through a few macros? For example, I have a blog posts section:
{% from "components/switchers/topCoupons.njk" import topBonuses %}
{{ topCoupons(
title = "Top Coupons",
blurb = "some body text content",
posts = collections.coupon
) }}
Then within the posts macro, I have a slider macro:
{% from "components/sliders/generalSlider.njk" import generalSlider %}
{{ generalSlider(
slides = posts
) }}
Then within the slider macro, I have a card macro:
{%- for slide in slides -%}
{% from "components/cards/card.njk" import card %}
{{ card(
title = posts
) }}
{%- endfor -%}
At the moment it is not working but I'm wondering how could I approach this situation and whether Eleventy and Nunjucks even offer this type of functionality, what the solution would be, or if I'm better off using another SSG that would have this kind of infrastructure?
At the moment, it is throwing this error when trying to compile:
[eleventy:dev] `TemplateContentRenderError` was thrown
[eleventy:dev] > (./src/index.njk)
[eleventy:dev] TypeError: Converting circular structure to JSON
Any and all insight is very much appreciated. Thanks :)
There's nothing inherently wrong with passing Eleventy collections through nested macros since a collection is just a regular JavaScript array. The issue arises depending on how you're using the collection inside Nunjucks since the collections object is a circular structure.
You can try this yourself by passing a collection into a macro and only accessing individual properties in the collection.
{# include.njk #}
{% macro navigation(data) %}
<ol>
{%- for page in data -%}
<li>
{{ page.data.title }}
</li>
{%- endfor -%}
</ol>
{% endmacro %}
{{ navigation(collections.all) }}
This setup works perfectly fine even with more macro nesting. The error you're running into comes from doing something like using the dump filter.
{# include.njk #}
{{ collections.all | dump }}
Since I don't know what your macros are doing, I'm not sure what is exactly causing the error to be thrown, but you might want to look for things that might be JSON.stringifying the collections object. Depending on your use case, you might have to manually parse the object yourself, find an external library, or only use the fields you need. Creating custom Eleventy filters may help.

Jekyll tags that have spaces or multiple words

I'm using tags in Jekyll for my blog posts. The following is an example of tags I declare in the front matter in the Markdown file:
---
tags: [jekyll, tags, user experience]
---
The problem I have is that the "user experience" is rendered with a space, which breaks the link for the tag. I'd like to know if it's possible to have tags that have spaces or multiple words.
This is what my code looks like:
Markup with Ruby:
{% if page.tags.size > 0 %}
<div class="post-tags">
<ul>
{% for tag in page.tags %}
<li>{{ tag }}{% if forloop.last == false %},{% endif %}</li>
{% endfor %}
</ul>
</div>
{% endif %}
Does anyone have any ideas on how I can do that? Thanks!
You can use url_encode filter : {{ tag | url_encode }}.
Note that url_encode will turn a space into a + sign instead of a percent-encoded character. cf. Liquid doc
I had the same problem when I created the tag pages in my blog.
My solution was simply to replace the blanks by dashes:
---
tags: [jekyll, tags, user-experience]
---
Example front-matter from one of my posts:
tags:
- backup
- bitbucket-backup
- roboshell-backup
- source-control
The finished HTML looks like this:
<p><small>tags:
<span class="blog_post_categories">
backup,
bitbucket-backup,
roboshell-backup,
source-control
</span>
</small></p>
I'm sure that there is a more elegant solution which displays the blanks actually as blanks, but for me, dashes were good enough.
I discovered a fix. When you have spaces in the tags, e.g., "user experience", Jekyll isn't concatenating the words as part of the rendered link, leaving the space between both words: your.site/tags/user experience
I needed the two words joined by a + because my Tags page rendered a link to that section as #user+experience. So I added replace like this:
{{ tag | replace: " ","+" }}
This still feels like a bug in Jekyll, but this process works. Replace the space with whatever syntax you need.

How to create sorted list of Pages in Octopress or Jekyll?

I have a Octopress website, which has posts and pages. Now I want to add another category of pages which I want to call as writepus ( notes which I will keep updating like a wiki via git commits).
I want to keep these notes in a folder called _notes, just like we have _posts in source folder of Octopress.
I have a folder called _writeups/ with files such as:
subject1.html
subject2.html
I have a file called notes/list.html with following content.
---
layout: page
navbar: Notes
title: Notes
footer: false
---
<div id="blog-archives">
{% for post in site.writeups reverse %}
{% capture this_year %}{{ post.date | date: "%Y" }}{% endcapture %}
{% unless year == this_year %}
{% assign year = this_year %}
<h2>{{ year }}</h2>
{% endunless %}
<article class="page-header">
{% include archive_post.html %}
</article>
{% endfor %}
</div>
Basically I want to create a listing of these writeups so that I can keep updating them as and when I get time. Also I want to keep these separate from posts and pages.
How can I achieve this functionality using Octopress / Jekyll ?
Jekyll version 2, which has only recently been released, has the ability to have extra collections and data. Collections is probably what you are looking for, so upgrade your version of Jekyll and visit http://jekyllrb.com/docs/collections/ to find out more about them.

Get a hash into a Jekyll Liquid template from a Plugin for use in the FOR loop?

This one's got me stumped...
I'd like to share a YAML hash from a single file among a few other Jekyll pages.
I know you can put it in the Front Matter (which would require duplicating it), and I know you can generate (write) pages via a plugin (but I'm using it in a few different types of pages, which would be complex). Neither is what I'm looking for.
I'd like to loop over the hash with Liquid in my pages, but I can't seem to get the hash from the plugin to Liquid. {% capture %} only works with strings and {% assign %} won't let you call a tag within itself, like {% assign projects = gethash %} where gethash is a custom Liquid tag.
Basically, I'd like to use the separate YAML file like a text-based database.
YAML File has this in it:
projects:
category1:
-
title: Project 1
desc: Description
etc...
-
title: Project 2
etc...
category2:
-
title: Project 3
desc: Description
etc...
-
title: Project 4
etc...
Plugin is calling (which gives a Ruby Hash of the YAML):
def...
YAML::load(File.read('projects.yml'))
end...
And in template, I want to:
{% for p in projects %}
...
This should be really simple, but it's one of those Liquid things that is a pain.
How can you get a hash into Liquid from a plugin for use in the {% for %} loop?
Here's the solution that I came up:
A Jekyll Plugin that create's a Liquid Tag: yaml_to_liquid. This plugin parses the yaml file into a hash and then adds it to the Jekyll page variable.
module Jekyll
class YamlToLiquid < Liquid::Tag
def initialize(tag_name, arg, tokens)
super
if arg.length == 0
raise 'Please enter a yaml file path'
else
#yml_path = arg
end
end
def render(context)
yml = YAML::load(File.read(#yml_path))
context.registers[:page]['yml'] = yml
end
end
end
Liquid::Template.register_tag('yaml_to_liquid', Jekyll::YamlToLiquid)
To use it. Place the tag at the top of your .html or .md page just below the Yaml Front Matter and then access the yml variable as usual. This loop will only output the code hash (allows you to access the whole hash or just sub-hashes):
---
layout: page
---
{% yaml_to_liquid work/_projects.yml %}
<ul>
{% for n in page.yml.projects.code %}
<li>
{{ n.title }}
</li>
{% endfor %}
</ul>
Example of work/_projects.yml:
projects:
code:
- title:
url:
- title:
url:
websites:
- title:
url:
- title:
url:
Well, if you don't need it to be a plugin, it can be put in your _config.yml. For a plugin, you might need to append the hash to the site variable.
I think that a generator would suffice. There is a page about plugins that you should take a look.
This is what I'd use (I can't test right now, so it might be wrong!):
module Jekyll
class ProjectsGenerator < Generator
safe true
def generate(site)
# This probably won't work.
site.projects = YAML::load(File.read('projects.yml'))
end
end
end
Anyway, I really think that if you don't need the extra complexity (having a separate file, creating a new plugin just for you, etc), just put the data in _config.yml. Simplicity is good.
Hope that helps. :)

An easy way to support tags in a jekyll blog

I am using the standard jekyll installation to maintain a blog, everything is going fine. Except I would really like to tag my posts.
I can tag a post using the YAML front matter, but how do I generate pages for each tag that can will list all posts for a tag?
Here is a solution with alphabetically sorted tags on a single page.
It uses Liquid only, which means that it works on GitHub Pages:
{% capture tags %}
{% for tag in site.tags %}
{{ tag[0] }}
{% endfor %}
{% endcapture %}
{% assign sortedtags = tags | split:' ' | sort %}
{% for tag in sortedtags %}
<h3 id="{{ tag }}">{{ tag }}</h3>
<ul>
{% for post in site.tags[tag] %}
<li>{{ post.title }}</li>
{% endfor %}
</ul>
{% endfor %}
You can see it in action here.
EDIT:
There's also a way to generate a separate page for each tag without plugins (which will work on GitHub Pages).
I have a more detailed explanation on my blog:
Separate pages per tag/category with Jekyll (without plugins)
First, you need a new layout file:
/_layouts/tagpage.html:
---
layout: default
---
<h1>{{ page.tag }}</h1>
<ul>
{% for post in site.tags[page.tag] %}
<li>
{{ post.date | date: "%B %d, %Y" }}: {{ post.title }}
</li>
{% endfor %}
</ul>
With this layout file, you can add a new tag page by adding a new file with just two lines of YAML front-matter.
Here's an example for the jekyll tag:
/tags/jekyll/index.html:
---
layout: tagpage
tag: jekyll
---
The only disadvantage of this approach: each time you use a new tag for the first time, you have to remember to create a new two-line file for it.
To generate the root index file (i.e. the list of tags that links to /tags/jekyll/index.html etc.), you can use a similar solution like the one on top of this answer where I generate a single page with alphebetically sorted tags:
{% capture tags %}
{% for tag in site.tags %}
{{ tag[0] }}
{% endfor %}
{% endcapture %}
{% assign sortedtags = tags | split:' ' | sort %}
{% for tag in sortedtags %}
{{ tag }}<br>
{% endfor %}
This will generate a list of links like this:
<ul>
<li>.net</li>
<li>authentication</li>
<li>backup</li>
</ul>
Note that this solution uses a blank to split tags, so it doesn't work when your tags contain blanks and Yevgeniy Brikman's comment applies here as well.
This gist will generate a page per category for you: https://gist.github.com/524748
It uses a Jekyll Generator plugin, plus a Page subclass.
Have a look at sites using jekyll. There are a few custom forks which have implemented tagging functionality, hopefully also in the way you want :-)
I had the same question, and stumbled upon this: http://gist.github.com/143571.
It's a rake task which generates a tag list. I modified it slightly, and my version is at:
http://github.com/mattfoster/mattfoster.github.com/blob/master/Rakefile.
Whilst this doesn't give you a page per tag, you can use anchors, which is half way there!
I use the great Jekyll Tagging plugin that automatically generates a tags cloud and tag pages. Easy to install and use.
Here is a page for the "photo" tag on my blog (in french), and you can see the tags cloud in the bottom.
Based on Christian's answer above I made a bash script that does what he described.
https://github.com/ObjectiveTruth/objectivetruth.github.io/blob/master/rebuild_tags.sh
Be sure to have the accompanying 14 line vim script in the /non_website_resources/ directory
AND
Make the /_layouts/tagpage.html shown in Christian's answer above but rename it to /_layouts/tag_pages.html
File structure should be like this:
.jekyll_website_root
├── _posts
├── _layout
│ ├── tag_pages.html
├── rebuild_tags.sh
Run from the root directory ./rebuild_tags.sh
If you get permission denied error be sure to run chmod 777 rebuild_tags.sh
If you look at scripts comments its fairly simple:
Uses sed to find all the tags in every .md file in _post directory
Uses sed to massage the data to proper format
Takes all the unique tags and makes a directory and a index.html for each
This way, if you have any new tags, just run the script to rebuild the pages before pushing to github
A nice simple non-plugin way to do tags
EDIT
Removed dependency on other files. Just need the one script!
I do these with CSS. First lists an element and use the tag name as its id.
<span id="{{ site.posts | map: 'tags' | uniq | join: '"></span><span id="' }}"></span>
And then lists all the post and use its tags as a value for the "tags" custom attribute.
{% for post in site.posts %}
<article class="post" tags="{% for tag in post.tags %}{{tag}}{% if forloop.last == false %}{{" "}}{% endif %}{% endfor %}">
<h3>{{post.title}}</h3>
</article>
{% endfor %}
And then in CSS, hide all the posts by default, and only show posts with tags matches the url id/ hash
.post {
display: none;
}
{% for tag in site.tags %}#{{tag[0]}}:target ~ [tags~={{tag[0]}}]{% if forloop.last == false %}, {% endif %}{% endfor %} {
display: block;
}
/*
The compiled version will look like this
#tagname:target ~ [tags~="tagname"], #tagname2:target ~ [tags~="tagname2"] {
display: block;
}
*/
I made an article about this here.

Resources