Skip to content

Commit eaa1c93

Browse files
committed
fix: shorten social meta descriptions
Extract the social preview description logic into a shared utility. Normalize whitespace and append the second sentence only when both normalized sentences are within the 140 character limit. Add focused Vitest coverage and the implementation plan doc for the change.
1 parent ef2ac39 commit eaa1c93

4 files changed

Lines changed: 302 additions & 4 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
open Vitest
2+
3+
test("returns the first sentence for a one-sentence description", async () => {
4+
let result = MetaDescription.shortenForSocialPreview(
5+
"JavaScript Made Simple for Humans and AI.",
6+
)
7+
8+
expect(result)->toBe("JavaScript Made Simple for Humans and AI.")
9+
})
10+
11+
test("includes the second sentence when both sentences are within 140 characters", async () => {
12+
let result = MetaDescription.shortenForSocialPreview(
13+
"JavaScript Made Simple for Humans and AI. ReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
14+
)
15+
16+
expect(result)->toBe(
17+
"JavaScript Made Simple for Humans and AI. ReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
18+
)
19+
})
20+
21+
test("returns only the first sentence when the first sentence exceeds 140 characters", async () => {
22+
let result = MetaDescription.shortenForSocialPreview(
23+
"This first sentence is intentionally long enough to cross the one hundred and forty character threshold before it reaches its final word in the sentence. Short follow up.",
24+
)
25+
26+
expect(result)->toBe(
27+
"This first sentence is intentionally long enough to cross the one hundred and forty character threshold before it reaches its final word in the sentence.",
28+
)
29+
})
30+
31+
test("returns only the first sentence when the second sentence exceeds 140 characters", async () => {
32+
let result = MetaDescription.shortenForSocialPreview(
33+
"Short opening sentence. This second sentence is intentionally long enough to cross the one hundred and forty character threshold before it reaches its final word in the sentence.",
34+
)
35+
36+
expect(result)->toBe("Short opening sentence.")
37+
})
38+
39+
test("collapses line breaks and repeated spaces before evaluating the sentences", async () => {
40+
let result = MetaDescription.shortenForSocialPreview(
41+
"JavaScript Made Simple for Humans and AI.\n\nReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
42+
)
43+
44+
expect(result)->toBe(
45+
"JavaScript Made Simple for Humans and AI. ReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
46+
)
47+
})
48+
49+
test("ignores empty sentence fragments created by repeated punctuation", async () => {
50+
let result = MetaDescription.shortenForSocialPreview(
51+
"First sentence... Second sentence stays short.",
52+
)
53+
54+
expect(result)->toBe("First sentence. Second sentence stays short.")
55+
})
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Meta Description Shortening Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** Extract social meta description shortening into a reusable utility, add focused Vitest coverage, and update `Meta.res` to use the helper for derived Open Graph and Twitter descriptions.
6+
7+
**Architecture:** Add a small string utility module under `src/common/` that normalizes whitespace, chooses the first sentence, and conditionally appends the second sentence when both are within the 140-character threshold. Cover that module directly with a dedicated Vitest file, then replace the inline `Meta.res` helper with the shared utility while keeping the page-level `description` meta tag unchanged.
8+
9+
**Tech Stack:** ReScript v12, Vitest 4 browser mode, React 19, React Router v7, Yarn 4
10+
11+
---
12+
13+
### Task 1: Create the utility and prove the base behavior with a failing test
14+
15+
**Files:**
16+
- Create: `src/common/MetaDescription.res`
17+
- Test: `__tests__/MetaDescription_.test.res`
18+
19+
- [ ] **Step 1: Write the failing test**
20+
21+
```rescript
22+
open Vitest
23+
24+
test("returns the first sentence for a one-sentence description", async () => {
25+
let result = MetaDescription.shortenForSocialPreview(
26+
"JavaScript Made Simple for Humans and AI.",
27+
)
28+
29+
expect(result)->toBe("JavaScript Made Simple for Humans and AI.")
30+
})
31+
```
32+
33+
- [ ] **Step 2: Run test to verify it fails**
34+
35+
Run: `yarn build:res && yarn vitest --run --browser.headless __tests__/MetaDescription_.test.jsx`
36+
Expected: FAIL with a compile error or runtime failure because `MetaDescription.shortenForSocialPreview` does not exist yet.
37+
38+
- [ ] **Step 3: Write minimal implementation**
39+
40+
```rescript
41+
let collapseWhitespace = value =>
42+
value
43+
->String.trim
44+
->String.replaceAllRegExp(/\s+/g, " ")
45+
46+
let ensurePeriod = sentence =>
47+
if sentence->String.endsWith(".") {
48+
sentence
49+
} else {
50+
sentence ++ "."
51+
}
52+
53+
let shortenForSocialPreview = description => {
54+
let normalized = collapseWhitespace(description)
55+
56+
switch normalized->String.split(".")->Array.get(0) {
57+
| Some(firstSentence) => firstSentence->String.trim->ensurePeriod
58+
| None => normalized
59+
}
60+
}
61+
```
62+
63+
- [ ] **Step 4: Run test to verify it passes**
64+
65+
Run: `yarn build:res && yarn vitest --run --browser.headless __tests__/MetaDescription_.test.jsx`
66+
Expected: PASS for the single test.
67+
68+
- [ ] **Step 5: Commit**
69+
70+
```bash
71+
git add __tests__/MetaDescription_.test.res src/common/MetaDescription.res
72+
git commit -m "feat: add meta description utility"
73+
```
74+
75+
### Task 2: Extend the tests and utility to cover sentence-length and whitespace rules
76+
77+
**Files:**
78+
- Modify: `src/common/MetaDescription.res`
79+
- Modify: `__tests__/MetaDescription_.test.res`
80+
81+
- [ ] **Step 1: Write the failing tests**
82+
83+
```rescript
84+
test("includes the second sentence when both sentences are within 140 characters", async () => {
85+
let result = MetaDescription.shortenForSocialPreview(
86+
"JavaScript Made Simple for Humans and AI. ReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
87+
)
88+
89+
expect(result)->toBe(
90+
"JavaScript Made Simple for Humans and AI. ReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
91+
)
92+
})
93+
94+
test("returns only the first sentence when the first sentence exceeds 140 characters", async () => {
95+
let result = MetaDescription.shortenForSocialPreview(
96+
"This first sentence is intentionally long enough to cross the one hundred and forty character threshold before it reaches its final word in the sentence. Short follow up.",
97+
)
98+
99+
expect(result)->toBe(
100+
"This first sentence is intentionally long enough to cross the one hundred and forty character threshold before it reaches its final word in the sentence.",
101+
)
102+
})
103+
104+
test("returns only the first sentence when the second sentence exceeds 140 characters", async () => {
105+
let result = MetaDescription.shortenForSocialPreview(
106+
"Short opening sentence. This second sentence is intentionally long enough to cross the one hundred and forty character threshold before it reaches its final word in the sentence.",
107+
)
108+
109+
expect(result)->toBe("Short opening sentence.")
110+
})
111+
112+
test("collapses line breaks and repeated spaces before evaluating the sentences", async () => {
113+
let result = MetaDescription.shortenForSocialPreview(
114+
"JavaScript Made Simple for Humans and AI.\n\nReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
115+
)
116+
117+
expect(result)->toBe(
118+
"JavaScript Made Simple for Humans and AI. ReScript is a strongly typed language that compiles to clean, efficient JavaScript that humans and AI tools can read and understand.",
119+
)
120+
})
121+
122+
test("ignores empty sentence fragments created by repeated punctuation", async () => {
123+
let result = MetaDescription.shortenForSocialPreview(
124+
"First sentence... Second sentence stays short.",
125+
)
126+
127+
expect(result)->toBe("First sentence. Second sentence stays short.")
128+
})
129+
```
130+
131+
- [ ] **Step 2: Run test to verify it fails**
132+
133+
Run: `yarn build:res && yarn vitest --run --browser.headless __tests__/MetaDescription_.test.jsx`
134+
Expected: FAIL on the new tests because the utility still returns only the first sentence and does not normalize multi-space and newline input correctly.
135+
136+
- [ ] **Step 3: Write minimal implementation**
137+
138+
```rescript
139+
let maxSentenceLength = 140
140+
141+
let collapseWhitespace = description =>
142+
description
143+
->String.trim
144+
->String.replaceAllRegExp(/\s+/g, " ")
145+
146+
let sentenceParts = description =>
147+
description
148+
->collapseWhitespace
149+
->String.split(".")
150+
->Array.map(String.trim)
151+
->Array.keep(part => part != "")
152+
153+
let ensurePeriod = sentence =>
154+
if sentence->String.endsWith(".") {
155+
sentence
156+
} else {
157+
sentence ++ "."
158+
}
159+
160+
let shortenForSocialPreview = description => {
161+
let normalized = collapseWhitespace(description)
162+
let parts = sentenceParts(normalized)
163+
164+
switch (parts->Array.get(0), parts->Array.get(1)) {
165+
| (Some(firstSentence), Some(secondSentence))
166+
if String.length(firstSentence) <= maxSentenceLength &&
167+
String.length(secondSentence) <= maxSentenceLength =>
168+
ensurePeriod(firstSentence) ++ " " ++ ensurePeriod(secondSentence)
169+
| (Some(firstSentence), _) => ensurePeriod(firstSentence)
170+
| _ => normalized
171+
}
172+
}
173+
```
174+
175+
- [ ] **Step 4: Run test to verify it passes**
176+
177+
Run: `yarn build:res && yarn vitest --run --browser.headless __tests__/MetaDescription_.test.jsx`
178+
Expected: PASS for all `MetaDescription_` tests.
179+
180+
- [ ] **Step 5: Commit**
181+
182+
```bash
183+
git add __tests__/MetaDescription_.test.res src/common/MetaDescription.res
184+
git commit -m "test: cover meta description shortening rules"
185+
```
186+
187+
### Task 3: Replace the inline `Meta` helper with the shared utility
188+
189+
**Files:**
190+
- Modify: `src/components/Meta.res`
191+
- Test: `__tests__/MetaDescription_.test.res`
192+
193+
- [ ] **Step 1: Update `Meta.res` to call the utility**
194+
195+
```rescript
196+
let ogDescription = switch ogDescription {
197+
| None => MetaDescription.shortenForSocialPreview(description)
198+
| Some(description) => description
199+
}
200+
```
201+
202+
Delete the inline `shortenDesciption` helper from `src/components/Meta.res`.
203+
204+
- [ ] **Step 2: Run compilation and the focused test file**
205+
206+
Run: `yarn build:res && yarn vitest --run --browser.headless __tests__/MetaDescription_.test.jsx`
207+
Expected: PASS. `Meta.res` should compile cleanly and the utility tests should stay green.
208+
209+
- [ ] **Step 3: Run a broader regression check**
210+
211+
Run: `yarn ci:test`
212+
Expected: PASS with no failing Vitest browser tests.
213+
214+
- [ ] **Step 4: Commit**
215+
216+
```bash
217+
git add src/components/Meta.res src/common/MetaDescription.res __tests__/MetaDescription_.test.res
218+
git commit -m "fix: shorten social meta descriptions"
219+
```

src/common/MetaDescription.res

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
let maxSentenceLength = 140
2+
3+
let collapseWhitespace = value => value->String.trim->String.replaceAllRegExp(/\s+/g, " ")
4+
5+
let ensurePeriod = sentence =>
6+
if sentence->String.endsWith(".") {
7+
sentence
8+
} else {
9+
sentence ++ "."
10+
}
11+
12+
let shortenForSocialPreview = description => {
13+
let normalized = collapseWhitespace(description)
14+
let sentences = normalized->String.split(".")->Array.map(String.trim)->Array.filter(sentence => sentence != "")
15+
16+
switch (sentences->Array.get(0), sentences->Array.get(1)) {
17+
| (Some(firstSentence), Some(secondSentence))
18+
if String.length(firstSentence) <= maxSentenceLength &&
19+
String.length(secondSentence) <= maxSentenceLength =>
20+
ensurePeriod(firstSentence) ++ " " ++ ensurePeriod(secondSentence)
21+
| (Some(firstSentence), _) => ensurePeriod(firstSentence)
22+
| _ => normalized
23+
}
24+
}

src/components/Meta.res

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
/*
2-
canonical: Set a canonical URL pointing to the original content.
3-
*/
41
@react.component
52
let make = (
63
~siteName="ReScript Documentation",
74
~keywords: array<string>=[],
85
~description="The ReScript language and ecosystem docs",
6+
/*
7+
* canonical: Set a canonical URL pointing to the original content.
8+
*/
99
~canonical=?,
1010
~title=?,
1111
~ogLocale="en_US",
@@ -33,7 +33,7 @@ let make = (
3333
}
3434

3535
let ogDescription = switch ogDescription {
36-
| None => description->String.split(".")->Array.get(0)->Option.getOr("")
36+
| None => MetaDescription.shortenForSocialPreview(description)
3737
| Some(description) => description
3838
}
3939

0 commit comments

Comments
 (0)