├── 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 | ![](resources/nope.jpg) 10 | 11 | 12 | 13 | ```twig 14 | Category Link 15 | ``` 16 | 17 | 18 | 19 | ## Yep! 20 | 21 | ![](resources/yep.jpg) 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 | 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 | ![](resources/nope.jpg) 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 | 18 | ``` 19 | 20 | 21 | 22 | ## Getting better… 23 | 24 | ![](resources/yep.jpg) 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 | 46 | ``` 47 | 48 | 49 | 50 | ## Final answer 💯 51 | 52 | ![](resources/clap.gif) 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 | 23 | {% endif %} 24 | ``` 25 | 26 | 27 | -------------------------------------------------------------------------------- /10-arrow-fn.md: -------------------------------------------------------------------------------- 1 | # Use arrow functions in Twig. 2 | 3 | ## Nope… 4 | 5 | ![](resources/nope.jpg) 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 | ![](resources/yep.jpg) 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 | ![](resources/nope.jpg) 6 | 7 | 8 | 9 | ```twig 10 | {% if entry.assetsField.exists %} 11 | 12 | {% endif %} 13 | ``` 14 | 15 | 16 | 17 | ## Yep! 18 | 19 | ![](resources/yep.jpg) 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 | ![](resources/nope.jpg) 34 | 35 | 36 | 37 | ```twig 38 | {% set featuredImgSrc = 'resources/nope.jpg' %} 39 | {% set featuredImgAlt = 'Nope' %} 40 | {{ featuredImgAlt }} 41 | ``` 42 | 43 | 44 | 45 | ## Yep! 46 | 47 | ![](resources/yep.jpg) 48 | 49 | 50 | 51 | ```twig 52 | {% with { 53 | src: 'resources/yep.jpg', 54 | alt: 'Yep!', 55 | } %} 56 | {{ alt }} 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 | ![](resources/nope.jpg) 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 | 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 --------------------------------------------------------------------------------