├── .eleventy.js
├── .gitignore
├── LICENSE
├── README.md
├── github-readme.svg
├── netlify.toml
├── package-lock.json
├── package.json
└── src
├── _data
├── nav.json
└── site.json
├── _includes
├── components
│ ├── footer.njk
│ ├── navigation.njk
│ ├── search.njk
│ └── taglist.njk
├── icons
│ ├── close.svg
│ ├── logo.svg
│ ├── search.svg
│ └── time.svg
└── layouts
│ ├── base.njk
│ ├── home.njk
│ ├── recipe.njk
│ └── recipes-list.njk
├── about.md
├── admin
├── config.yml
└── index.html
├── fonts
├── Vollkorn-Regular.woff
├── Vollkorn-Regular.woff2
├── Vollkorn-SemiBold.woff
└── Vollkorn-SemiBold.woff2
├── img
├── about.jpg
├── favicon-alt.ico
└── recipes
│ ├── brownies.jpg
│ ├── coconut-lentil-soup.jpg
│ └── courgette-lemon-risotto.jpg
├── index.md
├── recipes.md
├── recipes
├── coconut-lentil-soup.md
├── courgette-lemon-risotto.md
├── recipes.json
└── simple-brownies.md
├── scss
├── _global.scss
├── _layout.scss
├── _mixins.scss
├── _reset.scss
├── _typography.scss
├── _utility.scss
├── components
│ ├── _about.scss
│ ├── _card.scss
│ ├── _home.scss
│ ├── _nav.scss
│ ├── _recipe-tags.scss
│ ├── _recipe.scss
│ └── _search.scss
└── main.scss
├── search.njk
└── tag.md
/.eleventy.js:
--------------------------------------------------------------------------------
1 | const Image = require('@11ty/eleventy-img');
2 | const util = require('util');
3 |
4 | const emojiRegex = /[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/ug
5 |
6 | module.exports = config => {
7 | config.setUseGitIgnore(false);
8 | config.addWatchTarget("./src/_includes/css/main.css");
9 |
10 | config.addPassthroughCopy({ public: './' });
11 | config.addPassthroughCopy('src/img');
12 | config.addPassthroughCopy('src/fonts');
13 | config.addPassthroughCopy('src/admin');
14 |
15 |
16 | /* Collections */
17 | config.addCollection('recipes', collection => {
18 | return [...collection.getFilteredByGlob('./src/recipes/*.md')];
19 | });
20 |
21 | config.addCollection('tagList', collection => {
22 | const tagsSet = new Set();
23 | collection.getAll().forEach(item => {
24 | if (!item.data.tags) return;
25 | item.data.tags
26 | .filter(tag => !['recipes'].includes(tag))
27 | .forEach(tag => tagsSet.add(tag));
28 | });
29 | return Array.from(tagsSet).sort((first, second) => {
30 | const firstStartingLetter = first.replace(emojiRegex, '').trim()[0].toLowerCase();
31 | const secondStartingLetter = second.replace(emojiRegex, '').trim()[0].toLowerCase();
32 |
33 | if(firstStartingLetter < secondStartingLetter) { return -1; }
34 | if(firstStartingLetter > secondStartingLetter) { return 1; }
35 | return 0;
36 | });
37 | });
38 |
39 |
40 | /* Filters */
41 | config.addFilter('console', function(value) {
42 | return util.inspect(value);
43 | });
44 |
45 | config.addFilter('noEmoji', function(value) {
46 | return value.replace(emojiRegex, '').trim();
47 | });
48 |
49 | config.addFilter('onlyEmoji', function(value) {
50 | let match = value.match(emojiRegex);
51 | // If the string doesn't contain any emoji, instead we output the first letter wrapped in some custom styles
52 | if (!match) {
53 | match = `${value.charAt(0)} `
54 | }
55 | return Array.isArray(match) ? match.join('') : match;
56 | });
57 |
58 | config.addFilter('limit', (arr, limit) => arr.slice(0, limit));
59 |
60 | config.addFilter('lowercase', function(value) {
61 | return value.toLowerCase();
62 | });
63 |
64 | // This workaround is needed so we can transform it back into an array with Alpine (we can't split on "," as it can be included within the items)
65 | config.addFilter('arrayToString', function(value) {
66 | return encodeURI(value.join('£'));
67 | });
68 |
69 | /* Shortcodes */
70 | const imageShortcode = async (src, className, alt, sizes) => {
71 | let metadata = await Image(src.includes('http') ? src : `./src/${src}`, {
72 | widths: [600, 1500, 3000],
73 | formats: ['webp', 'jpeg'],
74 | outputDir: './_site/img/recipes',
75 | urlPath: '/img/recipes/'
76 | });
77 |
78 | let imageAttributes = {
79 | class: className,
80 | alt,
81 | sizes,
82 | loading: "lazy",
83 | decoding: "async"
84 | };
85 |
86 | return Image.generateHTML(metadata, imageAttributes);
87 | }
88 |
89 | config.addNunjucksAsyncShortcode('recipeimage', imageShortcode);
90 | config.addShortcode('year', () => `${new Date().getFullYear()}`);
91 |
92 | return {
93 | dir: {
94 | input: 'src',
95 | output: '_site',
96 | includes: '_includes',
97 | data: '_data'
98 | },
99 | passthroughFileCopy: true
100 | }
101 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | _site/
3 | src/_includes/css/
4 | node_modules/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Maël Brunet
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # My Online Cookbook
4 |
5 | My Online Cookbook is a starter kit to create your own website of recipes, using [Eleventy](https://11ty.io) and [Netlify CMS](https://www.netlifycms.org/). It is meant to be both highly accessible (including to non-developers), as well as fully customisable should you want to use it as a starting off point.
6 |
7 | Presentation & set-up instructions : https://myonlinecookbook.xyz/
8 |
9 | Demo (this is what you get out of the box) : [https://myonlinecookbook.netlify.app/](https://myonlinecookbook.netlify.app/)
10 |
11 | Get started now by forking the project or deploy to Netlify : [](https://app.netlify.com/start/deploy?repository=https://github.com/maeligg/my-online-cookbook&stack=cms)
12 |
13 | ## Features
14 |
15 | ### 📘 Optimised for recipes
16 | Unlike other general-purpose templates and website builders, My Online Cookbook is optimised for writing, reading and easily finding back your recipes. Quickly visualise which ingredients you need, navigate between recipes in the same categories, and automatically adapt quantities based on the number of servings.
17 |
18 | ### 💪 Powerful search
19 | The kit includes a powerful live search system offering a UX on-par with third-party services like [Algolia](https://www.algolia.com/), without needing any external dependency or subscription service.
20 |
21 |
22 | ### 🧰 Lightweight & easily extendable
23 | Easily customise the theme color and other site attributes using the global data files, or dive into the code and change anything. The CSS is authored using [Sass](https://sass-lang.com/) and following the [BEM](https://en.bem.info/) naming convention. JavaScript is added where needed using [Alpine](https://github.com/alpinejs/alpine) and following a component-based approach. Images are processed and optimised at build-time using the [Eleventy image plugin](https://www.11ty.dev/docs/plugins/image/). Apart from Alpine, there are no run-time dependencies, making the site both extremely lightweight and easy to pick up and modify.
24 |
25 |
26 | ## Running the site locally
27 | 1. `npm install` to install all dependencies
28 | 2. `npm run dev` to serve the site locally
29 | 3. `npm run build` to build the site for production
30 |
31 |
32 | ## Access the CMS admin interface
33 | Go to `/admin` to access the admin interface (this also works locally). You'll need to configure a user with [Netlify Identity](https://docs.netlify.com/visitor-access/identity/) to log in. For more information on how to use or configure Netlify CMS go to [their documentation](https://www.netlifycms.org/docs/intro/). In addition to recipes, all site settings (primary color, etc) as well as labels are editable from this interface.
34 |
35 |
36 | ## Directory structure
37 | * `.eleventy.js` has all the custom configuration for [Eleventy](https://11ty.io), including collections, filters and shortcodes.
38 | * `src/_data` contains nav and site settings, also editable from the CMS admin interface.
39 | * `src/_includes` contains layouts and reusable components (including SVG icons).
40 | * `src/admin` contains the configuration for editable fields in Netlify CMS.
41 | * `src/img` contains all images. Note that only images placed in `src/img/recipes` are editable from the CMS admin interface.
42 | * `src/recipes` is your main content, with each recipe saved as a markdown file.
43 | * Each other page is located at the root of `src/` as its own markdown or nunjucks file.
44 |
--------------------------------------------------------------------------------
/github-readme.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [[headers]]
2 | for = "/fonts/*"
3 | [headers.values]
4 | Cache-Control = "public, max-age=31536000"
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-online-cookbook",
3 | "version": "0.1.0",
4 | "description": "A starter kit to make your own online recipe cookbook",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev:css": "sass src/scss/main.scss:src/_includes/css/main.css --watch --style=compressed",
8 | "dev:11ty": "eleventy --serve",
9 | "dev": "npm-run-all --parallel dev:css dev:11ty",
10 | "prod:css": "sass src/scss/main.scss:src/_includes/css/main.css --style=compressed",
11 | "prod:11ty": "eleventy",
12 | "build": "npm-run-all prod:css prod:11ty"
13 | },
14 | "author": "Maël Brunet",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@11ty/eleventy": "^0.12.1",
18 | "@11ty/eleventy-img": "^0.8.3",
19 | "npm-run-all": "^4.1.5",
20 | "sass": "^1.32.12"
21 | },
22 | "dependencies": {
23 | "alpinejs": "^2.8.2"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "git+https://github.com/maeligg/my-online-cookbook"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/_data/nav.json:
--------------------------------------------------------------------------------
1 | {
2 | "items": [
3 | {
4 | "text": "Home",
5 | "url": "/"
6 | },
7 | {
8 | "text": "All recipes",
9 | "url": "/recipes/"
10 | },
11 | {
12 | "text": "About",
13 | "url": "/about/"
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/src/_data/site.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "My Online Cookbook",
3 | "metaDescription": "The online cookbook of John Doe",
4 | "author": "John Doe",
5 | "primaryColor": "#ffdb70",
6 | "secondaryColor": "#32816e",
7 | "searchLabel": "Find recipes by name, tag or ingredients",
8 | "searchContainsLabel": "Contains",
9 | "servingsLabel": "servings",
10 | "ingredientsLabel": "Ingredients",
11 | "baseURL": "https://myonlinecookbook.netlify.app"
12 | }
--------------------------------------------------------------------------------
/src/_includes/components/footer.njk:
--------------------------------------------------------------------------------
1 |
Made with My Online Cookbook
2 | © {{ site.author }} {% year %}
--------------------------------------------------------------------------------
/src/_includes/components/navigation.njk:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
document.querySelector('#search').focus(
20 | ));
21 | ">
22 | Search
23 | {% include "icons/search.svg" %}
24 | {% include "icons/close.svg" %}
25 |
26 |
27 | {% include "components/search.njk" %}
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/_includes/components/search.njk:
--------------------------------------------------------------------------------
1 |
2 |
{{ site.searchLabel }} :
3 |
4 | res.json()).then(res => {
9 | sessionStorage.setItem('searchResults', JSON.stringify(res));
10 | searchResults = res;
11 | });
12 | }
13 | " @input="
14 | matches = [];
15 |
16 | if (searchInput.length < 3) { return };
17 |
18 | searchResults.forEach(recipe => {
19 | const matchTitle = recipe.title.toLowerCase().includes(searchInput.toLowerCase());
20 | const matchIngredient = recipe.ingredients.find(ingredient => ingredient.toLowerCase().includes(searchInput.toLowerCase()));
21 | const matchTag = recipe.tags.find(tag => tag.toLowerCase().includes(searchInput.toLowerCase()));
22 |
23 | if (!matchTitle && !matchIngredient && !matchTag) { return };
24 |
25 | const match = {...recipe};
26 | if (matchTitle) { match.matchTitle = matchTitle };
27 | if (matchIngredient) { match.matchIngredient = matchIngredient };
28 | if (matchTag) { match.matchTag = matchTag };
29 |
30 | matches.push(match)
31 | });
32 | ">
33 | {% if homeSearch %}{% include "icons/search.svg" %}{% endif %}
34 |
38 | Close
39 | {% include "icons/close.svg" %}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
54 |
55 |
56 | {{ site.searchContainsLabel }}:
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/_includes/components/taglist.njk:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/src/_includes/icons/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/_includes/icons/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/_includes/icons/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/_includes/icons/time.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/_includes/layouts/base.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ metaTitle or title }} - {{ site.name }}
7 |
8 |
9 |
10 |
11 | {% if image %}
12 |
13 |
14 |
15 |
16 |
17 | {% endif %}
18 |
19 |
26 |
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 | {% block content %}
37 |
38 |
{{title}}
39 | {{content | safe}}
40 |
41 | {% endblock content %}
42 |
43 |
44 |
47 |
48 |
--------------------------------------------------------------------------------
/src/_includes/layouts/home.njk:
--------------------------------------------------------------------------------
1 | ---
2 | bodyClass: c-home
3 | ---
4 |
5 | {% extends 'layouts/base.njk' %}
6 |
7 | {% block content %}
8 |
9 |
10 |
{{title}}{% if authorInTitle %} {{ site.author }}{% endif %}
11 |
12 |
13 |
14 |
15 |
16 | {% set homeSearch = true %}
17 | {% include "components/search.njk" %}
18 |
19 |
20 |
21 | {% if collections[highlightedTag] %}
22 |
23 |
24 |
{{ highlightedTitle }}
25 |
26 | {% set favouriteRecipes = collections[highlightedTag] | limit(4) %}
27 | {% for recipe in favouriteRecipes %}
28 |
29 | {% recipeimage recipe.data.image, "c-card__image", recipe.data.title, "(min-width: 1150px) 25vw, (min-width: 850px) 33vw, (min-width: 550px) 50vw, 100vw" %}
30 |
31 |
32 | {% for tag in recipe.data.tags %}
33 |
{{ tag | onlyEmoji | safe }}
34 | {% endfor %}
35 |
36 | {% if recipe.data.time %}
37 |
38 | {% include 'icons/time.svg' %}
39 | {{ recipe.data.time }}
40 |
41 | {% endif %}
42 |
43 |
44 | {{ recipe.data.title }}
45 |
46 |
47 | {% endfor %}
48 |
49 |
{{ highlightedLinkText }}
50 |
51 |
52 | {% endif %}
53 | {% endblock %}
--------------------------------------------------------------------------------
/src/_includes/layouts/recipe.njk:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.njk' %}
2 |
3 | {% block content %}
4 | {% recipeimage image, "c-recipe__header-image", title, "100vw" %}
5 | {{ title }}
6 |
7 |
8 |
9 |
10 | {% for tag in tags %}
11 |
{{ tag }}
12 | {% endfor %}
13 |
14 |
15 | {% if time or not servings == "" %}
16 |
17 | {% if time %}
18 |
{% include "icons/time.svg" %}{{ time }}
19 | {% endif %}
20 | {% if not servings == "" %}
21 |
22 |
23 |
24 | -
25 | Remove 1 serving
26 |
27 |
28 |
29 |
30 | +
31 | Add 1 serving
32 |
33 | {{ site.servingsLabel }}
34 |
35 | {% endif %}
36 |
37 | {% endif %}
38 |
39 |
{{ site.ingredientsLabel }}
40 |
41 |
46 |
47 |
48 |
49 |
50 | {% for ingredient in ingredients %}
51 | {{ ingredient }}
52 | {% endfor %}
53 |
54 |
55 |
56 |
57 |
58 | {% if sourceLabel and sourceURL %}
59 |
Source : {{ sourceLabel }}
60 | {% endif %}
61 | {{ content | safe }}
62 |
63 |
64 |
65 |
66 |
67 |
72 | {% endblock %}
--------------------------------------------------------------------------------
/src/_includes/layouts/recipes-list.njk:
--------------------------------------------------------------------------------
1 | {% extends 'layouts/base.njk' %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
{{ selectedTag }}{{ title }}
8 |
9 | {% if not selectedTag %}
10 | {% include 'components/taglist.njk' %}
11 | {% endif %}
12 |
13 |
14 |
15 |
16 |
17 | {% for recipe in collections.recipes %}
18 | {% if not selectedTag or selectedTag in recipe.data.tags %} {# If we don't have a selectedTag, we are on the all recipes page #}
19 |
20 | {% recipeimage recipe.data.image, "c-card__image", recipe.data.title, "(min-width: 1150px) 25vw, (min-width: 850px) 33vw, (min-width: 550px) 50vw, 100vw" %}
21 |
22 | {% if not recipe.data.tags | length %}
23 |
24 | {% for tag in recipe.data.tags %}
25 |
{{ tag | onlyEmoji | safe }}
26 | {% endfor %}
27 |
28 |
29 | {% endif %}
30 | {% if recipe.data.time %}
31 |
32 | {% include 'icons/time.svg' %}
33 | {{ recipe.data.time }}
34 |
35 | {% endif %}
36 |
37 |
38 | {{ recipe.data.title }}
39 |
40 |
41 | {% endif %}
42 | {% endfor %}
43 |
44 |
45 |
46 | {% if selectedTag %}
47 |
48 |
49 | {% include 'components/taglist.njk' %}
50 |
51 |
52 | {% endif %}
53 |
54 | {% endblock %}
--------------------------------------------------------------------------------
/src/about.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/base.njk
3 | title: About
4 | bodyClass: u-free-text
5 | ---
6 | 
7 | \
8 | \
9 | This page is for you to present yourself and/or explain what your cookbook is about.
--------------------------------------------------------------------------------
/src/admin/config.yml:
--------------------------------------------------------------------------------
1 | backend:
2 | name: git-gateway
3 | branch: main
4 | media_folder: "src/img/recipes"
5 | public_folder: "/img/recipes"
6 |
7 | collections:
8 | - name: "recipes"
9 | label: "Recipes"
10 | folder: "src/recipes"
11 | create: true
12 | fields:
13 | - {label: "Title", name: "title", widget: "string"}
14 | - {label: "Image", name: "image", widget: "image"}
15 | - label: "Tags"
16 | name: "tags"
17 | widget: "list"
18 | required: false
19 | summary: "{{fields.tag}}"
20 | field: {label: "Tag", name: "tag", widget: "string"}
21 | - {label: "Preparation time", name: "time", widget: "string", required: false}
22 | - {label: "Number of servings", name: "servings", widget: "number", required: false}
23 | - {label: "Source of the recipe", name: "sourceLabel", widget: "string", required: false}
24 | - {label: "URL of the source of the recipe", name: "sourceURL", widget: "string", required: false}
25 | - label: "Ingredients"
26 | name: "ingredients"
27 | widget: "list"
28 | summary: "{{fields.ingredient}}"
29 | field: {label: "Ingredient", name: "ingredient", widget: "string"}
30 | - {label: "Body", name: "body", widget: "markdown"}
31 | - name: "settings"
32 | label: "Settings"
33 | files:
34 | - name: "site"
35 | label: "Site settings"
36 | delete: false
37 | file: "src/_data/site.json"
38 | fields:
39 | - {label: "Site name", name: "name", widget: "string", hint: "used for SEO"}
40 | - {label: "Site meta description", name: "metaDescription", widget: "string", hint: "used for SEO"}
41 | - {label: "Author", name: "author", widget: "string"}
42 | - {label: "Primary color", name: "primaryColor", widget: "color", hint: "make sure to choose a color that is light enough to display as a background behind dark text"}
43 | - {label: "Secondary color", name: "secondaryColor", widget: "color", hint: "used for links and focus styles"}
44 | - {label: "Search (label)", name: "searchLabel", widget: "string"}
45 | - {label: "Search \"contains\" (label)", name: "searchContainsLabel", widget: "string"}
46 | - {label: "Servings (label)", name: "servingsLabel", widget: "string"}
47 | - {label: "Ingredients (label)", name: "ingredientsLabel", widget: "string"}
48 | - name: "nav"
49 | label: "Navigation"
50 | delete: false
51 | file: "src/_data/nav.json"
52 | fields:
53 | - label: "Items"
54 | name: "items"
55 | widget: "list"
56 | fields:
57 | - {label: "Text", name: "text", widget: "string"}
58 | - name: "pages"
59 | label: "Pages"
60 | files:
61 | - name: "home"
62 | label: "Homepage"
63 | file: "src/index.md"
64 | fields:
65 | - {label: "Title", name: "title", widget: "string"}
66 | - {label: "Add author to title", name: "authorInTitle", widget: "boolean"}
67 | - {label: "Meta title", name: "metaTitle", widget: "string", hint: "used for SEO"}
68 | - {label: "Highlighted tag", name: "highlightedTag", widget: "string", hint: "This must correspond to one of your recipes' tag !"}
69 | - {label: "Title of the highlighted tag section (label)", name: "highlightedTitle", widget: "string"}
70 | - {label: "Link text to all recipes with the highlighted tag (label)", name: "highlightedLinkText", widget: "string"}
71 | - name: "recipes"
72 | label: "All recipes page"
73 | file: "src/recipes.md"
74 | fields:
75 | - {label: "Title", name: "title", widget: "string"}
76 | - name: "tags"
77 | label: "Tags pages"
78 | file: "src/tag.md"
79 | fields:
80 | - {label: "All recipes (label)", name: "allRecipesLabel", widget: "string"}
81 | - name: "about"
82 | label: "About page"
83 | file: "src/about.md"
84 | fields:
85 | - {label: "Title", name: "title", widget: "string"}
86 | - {label: "Body", name: "body", widget: "markdown"}
--------------------------------------------------------------------------------
/src/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Content Manager
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/fonts/Vollkorn-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/fonts/Vollkorn-Regular.woff
--------------------------------------------------------------------------------
/src/fonts/Vollkorn-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/fonts/Vollkorn-Regular.woff2
--------------------------------------------------------------------------------
/src/fonts/Vollkorn-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/fonts/Vollkorn-SemiBold.woff
--------------------------------------------------------------------------------
/src/fonts/Vollkorn-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/fonts/Vollkorn-SemiBold.woff2
--------------------------------------------------------------------------------
/src/img/about.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/img/about.jpg
--------------------------------------------------------------------------------
/src/img/favicon-alt.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/img/favicon-alt.ico
--------------------------------------------------------------------------------
/src/img/recipes/brownies.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/img/recipes/brownies.jpg
--------------------------------------------------------------------------------
/src/img/recipes/coconut-lentil-soup.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/img/recipes/coconut-lentil-soup.jpg
--------------------------------------------------------------------------------
/src/img/recipes/courgette-lemon-risotto.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maeligg/my-online-cookbook/20dac4790c4cad9884c25e3460328c6243376d0d/src/img/recipes/courgette-lemon-risotto.jpg
--------------------------------------------------------------------------------
/src/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/home.njk
3 | title: This cookbook belongs to
4 | authorInTitle: true
5 | metaTitle: Home
6 | highlightedTag: Favourite ⭐
7 | highlightedTitle: Some of my favourite recipes
8 | highlightedLinkText: All my favourite recipes
9 | ---
--------------------------------------------------------------------------------
/src/recipes.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: layouts/recipes-list.njk
3 | title: All recipes
4 | ---
--------------------------------------------------------------------------------
/src/recipes/coconut-lentil-soup.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Coconut lentil soup
3 | image: /img/recipes/coconut-lentil-soup.jpg
4 | tags:
5 | - Soup 🥣
6 | - Vegetarian 🌿
7 | - Vegan 🌱
8 | sourceLabel: Bon Appétit
9 | sourceURL: https://www.bonappetit.com/recipe/vegan-coconut-lentil-soup
10 | servings: 4
11 | ingredients:
12 | - 1 large onion
13 | - 6 garlic cloves
14 | - 3 tablespoons of grated ginger
15 | - 2 tablespoons virgin coconut oil
16 | - 5 teaspoons curry powder
17 | - 0.5 teaspoon cayenne pepper
18 | - 400g can unsweetened coconut milk
19 | - 150g split red lentils
20 | - 8 tablespoons unsweetened shredded coconut
21 | - 2 teaspoons kosher salt
22 | - 300g spinach
23 | - 1 can crushed tomatoes
24 | - plain whole-milk or non-dairy yogurt (for serving; optional)
25 | ---
26 |
27 | Peel 1 onion and chop. Smash 6 garlic cloves with the flat side of your knife. Peel, then finely chop. Peel the ginger with a small spoon, then finely chop.
28 |
29 | Heat 2 Tbsp. oil in large Dutch oven over medium. Add onion and cook, stirring often, just until translucent, 6–8 minutes. Add garlic and ginger and cook, stirring often, until garlic is starting to turn golden, about 5 minutes. Add 5 tsp. curry powder and 0.5 tsp. cayenne and cook, stirring constantly, until spices are aromatic and starting to stick to bottom of pot, about 1 minute. Add the coconut milk and stir to loosen spices, then stir in the lentils, the shredded coconut, 2 tsp. salt, and 1 liter of water.
30 |
31 | Bring to a boil over medium-high heat, then reduce heat to medium-low to keep soup at a gentle simmer. Cook, stirring occasionally, until lentils are broken down and soup is thickened, 25–30 minutes.
32 |
33 | Meanwhile, coarsely chop the spinach. Add spinach and the tomatoes to pot and stir to combine. Taste and season with more salt. Simmer just to let flavors meld, about 5 minutes. Taste and season again with more salt.
34 |
35 | Ladle soup into bowls. Top with yogurt, if desired.
36 |
--------------------------------------------------------------------------------
/src/recipes/courgette-lemon-risotto.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Courgette & lemon risotto
3 | image: /img/recipes/courgette-lemon-risotto.jpg
4 | tags:
5 | - Italian 🇮🇹
6 | - Vegetarian 🌿
7 | - Favourite ⭐
8 | time: 50 min
9 | servings: 2
10 | sourceLabel: BBC good food
11 | sourceURL: https://www.bbcgoodfood.com/recipes/courgette-lemon-risotto/
12 | ingredients:
13 | - 50g butter
14 | - 1 onion, finely chopped
15 | - 1 large garlic clove crushed
16 | - 180g risotto rice
17 | - 1 vegetable stock cube
18 | - zest and juice 1 lemon
19 | - 2 lemon thyme sprigs
20 | - 250g courgette diced
21 | - 50g parmesan (or vegetarian alternative) grated
22 | - 2 tbsp crème fraîche
23 | ---
24 |
25 | Melt the butter in a deep frying pan. Add the onion and fry gently until softened for about 8 mins, then add the garlic and stir for 1 min. Stir in the rice to coat it in the buttery onions and garlic for 1-2 mins.
26 |
27 | Dissolve the stock cube in 1 litre of boiling water, then add a ladle of the stock to the rice, along with the lemon juice and thyme. Bubble over a medium heat, stirring constantly. When almost all the liquid has been absorbed, add another ladle of stock and keep stirring. Tip in the courgette and keep adding the stock, stirring every now and then until the rice is just tender and creamy.
28 |
29 | To serve, stir in some seasoning, the lemon zest, Parmesan and crème fraîche.
--------------------------------------------------------------------------------
/src/recipes/recipes.json:
--------------------------------------------------------------------------------
1 | {
2 | "layout": "layouts/recipe",
3 | "servings": 0,
4 | "ingredients": []
5 | }
--------------------------------------------------------------------------------
/src/recipes/simple-brownies.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Simple brownies
3 | image: https://ichef.bbci.co.uk/food/ic/food_16x9_1600/recipes/richchocolatebrownie_1933_16x9.jpg
4 | tags:
5 | - Sweet 🍬
6 | - Cake 🍰
7 | - Vegetarian 🌿
8 | - Sharable
9 | - Favourite ⭐
10 | time: 45 min
11 | servings: 4
12 | sourceLabel: BBC good food
13 | sourceURL: https://www.bbc.co.uk/food/recipes/richchocolatebrownie_1933/
14 | ingredients:
15 | - 225g butter (preferably unsalted)
16 | - 450g caster sugar
17 | - 140g dark chocolate broken into pieces
18 | - 5 free-range medium eggs
19 | - 110g plain flour
20 | - 55g cocoa powder
21 | ---
22 |
23 | Heat the oven to 190C/170C Fan/Gas 5. Line a 20x30cm baking tin with baking paper.
24 |
25 | Gently melt the butter and the sugar together in a large pan. Once melted, take off the heat and add the chocolate. Stir until melted.
26 |
27 | Beat in the eggs, then stir in the flour and the cocoa powder.
28 |
29 | Pour the brownie batter into the prepared tin and bake for 30–35 minutes, or until the top of the brownie is just firm but there is still a gentle wobble in the middle.
30 |
31 | Take out of the oven and leave to cool in the tin. Cut the brownies into 5cm squares when only just warm, or completely cool.
--------------------------------------------------------------------------------
/src/scss/_global.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --color-grey-100: #F9FAFB;
3 | --color-grey-200: #F3F4F6;
4 | --color-grey-300: #E5E7EB;
5 | --color-grey-400: #D1D5DB;
6 | --color-grey-500: #9CA3AF;
7 | --color-grey-800: #4B5563;
8 | --color-grey-900: #3A3A3A;
9 | --color-white: #ffffff;
10 |
11 | --shadow-md: 0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06);
12 | --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05);
13 | --shadow-xl: 0 25px 50px -12px rgba(0,0,0,0.25);
14 | }
15 |
16 | body {
17 | display: flex;
18 | flex-direction: column;
19 | }
20 |
21 | main {
22 | display: flex;
23 | flex-direction: column;
24 | margin-bottom: 30px;
25 | }
26 |
27 | // The x-cloak attribute is automatically removed from elements when Alpine initializes. They should not be visible before then to avoid flickering content
28 | [x-cloak] {
29 | display: none !important;
30 | }
31 |
32 | section {
33 | padding: 30px 0;
34 |
35 | @include mq(medium) {
36 | padding: 60px 0;
37 | }
38 | }
39 |
40 | a {
41 | color: var(--color-secondary);
42 | text-decoration: underline 2px;
43 | text-underline-offset: 2px;
44 |
45 | &:hover, &:focus {
46 | outline: none;
47 | text-decoration: none;
48 | }
49 |
50 | &:focus-visible {
51 | outline: 2px solid var(--color-secondary);
52 | }
53 | }
54 |
55 | button {
56 | cursor: pointer;
57 | }
58 |
59 | input {
60 | &:focus {
61 | outline: none;
62 | box-shadow: 0 0 0 2px var(--color-secondary);
63 | }
64 | }
--------------------------------------------------------------------------------
/src/scss/_layout.scss:
--------------------------------------------------------------------------------
1 | .l-container {
2 | margin: 0 auto;
3 | max-width: 1140px;
4 | padding-left: 18px;
5 | padding-right: 18px;
6 | }
7 |
8 | .l-header, .l-footer {
9 | padding: 12px;
10 | display: flex;
11 | justify-content: space-between;
12 | }
13 |
14 | .l-footer {
15 | margin-top: auto;
16 | }
--------------------------------------------------------------------------------
/src/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | $breakpoints-map: (
2 | small: "all and (min-width: 576px)",
3 | medium: "all and (min-width: 768px)",
4 | large: "all and (min-width: 1200px)"
5 | );
6 |
7 | // -------------------------------------
8 | // Mixin
9 | // -------------------------------------
10 | @mixin mq($breakpoint-name) {
11 | // sanitize variable
12 | $breakpoint-name: unquote($breakpoint-name);
13 |
14 | // check if passed name is in $breakpoints-map
15 | @if map-has-key($breakpoints-map, $breakpoint-name) {
16 | $breakpoint-query: map-get($breakpoints-map, $breakpoint-name);
17 |
18 | // write media query
19 | @media #{$breakpoint-query} {
20 | @content;
21 | }
22 |
23 | // throw error if passed parameter is not a key in $breakpoints-map
24 | } @else {
25 | @error "#{$breakpoint-name} is not a key in $breakpoints-map";
26 | }
27 | }
--------------------------------------------------------------------------------
/src/scss/_reset.scss:
--------------------------------------------------------------------------------
1 | /* Box sizing rules */
2 | *,
3 | *::before,
4 | *::after {
5 | box-sizing: border-box;
6 | }
7 |
8 | /* Remove default margin */
9 | body,
10 | h1,
11 | h2,
12 | h3,
13 | h4,
14 | p,
15 | ul,
16 | ol,
17 | li,
18 | figure,
19 | figcaption,
20 | blockquote,
21 | dl,
22 | dd {
23 | margin: 0;
24 | }
25 |
26 | /* Set core body defaults */
27 | body {
28 | min-height: 100vh;
29 | margin: 0;
30 | padding: 0;
31 | scroll-behavior: smooth;
32 | text-rendering: optimizeSpeed;
33 | }
34 |
35 | /* Remove list styles on ul, ol elements with a class attribute */
36 | ol[class], ul[class] {
37 | list-style: none;
38 | padding: 0;
39 | }
40 |
41 | /* Make images easier to work with */
42 | img {
43 | max-width: 100%;
44 | display: block;
45 | }
46 |
47 | /* Inherit fonts for inputs and buttons */
48 | input,
49 | button,
50 | textarea,
51 | select {
52 | font: inherit;
53 | }
54 |
55 | /* Remove all animations and transitions for people that prefer not to see them */
56 | @media (prefers-reduced-motion: reduce) {
57 | * {
58 | animation-duration: 0.01ms !important;
59 | animation-iteration-count: 1 !important;
60 | transition-duration: 0.01ms !important;
61 | scroll-behavior: auto !important;
62 | }
63 | }
--------------------------------------------------------------------------------
/src/scss/_typography.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Vollkorn';
3 | font-style: normal;
4 | font-weight: 400;
5 | font-display: swap;
6 | src: local('Vollkorn Regular'),
7 | local('Vollkorn-Regular'),
8 | url('/fonts/Vollkorn-Regular.woff2') format('woff2'),
9 | url('/fonts/Vollkorn-Regular.woff') format('woff');
10 | }
11 |
12 | @font-face {
13 | font-family: 'Vollkorn';
14 | font-style: normal;
15 | font-weight: 600;
16 | font-display: swap;
17 | src: local('Vollkorn SemiBold'),
18 | local('Vollkorn-SemiBold'),
19 | url('/fonts/Vollkorn-SemiBold.woff2') format('woff2'),
20 | url('/fonts/Vollkorn-SemiBold.woff') format('woff');
21 | }
22 |
23 | body {
24 | font-size: 1rem;
25 | font-family: Sentinel SSm A, Sentinel SSm B, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
26 | line-height: 1.5;
27 | }
28 |
29 | h1, h2, h3, h4, h5, h6 {
30 | font-family: 'Vollkorn', serif;
31 | font-weight: 600;
32 | }
33 |
34 | h1 {
35 | text-align: center;
36 | font-size: clamp(2rem,calc(1rem + 3vw),4rem);
37 | }
38 |
39 | h2 {
40 | text-align: center;
41 | font-size: clamp(1.6rem,calc(1rem + 2vw),3.4rem);
42 | }
43 |
44 | h3 {
45 | font-size: clamp(1.2rem,calc(1rem + 1vw),2.6rem);
46 | }
--------------------------------------------------------------------------------
/src/scss/_utility.scss:
--------------------------------------------------------------------------------
1 | .u-hide {
2 | display: none !important;
3 | }
4 |
5 | .u-show {
6 | display: initial !important;
7 | }
8 |
9 | .u-sr-only {
10 | position:absolute;
11 | left:-10000px;
12 | top:auto;
13 | width:1px;
14 | height:1px;
15 | overflow:hidden;
16 | }
17 |
18 | .u-print-only {
19 | display: none !important;
20 | }
21 |
22 | @media print {
23 | .u-print-only {
24 | display: initial !important;
25 | }
26 | }
27 |
28 |
29 | .u-bgc-grey-100 {
30 | background-color: var(--color-grey-100);
31 | }
32 |
33 | .u-highlight {
34 | background-color: rgba(250, 243, 145, 0.5);
35 | }
36 |
37 | .u-free-text {
38 | p, h1, h2, h3, h4, h5, h6 {
39 | margin-bottom: 18px;
40 | }
41 |
42 | img {
43 | margin: 20px auto;
44 | }
45 | }
46 |
47 | .u-mb-large {
48 | margin-bottom: 30px;
49 | }
--------------------------------------------------------------------------------
/src/scss/components/_about.scss:
--------------------------------------------------------------------------------
1 | // Global styles for the about page. These don't use any classes since they are authored in markdown.
2 | .about {
3 | img {
4 | margin: 0 auto;
5 | }
6 | }
--------------------------------------------------------------------------------
/src/scss/components/_card.scss:
--------------------------------------------------------------------------------
1 | .c-card__wrapper {
2 | display: grid;
3 | grid-template-columns: repeat(auto-fit,minmax(250px, 1fr));
4 | gap: 30px;
5 | }
6 |
7 | .c-card {
8 | position: relative;
9 | width: 100%;
10 | max-width: 600px;
11 | margin: 0 auto;
12 | display: flex;
13 | flex-direction: column;
14 | border-radius: 20px;
15 | background-color: var(--color-white);
16 | box-shadow: var(--shadow-md);
17 | text-decoration: none;
18 | color: var(--color-grey-900);
19 | }
20 |
21 | .c-card__image {
22 | height: auto;
23 | aspect-ratio: 16/9;
24 | object-fit: cover;
25 | background-color: var(--color-grey-500); // fallback while image is loading
26 | border-radius: 20px 20px 0 0;
27 | }
28 |
29 | .c-card__info {
30 | padding: 12px;
31 | display: flex;
32 | gap: 18px;
33 | justify-content: space-between;
34 | align-items: center;
35 | background-color: var(--color-primary);
36 | }
37 |
38 | .c-card__tag-abbr {
39 | text-decoration: none;
40 | }
41 |
42 | .c-card__tag-first-letter {
43 | width: 22px;
44 | height: 22px;
45 | display: inline-flex;
46 | justify-content: center;
47 | align-items: center;
48 | font-weight: bold;
49 | background-color: var(--color-grey-900);
50 | color: var(--color-white);
51 | border-radius: 50%;
52 | }
53 |
54 | .card__time {
55 | display: flex;
56 | align-items: center;
57 |
58 | svg {
59 | margin-right: 6px;
60 | fill: var(--color-grey-900);
61 | }
62 | }
63 |
64 | .c-card__title-wrapper {
65 | flex-grow: 1;
66 | display: flex;
67 | align-items: center;
68 | padding: 12px 18px;
69 | text-decoration: none;
70 | color: var(--color-grey-900);
71 |
72 | &::before {
73 | content: "";
74 | position: absolute;
75 | top: 0;
76 | left: 0;
77 | height: 100%;
78 | width: 100%;
79 | border-radius: 20px;
80 | }
81 |
82 | &:hover, &:focus {
83 | text-decoration: none;
84 |
85 | &::before {
86 | outline: none;
87 | box-shadow: var(--shadow-lg);
88 | color: var(--color-grey-900);
89 | text-decoration: none;
90 | }
91 | }
92 |
93 | &:focus-visible {
94 | &::before {
95 | box-shadow: 0 0 0 2px var(--color-secondary), var(--shadow-lg);
96 | }
97 | }
98 | }
99 |
100 | .c-card__title {
101 | font-size: 1.4rem;
102 | }
--------------------------------------------------------------------------------
/src/scss/components/_home.scss:
--------------------------------------------------------------------------------
1 | .c-home {
2 | &::before {
3 | content: "";
4 | position: absolute;
5 | z-index: -10;
6 | width: 100%;
7 | height: 400px;
8 | background-image: linear-gradient(to bottom, var(--color-primary), var(--color-white));
9 | background-color: hsla(0, 0%, 100%, .3);
10 | background-blend-mode: overlay;
11 | }
12 | }
13 |
14 | .c-home__highlighted-tag-link {
15 | margin-top: 30px;
16 | display: block;
17 | text-align: center;
18 | }
--------------------------------------------------------------------------------
/src/scss/components/_nav.scss:
--------------------------------------------------------------------------------
1 | .c-tags__label {
2 | margin-bottom: 6px;
3 | text-align: center;
4 | }
5 |
6 | .c-nav {
7 | position: relative;
8 | width: 100%;
9 | display: flex;
10 | justify-content: space-between;
11 | align-items: center;
12 | font-size: var(--text-sm);
13 | font-weight: 600;
14 | font-size: 1.2rem;
15 | }
16 |
17 | .c-nav__list {
18 | display: flex;
19 | align-items: center;
20 | gap: 15px;
21 | }
22 |
23 | .c-nav__logo svg {
24 | box-shadow: var(--shadow-md);
25 | }
26 |
27 | .c-nav__home {
28 | display: flex;
29 | align-items: center;
30 | color: var(--color-grey-900);
31 | text-decoration: none;
32 |
33 | &:hover, &:focus {
34 | text-decoration: none;
35 | }
36 | }
37 |
38 | .c-nav__home-text {
39 | margin-left: 18px;
40 |
41 | @media all and (max-width: 576px) {
42 | position:absolute;
43 | left:-10000px;
44 | top:auto;
45 | width:1px;
46 | height:1px;
47 | overflow:hidden;
48 | }
49 | }
50 |
51 | .c-nav__nav-item {
52 | padding: 4px 8px;
53 | display: flex;
54 | color: var(--color-grey-900);
55 | text-decoration: none;
56 |
57 | &:hover, &:focus {
58 | text-decoration: none;
59 | background-color: var(--color-grey-900);
60 | color: var(--color-white);
61 | border-radius: 6px;
62 | }
63 | }
64 |
65 | .c-nav__nav-item--active {
66 | background-color: var(--color-grey-900);
67 | color: var(--color-white);
68 | border-radius: 6px;
69 | }
--------------------------------------------------------------------------------
/src/scss/components/_recipe-tags.scss:
--------------------------------------------------------------------------------
1 | .c-tags {
2 | display: flex;
3 | gap: 12px;
4 | justify-content: center;
5 | flex-wrap: wrap;
6 | }
7 |
8 | .c-tags__tag {
9 | display: inline-flex;
10 | padding: 2px 6px;
11 | color: var(--color-grey-900);
12 | text-decoration: none;
13 | border: 1px solid currentColor;
14 | border-radius: 8px;
15 | font-size: 1rem;
16 |
17 | &:hover, &:focus {
18 | text-decoration: none;
19 | color: var(--color-white);
20 | background-color: var(--color-grey-900);
21 | }
22 | }
23 |
24 | .c-tag__tag--selected {
25 | color: var(--color-white);
26 | background-color: var(--color-grey-900);
27 | border: 1px solid currentColor;
28 | }
--------------------------------------------------------------------------------
/src/scss/components/_recipe.scss:
--------------------------------------------------------------------------------
1 | .c-recipe__header-image {
2 | width: 100%;
3 | object-fit: cover;
4 | height: 50vh;
5 | min-height: 300px;
6 | background-color: var(--color-grey-500); // fallback while image is loading
7 | }
8 |
9 | .c-recipe__title {
10 | padding: 20px;
11 | background-color: var(--color-primary);
12 | }
13 |
14 | .c-recipe__ingredients-list {
15 | li {
16 | margin-bottom: 12px;
17 |
18 | &::before {
19 | content: "-";
20 | margin-right: 6px;
21 | }
22 | }
23 | }
24 |
25 | .c-recipe__additional-info {
26 | margin-bottom: 30px;
27 |
28 | svg {
29 | margin-right: 6px;
30 | fill: var(--color-grey-900);
31 | }
32 | }
33 |
34 | .c-recipe__recipe-content-wrapper {
35 | display: flex;
36 | flex-direction: column;
37 | gap: 30px;
38 |
39 | @include mq(medium) {
40 | display: grid;
41 | grid-template-columns: 400px 1fr;
42 | grid-template-rows: min-content 1fr;
43 | grid-template-areas:
44 | "ingredients tags"
45 | "ingredients instructions";
46 | }
47 | }
48 |
49 | .c-recipe__tag-list {
50 | margin-bottom: 12px;
51 | display: flex;
52 | flex-wrap: wrap;
53 | gap: 12px;
54 |
55 | @include mq(medium) {
56 | grid-area: tags;
57 | margin-bottom: 0;
58 | }
59 | }
60 |
61 |
62 | .c-recipe__ingredients-wrapper {
63 | @include mq(medium) {
64 | grid-area: ingredients;
65 | }
66 | }
67 |
68 | .c-recipe__serving-button {
69 | height: 20px;
70 | width: 20px;
71 | padding: 0;
72 | display: inline-flex;
73 | justify-content: center;
74 | align-items: center;
75 | background-color: var(--color-grey-900);
76 | border: none;
77 | border-radius: 50%;
78 | color: var(--color-white);
79 |
80 | &:hover, &:focus {
81 | background-color: var(--color-grey-800);
82 | }
83 | }
84 |
85 | .c-recipe__instructions-wrapper {
86 | @include mq(medium) {
87 | grid-area: instructions;
88 | }
89 | }
--------------------------------------------------------------------------------
/src/scss/components/_search.scss:
--------------------------------------------------------------------------------
1 | .c-search__search-toggle {
2 | margin-left: 20px;
3 | padding: 10px;
4 | display: flex;
5 | background-color: transparent;
6 | border: none;
7 |
8 | > * {
9 | display: flex;
10 | }
11 | }
12 |
13 | .c-search__search-wrapper {
14 | font-weight: normal;
15 |
16 | > * + * {
17 | margin-top: 12px;
18 | }
19 | }
20 |
21 | .c-search__search-wrapper:not(.c-search__search-wrapper--home) {
22 | position: absolute;
23 | z-index: 10;
24 | top: 60px;
25 | right: 0;
26 | width: 100%;
27 | background-color: var(--color-white);
28 | padding: 24px;
29 | border: 1px solid var(--color-grey-300);
30 | border-radius: 10px;
31 | box-shadow: var(--shadow-xl);
32 | font-size: 1rem;
33 |
34 | @include mq(medium) {
35 | max-width: 500px;
36 | }
37 | }
38 |
39 | .c-search__label {
40 | display: block;
41 | }
42 |
43 | .c-search__label--home {
44 | text-align: center;
45 | font-weight: bold;
46 | font-size: 1.2rem;
47 | }
48 |
49 | .c-search__input-wrapper {
50 | position: relative;
51 | }
52 |
53 | .c-search__input-wrapper--home {
54 | position: relative;
55 | width: 100%;
56 | max-width: 500px;
57 | margin-left: auto;
58 | margin-right: auto;
59 |
60 | > svg {
61 | position: absolute;
62 | left: 16px;
63 | top: 16px;
64 | }
65 | }
66 |
67 | .c-search__input {
68 | width: 100%;
69 | padding: 4px 12px;
70 | border: 2px solid var(--color-grey-900);
71 | border-radius: 30px;
72 | }
73 |
74 | .c-search__input--home {
75 | padding: 12px 18px 12px 46px;
76 | width: 100%;
77 | padding: 12px 18px 12px 46px;
78 | border: 2px solid var(--color-grey-900);
79 | }
80 |
81 | .c-search__close-button {
82 | display: flex;
83 | position: absolute;
84 | right: 12px;
85 | top: 8px;
86 | padding: 0;
87 | border: none;
88 | background-color: transparent;
89 |
90 | svg {
91 | width: 12px;
92 | }
93 | }
94 |
95 | .c-search__close-button--home {
96 | right: 16px;
97 | top: 16px;
98 |
99 | svg {
100 | width: 17px;
101 | }
102 | }
103 |
104 | .c-search__results-wrapper {
105 | position: relative;
106 | margin: 0;
107 | }
108 |
109 | .c-search__results {
110 | max-height: 100vh;
111 | overflow-y: auto;
112 |
113 | > * + * {
114 | margin-top: 10px;
115 | }
116 | }
117 |
118 | .c-search__results.c-search__results--home {
119 | position: absolute;
120 | z-index: 10;
121 | top: 10px;
122 | left: 50%;
123 | transform: translateX(-50%);
124 | width: 100%;
125 | max-width: 500px;
126 | padding: 24px;
127 | background-color: var(--color-white);
128 | border: 1px solid var(--color-grey-300);
129 | box-shadow: var(--shadow-xl);
130 | border-radius: 10px;
131 | }
132 |
133 | .c-search__result-link {
134 | display: flex;
135 | align-items: flex-start;
136 | flex-wrap: wrap;
137 | gap: 10px;
138 | }
139 |
140 | .c-search__result-link--home {
141 | font-size: 1.2rem;
142 | }
143 |
144 | .c-search__result-ingredients {
145 | font-style: italic;
146 | }
147 |
148 | .c-search__result-ingredients--home {
149 | font-size: 1rem;
150 | }
151 |
--------------------------------------------------------------------------------
/src/scss/main.scss:
--------------------------------------------------------------------------------
1 | @import "reset.scss";
2 | @import "mixins.scss";
3 | @import "utility.scss";
4 | @import "global.scss";
5 |
6 | @import "typography.scss";
7 | @import "layout.scss";
8 |
9 | @import "components/about.scss";
10 | @import "components/card.scss";
11 | @import "components/home.scss";
12 | @import "components/nav.scss";
13 | @import "components/recipe.scss";
14 | @import "components/recipe-tags.scss";
15 | @import "components/search.scss";
16 |
--------------------------------------------------------------------------------
/src/search.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: search.json
3 | ---
4 |
5 | [
6 | {% for recipe in collections.recipes %}
7 | {
8 | "title" : "{{ recipe.data.title }}",
9 | "url" : "{{ recipe.url }}",
10 | "ingredients" : [{% for ingredient in recipe.data.ingredients %}"{{ingredient}}"{% if not loop.last %},{% endif %}{% endfor %}],
11 | "tags" : [{% for tag in recipe.data.tags %}"{{tag}}"{% if not loop.last %},{% endif %}{% endfor %}]
12 | }{% if not loop.last %},{% endif %}
13 | {% endfor %}
14 | ]
--------------------------------------------------------------------------------
/src/tag.md:
--------------------------------------------------------------------------------
1 | ---
2 | pagination:
3 | data: collections
4 | size: 1
5 | alias: selectedTag
6 | permalink: /tags/{{ selectedTag | noEmoji | slug }}/
7 | layout: layouts/recipes-list.njk
8 | allRecipesLabel: All recipes
9 | eleventyComputed:
10 | metaTitle: "{{ selectedTag | noEmoji }}"
11 | ---
12 |
--------------------------------------------------------------------------------