• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

scriptype / writ-cms / 18160146944

01 Oct 2025 10:55AM UTC coverage: 72.83% (-2.7%) from 75.488%
18160146944

push

github

scriptype
[CM2] Introduce facets

An interactive rebase of months worth of thinking, doing and not doing.

Faceting is similar to the earlier tag indexing. With facets, posts can be indexed by any fields, not just tags.

With this, tag indexing is no longer a special case. There's no need for modelling or rendering tags.

Facets are created based on 'facetKeys' of a contentType. A simple array of keys works.

Now, each collection has a new route: '/by' which is listing all available facet views in the collection.

And each facet view has a route: /by/facet-name. E.g. /by/author, /by/date, /by/tags. In that page, possible values for the facet can be listed.

And each possible value for the facet has the route: /by/facet-name/value. E.g. /by/author/enes, /by/date/1970-11-23, /by/tags/css. There, posts matching the criteria can be shown.

These new render targets should be implemented inside the theme, and that's what should come soon. So far, working with a local laboratory that has a custom theme.

A notable side-effect of turning a field into a facet is that the field becomes an object in the shape of: { value, facetPermalink }. So, if 'date' field is a facet, templates should do: {{date.value}} to display its value and {{date.facetPermalink}} to render a link to the facet value page in the collection. The name 'facetPermalink' is so ugly tho.

If a facet field links to another entry, then the field's value, which is populated with the linked entry, just gets a 'facetPermalink', to make it possible to render links both to the linked entry and to the facet value page.

Facets work across deep category tree. So, all of the mentioned routes exist at each stop.

E.g.
site.com/photos/by/subject,
site.com/photos/astro/by/subject,
site.com/photos/astro/deep-space/by-subject

An 'afterEffects' mechanism is invented for models. Facets needed to be collected after the tree-building and linking are finished. So, all the way from the root, each model does whatever side-eff... (continued)

495 of 810 branches covered (61.11%)

Branch coverage included in aggregate %.

45 of 144 new or added lines in 8 files covered. (31.25%)

8 existing lines in 3 files now uncovered.

2282 of 3003 relevant lines covered (75.99%)

242.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

53.66
/src/compiler/contentModel2/models/subpage.js
1
const { join, resolve } = require('path')
7✔
2
const { isTemplateFile, makePermalink } = require('../helpers')
7✔
3
const models = {
7✔
4
  _baseEntry: require('./_baseEntry'),
5
  attachment: require('./attachment'),
6
}
7

8
const defaultSettings = {
7✔
9
  pagesDirectory: 'pages'
10
}
11
module.exports = function Subpage(settings = defaultSettings) {
7!
12
  const indexFileNameOptions = ['page', 'index']
7✔
13
  const pagesDirectoryNameOptions = [settings.pagesDirectory, 'subpages', 'pages']
7✔
14

15
  const isSubpageIndexFile = (node) => {
7✔
16
    return (
28✔
17
      isTemplateFile(node) &&
35✔
18
      node.name.match(
19
        new RegExp(`^(${indexFileNameOptions.join('|')})\\..+$`)
20
      )
21
    )
22
  }
23

24
  const isFolderedSubpage = (node) => {
7✔
25
    return node.children?.find(isSubpageIndexFile)
7✔
26
  }
27

28
  const isPagesDirectory = (node) => {
7✔
29
    return (
7✔
30
      node.children &&
14✔
31
      node.name.match(
32
        new RegExp(`^(${pagesDirectoryNameOptions.join('|')})$`)
33
      )
34
    )
35
  }
36

37
  return {
7✔
38
    match: node => isTemplateFile(node) || isFolderedSubpage(node),
7✔
39
    matchPagesDirectory: node => isPagesDirectory(node),
7✔
40

41
    create: (node, context) => {
42
      const baseEntryProps = models._baseEntry(node, indexFileNameOptions)
×
43

44
      const permalink = makePermalink(
×
45
        context.peek().permalink,
46
        baseEntryProps.slug
47
      ) + (baseEntryProps.hasIndex ? '' : '.html')
×
48

49
      const outputPath = join(
×
50
        context.peek().outputPath,
51
        baseEntryProps.slug
52
      )
53

54
      const pageContext = {
×
55
        title: baseEntryProps.title,
56
        slug: baseEntryProps.slug,
57
        permalink,
58
        outputPath
59
      }
60

61
      return {
×
62
        ...baseEntryProps,
63
        ...pageContext,
64
        context,
65
        attachments: baseEntryProps.attachments.map(
66
          attachment => attachment(context.push({
×
67
            ...pageContext,
68
            key: 'page'
69
          }))
70
        )
71
      }
72
    },
73

74
    afterEffects: (contentModel, subpage) => {
NEW
75
      subpage.attachments.forEach(attachment => {
×
NEW
76
        models.attachment().afterEffects(contentModel, attachment)
×
77
      })
78
    },
79

80
    render: (renderer, subpage, { contentModel, settings, debug }) => {
81
      const renderSubpage = () => {
×
82
        return renderer.render({
×
83
          templates: [
84
            `pages/${subpage.template}`,
85
            `pages/subpage/${subpage.contentType}`,
86
            `pages/subpage/default`
87
          ],
88
          outputPath: join(...[
89
            subpage.outputPath,
90
            subpage.hasIndex ? 'index' : ''
×
91
          ]) + '.html',
92
          content: subpage.content,
93
          data: {
94
            ...contentModel,
95
            subpage,
96
            settings,
97
            debug
98
          }
99
        })
100
      }
101

102
      const renderAttachments = () => {
×
103
        return Promise.all(
×
104
          subpage.attachments.map(attachment => {
105
            return models.attachment().render(renderer, attachment)
×
106
          })
107
        )
108
      }
109

110
      return Promise.all([
×
111
        renderSubpage(),
112
        renderAttachments()
113
      ])
114
    }
115
  }
116
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc