Skip to content

Commit a78520d

Browse files
authored
Merge pull request #2 from buzcarter/spike/recipe-customization-statements
Spike/recipe customization statements
2 parents 4afe94c + 3a60b4c commit a78520d

36 files changed

+2075
-344
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
.lab
55
node_modules
66
output
7+
*.zip
8+
*.rar

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,64 @@ This is based on Jeff Thompson's [recipes PHP project](https://github.com/jeffTh
1010

1111
This project generates static HTML. You may view locally just by dragging one of the output files into your browser, loading onto your webserver, or by using a utility such as [http-server (node)](https://github.com/http-party/http-server).
1212

13+
#### The "Recipe Book" Index
14+
15+
![Recipe Book Index](./docs/images/recipe-index-box-views.jpg)
16+
17+
Choose your preferred view: "Compact List", "List with Thumbnail", or "Recipe Card Grid". (you may set the default view in `config.js`)
18+
19+
![Choose your Recipe Book view preference](./docs/images/index-view-modes.png)
20+
21+
22+
#### New: Per Recipe "Styles"
23+
24+
![Recipe Book Index](./docs/images/recipe-styles.jpg)
25+
26+
Choose your preferred recipe layout **per recipe**: clean, minimal default, Washington Post Recipe Layout ("washington-post"), or New York Times Cooking ("nytimes").
27+
28+
Just add a hidden comment somewhere in your recipe text (*tip*: near the top makes it easier to find and change)
29+
30+
```html
31+
<!-- recipe-style: washington-post -->
32+
```
33+
34+
A *style* tag (or comment) tells the app to swap the default styles for whichever one you specify. If you want _all_ of your recipes to render in a specific style just edit `config.js`, setting `defaultTheme` as desired.
35+
36+
```html
37+
<!-- recipe-style: nytimes -->
38+
39+
# Gran's Killer Kowboy Kookies
40+
41+
```
42+
43+
Create your own... ummm, I owe y'all some documentation here, don't I? Soon... And should be consistent with "theme" v. "style". Sorry. Soon.
44+
1345
### Features:
1446

1547
* Recipes are written in plain text using the intuitive-ish [Markdown format](https://daringfireball.net/projects/markdown).
1648
* Publishes a recipe index, including on-page links to more easily locate a recipe.
1749
* Template driven layout, customize or use the provided template
50+
* Customize with themes -- color and minor layout settings. Plus, each recipe may choose its own theme.
1851
* No more overlooked ingredients or skipped Steps as recipe pages allow you to flag your current step and mark-off those you've already done. In the Ingredients and Steps sections:
1952
* Tap an item once to highlight it, tappin again removes highlighting.
2053
* Double tab an item to mark as "completed" or "used" (the text will be italicized and have a strike-through through it). Double tapping again removes the line.
2154
* Navigate through Steps via arrow keys: when a Step is highlighted step using the left/right arrow keys highlights the previous/next step.
22-
* Auto-generated links to a Google image search for that dish, recipes on Serious Eats and Google, and for restaurants on Yelp (in case you burn something and need takeout fast)
55+
* Auto-generated links to a Google image search for that dish, recipes on Serious Eats and Google, and for restaurants on Yelp (in case you burn something and need takeout fast)
56+
* Access to the original, plain text source for any recipe is but one click away
57+
58+
![view-original-markdown-plain-text-recipes](./docs/images/view-original-markdown-plain-text-recipes.png)
2359

2460
EXAMPLE: below a recipe with a few ingredients "ticked" off (indicating they've been used) and the current step being highlighted:
2561

2662
![Recipe Helpers - Highlighter & Ingredient Indicator](./docs/images/carnitas-with-highlights.png)
2763

28-
The "Recipe Book" Index:
29-
30-
![Recipe Book Index](./docs/images/recipe-book-index.png)
31-
3264
## MORE INFO
3365

3466
* [Installation](#installation)
3567
* [Recipe format](#recipe-format)
3668
* [Adding images](#adding-images)
3769
* [Other options](#other-options)
38-
* [Suggestions welcome!](#suggestions-welcome)
70+
* [Advanced customizations](#advanced-customizations)
3971
* [Colophon & Thank Yous](#Colophon)
4072

4173
## Installation
@@ -170,6 +202,8 @@ The `recipe.html` template file also includes some more options you can customiz
170202
* `autoUrlSections`: list of sections in the recepe template where you want raw URLs (ex: www.instagram.com) to be turned into real links. Great for the `Based On` section but not so good if you want to include Markdown-formatted links in other sections
171203
* `shortenUrls`: turns a super-long url into just the main domain name (link will still work as normal, just less cluttered). Off by default but exists if you want it
172204

205+
206+
173207
## Colophon & Thank Yous
174208

175209
Site built with:

build.js

Lines changed: 67 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,93 @@
1+
/* eslint-disable no-console */
12
const { basename, extname, resolve } = require('path');
2-
const { cpSync, readdirSync, readFileSync, rmSync, mkdirSync } = require('fs');
3+
const { rmSync, mkdirSync } = require('fs');
4+
const { cp, readdir, readFile } = require('fs/promises');
5+
const sharp = require('sharp');
36

47
const { buildRecipes } = require('./src/buildRecipes');
58
const buildRecipeIndex = require('./src/buildRecipeIndex');
69
const configs = require('./config');
710

8-
const MARKDOWN_EXT = '.md';
11+
const THUMBNAIL_WIDTH = 260;
912

10-
function getRecipeFileList({ recipesPath } = {}) {
11-
return readdirSync(recipesPath)
12-
.filter(fileName => extname(fileName) === MARKDOWN_EXT)
13-
.map(fileName => ({
14-
file: resolve(recipesPath, fileName),
15-
name: basename(fileName, MARKDOWN_EXT),
16-
}));
17-
}
18-
19-
function readTemplates({ templatesPath }) {
20-
return {
21-
indexTemplate: readFileSync(resolve(templatesPath, 'index.html'), { encoding: 'utf8' }),
22-
recipeTemplate: readFileSync(resolve(templatesPath, 'recipe.html'), { encoding: 'utf8' }),
23-
};
24-
}
13+
const filterByExtension = (fileList, recipesPath, allowedExtensions) => fileList
14+
.filter(fileName => allowedExtensions.includes(extname(fileName)))
15+
.map(fileName => ({
16+
file: resolve(recipesPath, fileName),
17+
fileName,
18+
name: basename(fileName, extname(fileName)),
19+
}));
2520

2621
function setupOutputDir(outputPath) {
2722
rmSync(outputPath, { recursive: true, force: true });
2823
mkdirSync(outputPath);
24+
mkdirSync(resolve(outputPath, 'sources'));
25+
mkdirSync(resolve(outputPath, 'images'));
26+
mkdirSync(resolve(outputPath, 'images/thumbnails'));
27+
}
2928

29+
function copyStatic({staticPath, imagesPath, outputPath, recipesPath}) {
30+
Promise.all([
31+
cp(staticPath, outputPath, { recursive: true }),
32+
cp(imagesPath, resolve(outputPath, 'images'), { recursive: true }),
33+
cp(recipesPath, resolve(outputPath, 'sources'), { recursive: true }),
34+
])
35+
.catch((err) => {
36+
console.error(err);
37+
});
3038
}
3139

32-
function copyStatic(staticPath, imagesPath, outputPath) {
33-
cpSync(staticPath, outputPath, { recursive: true });
34-
cpSync(imagesPath, resolve(outputPath, 'images'), { recursive: true });
40+
function makeThumbnails(outputPath, images) {
41+
images
42+
.forEach(({ file, name }) => {
43+
const thumbnailPath = resolve(outputPath, `images/thumbnails/${name}.jpg`);
44+
sharp(file)
45+
.resize(THUMBNAIL_WIDTH)
46+
.jpeg({ quality: 70 })
47+
.toFile(thumbnailPath)
48+
.catch(err => console.error(`Problem generating ${thumbnailPath}`, err));
49+
});
3550
}
3651

3752
function main(configs) {
53+
const startTime = new Date();
54+
const imagesPath = resolve(__dirname, configs.imageDir);
55+
const outputPath = resolve(__dirname, configs.outputDir);
56+
const recipesPath = resolve(__dirname, configs.recipeDir);
57+
const staticPath = resolve(__dirname, './src/static/');
58+
const templatesPath = resolve(__dirname, './src/templates/');
59+
3860
const options = {
3961
...configs,
40-
imagesPath: resolve(__dirname, configs.imageDir),
41-
outputPath: resolve(__dirname, configs.outputDir),
42-
recipesPath: resolve(__dirname, configs.recipeDir),
43-
staticPath: resolve(__dirname, './src/static/'),
44-
templatesPath: resolve(__dirname, './src/templates/'),
62+
imagesPath,
63+
outputPath,
64+
recipesPath,
65+
staticPath,
66+
templatesPath,
4567
};
4668

47-
const fileList = getRecipeFileList(options);
48-
const { indexTemplate, recipeTemplate } = readTemplates(options);
69+
Promise.all([
70+
readdir(recipesPath),
71+
readdir(imagesPath),
72+
readFile(resolve(templatesPath, 'index.html'), { encoding: 'utf8' }),
73+
readFile(resolve(templatesPath, 'recipe.html'), { encoding: 'utf8' }),
74+
])
75+
.then(([markdownFiles, images, indexTemplate, recipeTemplate]) => {
76+
markdownFiles = filterByExtension(markdownFiles, recipesPath, ['.md']);
77+
images = filterByExtension(images, imagesPath, ['.jpg', '.jpeg', '.png', '.webp', '.avif']);
4978

50-
setupOutputDir(options.outputPath);
51-
copyStatic(options.staticPath, options.imagesPath, options.outputPath);
52-
buildRecipes(recipeTemplate, options, fileList);
53-
buildRecipeIndex(indexTemplate, options, fileList);
79+
setupOutputDir(outputPath);
80+
copyStatic({staticPath, imagesPath, outputPath, recipesPath});
81+
makeThumbnails(outputPath, images);
82+
buildRecipes(recipeTemplate, options, markdownFiles, images);
83+
buildRecipeIndex(indexTemplate, options, markdownFiles, images);
5484

55-
// eslint-disable-next-line no-console
56-
console.log(`Processed ${fileList.length} recipes`);
85+
const endTime = new Date();
86+
console.log(`Processed ${markdownFiles.length} recipes in ${endTime - startTime}ms`);
87+
})
88+
.catch((err) => {
89+
console.error(err);
90+
});
5791
}
5892

5993
main(configs);

config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ module.exports = {
1818

1919
includeHelpLinks: false,
2020

21+
defaultTheme: 'default',
22+
23+
/**
24+
* Options are:
25+
* * 'content'
26+
* * 'compact-list'
27+
* * 'grid'
28+
*/
29+
initialIndexView: 'content',
2130
/**
2231
* help urls to include (will be listed in the order below)
2332
* label = text displayed

docs/images/index-view-modes.png

4.15 KB
Loading

docs/images/recipe-book-index.png

-27.7 KB
Binary file not shown.
72.4 KB
Loading

docs/images/recipe-styles.jpg

182 KB
Loading
Loading

images/pizza-dough.avif

224 KB
Binary file not shown.
Loading

0 commit comments

Comments
 (0)