├── 01-docs.md
├── 02-url.md
├── 03-svg.md
├── 04-concat.md
├── 05-uuid.md
├── 06-truncation.md
├── 07-inflection.md
├── 08-attr.md
├── 09-component-attr.md
├── 10-arrow-fn.md
├── 11-scoping.md
├── 12-naming.md
├── 20-componentizing.md
├── 20.1-componentizing.md
├── 20.2-componentizing.md
├── 20.3-componentizing.md
├── 20.4-componentizing.md
├── 20.5-componentizing.md
├── 20.6-componentizing.md
├── 30-macros.md
├── 40-php.md
├── 40.1-php.md
├── README.md
└── resources
├── clap.gif
├── nope.jpg
└── yep.jpg
/01-docs.md:
--------------------------------------------------------------------------------
1 | # Be aware of what's there.
2 |
3 | Read the docs and know your environment.
4 |
5 | [https://twig.symfony.com/doc/3.x/](https://twig.symfony.com/doc/3.x/)
6 |
7 | [https://craftcms.com/docs/3.x/dev/twig-primer.html](https://craftcms.com/docs/3.x/dev/twig-primer.html)
8 |
--------------------------------------------------------------------------------
/02-url.md:
--------------------------------------------------------------------------------
1 | # ALWAYS use the `url()` function.
2 |
3 | Yes, ALWAYS.
4 |
5 | [https://craftcms.com/docs/3.x/dev/functions.html#url](https://craftcms.com/docs/3.x/dev/functions.html#url)
6 |
7 | ## Nope…
8 |
9 | 
10 |
11 |
12 |
13 | ```twig
14 | Category Link
15 | ```
16 |
17 |
18 |
19 | ## Yep!
20 |
21 | 
22 |
23 |
24 |
25 | ```twig
26 | Category Link
27 | ```
28 |
29 |
30 |
--------------------------------------------------------------------------------
/03-svg.md:
--------------------------------------------------------------------------------
1 | # Use the `svg()` function.
2 |
3 | [https://craftcms.com/docs/3.x/dev/functions.html#svg](https://craftcms.com/docs/3.x/dev/functions.html#svg)
4 |
5 | Can also pass an asset!
6 |
7 |
8 |
9 | ```twig
10 | {{ svg('@webroot/icons/lemon.svg')|attr({ class: 'w-4 h-4' }) }}
11 | ```
12 |
13 |
14 |
--------------------------------------------------------------------------------
/04-concat.md:
--------------------------------------------------------------------------------
1 | # Many ways to skin a concat.
2 |
3 |
4 |
5 | ```twig
6 | {# Broke #}
7 | {% set string = foo ~ ' ' ~ bar ~ '!' %}
8 |
9 | {# Woke #}
10 | {% set string = [foo, ' ', bar, '!']|join() %}
11 |
12 | {# Bespoke #}
13 | {% set string = "#{foo} #{bar}!" %}
14 |
15 | {# You do you, I guess… #}
16 | {% set string = "%s %s!"|format(foo, bar) %}
17 | ```
18 |
19 |
20 |
--------------------------------------------------------------------------------
/05-uuid.md:
--------------------------------------------------------------------------------
1 | # UUID for when your component needs a unique ID.
2 |
3 |
4 |
5 | ```twig
6 | {% set uuid = create('craft\\helpers\\StringHelper').UUID() %}
7 |
8 |
9 |
10 | {% for i in 0..3 %}
11 | Tab 1
12 | {% endfor %}
13 |
14 | {% for i in 0..3 %}
15 |
16 |
17 | {% endfor %}
18 |
19 | ```
20 |
21 |
22 |
--------------------------------------------------------------------------------
/06-truncation.md:
--------------------------------------------------------------------------------
1 | # Trunction
2 |
3 | ## With SEOMatic
4 |
5 | [https://nystudio107.com/docs/seomatic/Using.html#helper-functions-seomatic-helper](https://nystudio107.com/docs/seomatic/Using.html#helper-functions-seomatic-helper)
6 |
7 |
8 |
9 | ```twig
10 | {% set excerpt = seomatic.helper.truncateOnWord(entry.richText, 180) %}
11 |
12 | {% set matrixText = seomatic.helper.extractTextFromField(entry.contentMatrix) %}
13 | {% set excerpt = seomatic.helper.truncateOnWord(matrixText, 180) %}
14 | ```
15 |
16 |
17 |
18 | ## With `StringHelper`
19 |
20 |
21 |
22 | ```twig
23 | {% set stringHelper = create('craft\\helpers\\StringHelper') %}
24 | {% set string = 'Lorem ipsum dolor sit amet' %}
25 | {% set excerpt = stringHelper.safeTruncate(string, 10) %}
26 | ```
27 |
28 |
29 |
--------------------------------------------------------------------------------
/07-inflection.md:
--------------------------------------------------------------------------------
1 | # Inflection
2 |
3 |
4 |
5 | ```twig
6 | {% set inflector = create('craft\\helpers\\Inflector') %}
7 | {% set cacti = inflector.pluralize('cactus') %}
8 | {% set cactus = inflector.singularize(cacti) %}
9 | {% set totalCacti = 4 %}
10 | {% set bestCactus = 3 %}
11 |
12 | {{ "I love the #{inflector.ordinalize(totalCacti)} #{cactus}." }}
13 | {{ "I love the #{totalCacti > 1 ? cacti : cactus }." }}
14 | ```
15 |
16 |
17 |
--------------------------------------------------------------------------------
/08-attr.md:
--------------------------------------------------------------------------------
1 | # Use `attr` and `tag`.
2 |
3 | ## Ugly…
4 |
5 | 
6 |
7 |
8 |
9 | ```twig
10 | {% set tag = link ? 'a' : 'div' %}
11 |
12 | <{{ tag }}
13 | class="text-lg{% if error %} text-red{% endif %}"
14 | {% if %}href=""{% endif %}
15 | data-foo="{{ {bar: 'baz'}|json_encode|html_attr }}">
16 | Drake is not impressed.
17 | {{ tag }}>
18 | ```
19 |
20 |
21 |
22 | ## Getting better…
23 |
24 | 
25 |
26 |
27 |
28 | ```twig
29 | {% set link = link ?? null %}
30 | {% set error = error ?? null %}
31 | {% set tag = link ? 'a' : 'div' %}
32 | {% set fooData = {
33 | bar: 'baz'
34 | } %}
35 |
36 | <{{ tag }}
37 | {{ attr({
38 | class: ['text-lg', error ? 'text-red'],
39 | href: link,
40 | data: {
41 | foo: fooData
42 | }
43 | }) }}
44 | Drake is happy.
45 | {{ tag }}>
46 | ```
47 |
48 |
49 |
50 | ## Final answer 💯
51 |
52 | 
53 |
54 |
55 |
56 | ```twig
57 | {% set link = link ?? null %}
58 | {% set error = error ?? null %}
59 | {% set tag = link ? 'a' : 'div' %}
60 | {% set fooData = {
61 | bar: 'baz'
62 | } %}
63 |
64 | {{ tag(tag, {
65 | class: ['text-lg', error ? 'text-red'],
66 | href: link,
67 | data: {
68 | foo: fooData
69 | },
70 | text: 'Drake wants your number',
71 | })}}
72 | ```
73 |
74 |
75 |
--------------------------------------------------------------------------------
/09-component-attr.md:
--------------------------------------------------------------------------------
1 | # Set default component attr and override with passed attrs.
2 |
3 |
4 |
5 | ```twig
6 | {% include "_components/forms/button" with {
7 | text: "Submit",
8 | attrs: {
9 | type: "submit",
10 | },
11 | } only %}
12 | ```
13 |
14 | ```twig
15 | {% set text = text ?? null %}
16 | {% set attrs = {
17 | class: 'button',
18 | type: 'button',
19 | }|merge(attrs ?? {}) %}
20 |
21 | {% if text %}
22 | {{ text }}
23 | {% endif %}
24 | ```
25 |
26 |
27 |
--------------------------------------------------------------------------------
/10-arrow-fn.md:
--------------------------------------------------------------------------------
1 | # Use arrow functions in Twig.
2 |
3 | ## Nope…
4 |
5 | 
6 |
7 |
8 |
9 | ```twig
10 | {% set youngest = 0 %}
11 |
12 | Over 21:
13 | {% for entry in craft.entries.all() if entry.firstName and entry.age > 21 %}
14 | {{ entry.firstName }}{{ not loop.last ? ',' }}
15 | {% if youngest < entry.age %}
16 | {% set youngest = entry.age %}
17 | {% endif %}
18 | {% endfor %}
19 |
20 | Youngest: {{ youngest }}
21 | ```
22 |
23 |
24 |
25 | ## #nailedit
26 |
27 | 
28 |
29 |
30 |
31 | ```twig
32 | {% set over21 = craft.entries.all()|filter(e => e.firstName and entry.age > 21) %}
33 | {% set ages = over21|reduce((curr, prev) => prev|merge(curr.age), []) %}
34 |
35 | Over 21: {{ over21|map(e => e.firstName)|join(',') }}
36 | Youngest: {{ ages|min }}
37 | ```
38 |
39 |
40 |
--------------------------------------------------------------------------------
/11-scoping.md:
--------------------------------------------------------------------------------
1 | # Scoping with `for` and `with`
2 |
3 | ## Nope…
4 |
5 | 
6 |
7 |
8 |
9 | ```twig
10 | {% if entry.assetsField.exists %}
11 |
12 | {% endif %}
13 | ```
14 |
15 |
16 |
17 | ## Yep!
18 |
19 | 
20 |
21 |
22 |
23 | ```twig
24 | {% for asset in entry.assetsField.limit(1).all() %}
25 |
26 | {% endfor %}
27 | ```
28 |
29 |
30 |
31 | ## Nope…
32 |
33 | 
34 |
35 |
36 |
37 | ```twig
38 | {% set featuredImgSrc = 'resources/nope.jpg' %}
39 | {% set featuredImgAlt = 'Nope' %}
40 |
41 | ```
42 |
43 |
44 |
45 | ## Yep!
46 |
47 | 
48 |
49 |
50 |
51 | ```twig
52 | {% with {
53 | src: 'resources/yep.jpg',
54 | alt: 'Yep!',
55 | } %}
56 |
57 | {% endwith %}
58 | ```
59 |
60 |
61 |
--------------------------------------------------------------------------------
/12-naming.md:
--------------------------------------------------------------------------------
1 | # Naming
2 |
3 | ## Template naming rules
4 |
5 | _Only_ underscore templates to hide them from routes (not to indicate a partial).
6 |
7 | Examples:
8 |
9 | - **Nope…**: `_components/_cards/basic.twig`
10 | - **Nope…**: `_components/_cards/_basic.twig`
11 | - **Maybe…**: `components/cards/_basic.twig`
12 | - **Maybe…**: `components/_cards/basic.twig`
13 | - **Yep!**: `_components/cards/basic.twig`
14 |
15 | - **Nope…**: `events/detail.twig`
16 | - **Yep!**: `events/_detail.twig`
17 | - **Yep!**: `events/_types/detail.twig`
18 |
19 | ## Block naming
20 |
21 | - Stick to one convention for blocks, e.g. `main`.
22 | - Deal with nested blocks by prefixing, e.g. `headerMain`
23 | - Avoid ambiguity, e.g.
24 | - `body`: are we talking about the html tag, or the "article body"?
25 | - `content`: isn't everything content?
26 |
--------------------------------------------------------------------------------
/20-componentizing.md:
--------------------------------------------------------------------------------
1 | # Know when to make a component.
2 |
3 | A component can be anything you find yourself repeating across the site.
4 |
5 | That said, don't make components until you find yourself repeating something.
6 |
7 | https://tailwindcss.com/docs/extracting-components#extracting-html-components
8 |
9 | 
10 |
11 |
12 |
13 | ```twig
14 | {# _news/index.twig #}
15 |
16 | {% for entry in craft.entries({
17 | section: "news",
18 | limit: 10,
19 | }).all() %}
20 |
21 |
22 |
23 | {% for asset in entry.listingImage.limit(1).all() %}
24 |
25 | {% endfor %}
26 |
{{ entry.title }}
27 | {{ entry.description }}
28 |
29 |
30 | {% endfor %}
31 | ```
32 |
33 |
34 |
--------------------------------------------------------------------------------
/20.1-componentizing.md:
--------------------------------------------------------------------------------
1 | # Just make one.
2 |
3 |
4 |
5 | ```twig
6 | {# _news/index.twig #}
7 |
8 | {% for entry in craft.entries({
9 | section: "news",
10 | limit: 10,
11 | }).all() %}
12 |
13 | {% include "_components/card" %}
14 |
15 | {% endfor %}
16 | ```
17 |
18 |
19 |
20 | ```twig
21 | {# _components/card.twig #}
22 |
23 |
24 | {% for asset in entry.listingImage.limit(1).all() %}
25 |
26 | {% endfor %}
27 |
{{ entry.title }}
28 | {{ entry.description }}
29 |
30 | ```
31 |
32 |
33 |
--------------------------------------------------------------------------------
/20.2-componentizing.md:
--------------------------------------------------------------------------------
1 | # Don't rely on the parent template's variable scope.
2 |
3 |
4 |
5 | ```twig
6 | {# _news/index.twig #}
7 |
8 | {% for entry in craft.entries({
9 | section: "news",
10 | limit: 10,
11 | }).all() %}
12 |
13 | {% include "_components/card" with {
14 | entry: entry,
15 | } only %}
16 |
17 | {% endfor %}
18 | ```
19 |
20 |
21 |
22 | ```twig
23 | {# _components/card.twig #}
24 |
25 |
26 | {% for asset in entry.listingImage.limit(1).all() %}
27 |
28 | {% endfor %}
29 |
{{ entry.title }}
30 | {{ entry.description }}
31 |
32 | ```
33 |
34 |
35 |
--------------------------------------------------------------------------------
/20.3-componentizing.md:
--------------------------------------------------------------------------------
1 | # Don't rely on an element's content model.
2 |
3 | It's good practice to make your component's data agnostic.
4 |
5 | Let's you easily do things like eagerload an asset in one spot but not in another or use the component with a category and not with an entry. Also helps with static data for prototyping in the early stages of a project.
6 |
7 |
8 |
9 | ```twig
10 | {# _news/index.twig #}
11 |
12 | {% for entry in craft.entries({
13 | section: "news",
14 | limit: 10,
15 | }).all() %}
16 |
17 | {% include "_components/card" with {
18 | image: entry.listingImage.one(),
19 | heading: entry.title,
20 | description: entry.description,
21 | } only %}
22 |
23 | {% endfor %}
24 | ```
25 |
26 | ```twig
27 | {# _news/index-alt.twig #}
28 |
29 | {% for entry in craft.entries({
30 | section: "news",
31 | limit: 10,
32 | with: ["listingImage"],
33 | }).all() %}
34 |
35 | {% include "_components/card" with {
36 | image: entry.listingImage|slice(0, 1),
37 | heading: entry.title,
38 | description: entry.description,
39 | } only %}
40 |
41 | {% endfor %}
42 | ```
43 |
44 | ```twig
45 | {# _components/card.twig #}
46 |
47 |
48 | {% if image %}
49 |
50 | {% endif %}
51 | {% if heading %}
52 |
{{ heading }}
53 | {% endif %}
54 | {{ description }}
55 |
56 | ```
57 |
58 |
59 |
--------------------------------------------------------------------------------
/20.4-componentizing.md:
--------------------------------------------------------------------------------
1 | # Declare all variables first
2 |
3 | This allows you a better defense against what is being passed through.
4 |
5 | And give you an opportunity to set defaults.
6 |
7 | It also provides more clarity and a little separation between data and markup.
8 |
9 |
10 |
11 | ```twig
12 | {# _news/index.twig #}
13 |
14 | {% for entry in craft.entries({
15 | section: "news",
16 | limit: 10,
17 | }).all() %}
18 |
19 | {% include "_components/card" with {
20 | image: entry.listingImage.one(),
21 | heading: entry.title,
22 | description: entry.description,
23 | } only %}
24 |
25 | {% endfor %}
26 | ```
27 |
28 | ```twig
29 | {# _components/card.twig #}
30 |
31 | {% set image = image ?? null %}
32 | {% set heading = heading ?? "Lorem ipsum dolor sit amet" %}
33 | {% set description = description ?? null %}
34 |
35 |
36 | {% if image %}
37 |
38 | {% endif %}
39 |
{{ heading }}
40 | {{ description }}
41 |
42 | ```
43 |
44 |
45 |
--------------------------------------------------------------------------------
/20.5-componentizing.md:
--------------------------------------------------------------------------------
1 | # Make your include an embed, if needed.
2 |
3 | Right before launch, the client request a one-off new card layout that re-uses the card styling, but has different contents.
4 |
5 | Instead of duplicating the card component and changing the contents, you can use your include as an embed where needed.
6 |
7 |
8 |
9 | ```twig
10 | {# _news/index.twig #}
11 |
12 | {% for entry in craft.entries({
13 | section: "news",
14 | limit: 10,
15 | }).all() %}
16 |
17 | {% embed "_components/card" only %}
18 | {% block main %}
19 | One-off card content goes here.
20 | {% endblock %}
21 | {% endembed %}
22 |
23 | {% endfor %}
24 | ```
25 |
26 | ```twig
27 | {# _components/card.twig #}
28 |
29 | {% set image = image ?? null %}
30 | {% set heading = heading ?? "Lorem ipsum dolor sit amet" %}
31 | {% set description = description ?? null %}
32 |
33 |
34 | {% block main %}
35 | {% if image %}
36 |
37 | {% endif %}
38 |
{{ heading }}
39 | {{ description }}
40 | {% endblock %}
41 |
42 | ```
43 |
44 |
45 |
--------------------------------------------------------------------------------
/20.6-componentizing.md:
--------------------------------------------------------------------------------
1 | # Embeds
2 |
3 | Embeds rock.
4 |
5 | They are for big structures:
6 |
7 |
8 |
9 | ```
10 | {#
11 | --- Example Usage ---
12 |
13 | {% embed "_components/_content-w-sidebar" %}
14 | {% block content %}
15 | CONTENT
16 | {% endblock %}
17 |
18 | {% block sidebar %}
19 | SIDEBAR
20 | {% endblock %}
21 | {% endembed %}
22 | #}
23 |
24 |
25 |
26 | {% block content %}{% endblock %}
27 |
28 |
29 | {% block sidebar %}{% endblock %}
30 |
31 |
32 | ```
33 |
34 | They can be used for smaller components too:
35 |
36 | ```
37 | {#
38 | --- Example Usage ---
39 |
40 | {% include '_components/_atoms/_heading' with {
41 | text: 'What’s New', // Default to lorem ipsum
42 | tag: 'h2', // Defaults to a div
43 | preset: 'serif-xl', // Defaults to 'display-7xl'
44 | } only %}
45 |
46 | {% embed '_components/_atoms/_heading' with {
47 | tag: 'h2', // Defaults to a div
48 | preset: 'serif-xl', // Defaults to 'display-7xl'
49 | } only %}
50 | {% block main %}
51 | {{ svg('@webroot/icons/lemon.svg')|attr({ class: 'w-4 h-4' }) }}
52 | What’s New
53 | {% endblock %}
54 | {% endembed %}
55 | #}
56 |
57 | {% set text = text ?? 'Lorem Ipsum Dolor Sit Amet' %}
58 | {% set tag = tag ?? 'div' %}
59 | {% set preset = preset ?? 'display-7xl' %}
60 |
61 | {% set presets = {
62 | 'display-7xl': 'font-display text-42px md:text-7xl uppercase leading-cramped',
63 | 'display-4xl': 'font-display text-4xl uppercase leading-none',
64 | 'display-lg': 'font-display text-lg uppercase leading-none',
65 | 'display-sm': 'font-display text-sm uppercase leading-tightest tracking-wider',
66 | 'serif-3xl': 'font-serif font-bold text-3xl leading-tighter tracking-tight',
67 | 'serif-xl': 'font-serif font-bold text-xl leading-tightest tracking-tight',
68 | } %}
69 |
70 | {% set attrs = {
71 | class: presets[preset],
72 | }|merge(attrs ?? {}) %}
73 |
74 | <{{ tag }} {{ attr(attrs) }}>
75 | {% block main %}
76 | {{ text }}
77 | {% endblock %}
78 | {{ tag }}>
79 | ```
80 |
81 |
82 |
--------------------------------------------------------------------------------
/30-macros.md:
--------------------------------------------------------------------------------
1 | # Trigger Warning: Macros suck, avoid them
2 |
3 | Includes and embeds are almost always a better choice.
4 | One exception is macros using `_self` for micro-templates that aren't worth an separate file:
5 |
6 |
7 |
8 | ```twig
9 | {% set data = [
10 | {
11 | label: 'Foo',
12 | value: 'foo',
13 | },
14 | {
15 | label: 'Foo',
16 | value: 'bar'
17 | }
18 | ] %}
19 |
20 | {% macro cell(data = { class: 'text-left'}, tag = 'td') %}
21 | {{ tag(tag, data) }}
22 | {% endmacro %}
23 |
24 |
25 |
26 |
27 | {% for col in rows|first %}
28 | {{ _self.cell({ text: col.label, class: 'text-center' }, 'th') }}
29 | {% endfor %}
30 |
31 |
32 |
33 | {% for row in data %}
34 |
35 | {% for col in row %}
36 | {{ _self.cell(col.value) }}
37 | {% endfor %}
38 |
39 | {% endfor %}
40 |
41 |
42 | ```
43 |
44 |
45 |
--------------------------------------------------------------------------------
/40-php.md:
--------------------------------------------------------------------------------
1 | # Don't Use Twig: Components/Helpers!
2 |
3 | ## With very little module code…
4 |
5 |
6 |
7 | ```php
8 | setComponents([
18 | 'inflector' => \craft\\helpers\\Inflector::class,
19 | 'string' => \craft\helpers\StringHelper::class,
20 | 'collection' => \Illuminate\Support\Collection::class,
21 | 'url' => \craft\helpers\UrlHelper::class,
22 | ]);
23 | }
24 | }
25 | ```
26 |
27 |
28 |
29 | ## You now have access to all these components from Twig or PHP:
30 |
31 |
32 |
33 | ```twig
34 | {{ craft.app.modules.appmodule.inflector.pluralize('cactus') }}
35 | {{ craft.app.modules.appmodule.url.rootRelativeUrl('https://site.com/foo') }}
36 | ```
37 |
38 |
39 |
--------------------------------------------------------------------------------
/40.1-php.md:
--------------------------------------------------------------------------------
1 | # Don't Use Twig: behaviors!
2 |
3 |
4 |
5 | ```php
6 | section('reviews')
18 | ->relatedTo([
19 | 'targetElement' => $this->owner,
20 | 'field' => 'product',
21 | ]);
22 | }
23 |
24 | public function getIsActive(): bool
25 | {
26 | return $this->owner->getFieldValue('isDiscontinued') || !$this->owner->manufacturer->exists();
27 | }
28 | }
29 | ```
30 |
31 |
32 |
33 | You now have access to all these methods from Twig or PHP:
34 |
35 |
36 |
37 | ```twig
38 | {% if entry.getIsActive() %}
39 | {% for review in entry.getReviews() %}
40 | {{ review.body }}
41 | {% endfor %}
42 | {% endif %}
43 | ```
44 |
45 |
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [Github Repo](https://github.com/timkelty/twig-tips)
2 |
3 | - [01-docs.md](01-docs.md)
4 | - [02-url.md](02-url.md)
5 | - [03-svg.md](03-svg.md)
6 | - [04-concat.md](04-concat.md)
7 | - [05-uuid.md](05-uuid.md)
8 | - [06-truncation.md](06-truncation.md)
9 | - [07-inflection.md](07-inflection.md)
10 | - [08-attr.md](08-attr.md)
11 | - [09-component-attr.md](09-component-attr.md)
12 | - [10-arrow-fn.md](10-arrow-fn.md)
13 | - [11-scoping.md](11-scoping.md)
14 | - [12-naming.md](12-naming.md)
15 | - [20-componentizing.md](20-componentizing.md)
16 | - [20.1-componentizing.md](20.1-componentizing.md)
17 | - [20.2-componentizing.md](20.2-componentizing.md)
18 | - [20.3-componentizing.md](20.3-componentizing.md)
19 | - [20.4-componentizing.md](20.4-componentizing.md)
20 | - [20.5-componentizing.md](20.5-componentizing.md)
21 | - [20.6-componentizing.md](20.6-componentizing.md)
22 | - [30-macros.md](30-macros.md)
23 | - [40-php.md](40-php.md)
24 | - [40.1-php.md](40.1-php.md)
25 |
--------------------------------------------------------------------------------
/resources/clap.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timkelty/twig-tips/ddb252052fbe7b6624529e31e24687cf0d983b1b/resources/clap.gif
--------------------------------------------------------------------------------
/resources/nope.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timkelty/twig-tips/ddb252052fbe7b6624529e31e24687cf0d983b1b/resources/nope.jpg
--------------------------------------------------------------------------------
/resources/yep.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timkelty/twig-tips/ddb252052fbe7b6624529e31e24687cf0d983b1b/resources/yep.jpg
--------------------------------------------------------------------------------