Skip to content

Commit e5fb373

Browse files
committed
Introduce ContentModel2/Collections (WIP)
Start of a major change in content modelling. Develop inside master without touching the working version. Goal: - The old contentModel structure becomes a "collection" of "entries". - Root possibly has many collections. - Each collection will be of a certain content type. e.g. TextEntry Collection - Collections have categorization and tagging within them - stuff like: example.com/blog, example.com/projects, example.com/podcast (each directory is a collection) Notes: - Bypassed bringing date from git during enhancements - Eliminated everything async from contentModel(2) (for clarity and testability) - Removed outputPath from categories and posts because it caused issues and also seemed it shouldn't be a concern of contentModel to calculate outputPath (we'll see). - Identified posts and categories by their fsObject.path - Removed subpages and homepage from linking enhancer - Allowed contentModel hook to be run for each collection - Computed entry and tag permalinks based on fsObject.path (feels like the containing collection should be available to entry scopes)
1 parent 953f414 commit e5fb373

File tree

20 files changed

+1486
-1
lines changed

20 files changed

+1486
-1
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = {
2+
COLLECTION: 'collection',
3+
CATEGORY: 'category',
4+
CATEGORY_INDEX: 'categoryIndex',
5+
POST: 'post',
6+
FOLDERED_POST_INDEX: 'folderedPostIndex'
7+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const _ = require('lodash')
2+
const Dictionary = require('../../../../dictionary')
3+
const { decorate } = require('../../../../decorations')
4+
5+
const withDates = (entry) => {
6+
if (!entry.publishDatePrototype) {
7+
return entry
8+
}
9+
const locale = Dictionary.getLocale()
10+
const decoratedEntry = entry //await decorate('publishDate', entry)
11+
const publishDate = new Date(decoratedEntry.publishDatePrototype.value)
12+
return {
13+
..._.omit(entry, 'publishDatePrototype'),
14+
publishDate,
15+
publishDateUTC: publishDate.toUTCString(),
16+
publishDateFull: publishDate.toLocaleString(locale, { dateStyle: 'full' }),
17+
publishDateLong: publishDate.toLocaleString(locale, { dateStyle: 'long' }),
18+
publishDateMedium: publishDate.toLocaleString(locale, { dateStyle: 'medium' }),
19+
publishDateShort: publishDate.toLocaleString(locale, { dateStyle: 'short' })
20+
}
21+
}
22+
23+
module.exports = (contentModel) => {
24+
return {
25+
...contentModel,
26+
categories: contentModel.categories.map(category => ({
27+
...category,
28+
posts: category.posts.map(withDates)
29+
})),
30+
posts: contentModel.posts.map(withDates)
31+
}
32+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
const { sep } = require('path')
2+
const _ = require('lodash')
3+
const Settings = require('../../../../settings')
4+
const { pipeSync, getSlug, makePermalink } = require('../../../../helpers')
5+
6+
const getScoreByOverlap = (arrayOfStrings1 = [], arrayOfStrings2 = []) => {
7+
const overlap = _.intersection(
8+
arrayOfStrings1.map(string => string.toUpperCase()),
9+
arrayOfStrings2.map(string => string.toUpperCase())
10+
).filter(Boolean)
11+
return overlap.length
12+
}
13+
14+
const getScoreByEquality = (thing1, thing2) => {
15+
return Number(thing1 === thing2)
16+
}
17+
18+
const getScoreByTextSimilarity = (text1, text2) => {
19+
return getScoreByOverlap(text1.split(' '), text2.split(' '))
20+
}
21+
22+
const attachPaging = (post, postIndex, posts) => {
23+
const paging = {}
24+
if (postIndex > 0) {
25+
paging.nextPost = {
26+
title: posts[postIndex - 1].title,
27+
permalink: posts[postIndex - 1].permalink
28+
}
29+
}
30+
if (postIndex < posts.length - 1) {
31+
paging.previousPost = {
32+
title: posts[postIndex + 1].title,
33+
permalink: posts[postIndex + 1].permalink
34+
}
35+
}
36+
return paging
37+
}
38+
39+
const normalizeText = (text = '') => {
40+
return text
41+
.replace(/<(p|a|img|pre|video|i|u|em|strong|small|div|section)>/i, '')
42+
.replace(/-_:'\(\)\[\]\{\}\./g, '')
43+
.replace(/\n/g, '')
44+
}
45+
46+
const attachRelevantPosts = (post, posts) => {
47+
const relevantPosts = []
48+
posts.forEach(otherPost => {
49+
if (otherPost.permalink === post.permalink) {
50+
return
51+
}
52+
const tagsOverlapScore = getScoreByOverlap(
53+
post.tags.map(t => t.tag),
54+
otherPost.tags.map(t => t.tag)
55+
)
56+
const titleSimilarityScore = getScoreByTextSimilarity(
57+
normalizeText(post.title),
58+
normalizeText(otherPost.title)
59+
)
60+
const sameCategoryScore = getScoreByEquality(post.category, otherPost.category)
61+
const relevancyScore = (
62+
tagsOverlapScore +
63+
titleSimilarityScore +
64+
sameCategoryScore * 3
65+
)
66+
if (relevancyScore > 0) {
67+
const relevantPost = {
68+
..._.pick(otherPost, [
69+
'title',
70+
'permalink',
71+
'category'
72+
]),
73+
relevancyScore
74+
}
75+
relevantPosts.push(relevantPost)
76+
}
77+
})
78+
return {
79+
relevantPosts: relevantPosts.sort(
80+
(a, b) => b.relevancyScore - a.relevancyScore
81+
)
82+
}
83+
}
84+
85+
const linkPosts = (contentModel) => {
86+
return {
87+
...contentModel,
88+
posts: contentModel.posts.map((post) => {
89+
const category = contentModel.categories.find(cat => {
90+
return cat.path === post.category.path
91+
})
92+
const postInCategory = category.posts.find(p => p.path === post.path)
93+
const postIndex = category.posts.indexOf(postInCategory)
94+
return {
95+
...post,
96+
links: {
97+
...(post.links || {}),
98+
...attachPaging(post, postIndex, category.posts),
99+
...attachRelevantPosts(post, category.posts)
100+
}
101+
}
102+
})
103+
}
104+
}
105+
106+
const attachMentionedEntries = (allEntries) => (entry) => {
107+
const mention = (contentModelEntry) => ({
108+
title: contentModelEntry.title || contentModelEntry.name,
109+
permalink: contentModelEntry.permalink,
110+
category: contentModelEntry.category
111+
})
112+
113+
const otherEntries = allEntries.filter(otherEntry => {
114+
return otherEntry.permalink !== entry.permalink
115+
})
116+
117+
const entriesMentioned = otherEntries
118+
.filter(otherEntry => entry.mentions.includes(otherEntry.permalink))
119+
.map(mention)
120+
121+
const entriesMentionedBy = otherEntries
122+
.filter(otherEntry => otherEntry.mentions.includes(entry.permalink))
123+
.map(mention)
124+
125+
return {
126+
..._.omit(entry, 'mentions'),
127+
links: {
128+
...(entry.links || {}),
129+
mentionedTo: entriesMentioned,
130+
mentionedBy: entriesMentionedBy
131+
}
132+
}
133+
}
134+
135+
const linkMentionedEntries = (contentModel) => {
136+
const attacher = attachMentionedEntries([
137+
...contentModel.posts,
138+
...contentModel.categories
139+
])
140+
return {
141+
...contentModel,
142+
categories: contentModel.categories.map(attacher),
143+
posts: contentModel.posts.map(attacher),
144+
}
145+
}
146+
147+
const _linkPostTags = (post, tags) => {
148+
const { permalinkPrefix } = Settings.getSettings()
149+
const collectionPath = post.path.split(sep)[0]
150+
return {
151+
...post,
152+
tags: post.tags.map(tag => {
153+
const permalink = makePermalink({
154+
prefix: permalinkPrefix,
155+
parts: [collectionPath, 'tags', tag]
156+
})
157+
return {
158+
tag,
159+
slug: getSlug(tag),
160+
permalink
161+
}
162+
})
163+
}
164+
}
165+
166+
const linkPostTags = (contentModel) => {
167+
return {
168+
...contentModel,
169+
categories: contentModel.categories.map(category => {
170+
return {
171+
...category,
172+
posts: category.posts.map(post => _linkPostTags(post, contentModel.tags))
173+
}
174+
}),
175+
posts: contentModel.posts.map(post => _linkPostTags(post, contentModel.tags))
176+
}
177+
}
178+
179+
module.exports = (contentModel) => {
180+
return pipeSync(contentModel, [
181+
linkPostTags,
182+
linkPosts,
183+
linkMentionedEntries
184+
])
185+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const sortPosts = (a, b) => {
2+
return new Date(b.publishDate) - new Date(a.publishDate)
3+
}
4+
5+
module.exports = (contentModel) => {
6+
return {
7+
...contentModel,
8+
categories: contentModel.categories.map(category => {
9+
return {
10+
...category,
11+
posts: [...category.posts].sort(sortPosts)
12+
}
13+
}),
14+
posts: [...contentModel.posts].sort(sortPosts)
15+
}
16+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const { join } = require('path')
2+
3+
/* Extract tags from posts as an array of tags with posts */
4+
const getTags = (posts) => {
5+
const tags = posts.map((post) => {
6+
return post.tags.map((tag) => {
7+
return { tag, post }
8+
})
9+
})
10+
11+
const flatTags = [].concat(...tags)
12+
13+
const tagsIndex = flatTags.reduce((acc, tagWithPost) => {
14+
const { tag, post } = tagWithPost
15+
return {
16+
...acc,
17+
[tag.tag]: {
18+
...tag,
19+
posts: [
20+
...(acc[tag.tag] ? acc[tag.tag].posts : []),
21+
post
22+
]
23+
}
24+
}
25+
}, {})
26+
27+
return Object
28+
.keys(tagsIndex)
29+
.map(key => tagsIndex[key])
30+
.sort((a, b) => b.posts.length - a.posts.length)
31+
}
32+
33+
module.exports = (contentModel) => {
34+
return {
35+
...contentModel,
36+
tags: getTags(contentModel.posts)
37+
}
38+
}

0 commit comments

Comments
 (0)