poops 1.5.3 → 1.7.0
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 +191 -9
- package/lib/exec.js +44 -0
- package/lib/markup/collections.js +240 -42
- package/lib/markup/engines/liquid.js +9 -3
- package/lib/markup/engines/nunjucks.js +9 -3
- package/lib/markup/helpers.js +314 -7
- package/lib/markup/indexer.js +371 -8
- package/lib/markups.js +57 -7
- package/package.json +2 -2
- package/poops.js +22 -9
package/README.md
CHANGED
|
@@ -40,12 +40,13 @@ It uses a simple config file where you define your input and output paths and it
|
|
|
40
40
|
- [Nunjucks vs Liquid](#nunjucks-vs-liquid)
|
|
41
41
|
- [Custom Engines](#custom-engines)
|
|
42
42
|
- [Collections & Pagination](#collections--pagination)
|
|
43
|
+
- [Taxonomies (Tags & Categories)](#taxonomies-tags--categories)
|
|
43
44
|
- [Custom Tags](#custom-tags)
|
|
44
45
|
- [image](#image)
|
|
45
46
|
- [googleFonts](#googlefonts)
|
|
46
47
|
- [highlight](#highlight)
|
|
47
48
|
- [Custom Filters](#custom-filters)
|
|
48
|
-
- [Search Index, Sitemap & Navigation](#search-index-sitemap--navigation)
|
|
49
|
+
- [Search Index, Sitemap, llms.txt, robots.txt & Navigation](#search-index-sitemap-llmstxt-robotstxt--navigation)
|
|
49
50
|
- [Images (optional)](#images-optional)
|
|
50
51
|
- [Copy](#copy)
|
|
51
52
|
- [Banner (optional)](#banner-optional)
|
|
@@ -71,6 +72,7 @@ It uses a simple config file where you define your input and output paths and it
|
|
|
71
72
|
- Resolves node modules
|
|
72
73
|
- Can add a templatable banner to output files (optional)
|
|
73
74
|
- Static site generation with swappable template engines: [Nunjucks](https://mozilla.github.io/nunjucks/) (default) or [Liquid](https://liquidjs.com/) — with blogging option (optional)
|
|
75
|
+
- Collections with pagination, and taxonomies — tags/categories as paginated, crawlable landing pages (with localizable labels)
|
|
74
76
|
- Has a configurable local server (optional)
|
|
75
77
|
- Rebuilds on file changes (optional)
|
|
76
78
|
- Live reloads on file changes (optional)
|
|
@@ -703,7 +705,7 @@ sort: date
|
|
|
703
705
|
- `url` - the item's output path relative to the site root (e.g. `changelog/my-post.html`)
|
|
704
706
|
- `title` - falls back to the file name if not set in front matter
|
|
705
707
|
- `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.
|
|
706
|
-
- `wordcount`, `fileName`, `filePath`, `collection`
|
|
708
|
+
- `wordcount`, `excerpt` (first paragraph, plain text — a meta-description fallback), `fileName`, `filePath`, `collection`
|
|
707
709
|
|
|
708
710
|
An item with `published: false` in its front matter is excluded from the collection and its page is not built.
|
|
709
711
|
|
|
@@ -749,8 +751,73 @@ Or use the `{% pagination %}` shorthand tag (available in both engines), which r
|
|
|
749
751
|
{% pagination changelog %}
|
|
750
752
|
```
|
|
751
753
|
|
|
754
|
+
Pages 2..N automatically get a distinct `<title>` — `Changelog — Page 2` — so paginated pages don't all share the landing page's title (and its `og`/`jsonld` metadata). Page 1 keeps its own title.
|
|
755
|
+
|
|
756
|
+
**Localizing the labels.** The `— Page N` title suffix and the `{% pagination %}` tag's `Previous`/`Next`/`of` wording default to English. Override them site-wide under `site.pagination`:
|
|
757
|
+
|
|
758
|
+
```json
|
|
759
|
+
{
|
|
760
|
+
"markup": {
|
|
761
|
+
"site": {
|
|
762
|
+
"pagination": {
|
|
763
|
+
"title": "{title} — Seite {n}",
|
|
764
|
+
"prev": "Zurück",
|
|
765
|
+
"next": "Weiter",
|
|
766
|
+
"of": "von"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
`title` accepts `{title}`, `{n}` and `{total}` tokens and applies to pages 2..N (and taxonomy term pages); `prev`/`next`/`of` localize the `{% pagination %}` tag (`{n} of {total}` → `{n} von {total}`).
|
|
774
|
+
|
|
752
775
|
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.
|
|
753
776
|
|
|
777
|
+
#### Taxonomies (Tags & Categories)
|
|
778
|
+
|
|
779
|
+
A **taxonomy** turns a front-matter field (tags, categories, authors) into its own paginated, crawlable landing page per term — `changelog/tag/feature/`, `blog/category/release/`. Declare which fields become taxonomies on the collection, alongside `paginate`/`sort` — either in the index front matter or the config entry:
|
|
780
|
+
|
|
781
|
+
```yaml
|
|
782
|
+
---
|
|
783
|
+
title: Changelog
|
|
784
|
+
collection: true
|
|
785
|
+
paginate: 10
|
|
786
|
+
taxonomies:
|
|
787
|
+
- name: tags # front-matter field to group on
|
|
788
|
+
path: tag # URL segment (defaults to name); "tag" for a singular URL
|
|
789
|
+
paginate: 5 # per-term page size (defaults to the collection's paginate)
|
|
790
|
+
---
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
Shorthand: a bare string list (`taxonomies: [tags, category]`) uses each field name as the URL segment and inherits the collection's `paginate`. Array-valued fields split per element — a post with `tags: [js, css]` lands under **both** `tag/js/` and `tag/css/`. Terms are slugified for the URL (`Static Site` → `static-site`).
|
|
794
|
+
|
|
795
|
+
Term pages render with the **collection's own index template** — no extra file. On a term page the collection object carries the term context; branch on `activeTerm` to render a term view:
|
|
796
|
+
|
|
797
|
+
```nunjucks
|
|
798
|
+
{% if changelog.activeTerm %}
|
|
799
|
+
<h1>Tagged {{ changelog.activeTerm | humanize }}</h1>
|
|
800
|
+
{% for post in changelog.pageItems %}
|
|
801
|
+
<a href="{{ relativePathPrefix }}{{ post.url }}">{{ post.title }}</a>
|
|
802
|
+
{% endfor %}
|
|
803
|
+
{% pagination changelog %}
|
|
804
|
+
{% endif %}
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
On a term page `items`/`pageItems` are scoped to that term (so `pagination` and `groupby` narrow to it too); `activeTaxonomy` holds the URL segment and `activeTermSlug` the slug. Build tag links anywhere from `collection.taxonomies`:
|
|
808
|
+
|
|
809
|
+
```nunjucks
|
|
810
|
+
{% for tax in changelog.taxonomies %}
|
|
811
|
+
{% for term in tax.terms %}
|
|
812
|
+
<a href="{{ relativePathPrefix }}{{ term.url }}">{{ term.term | humanize }} ({{ term.count }})</a>
|
|
813
|
+
{% endfor %}
|
|
814
|
+
{% endfor %}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
Each term exposes `term`, `slug`, `url`, `count` and `totalPages`.
|
|
818
|
+
|
|
819
|
+
Term pages get a distinct `<title>` and `og`/`jsonld` metadata (`Tag: Feature`, paged `Tag: Feature — Page 2`), and the `breadcrumb`/`jsonld` filters resolve them to a **Home › Collection › Tag: Term** trail automatically (skipping the non-page `tag`/`category` URL segment). The `Tag:`/`Category:` label comes from `path`, so it localizes by naming the path in your language (`path: etiqueta` → `Etiqueta: …`). Term pages are listed in the sitemap but kept out of the search index and nav.
|
|
820
|
+
|
|
754
821
|
#### Custom Tags
|
|
755
822
|
|
|
756
823
|
##### image
|
|
@@ -908,6 +975,8 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
908
975
|
|
|
909
976
|
- `slugify` — slugifies a string. Usage: `{{ "My Awesome Title" | slugify }}` will output `my-awesome-title`
|
|
910
977
|
|
|
978
|
+
- `humanize` — the inverse of `slugify`: turns a slug or raw term into a display label. Usage: `{{ "static-site" | humanize }}` will output `Static Site`
|
|
979
|
+
|
|
911
980
|
- `jsonify` — serializes a value to JSON. Usage: `{{ myObject | jsonify }}`
|
|
912
981
|
|
|
913
982
|
- `markdown` — renders a markdown string to HTML with GitHub Flavored Markdown extras: emoji shortcodes (e.g. `:rocket:` → 🚀), alert callouts (`> [!NOTE]`, `[!TIP]`, `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`, `[!INFO]`) and footnotes (`[^1]`). Code fences are syntax-highlighted and headings get slug `id`s plus permalink anchors. Usage: `{{ "**bold** :rocket:" | markdown }}`
|
|
@@ -930,7 +999,55 @@ All filters are available in both engines. The only syntax difference is how arg
|
|
|
930
999
|
- Nunjucks: `{{ someCodeVariable | highlight('javascript') }}`
|
|
931
1000
|
- Liquid: `{{ someCodeVariable | highlight: 'javascript' }}`
|
|
932
1001
|
|
|
933
|
-
- `
|
|
1002
|
+
- `og` — generates Open Graph (and a Twitter card) `<meta>` tags from a page's front matter and your `site` data, for link previews on social/chat platforms. Put it in your layout `<head>`. `og:type` auto-detects: `article` when the page has a `date`, otherwise `website`.
|
|
1003
|
+
- Nunjucks: `{{ page | og(site) }}`
|
|
1004
|
+
- Liquid: `{{ page | og: site }}`
|
|
1005
|
+
|
|
1006
|
+
Emits `og:title`, `og:description` (a missing `description` falls back to the page's auto-`excerpt`, then `site.description`), `og:type`, `og:url` (made absolute with `site.url`), `og:site_name` (from `site.title`), `og:locale` (`page.lang`/`site.lang`), `og:image` (`page.image`/`site.image`, made absolute), and `twitter:card` (`summary_large_image` when there's an image, else `summary`). For articles it adds `article:published_time`, `article:modified_time` and `article:author`. Attribute values are escaped. Set an `og` object in front matter to add or override any tag (e.g. `og:image:alt`, a fixed `twitter:card`):
|
|
1007
|
+
|
|
1008
|
+
```yaml
|
|
1009
|
+
---
|
|
1010
|
+
title: My post
|
|
1011
|
+
date: 2026-01-01
|
|
1012
|
+
image: static/cover.jpg
|
|
1013
|
+
og:
|
|
1014
|
+
"og:image:alt": Cover illustration
|
|
1015
|
+
---
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
- `canonical` — generates a `<link rel="canonical">` tag pointing at a page's authoritative absolute URL (`site.url` + the page's `url`), the dedup signal that stops query-string and duplicate URLs splitting your ranking. Put it in your layout `<head>`. Front matter `canonical` overrides the target — an absolute URL as-is, or a path resolved against `site.url` (for cross-domain or hand-picked canonicals). The homepage canonicals to the site root. Returns nothing without `site.url`.
|
|
1019
|
+
- Nunjucks: `{{ page | canonical(site) }}`
|
|
1020
|
+
- Liquid: `{{ page | canonical: site }}`
|
|
1021
|
+
|
|
1022
|
+
- `jsonld` — generates a schema.org JSON-LD `<script type="application/ld+json">` block from a page's front matter and your `site` data, for GEO (Generative Engine Optimization) and structured data. Put it in your layout `<head>`. The `@type` auto-detects: `BlogPosting` when the page has a `date`, otherwise `WebPage`.
|
|
1023
|
+
- Nunjucks: `{{ page | jsonld(site) }}`
|
|
1024
|
+
- Liquid: `{{ page | jsonld: site }}`
|
|
1025
|
+
|
|
1026
|
+
It reads these front-matter fields when present: `title`, `description` (falls back to the page's auto-`excerpt`, then `site.description`), `url` (made absolute with `site.url`), `date` → `datePublished`, `updated` → `dateModified`, `author` (string or `{ name }`, falls back to `site.author`), `image`, `lang` → `inLanguage`, and `wordcount`. `publisher` comes from `site.title`; set `site.logo` to add a `publisher.logo` ImageObject (made absolute) — Google Article rich results require it. Front-matter values are escaped so they can't break out of the `<script>` tag.
|
|
1027
|
+
|
|
1028
|
+
On the homepage (a page with no `url`) it also emits a site-level `WebSite` block with `name` + `url`, which declares the site name for search results. On nested pages (a `url` with at least one folder) it auto-appends a `BreadcrumbList` block derived from URL depth — a Google breadcrumb rich result, no extra markup (needs `site.url` for the absolute item URLs). See the `breadcrumb` filter below for a visible trail from the same data.
|
|
1029
|
+
|
|
1030
|
+
For full control, set a `jsonld` object in front matter — its keys are merged over (and override) the generated defaults, including `@type`:
|
|
1031
|
+
|
|
1032
|
+
```yaml
|
|
1033
|
+
---
|
|
1034
|
+
title: How to brew coffee
|
|
1035
|
+
date: 2026-01-01
|
|
1036
|
+
jsonld:
|
|
1037
|
+
"@type": HowTo
|
|
1038
|
+
totalTime: PT5M
|
|
1039
|
+
---
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
poops auto-picks `BlogPosting` (page has a `date`) or `WebPage`. Override `@type` with the `jsonld` object for any schema.org type — common ones search/generative engines act on: `Article`, `NewsArticle`, `HowTo`, `FAQPage`, `QAPage`, `Product`, `Recipe`, `Event`, `Course`, `VideoObject`, `SoftwareApplication`, `Organization`, `Person`, `BreadcrumbList`, `WebSite`. Full list at [schema.org/docs/full](https://schema.org/docs/full.html); validate with the [Rich Results Test](https://search.google.com/test/rich-results). A per-`@type` table with the notable fields is in the [Templating docs](example/src/markup/docs/quick-start/templating-html.md).
|
|
1043
|
+
|
|
1044
|
+
- `breadcrumb` — generates a visible breadcrumb `<nav class="breadcrumb"><ol>…</ol></nav>` trail for the page body (blog posts, nested pages), from the same URL-depth data the `jsonld` `BreadcrumbList` uses: the site root, each ancestor folder (humanized, e.g. `docs/static-site` → *Static Site*), then the current page as `aria-current` text. Pass `relativePathPrefix` so links resolve against the current output location (localhost in dev, your deployed subpath in prod) — not the absolute domain.
|
|
1045
|
+
- Nunjucks: `{{ page | breadcrumb(site, relativePathPrefix) }}`
|
|
1046
|
+
- Liquid: `{{ page | breadcrumb: site, relativePathPrefix }}`
|
|
1047
|
+
|
|
1048
|
+
The home crumb is optional: set `breadcrumb: { home: false }` (or `{ homeLabel: "Start" }` to rename it) under `site` or in a page's front matter — front matter wins. With the home crumb off, top-level pages fall to a single crumb and render nothing, while nested pages still show their folder trail. `breadcrumb: false` on a page or on `site` disables both the visible trail and the auto `BreadcrumbList` JSON-LD. Returns nothing on the homepage or any single-crumb page.
|
|
1049
|
+
|
|
1050
|
+
- `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. Array-valued fields split per element — an item with `tags: [js, css]` appears in **both** the `js` and `css` groups (the mechanism behind [taxonomies](#taxonomies-tags--categories)).
|
|
934
1051
|
- Nunjucks: `{{ changelog.items | groupby("author") }}` or `{{ changelog.items | groupby("date", "year") }}`
|
|
935
1052
|
- Liquid: `{{ changelog.items | groupby: "author" }}` or `{{ changelog.items | groupby: "date", "year" }}`
|
|
936
1053
|
|
|
@@ -1026,11 +1143,11 @@ Returns: `static/photo-320w.webp 320w, static/photo-640w.webp 640w, static/photo
|
|
|
1026
1143
|
{% endfor %}
|
|
1027
1144
|
```
|
|
1028
1145
|
|
|
1029
|
-
#### Search Index, Sitemap & Navigation
|
|
1146
|
+
#### Search Index, Sitemap, llms.txt, robots.txt & Navigation
|
|
1030
1147
|
|
|
1031
|
-
Poops can automatically generate a JSON search index, an XML sitemap and a navigation tree from your compiled pages. All are generated in a single pass during the markup compilation phase.
|
|
1148
|
+
Poops can automatically generate a JSON search index, an XML sitemap, an `llms.txt`, a `robots.txt` and a navigation tree from your compiled pages. All are generated in a single pass during the markup compilation phase.
|
|
1032
1149
|
|
|
1033
|
-
To enable, add `searchIndex`, `sitemap` and/or `nav` to your markup config:
|
|
1150
|
+
To enable, add `searchIndex`, `sitemap`, `llms`, `robots` and/or `nav` to your markup config:
|
|
1034
1151
|
|
|
1035
1152
|
```json
|
|
1036
1153
|
{
|
|
@@ -1039,7 +1156,9 @@ To enable, add `searchIndex`, `sitemap` and/or `nav` to your markup config:
|
|
|
1039
1156
|
"out": "dist",
|
|
1040
1157
|
"options": {
|
|
1041
1158
|
"searchIndex": "search-index.json",
|
|
1042
|
-
"sitemap": "sitemap.xml"
|
|
1159
|
+
"sitemap": "sitemap.xml",
|
|
1160
|
+
"llms": "llms.txt",
|
|
1161
|
+
"robots": "robots.txt"
|
|
1043
1162
|
}
|
|
1044
1163
|
}
|
|
1045
1164
|
}
|
|
@@ -1058,6 +1177,13 @@ The string shorthand sets the output filename with default options. For more con
|
|
|
1058
1177
|
},
|
|
1059
1178
|
"sitemap": {
|
|
1060
1179
|
"output": "sitemap.xml"
|
|
1180
|
+
},
|
|
1181
|
+
"llms": {
|
|
1182
|
+
"output": "llms.txt",
|
|
1183
|
+
"title": "My Site",
|
|
1184
|
+
"description": "One-line summary of the site.",
|
|
1185
|
+
"intro": "src/llms-intro.md",
|
|
1186
|
+
"full": true
|
|
1061
1187
|
}
|
|
1062
1188
|
}
|
|
1063
1189
|
```
|
|
@@ -1092,9 +1218,31 @@ All front matter fields are passed through to the index automatically. Internal
|
|
|
1092
1218
|
]
|
|
1093
1219
|
```
|
|
1094
1220
|
|
|
1095
|
-
**Sitemap** generates a standard `sitemap.xml` with `<loc>` and `<lastmod>` (from front matter `date`). If `site.url` is set in your markup config, it is prepended to all URLs. Collection index/pagination pages are included in the sitemap but excluded from the search index.
|
|
1221
|
+
**Sitemap** generates a standard `sitemap.xml` with `<loc>` and `<lastmod>` (from front matter `date`). If `site.url` is set in your markup config, it is prepended to all URLs. Collection index/pagination and taxonomy term pages are included in the sitemap but excluded from the search index.
|
|
1222
|
+
|
|
1223
|
+
**llms.txt** generates an [`llms.txt`](https://llmstxt.org) — a Markdown index of your pages that LLMs and generative engines (GEO) read to understand your site. It has an `# H1` title, a `> ` blockquote summary, then `- [title](url): description` links grouped by URL path: the first folder is a `## section`, a second folder nests as a `### subsection` under it, and root-level pages fall under a lead "Pages" section. So `docs/config-reference.html` lands directly under `## Docs` while `docs/quick-start/x.html` lands under `### Quick Start` inside it. Collection items (which live under `collection/…`) group the same way and are ordered newest-first by their `date`; other sections keep file order. Set `intro` to a Markdown file path (relative to the project root) to insert free-form context between the blockquote and the link sections — a file authored for LLMs, e.g. `llms-intro.md`. Avoid `##` headings in it; they read as sections. (A raw README is a poor fit — badges, install noise and its own headings collide.) `title` and `description` default to your `site.title`/`site.description`; override them (and the lead section name via `sectionTitle`) with the object form. `site.url` makes the links absolute. Collection index/pagination pages are skipped, like the search index.
|
|
1224
|
+
|
|
1225
|
+
Set `full` to also write a companion full-content file — every page's Markdown body concatenated into one file an LLM can ingest whole (the index is the link map; this is the corpus). `true` names it after `output` with a `-full` suffix (`llms.txt` → `llms-full.txt`, `ai.txt` → `ai-full.txt`); pass a string to set the path yourself. The file opens with a `# Full Documentation Archive for {title}` header, a one-line intro naming the site and a `> ` blockquote of the `description` so a whole-file ingest starts with context, then each page becomes an `# title` (its own leading H1 if it has one) + `URL:` line + body, joined by `---`. Set `fullIntro` to a Markdown file path (from the project root) to insert your own preamble after that header — the `full` counterpart to `intro` (inserted verbatim; a missing file warns and is skipped). Only `.md`/`.markdown` sources qualify (a `.njk`/`.liquid` source is template code, not prose); `noindex` and collection-index pages are dropped. Content is the Markdown **source**, so unrendered `{% raw %}{% … %}{% endraw %}` tags or shortcodes in a body pass through verbatim.
|
|
1226
|
+
|
|
1227
|
+
**robots.txt** generates a `robots.txt`. The string shorthand writes an allow-all file (`User-agent: *`, empty `Disallow:`) with a `Sitemap:` line pointing at your generated sitemap — absolute when `site.url` is set. The object form takes `output`, `userAgent`, `allow`/`disallow` (a path or array of paths), and `sitemap` (an explicit URL, or `false` to omit the line):
|
|
1228
|
+
|
|
1229
|
+
```json
|
|
1230
|
+
{
|
|
1231
|
+
"robots": {
|
|
1232
|
+
"output": "robots.txt",
|
|
1233
|
+
"disallow": ["/admin", "/drafts"],
|
|
1234
|
+
"sitemap": false
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
Pages with `published: false` in their front matter are excluded from all outputs.
|
|
1096
1240
|
|
|
1097
|
-
|
|
1241
|
+
A page's front matter `robots: noindex` (or `none`) drops it from the **sitemap and llms.txt** — for drafts, thin or utility pages (a 404, say) you don't want crawled or fed to LLMs. It stays in the search index (that's your own on-site search). Emit the matching crawler directive in your layout `<head>` so the page itself carries it:
|
|
1242
|
+
|
|
1243
|
+
```html
|
|
1244
|
+
{% if page.robots %}<meta name="robots" content="{{ page.robots }}">{% endif %}
|
|
1245
|
+
```
|
|
1098
1246
|
|
|
1099
1247
|
**Navigation tree** builds your page hierarchy as sidebar-ready data, exposed two ways: as the `nav` template global (loaded automatically, always reflecting the current build) and as a nested JSON file for client-side rendering. Subpages nest automatically from URL structure: `guide/index.md` becomes a parent node and `guide/getting-started.md`, `guide/advanced/config.md` become its (and its subsections') children. Add `nav` to your markup config:
|
|
1100
1248
|
|
|
@@ -1206,6 +1354,40 @@ Liquid — a partial that recurses via `render` (save as `_partials/navtree.liqu
|
|
|
1206
1354
|
{% render 'navtree', items: nav, relativePathPrefix: relativePathPrefix %}
|
|
1207
1355
|
```
|
|
1208
1356
|
|
|
1357
|
+
#### RSS / Atom feeds
|
|
1358
|
+
|
|
1359
|
+
Generate a subscription feed for a [collection](#collections) — no hand-authored feed template. Each feed lists the collection's posts newest-first (by `date`), with the channel metadata taken from your `site` data.
|
|
1360
|
+
|
|
1361
|
+
```json
|
|
1362
|
+
{
|
|
1363
|
+
"markup": {
|
|
1364
|
+
"in": "src/markup",
|
|
1365
|
+
"out": "dist",
|
|
1366
|
+
"options": {
|
|
1367
|
+
"feed": { "collection": "blog", "output": "blog/feed.rss" }
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
**Feed options:**
|
|
1374
|
+
|
|
1375
|
+
- `collection` — the collection to build the feed from. Omit it to emit a feed for **every** collection.
|
|
1376
|
+
- `output` — the file to write. A bare filename (`feed.xml`, the default) is placed inside the collection's own folder (`blog/feed.xml`); a value with a slash is used as-is under the output directory.
|
|
1377
|
+
- `type` — `"rss"` (default) or `"atom"`.
|
|
1378
|
+
- `limit` — max items, newest first (default `20`).
|
|
1379
|
+
- `title` — channel title (default `"<Collection> | <site.title>"`).
|
|
1380
|
+
- `description` — channel description (default `site.description`).
|
|
1381
|
+
- `author`, `lang` — default to `site.author` / `site.lang`.
|
|
1382
|
+
|
|
1383
|
+
Shorthand forms: `"feed": true` (or a filename string) emits an RSS feed for every collection; an array of the objects above generates several feeds at once (e.g. an RSS and an Atom for the same collection). Item `<description>` uses each post's `description`, falling back to its auto-`excerpt`; links, `guid`s and `<atom:link rel="self">` are made absolute with `site.url`. `robots: noindex` posts are excluded, matching the sitemap.
|
|
1384
|
+
|
|
1385
|
+
Point browsers and readers at it from your layout `<head>`:
|
|
1386
|
+
|
|
1387
|
+
```html
|
|
1388
|
+
<link rel="alternate" type="application/rss+xml" href="{{ site.url }}/blog/feed.rss">
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1209
1391
|
### Images (optional)
|
|
1210
1392
|
|
|
1211
1393
|
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).
|
package/lib/exec.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import log, { styledLog } from './utils/log.js'
|
|
3
|
+
|
|
4
|
+
// Every stage runExec fires a hook for. A key in config.exec outside this set
|
|
5
|
+
// never runs (runExec only looks up known stages), so it's almost always a typo
|
|
6
|
+
// (e.g. `style`, `script`). validateExec warns once at startup instead of
|
|
7
|
+
// letting the hook silently no-op.
|
|
8
|
+
export const EXEC_STAGES = ['reactor', 'scripts', 'images', 'markup', 'styles', 'copy', 'build']
|
|
9
|
+
|
|
10
|
+
// Returns the unknown stage keys (for tests); warns for each as a side effect.
|
|
11
|
+
export function validateExec(config) {
|
|
12
|
+
const unknown = Object.keys(config.exec || {}).filter((key) => !EXEC_STAGES.includes(key))
|
|
13
|
+
for (const key of unknown) {
|
|
14
|
+
log({ tag: 'exec', warn: true, text: `unknown stage "${key}" — never runs. Valid: ${EXEC_STAGES.join(', ')}` })
|
|
15
|
+
}
|
|
16
|
+
return unknown
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Post-stage shell hooks. `config.exec` maps a pipeline stage to a command (or
|
|
20
|
+
// array of commands) run after that stage compiles — in both build and watch,
|
|
21
|
+
// so a post-processor (e.g. stripping CSS comments, regenerating a reference)
|
|
22
|
+
// stays in sync during dev instead of only running behind `poops -b && cmd`.
|
|
23
|
+
// Sync + stdio:inherit so ordering is deterministic and output streams live;
|
|
24
|
+
// cwd is the project root, matching how the compilers resolve their paths.
|
|
25
|
+
// Returns true if any command failed, so build can fail the exit code; in watch
|
|
26
|
+
// the failure is logged and swallowed so the watcher survives.
|
|
27
|
+
// Stages: reactor, scripts, images, markup, styles (fires after PostCSS, so the
|
|
28
|
+
// CSS is final), copy, and build (once, after the full initial pipeline).
|
|
29
|
+
// `run` is injectable so tests exercise the branching without forking a shell.
|
|
30
|
+
export default function runExec(config, cwd, stage, run = execSync) {
|
|
31
|
+
const hooks = config.exec && config.exec[stage]
|
|
32
|
+
if (!hooks) return false
|
|
33
|
+
let failed = false
|
|
34
|
+
for (const cmd of [hooks].flat()) {
|
|
35
|
+
try {
|
|
36
|
+
styledLog(`⚡ {dim}exec ${stage}:{/} ${cmd}`)
|
|
37
|
+
run(cmd, { cwd, stdio: 'inherit' })
|
|
38
|
+
} catch (err) {
|
|
39
|
+
failed = true
|
|
40
|
+
log({ tag: 'error', text: `exec ${stage} failed: ${cmd}` })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return failed
|
|
44
|
+
}
|