poops 1.2.2 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -58
- package/lib/copy.js +4 -2
- package/lib/images.js +57 -0
- package/lib/markup/collections.js +38 -14
- package/lib/markup/engines/liquid.js +17 -28
- package/lib/markup/engines/nunjucks.js +22 -34
- package/lib/markup/helpers.js +180 -37
- package/lib/markup/image-cache.js +114 -0
- package/lib/markup/indexer.js +6 -2
- package/lib/markups.js +56 -11
- package/lib/postcss.js +16 -30
- package/lib/reactor.js +11 -29
- package/lib/scripts.js +37 -34
- package/lib/styles.js +17 -31
- package/lib/utils/helpers.js +20 -16
- package/lib/utils/log.js +15 -1
- package/lib/utils/minify.js +41 -0
- package/package.json +7 -3
- package/poops.js +114 -23
package/README.md
CHANGED
|
@@ -68,6 +68,22 @@ or pass a custom config. This is useful when you have multiple environments:
|
|
|
68
68
|
|
|
69
69
|
`poops yourAwesomeConfig.json` or `💩 yourAwesomeConfig.json`
|
|
70
70
|
|
|
71
|
+
**CLI Options:**
|
|
72
|
+
|
|
73
|
+
| Flag | Short | Description |
|
|
74
|
+
| ---------------------------- | ----- | ---------------------------------------------------- |
|
|
75
|
+
| `--build` | `-b` | Build the project and exit |
|
|
76
|
+
| `--config <path>` | `-c` | Specify the config file |
|
|
77
|
+
| `--port <number>` | `-p` | Specify the server port, overrides config |
|
|
78
|
+
| `--livereload-port <number>` | `-l` | Specify the livereload port, overrides config |
|
|
79
|
+
| `--base-url <path>` | `-u` | Set the base URL prefix for markup, overrides config |
|
|
80
|
+
|
|
81
|
+
The `--base-url` flag is particularly useful for CI/CD pipelines where the deploy path may differ per environment:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
poops --build --base-url /blog
|
|
85
|
+
```
|
|
86
|
+
|
|
71
87
|
If you have installed Poops locally you can run it with `npx poops` or `npx 💩` or add a script to your `package.json`:
|
|
72
88
|
|
|
73
89
|
```json
|
|
@@ -130,14 +146,8 @@ Just create a `poops.json` file in the root of your project and add the followin
|
|
|
130
146
|
"title": "Poops",
|
|
131
147
|
"description": "A super simple bundler for simple web projects."
|
|
132
148
|
},
|
|
133
|
-
"data": [
|
|
134
|
-
|
|
135
|
-
"data/poops.yaml"
|
|
136
|
-
],
|
|
137
|
-
"includePaths": [
|
|
138
|
-
"_layouts",
|
|
139
|
-
"_partials"
|
|
140
|
-
]
|
|
149
|
+
"data": ["data/links.json", "data/poops.yaml"],
|
|
150
|
+
"includePaths": ["_layouts", "_partials"]
|
|
141
151
|
},
|
|
142
152
|
"copy": [
|
|
143
153
|
{
|
|
@@ -359,13 +369,15 @@ Add `tokenPaths` to your styles config:
|
|
|
359
369
|
|
|
360
370
|
```json
|
|
361
371
|
{
|
|
362
|
-
"styles": [
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"
|
|
372
|
+
"styles": [
|
|
373
|
+
{
|
|
374
|
+
"in": "src/scss/index.scss",
|
|
375
|
+
"out": "dist/css/styles.css",
|
|
376
|
+
"options": {
|
|
377
|
+
"tokenPaths": ["src/tokens"]
|
|
378
|
+
}
|
|
367
379
|
}
|
|
368
|
-
|
|
380
|
+
]
|
|
369
381
|
}
|
|
370
382
|
```
|
|
371
383
|
|
|
@@ -443,9 +455,7 @@ You can also pass options to plugins using the tuple form:
|
|
|
443
455
|
"in": "src/css/main.css",
|
|
444
456
|
"out": "dist/css/main.css",
|
|
445
457
|
"options": {
|
|
446
|
-
"plugins": [
|
|
447
|
-
["autoprefixer", { "grid": true }]
|
|
448
|
-
]
|
|
458
|
+
"plugins": [["autoprefixer", { "grid": true }]]
|
|
449
459
|
}
|
|
450
460
|
}
|
|
451
461
|
}
|
|
@@ -522,13 +532,8 @@ Here is a sample markup configuration using the default Nunjucks engine:
|
|
|
522
532
|
"title": "My Awesome Site",
|
|
523
533
|
"description": "This is my awesome site"
|
|
524
534
|
},
|
|
525
|
-
"data": [
|
|
526
|
-
|
|
527
|
-
"data/other.yaml"
|
|
528
|
-
],
|
|
529
|
-
"includePaths": [
|
|
530
|
-
"_includes"
|
|
531
|
-
],
|
|
535
|
+
"data": ["data/links.json", "data/other.yaml"],
|
|
536
|
+
"includePaths": ["_includes"],
|
|
532
537
|
"baseURL": "/blog"
|
|
533
538
|
}
|
|
534
539
|
}
|
|
@@ -546,14 +551,8 @@ To use Liquid instead, set the `engine` property:
|
|
|
546
551
|
"title": "My Awesome Site",
|
|
547
552
|
"description": "This is my awesome site"
|
|
548
553
|
},
|
|
549
|
-
"data": [
|
|
550
|
-
|
|
551
|
-
"_data/other.yaml"
|
|
552
|
-
],
|
|
553
|
-
"includePaths": [
|
|
554
|
-
"_layouts",
|
|
555
|
-
"_partials"
|
|
556
|
-
]
|
|
554
|
+
"data": ["_data/links.json", "_data/other.yaml"],
|
|
555
|
+
"includePaths": ["_layouts", "_partials"]
|
|
557
556
|
}
|
|
558
557
|
}
|
|
559
558
|
```
|
|
@@ -564,17 +563,104 @@ If your project doesn't have markups, you can remove the `markup` property from
|
|
|
564
563
|
|
|
565
564
|
Both engines support the same feature set (collections, pagination, search index, sitemap, custom tags, and filters). The main differences are in template syntax:
|
|
566
565
|
|
|
567
|
-
| Feature
|
|
568
|
-
|
|
569
|
-
| File extension | `.njk`
|
|
570
|
-
| Inheritance
|
|
571
|
-
| Default values | `{{ x or "fallback" }}`
|
|
572
|
-
| Contains check | `{% if "x" in items %}`
|
|
573
|
-
| Safe output
|
|
574
|
-
| Includes
|
|
566
|
+
| Feature | Nunjucks | Liquid |
|
|
567
|
+
| -------------- | ----------------------------- | ------------------------------------- |
|
|
568
|
+
| File extension | `.njk` | `.liquid` |
|
|
569
|
+
| Inheritance | `{% extends "base.html" %}` | `{% layout "base.liquid" %}` |
|
|
570
|
+
| Default values | `{{ x or "fallback" }}` | `{{ x \| default: "fallback" }}` |
|
|
571
|
+
| Contains check | `{% if "x" in items %}` | `{% if items contains "x" %}` |
|
|
572
|
+
| Safe output | `{{ html \| safe }}` | `{{ html }}` (no escaping by default) |
|
|
573
|
+
| Includes | `{% include "partial.njk" %}` | `{% render "partial.liquid" %}` |
|
|
575
574
|
|
|
576
575
|
Both engines process `.html` and `.md` files in addition to their native extension.
|
|
577
576
|
|
|
577
|
+
#### Collections & Pagination
|
|
578
|
+
|
|
579
|
+
Collections turn a directory of pages into a sorted, optionally paginated list — blog posts, changelog entries, documentation. A collection maps to a direct subdirectory of your markup `in` directory: every `.html`, `.njk`, `.liquid` or `.md` file inside it (except the `index.*` file) becomes a collection item.
|
|
580
|
+
|
|
581
|
+
There are two ways to declare a collection:
|
|
582
|
+
|
|
583
|
+
**1. Front matter auto-discovery** — add `collection` to the front matter of the directory's index file:
|
|
584
|
+
|
|
585
|
+
```yaml
|
|
586
|
+
---
|
|
587
|
+
title: Changelog
|
|
588
|
+
collection: true
|
|
589
|
+
paginate: 10
|
|
590
|
+
sort: date
|
|
591
|
+
---
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
`collection: true` uses the directory name as the collection name; a string (e.g. `collection: changelog`) names it explicitly. `paginate` and `sort` are optional.
|
|
595
|
+
|
|
596
|
+
**2. Config** — list collections in the markup config. The name must match a subdirectory of `in`:
|
|
597
|
+
|
|
598
|
+
```json
|
|
599
|
+
{
|
|
600
|
+
"markup": {
|
|
601
|
+
"in": "src/markup",
|
|
602
|
+
"out": "dist",
|
|
603
|
+
"collections": [
|
|
604
|
+
"changelog",
|
|
605
|
+
{
|
|
606
|
+
"name": "blog",
|
|
607
|
+
"paginate": 5,
|
|
608
|
+
"sort": { "by": "title", "order": "asc" }
|
|
609
|
+
}
|
|
610
|
+
]
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
**Sorting.** By default items are sorted by `date`, newest first. `sort` can be a field name shorthand (`"sort": "title"`) or an object `{ "by": "field", "order": "asc" | "desc" }`. Sorting by `date` compares dates (default order `desc`); any other field compares alphabetically (default order `asc`).
|
|
616
|
+
|
|
617
|
+
**Items.** Each item exposes its own front matter plus properties Poops adds:
|
|
618
|
+
|
|
619
|
+
- `url` - the item's output path relative to the site root (e.g. `changelog/my-post.html`)
|
|
620
|
+
- `title` - falls back to the file name if not set in front matter
|
|
621
|
+
- `date` - falls back to the file's modification time if not set, with a build warning. Set a real `date` in front matter — mtime is meaningless on CI checkouts (git clone resets it), so undated posts will reshuffle between deploys.
|
|
622
|
+
- `wordcount`, `fileName`, `filePath`, `collection`
|
|
623
|
+
|
|
624
|
+
An item with `published: false` in its front matter is excluded from the collection and its page is not built.
|
|
625
|
+
|
|
626
|
+
**Using collections in templates.** Every collection is available as a global variable named after it, on every page:
|
|
627
|
+
|
|
628
|
+
```nunjucks
|
|
629
|
+
{% for post in changelog.items %}
|
|
630
|
+
<a href="{{ relativePathPrefix }}{{ post.url }}">{{ post.title }}</a> — {{ post.date | date }}
|
|
631
|
+
{% endfor %}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
**Pagination.** With `paginate: N` set, the collection's index file is rendered once per page of N items: page 1 to `out/changelog/index.html`, page 2 to `out/changelog/2/index.html`, and so on. Inside the index template the collection object carries the page state:
|
|
635
|
+
|
|
636
|
+
| Variable | Description |
|
|
637
|
+
| --------------------------- | ------------------------------------------------------- |
|
|
638
|
+
| `pageItems` | the items on the current page |
|
|
639
|
+
| `pageNumber` / `totalPages` | current page (1-based) / total page count |
|
|
640
|
+
| `pageUrl` | URL of the current page (`changelog`, `changelog/2`, …) |
|
|
641
|
+
| `nextPage` / `nextPageUrl` | next page number / URL, `null` on the last page |
|
|
642
|
+
| `prevPage` / `prevPageUrl` | previous page number / URL, `null` on the first page |
|
|
643
|
+
|
|
644
|
+
From the example site's `changelog/index.html`:
|
|
645
|
+
|
|
646
|
+
```nunjucks
|
|
647
|
+
{% for post in changelog.pageItems %}
|
|
648
|
+
<div class="post">
|
|
649
|
+
<h2><a href="{{ relativePathPrefix }}{{ post.url }}">{{ post.title }}</a></h2>
|
|
650
|
+
<div class="date">{{ post.date | date }}</div>
|
|
651
|
+
{{ post.description }}
|
|
652
|
+
</div>
|
|
653
|
+
{% endfor %}
|
|
654
|
+
|
|
655
|
+
{% if changelog.totalPages > 1 %}
|
|
656
|
+
{% if changelog.nextPageUrl %}<a href="{{ relativePathPrefix }}{{ changelog.nextPageUrl }}">Next</a>{% endif %}
|
|
657
|
+
{{ changelog.pageNumber }} of {{ changelog.totalPages }}
|
|
658
|
+
{% if changelog.prevPageUrl %}<a href="{{ relativePathPrefix }}{{ changelog.prevPageUrl }}">Previous</a>{% endif %}
|
|
659
|
+
{% endif %}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Item pages themselves are compiled like any other markup file, preserving the directory structure: `src/markup/changelog/my-post.md` → `dist/changelog/my-post.html`. A collection directory without an index file still builds its items and exposes the collection to templates — only the paginated listing pages are skipped.
|
|
663
|
+
|
|
578
664
|
#### Custom Tags
|
|
579
665
|
|
|
580
666
|
##### image
|
|
@@ -586,11 +672,13 @@ Poops can generate responsive `<img>` elements with `srcset` attributes. Image p
|
|
|
586
672
|
**`{% image %}` tag** — generates a full `<img>` element:
|
|
587
673
|
|
|
588
674
|
Nunjucks:
|
|
675
|
+
|
|
589
676
|
```nunjucks
|
|
590
677
|
{% image 'static/photo.jpg', alt='Hero', class='hero-img', sizes='(max-width: 640px) 100vw, 50vw' %}
|
|
591
678
|
```
|
|
592
679
|
|
|
593
680
|
Liquid:
|
|
681
|
+
|
|
594
682
|
```liquid
|
|
595
683
|
{% image 'static/photo.jpg', alt: 'Hero', class: 'hero-img', sizes: '(max-width: 640px) 100vw, 50vw' %}
|
|
596
684
|
```
|
|
@@ -619,21 +707,31 @@ Output:
|
|
|
619
707
|
- Defaults: `sizes="100vw"`, `loading="lazy"`
|
|
620
708
|
- Falls back to a plain `<img src="...">` if no variants are found
|
|
621
709
|
|
|
710
|
+
**[poops-images](https://github.com/stamat/poops-images) integration:** if a `.poops-images-cache.json` compile cache is found in the output directory (poops-images writes one next to the images it generates), Poops reads variants from it instead of scanning the directory. On top of the scan behavior above, the cache gives you:
|
|
711
|
+
|
|
712
|
+
- `width` and `height` attributes on the `<img>` element (exact dimensions from the cache — prevents layout shift). Pass your own `width`/`height` kwargs to override.
|
|
713
|
+
- Correct `src` when the source format was converted (e.g. `photo.heic` → `photo.jpg`), even when there are no size variants.
|
|
714
|
+
- Named sizes (`photo-thumb-200w.jpg`) and preprocessed outputs (`photo-blurred-640w.jpg`) are kept out of the srcset — they are crops and effects with their own aspect ratios. Only plain `{name}-{width}w.{ext}` variants (from the poops-images `widths` option) are used.
|
|
715
|
+
- EXIF metadata via the `exif` filter (see below).
|
|
716
|
+
|
|
622
717
|
##### googleFonts
|
|
623
718
|
|
|
624
719
|
Generates Google Fonts `<link>` tags with preconnect hints. Accepts an array of font names (strings) or font objects with weight/italic options.
|
|
625
720
|
|
|
626
721
|
Nunjucks (supports inline arrays):
|
|
722
|
+
|
|
627
723
|
```nunjucks
|
|
628
724
|
{% googleFonts ["Open Sans", "Roboto"] %}
|
|
629
725
|
```
|
|
630
726
|
|
|
631
727
|
Liquid (pass a variable — inline arrays are not supported in Liquid syntax):
|
|
728
|
+
|
|
632
729
|
```liquid
|
|
633
730
|
{% googleFonts fonts %}
|
|
634
731
|
```
|
|
635
732
|
|
|
636
733
|
Where `fonts` is defined in a data file (e.g. `fonts.json`):
|
|
734
|
+
|
|
637
735
|
```json
|
|
638
736
|
["Open Sans", "Roboto"]
|
|
639
737
|
```
|
|
@@ -658,7 +756,7 @@ With specific weights and italics (Nunjucks):
|
|
|
658
756
|
With specific weights and italics (Liquid — via data file):
|
|
659
757
|
|
|
660
758
|
```json
|
|
661
|
-
["DM Sans", {"name": "Poppins", "weights": [400, 700], "ital": true}]
|
|
759
|
+
["DM Sans", { "name": "Poppins", "weights": [400, 700], "ital": true }]
|
|
662
760
|
```
|
|
663
761
|
|
|
664
762
|
Font object options:
|
|
@@ -728,6 +826,22 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
728
826
|
- Nunjucks: `{{ someCodeVariable | highlight('javascript') }}`
|
|
729
827
|
- Liquid: `{{ someCodeVariable | highlight: 'javascript' }}`
|
|
730
828
|
|
|
829
|
+
- `groupby` — groups an array of objects by a field value. Returns an array of `{ key, items }` objects. Supports an optional second argument for date part extraction (`year`, `month`, `day`). Groups preserve insertion order, so if items are sorted by date descending, groups will be too.
|
|
830
|
+
- Nunjucks: `{{ changelog.items | groupby("author") }}` or `{{ changelog.items | groupby("date", "year") }}`
|
|
831
|
+
- Liquid: `{{ changelog.items | groupby: "author" }}` or `{{ changelog.items | groupby: "date", "year" }}`
|
|
832
|
+
|
|
833
|
+
Example — group posts by year:
|
|
834
|
+
|
|
835
|
+
```nunjucks
|
|
836
|
+
{% set byYear = changelog.items | groupby("date", "year") %}
|
|
837
|
+
{% for group in byYear %}
|
|
838
|
+
<h2>{{ group.key }}</h2>
|
|
839
|
+
{% for post in group.items %}
|
|
840
|
+
<p>{{ post.title }}</p>
|
|
841
|
+
{% endfor %}
|
|
842
|
+
{% endfor %}
|
|
843
|
+
```
|
|
844
|
+
|
|
731
845
|
- `srcset` — returns just the srcset attribute value:
|
|
732
846
|
|
|
733
847
|
```html
|
|
@@ -741,6 +855,69 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
741
855
|
|
|
742
856
|
Returns: `static/photo-320w.webp 320w, static/photo-640w.webp 640w, static/photo-960w.webp 960w`
|
|
743
857
|
|
|
858
|
+
- `exif` — returns the EXIF metadata object for an image from the [poops-images](https://github.com/stamat/poops-images) compile cache (`.poops-images-cache.json` in the output directory), or `null` if there is no cache or no EXIF data. The object includes camera (`make`, `model`, `lensModel`), exposure (`fNumber`, `exposure.formatted`, `iso`, `focalLength35mm`), `dateTime`, and `gps` (`latitude.formatted`, `longitude.formatted`, `altitude`, and a ready-made `googleMapsUrl`).
|
|
859
|
+
|
|
860
|
+
Example — a photo with date and location caption:
|
|
861
|
+
|
|
862
|
+
```nunjucks
|
|
863
|
+
{% set meta = 'static/photo.jpeg' | exif %}
|
|
864
|
+
<figure>
|
|
865
|
+
{% image 'static/photo.jpeg', alt='Sendai at dusk' %}
|
|
866
|
+
{% if meta %}
|
|
867
|
+
<figcaption>
|
|
868
|
+
{{ meta.dateTime | date("MMMM D, YYYY") }}
|
|
869
|
+
{% if meta.gps %}
|
|
870
|
+
— <a href="{{ meta.gps.googleMapsUrl }}">{{ meta.gps.latitude.formatted }}, {{ meta.gps.longitude.formatted }}</a>
|
|
871
|
+
{% endif %}
|
|
872
|
+
{% if meta.model %}· {{ meta.model }}{% endif %}
|
|
873
|
+
</figcaption>
|
|
874
|
+
{% endif %}
|
|
875
|
+
</figure>
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
- `images` — lists all images under a site-relative directory from the [poops-images](https://github.com/stamat/poops-images) compile cache. Returns an array of `{ path, width, height, date, exif, outputs }` objects, or an empty array if there is no cache:
|
|
879
|
+
- `path` — site-relative source path, feeds straight into the `{% image %}` tag
|
|
880
|
+
- `date` — `exif.dateTime` when the photo has EXIF, file modification time otherwise — so sorting and grouping work for every image
|
|
881
|
+
- `outputs` — every generated file for the image (site-relative), useful for picking LQIP or preprocessed variants
|
|
882
|
+
- Pass a subdirectory (`'images/2025'`) to scope the list
|
|
883
|
+
|
|
884
|
+
Combined with `groupby`, engine-native sorting and the `{% image %}` tag, a photo gallery is a pure template concern:
|
|
885
|
+
|
|
886
|
+
Nunjucks:
|
|
887
|
+
|
|
888
|
+
```nunjucks
|
|
889
|
+
{% for group in 'images' | images | sort(reverse=true, attribute='date') | groupby("date", "year") %}
|
|
890
|
+
<h2>{{ group.key }}</h2>
|
|
891
|
+
<div class="grid">
|
|
892
|
+
{% for img in group.items %}
|
|
893
|
+
<figure>
|
|
894
|
+
{% image img.path, alt='', sizes='(max-width: 640px) 50vw, 25vw' %}
|
|
895
|
+
{% if img.exif and img.exif.gps %}
|
|
896
|
+
<figcaption>
|
|
897
|
+
<a href="{{ img.exif.gps.googleMapsUrl }}">📍</a> {{ img.date | date("MMM D, YYYY") }}
|
|
898
|
+
</figcaption>
|
|
899
|
+
{% endif %}
|
|
900
|
+
</figure>
|
|
901
|
+
{% endfor %}
|
|
902
|
+
</div>
|
|
903
|
+
{% endfor %}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
Liquid:
|
|
907
|
+
|
|
908
|
+
```liquid
|
|
909
|
+
{% assign imgs = 'images' | images | sort: 'date' | reverse %}
|
|
910
|
+
{% assign groups = imgs | groupby: "date", "year" %}
|
|
911
|
+
{% for group in groups %}
|
|
912
|
+
<h2>{{ group.key }}</h2>
|
|
913
|
+
<div class="grid">
|
|
914
|
+
{% for img in group.items %}
|
|
915
|
+
<figure>{% image img.path, alt: '' %}</figure>
|
|
916
|
+
{% endfor %}
|
|
917
|
+
</div>
|
|
918
|
+
{% endfor %}
|
|
919
|
+
```
|
|
920
|
+
|
|
744
921
|
#### Search Index & Sitemap
|
|
745
922
|
|
|
746
923
|
Poops can automatically generate a JSON search index and/or an XML sitemap from your compiled pages. Both are generated in a single pass during the markup compilation phase.
|
|
@@ -811,6 +988,39 @@ All front matter fields are passed through to the index automatically. Internal
|
|
|
811
988
|
|
|
812
989
|
Pages with `published: false` in their front matter are excluded from both outputs.
|
|
813
990
|
|
|
991
|
+
### Images (optional)
|
|
992
|
+
|
|
993
|
+
Process and optimize images — compression, responsive size variants, format conversion (WebP/AVIF), crops and EXIF extraction — by running [poops-images](https://github.com/stamat/poops-images) as part of the build. This is what feeds the `{% image %}` tag, the `exif`/`images` filters and the `.poops-images-cache.json` compile cache described in [Custom Tags](#custom-tags) and [Custom Filters](#custom-filters).
|
|
994
|
+
|
|
995
|
+
poops-images (and its `sharp` dependency) is **not** bundled with Poops. Install it in your project only if you use the `images` config:
|
|
996
|
+
|
|
997
|
+
```bash
|
|
998
|
+
npm i poops-images
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
If the `images` key is present but poops-images is not installed, Poops logs a warning and skips image processing — the rest of the build still runs.
|
|
1002
|
+
|
|
1003
|
+
The `images` value is a poops-images config object (see the [poops-images options reference](https://github.com/stamat/poops-images#configuration)). The most common keys:
|
|
1004
|
+
|
|
1005
|
+
- `in` — source images directory
|
|
1006
|
+
- `out` — output directory (keep it distinct from `in`, and outside your watched source, so generated variants don't retrigger the build)
|
|
1007
|
+
- `sizes` — responsive widths to generate
|
|
1008
|
+
- `format` — target formats (e.g. `["webp"]`, or `"smart"` to keep whichever of JPEG/WebP is smaller)
|
|
1009
|
+
- `verbose` - defaults to `false`, so you get a single `[image]` summary line (count + time) instead of one log per file. Set `"verbose": true` to restore the per-file logs.
|
|
1010
|
+
|
|
1011
|
+
```json
|
|
1012
|
+
{
|
|
1013
|
+
"images": {
|
|
1014
|
+
"in": "src/images",
|
|
1015
|
+
"out": "dist/images",
|
|
1016
|
+
"sizes": [{ "width": 640 }, { "width": 1280 }],
|
|
1017
|
+
"format": "smart"
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
Images are processed **before** markup, so `{% image %}` and the `images` filter always read a fresh cache. In watch mode, changing a source image reprocesses it and recompiles markup; deleting one removes its generated variants and updates the galleries that referenced it. Custom handlers and composite overlays resolve relative to your `poops.json`.
|
|
1023
|
+
|
|
814
1024
|
### Copy
|
|
815
1025
|
|
|
816
1026
|
Configuration entry to copy files or directories - copy your static files like images and fonts, for instance, from `src` to `dist` directory. This feature was added to enable moving static files if you deploy GitHub pages via a GitHub action. If you don't want to use this feature, simply exclude the `copy` property from your config file.
|
|
@@ -914,6 +1124,18 @@ Live reload options:
|
|
|
914
1124
|
|
|
915
1125
|
- `port` - the port on which the livereload server will run
|
|
916
1126
|
- `exclude` - an array of files and directories to exclude from livereload
|
|
1127
|
+
- `extraExts` - an array of extra file extensions (without the dot) that trigger a browser refresh, added to the defaults
|
|
1128
|
+
- `exts` - an array of file extensions that replaces the default list entirely
|
|
1129
|
+
|
|
1130
|
+
By default a refresh is triggered by changes to: `html`, `css`, `js`, `png`, `gif`, `jpg`, `php`, `php5`, `py`, `rb`, `erb`, `coffee`. If you work with other file types, for example Slim or Nunjucks templates, add them:
|
|
1131
|
+
|
|
1132
|
+
```json
|
|
1133
|
+
{
|
|
1134
|
+
"livereload": {
|
|
1135
|
+
"extraExts": ["slim", "njk"]
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
```
|
|
917
1139
|
|
|
918
1140
|
`livereload` can only be `true`, which means that it will run on the default port (`35729`) or you can specify a port:
|
|
919
1141
|
|
|
@@ -980,23 +1202,6 @@ Same as `watch` property, `includePaths` accepts an array of paths to include. I
|
|
|
980
1202
|
}
|
|
981
1203
|
```
|
|
982
1204
|
|
|
983
|
-
## Todo
|
|
984
|
-
|
|
985
|
-
- [ ] Run esbuild for each input path individually if there are multiple input paths
|
|
986
|
-
- [ ] Styles `in` should be able to support array of inputs like we have it on scripts
|
|
987
|
-
- [ ] Build a cli config creation helper tool. If the user doesn't have a config file, we can ask them a few questions and create a config file for them. Create Yeoman generator for poops projects.
|
|
988
|
-
- [x] Add nunjucks static templating
|
|
989
|
-
- [x] Refactor nunjucks implementation
|
|
990
|
-
- [x] Complete documentation for nunjucks
|
|
991
|
-
- [x] Add markdown support
|
|
992
|
-
- [x] Front Matter support
|
|
993
|
-
- [x] Future implementation: posts and custom collections, so we can have a real static site generator
|
|
994
|
-
- [x] Collection pagination system
|
|
995
|
-
- [x] Post published toggle
|
|
996
|
-
- [x] RSS and ATOM generation for collections
|
|
997
|
-
- [x] Support for images and creating srcsets
|
|
998
|
-
- [x] Add Liquid template engine as a swappable alternative to Nunjucks
|
|
999
|
-
|
|
1000
1205
|
## Why?
|
|
1001
1206
|
|
|
1002
1207
|
Why doesn't anyone maintain GULP anymore? Why does Parcel hate config files? Why are Rollup and Webpack so complex to setup for simple tasks? Vite???? What's going on?
|
package/lib/copy.js
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
pathIsDirectory,
|
|
5
5
|
mkDir,
|
|
6
6
|
copyDirectory,
|
|
7
|
-
buildTime
|
|
7
|
+
buildTime,
|
|
8
|
+
toPosix
|
|
8
9
|
} from './utils/helpers.js'
|
|
9
10
|
import fs from 'node:fs'
|
|
10
11
|
import path from 'node:path'
|
|
@@ -91,7 +92,8 @@ export default class Copy {
|
|
|
91
92
|
copyPaths.out = path.join(copyPaths.out, inBaseName)
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
// Watcher paths arrive with native separators, config `in` uses `/`
|
|
96
|
+
file = toPosix(file).replace(copyPaths.in, copyPaths.out)
|
|
95
97
|
|
|
96
98
|
const outputFilePath = path.join(process.cwd(), file)
|
|
97
99
|
|
package/lib/images.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import log from './utils/log.js'
|
|
2
|
+
|
|
3
|
+
// Runs poops-images (https://github.com/stamat/poops-images) as a regular
|
|
4
|
+
// runner when a `config.images` block is present AND the package is installed.
|
|
5
|
+
// poops-images stays an optional peer dependency — sharp never becomes a hard
|
|
6
|
+
// dep of poops. The markup engines read the cache poops-images writes, so this
|
|
7
|
+
// runner must execute before markups.compile().
|
|
8
|
+
export default class Images {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config
|
|
11
|
+
this.processor = null // lazily created on first compile()
|
|
12
|
+
this.disabled = false // no images config, or poops-images not installed
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async init() {
|
|
16
|
+
if (this.processor || this.disabled) return
|
|
17
|
+
if (!this.config.images) { this.disabled = true; return }
|
|
18
|
+
|
|
19
|
+
let ImageProcessor
|
|
20
|
+
try {
|
|
21
|
+
ImageProcessor = (await import('poops-images')).default
|
|
22
|
+
} catch (err) {
|
|
23
|
+
// Native ESM throws ERR_MODULE_NOT_FOUND; CJS-style/jest resolvers throw
|
|
24
|
+
// MODULE_NOT_FOUND. Either means the optional dep simply isn't installed.
|
|
25
|
+
if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'MODULE_NOT_FOUND') {
|
|
26
|
+
log({ tag: 'image', warn: true, text: 'images config found but poops-images is not installed — run: npm i poops-images' })
|
|
27
|
+
this.disabled = true
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
throw err
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Quiet by default inside poops — one summary line instead of per-image
|
|
34
|
+
// logs drowning the other runners. Opt back in with "verbose": true.
|
|
35
|
+
// A bad images config throws here, propagating to the build's step() so
|
|
36
|
+
// the build is marked failed rather than silently skipping images.
|
|
37
|
+
this.processor = new ImageProcessor({ verbose: false, ...this.config.images })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async compile() {
|
|
41
|
+
await this.init()
|
|
42
|
+
if (!this.processor) return
|
|
43
|
+
const stats = await this.processor.processAll()
|
|
44
|
+
// Route through poops' log so hasLoggedErrors() flips the build exit code.
|
|
45
|
+
// Older poops-images without the errors field: undefined is falsy, no-op.
|
|
46
|
+
if (stats.errors > 0) {
|
|
47
|
+
log({ tag: 'image', error: true, text: `${stats.errors} image(s) failed to process` })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Watch mode: a deleted source image removes its generated variants + cache entry.
|
|
52
|
+
async remove(file) {
|
|
53
|
+
await this.init()
|
|
54
|
+
if (!this.processor) return
|
|
55
|
+
this.processor.removeSource(file)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -2,17 +2,20 @@ import fs from 'node:fs'
|
|
|
2
2
|
import { globSync } from 'glob'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import log from '../utils/log.js'
|
|
5
|
-
import { mkDir } from '../utils/helpers.js'
|
|
6
|
-
import { replaceOutExtensions, getRelativePathPrefix, getPageUrl, parseFrontMatter } from './helpers.js'
|
|
5
|
+
import { mkDir, toPosix } from '../utils/helpers.js'
|
|
6
|
+
import { replaceOutExtensions, getRelativePathPrefix, getPageUrl, parseFrontMatter, wordcount } from './helpers.js'
|
|
7
7
|
|
|
8
8
|
export function getSingleCollectionData(markupInDir, collectionName) {
|
|
9
9
|
const collectionData = []
|
|
10
|
-
|
|
10
|
+
// glob patterns must use `/` — on Windows `\` is an escape character
|
|
11
|
+
globSync(toPosix(path.join(process.cwd(), markupInDir, collectionName, '**/*.+(html|njk|liquid|md)')), { ignore: ['**/index.+(html|njk|liquid|md)'] }).forEach((file) => {
|
|
11
12
|
let frontMatter = {}
|
|
12
13
|
|
|
14
|
+
let content = ''
|
|
13
15
|
try {
|
|
14
16
|
const frontMatterResult = parseFrontMatter(file)
|
|
15
17
|
frontMatter = frontMatterResult.frontMatter
|
|
18
|
+
content = frontMatterResult.content
|
|
16
19
|
} catch (err) {
|
|
17
20
|
log({ tag: 'error', text: 'Failed parsing front matter:', link: file })
|
|
18
21
|
console.error(err)
|
|
@@ -21,12 +24,16 @@ export function getSingleCollectionData(markupInDir, collectionName) {
|
|
|
21
24
|
if (frontMatter.published === false) return
|
|
22
25
|
|
|
23
26
|
if (!frontMatter.date) {
|
|
24
|
-
|
|
27
|
+
// mtime is only a local-dev approximation — git clone resets it, so CI
|
|
28
|
+
// builds date undated posts "now". The warning is the real fix.
|
|
29
|
+
frontMatter.date = fs.statSync(file).mtime.toISOString().slice(0, 16)
|
|
30
|
+
log({ tag: 'markup', warn: true, text: 'No date in front matter, falling back to file mtime:', link: file })
|
|
25
31
|
}
|
|
32
|
+
frontMatter.wordcount = wordcount(content)
|
|
26
33
|
frontMatter.fileName = path.basename(file)
|
|
27
34
|
frontMatter.filePath = path.relative(process.cwd(), file)
|
|
28
35
|
frontMatter.collection = collectionName
|
|
29
|
-
frontMatter.url = path.join(collectionName, path.basename(frontMatter.filePath))
|
|
36
|
+
frontMatter.url = toPosix(path.join(collectionName, path.basename(frontMatter.filePath)))
|
|
30
37
|
|
|
31
38
|
frontMatter.url = replaceOutExtensions(frontMatter.url)
|
|
32
39
|
|
|
@@ -40,7 +47,7 @@ export function getSingleCollectionData(markupInDir, collectionName) {
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export function collectionAutoDiscovery(markupInDir) {
|
|
43
|
-
const indexFiles = globSync(path.join(process.cwd(), markupInDir, '/**/index.+(html|njk|liquid|md)'))
|
|
50
|
+
const indexFiles = globSync(toPosix(path.join(process.cwd(), markupInDir, '/**/index.+(html|njk|liquid|md)')))
|
|
44
51
|
|
|
45
52
|
const collectionData = {}
|
|
46
53
|
|
|
@@ -177,11 +184,25 @@ export function buildCollectionPaginationData(collectionData) {
|
|
|
177
184
|
}
|
|
178
185
|
|
|
179
186
|
export function getCollectionIndexFile(markupInDir, collectionName) {
|
|
180
|
-
const indexFiles = globSync(path.join(process.cwd(), markupInDir, collectionName, 'index.+(html|njk|liquid|md)'))
|
|
187
|
+
const indexFiles = globSync(toPosix(path.join(process.cwd(), markupInDir, collectionName, 'index.+(html|njk|liquid|md)')))
|
|
181
188
|
if (indexFiles.length === 0) return null
|
|
182
189
|
return indexFiles[0]
|
|
183
190
|
}
|
|
184
191
|
|
|
192
|
+
export function pruneStalePaginationDirs(collectionName, markupInDir, markupOutDir, keepPages) {
|
|
193
|
+
const outDir = path.join(process.cwd(), markupOutDir, collectionName)
|
|
194
|
+
if (!fs.existsSync(outDir)) return
|
|
195
|
+
|
|
196
|
+
for (const entry of fs.readdirSync(outDir)) {
|
|
197
|
+
const pageNumber = parseInt(entry, 10)
|
|
198
|
+
// pagination only ever writes out/<name>/2..totalPages/
|
|
199
|
+
if (String(pageNumber) !== entry || pageNumber < 2 || pageNumber <= keepPages) continue
|
|
200
|
+
// numeric dir mirrored from a real source dir — not pagination output
|
|
201
|
+
if (fs.existsSync(path.join(process.cwd(), markupInDir, collectionName, entry))) continue
|
|
202
|
+
fs.rmSync(path.join(outDir, entry), { recursive: true, force: true })
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
185
206
|
export function generateCollectionPaginationPages(collectionData, markupInDir, markupOutDir, compileEntryFn, baseURL) {
|
|
186
207
|
if (!collectionData) return []
|
|
187
208
|
|
|
@@ -196,6 +217,11 @@ export function generateCollectionPaginationPages(collectionData, markupInDir, m
|
|
|
196
217
|
collection.pages = [collection.items]
|
|
197
218
|
}
|
|
198
219
|
|
|
220
|
+
// a shrunk page count (or removed index) leaves stale out/<name>/N/ dirs
|
|
221
|
+
pruneStalePaginationDirs(collection.name, markupInDir, markupOutDir, file ? collection.totalPages : 1)
|
|
222
|
+
|
|
223
|
+
if (!file) continue
|
|
224
|
+
|
|
199
225
|
for (let i = 0; i < collection.totalPages; i++) {
|
|
200
226
|
const pageNumber = i + 1
|
|
201
227
|
const pageUrl = pageNumber === 1 ? collection.name : `${collection.name}/${pageNumber}`
|
|
@@ -223,8 +249,6 @@ export function generateCollectionPaginationPages(collectionData, markupInDir, m
|
|
|
223
249
|
const fromPath = path.join(process.cwd(), markupOutDir)
|
|
224
250
|
const markupOutDirFull = path.dirname(markupOut)
|
|
225
251
|
|
|
226
|
-
mkDir(markupOutDirFull)
|
|
227
|
-
|
|
228
252
|
const context = {
|
|
229
253
|
...collectionData,
|
|
230
254
|
[collectionName]: pageSnapshot,
|
|
@@ -232,11 +256,11 @@ export function generateCollectionPaginationPages(collectionData, markupInDir, m
|
|
|
232
256
|
_url: getPageUrl(markupOut)
|
|
233
257
|
}
|
|
234
258
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
259
|
+
// mkDir only when a page is actually written: no empty pagination dirs
|
|
260
|
+
// for index-less or unpublished (skipped) collection indexes
|
|
261
|
+
const compilePromise = compileEntryFn(file, context).then(({ result, skipped }) => {
|
|
262
|
+
if (skipped) return
|
|
263
|
+
mkDir(markupOutDirFull)
|
|
240
264
|
fs.writeFileSync(markupOut, result)
|
|
241
265
|
})
|
|
242
266
|
compilePromises.push(compilePromise)
|