Skip to content

Commit abdffa0

Browse files
committed
Remove underline from footnote superscript, add local time zone and timeZone prop to Time component, spacing in Testimonials component and testimonials integration and that checks length
1 parent 174a897 commit abdffa0

11 files changed

Lines changed: 441 additions & 32 deletions

File tree

‎_TODO.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,18 @@ We have generated detailed outlines for each of the MDX proposed articles we hav
197197
Act as a principal software engineer. Your goal is to write a detailed technical article based on the provided outline. Context: The target audience is Senior DevOps and infrastructure Engineers. The tone should be authoritative, professional, and concise, avoiding fluff or filler words. Think step-by-step before writing to improve the accuracy of technical explanations. Use a friendly first-person voice. Anything that reads like generic marketing copy is not what we want but we still want the effect of being "real" and approachable - try not to sound like technical documentation. We want to show empathy for our readers.
198198

199199
Let's work through each article section by section based on the H2 headers in the outline. If the section looks good as-is, I'll just type "ok" so you know to continue to the next section.
200+
201+
## Move hard-coded options from integrations to astro.config.ts
202+
203+
### CTA validator
204+
205+
Let's also refactor the CtaValidator integration to allow passing in options from astro.config.ts instead of hard-coding things in the integration. We should be able to:
206+
207+
- pass in the required CallToActions in the src/components/CallToAction as an array
208+
- pass in the debug flag
209+
210+
I think we can remove the additionalPatterns option, it appears unused but need to check.
211+
212+
I added a new folder in the CallToAction component folder that won't be required for articles.
213+
214+

‎astro.config.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { callToActionValidator } from './src/integrations/CtaValidator'
3838
import { faviconGenerator } from './src/integrations/FaviconGenerator'
3939
import { packageRelease } from './src/integrations/PackageRelease'
4040
import { privacyPolicyVersion } from './src/integrations/PrivacyPolicyVersion'
41+
import { testimonialsLengthWarning } from './src/integrations/TestimonialsLengthWarning'
4142
import { pwaDevAssetServer } from './src/lib/plugins/pwaDevAssetServer'
4243
import { createSerializeFunction, pagesJsonWriter } from './src/integrations/sitemapSerialize'
4344

@@ -67,6 +68,8 @@ const standardIntegrations = [
6768
packageRelease(),
6869
/** Inject privacy policy version from git commit date for GDPR record keeping */
6970
privacyPolicyVersion(),
71+
/** Warn when testimonial bodies are too short/too long (helps keep carousel cards consistent) */
72+
testimonialsLengthWarning({ min: 350, max: 400 }),
7073
/** Only include Sentry integration in Vercel environments */
7174
...(isVercel() ? [sentry({
7275
project: "webstack-builders-corporate-website",

‎src/components/Testimonials/index.astro‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ const singleTestimonial = shouldRenderSingle ? testimonials.at(0) : undefined
5757
{
5858
testimonials.length > 0 &&
5959
(shouldRenderSingle ? (
60-
<section class="bg-page-base-offset py-16" aria-labelledby={headingId}>
60+
<section class="bg-page-base-offset py-8" aria-labelledby={headingId}>
6161
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
62-
<header class="text-center mb-12">
62+
<header class="text-center mb-6">
6363
<h2 id={headingId} class="text-3xl md:text-4xl font-bold mb-4">{title}</h2>
6464
{leadText && (
6565
<p class="text-xl text-content-active max-w-3xl mx-auto">{leadText}</p>
@@ -79,9 +79,9 @@ const singleTestimonial = shouldRenderSingle ? testimonials.at(0) : undefined
7979
aria-roledescription="carousel"
8080
aria-label={title}
8181
>
82-
<section class="bg-page-base-offset py-16" aria-labelledby={headingId}>
82+
<section class="bg-page-base-offset py-8" aria-labelledby={headingId}>
8383
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
84-
<header class="text-center mb-12">
84+
<header class="text-center mb-6">
8585
<h2 id={headingId} class="text-3xl md:text-4xl font-bold mb-4">{title}</h2>
8686
{leadText && (
8787
<p class="text-xl text-content-active max-w-3xl mx-auto">{leadText}</p>
@@ -111,7 +111,7 @@ const singleTestimonial = shouldRenderSingle ? testimonials.at(0) : undefined
111111
<>
112112
<button
113113
type="button"
114-
class="embla__button embla__button--prev absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 w-12 h-12 rounded-full bg-content-inverse border-2 border-trim shadow-lg hover:bg-gray-200 hover:border-gray-200 transition-all duration-300 flex items-center justify-center z-(--z-raised) disabled:opacity-30 disabled:cursor-not-allowed"
114+
class="embla__button embla__button--prev absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 w-12 h-12 rounded-full bg-content-inverse border-2 border-trim shadow-lg hover:bg-gray-200 hover:border-gray-200 transition-all duration-300 flex items-center justify-center z-(--z-raised) disabled:opacity-30 disabled:cursor-not-allowed focus-visible:outline-none after:pointer-events-none after:absolute after:-inset-0.5 after:rounded-none after:content-[''] focus-visible:after:border-2 focus-visible:after:border-spotlight"
115115
aria-label="Previous testimonial"
116116
>
117117
<svg
@@ -133,7 +133,7 @@ const singleTestimonial = shouldRenderSingle ? testimonials.at(0) : undefined
133133

134134
<button
135135
type="button"
136-
class="embla__button embla__button--next absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 w-12 h-12 rounded-full bg-content-inverse border-2 border-trim shadow-lg hover:bg-gray-200 hover:border-gray-200 transition-all duration-300 flex items-center justify-center z-(--z-raised) disabled:opacity-30 disabled:cursor-not-allowed"
136+
class="embla__button embla__button--next absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 w-12 h-12 rounded-full bg-content-inverse border-2 border-trim shadow-lg hover:bg-gray-200 hover:border-gray-200 transition-all duration-300 flex items-center justify-center z-(--z-raised) disabled:opacity-30 disabled:cursor-not-allowed focus-visible:outline-none after:pointer-events-none after:absolute after:-inset-0.5 after:rounded-none after:content-[''] focus-visible:after:border-2 focus-visible:after:border-spotlight"
137137
aria-label="Next testimonial"
138138
>
139139
<svg
@@ -161,7 +161,7 @@ const singleTestimonial = shouldRenderSingle ? testimonials.at(0) : undefined
161161
/>
162162
<button
163163
type="button"
164-
class="flex items-center justify-center rounded-full border border-trim bg-content-inverse p-1 shadow-lg transition-colors hover:bg-page-base-offset"
164+
class="relative flex items-center justify-center rounded-full border border-trim bg-content-inverse p-1 shadow-lg transition-colors hover:bg-page-base-offset focus-visible:outline-none after:pointer-events-none after:absolute after:-inset-0.5 after:rounded-none after:content-[''] focus-visible:after:border-2 focus-visible:after:border-spotlight"
165165
data-testimonials-autoplay-toggle
166166
aria-label="Pause testimonials"
167167
aria-pressed="false"

‎src/components/Time/__tests__/index.spec.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ describe('Time (Astro)', () => {
5353
})
5454
})
5555

56+
test('defaults to datetime format and renders fallback text when format is omitted', async () => {
57+
const Time = (await import('@components/Time/index.astro')).default
58+
59+
const renderedHtml = await container.renderToString(Time, {
60+
props: {
61+
datetime: '2026-01-01T00:00:00.000Z',
62+
},
63+
})
64+
65+
await withJsdomEnvironment(async ({ window }) => {
66+
window.document.body.innerHTML = renderedHtml
67+
68+
const element = window.document.querySelector('relative-time')
69+
expect(element?.getAttribute('format')).toBe('datetime')
70+
expect((element?.textContent ?? '').trim().length).toBeGreaterThan(0)
71+
})
72+
})
73+
5674
test('uses current time when now=true and datetime is omitted', async () => {
5775
vi.useFakeTimers()
5876
vi.setSystemTime(new Date('2030-01-02T03:04:05.000Z'))
@@ -99,6 +117,26 @@ describe('Time (Astro)', () => {
99117
})
100118
})
101119

120+
test('forces a timeZone when provided and appends it in parentheses', async () => {
121+
const Time = (await import('@components/Time/index.astro')).default
122+
123+
const renderedHtml = await container.renderToString(Time, {
124+
props: {
125+
format: 'datetime',
126+
datetime: '2020-01-01T00:00:00Z',
127+
timeZone: 'America/New_York',
128+
},
129+
})
130+
131+
await withJsdomEnvironment(async ({ window }) => {
132+
window.document.body.innerHTML = renderedHtml
133+
134+
const element = window.document.querySelector('relative-time')
135+
expect(element?.getAttribute('time-zone')).toBe('America/New_York')
136+
expect(window.document.body.textContent).toContain('(America/New_York)')
137+
})
138+
})
139+
102140
test('passes through supported attributes to <relative-time>', async () => {
103141
const Time = (await import('@components/Time/index.astro')).default
104142

‎src/components/Time/index.astro‎

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
---
22
type Common = {
3-
format: 'datetime' | 'relative' | 'duration'
3+
format?: 'datetime' | 'relative' | 'duration'
44
tense?: 'auto' | 'past' | 'future'
55
precision?: 'year' | 'month' | 'day' | 'hour' | 'minute' | 'second'
66
threshold?: string
77
prefix?: string
88
formatStyle?: 'long' | 'short' | 'narrow'
9+
timeZone?: string
910
second?: 'numeric' | '2-digit'
1011
minute?: 'numeric' | '2-digit'
1112
hour?: 'numeric' | '2-digit'
@@ -21,21 +22,91 @@ export interface Props extends Common {
2122
now?: boolean
2223
}
2324
24-
const { now, datetime, formatStyle, timeZoneName, ...attrs } = Astro.props as Props
25+
const {
26+
now,
27+
datetime,
28+
format = 'datetime',
29+
formatStyle,
30+
timeZone,
31+
timeZoneName,
32+
weekday,
33+
year,
34+
month,
35+
day,
36+
hour,
37+
minute,
38+
second,
39+
...attrs
40+
} = Astro.props as Props
2541
const resolvedDatetime = datetime ?? (now ? new Date() : undefined)
42+
43+
const resolvedDatetimeString =
44+
resolvedDatetime instanceof Date ? resolvedDatetime.toISOString() : resolvedDatetime
45+
46+
const parsedDate =
47+
typeof resolvedDatetimeString === 'string' ? new Date(resolvedDatetimeString) : undefined
48+
49+
const isValidDate = parsedDate instanceof Date && !Number.isNaN(parsedDate.getTime())
50+
51+
const dateStyleByFormatStyle: Record<NonNullable<Props['formatStyle']>, Intl.DateTimeFormatOptions['dateStyle']> = {
52+
long: 'long',
53+
short: 'short',
54+
narrow: 'short',
55+
}
56+
57+
const hasExplicitParts = Boolean(weekday || year || month || day || hour || minute || second || timeZoneName)
58+
59+
const fallbackText = (() => {
60+
if (!resolvedDatetimeString) {
61+
return ''
62+
}
63+
64+
if (format === 'duration') {
65+
return typeof resolvedDatetimeString === 'string' ? resolvedDatetimeString : ''
66+
}
67+
68+
if (!isValidDate || !parsedDate) {
69+
return ''
70+
}
71+
72+
const options: Intl.DateTimeFormatOptions = hasExplicitParts
73+
? {
74+
weekday,
75+
year,
76+
month,
77+
day,
78+
hour,
79+
minute,
80+
second,
81+
timeZoneName,
82+
timeZone,
83+
}
84+
: {
85+
dateStyle: formatStyle ? dateStyleByFormatStyle[formatStyle] : 'medium',
86+
timeZone,
87+
}
88+
89+
try {
90+
return new Intl.DateTimeFormat(undefined, options).format(parsedDate)
91+
} catch {
92+
return ''
93+
}
94+
})()
2695
---
2796

2897
<script>
2998
import '@github/relative-time-element'
3099
</script>
31100

32101
<relative-time
33-
datetime={resolvedDatetime instanceof Date ? resolvedDatetime.toISOString() : resolvedDatetime}
102+
datetime={resolvedDatetimeString}
34103
{...{
35104
...attrs,
105+
format,
36106
'format-style': formatStyle,
37107
'time-zone-name': timeZoneName,
108+
'time-zone': timeZone,
38109
}}
39110
>
40-
<slot />
41-
</relative-time>
111+
{/** Closing tag must be on same line to prevent trailing space */ }
112+
<slot>{fallbackText}</slot></relative-time>{timeZone ? ` (${timeZone})` : ''}

‎src/content/articles/demo/index.mdx‎

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,6 @@ import Demo from '@components/Test/Demo.mdx'
1313

1414
## Components (looping)
1515

16-
### Social Sharing Component
17-
18-
Add social sharing buttons for the article. The social image is auto-generated, and can be passed as an optional param to override the generated version.
19-
20-
```markdown
21-
<Shares
22-
title="Demo Page"
23-
description="This is just a demo page to show how things work on the site"
24-
url="/articles/demo"
25-
/>
26-
```
27-
28-
<Shares
29-
title="Demo Page"
30-
description="This is just a demo page to show how things work on the site"
31-
url="/articles/demo"
32-
/>
33-
3416
### Testimonials Component
3517

3618
Display customer feedback and testimonials:
@@ -49,6 +31,42 @@ Let's go to the disco <Time datetime="2026-01-01T00:00:00.000Z" />.
4931

5032
Let's go to the disco <Time datetime="2026-01-01T00:00:00.000Z" />.
5133

34+
```markdown
35+
This demonstrates `format="datetime"` explicitly (same as the default): <Time format="datetime" datetime="2026-01-01T00:00:00.000Z" />.
36+
```
37+
38+
This demonstrates `format="datetime"` explicitly (same as the default): <Time format="datetime" datetime="2026-01-01T00:00:00.000Z" />.
39+
40+
```markdown
41+
This demonstrates `formatStyle="short"` for a compact date: <Time format="datetime" formatStyle="short" datetime="2026-01-01T00:00:00.000Z" />.
42+
```
43+
44+
This demonstrates `formatStyle="short"` for a compact date: <Time format="datetime" formatStyle="short" datetime="2026-01-01T00:00:00.000Z" />.
45+
46+
```markdown
47+
This demonstrates showing a time and time zone name: <Time format="datetime" hour="2-digit" minute="2-digit" datetime="2026-01-01T00:00:00.000Z" />.
48+
```
49+
50+
This demostrates showing a time that should adjust to user's local timezone: <Time format="datetime" hour="2-digit" minute="2-digit" datetime="2026-01-01T00:00:00.000Z" />.
51+
52+
```markdown
53+
This demonstrates showing a time and time zone name: <Time format="datetime" hour="2-digit" minute="2-digit" timeZoneName="short" datetime="2026-01-01T00:00:00.000Z" />.
54+
```
55+
56+
This demostrates showing a time and time zone name: <Time format="datetime" hour="2-digit" minute="2-digit" timeZoneName="short" datetime="2026-01-01T00:00:00.000Z" />.
57+
58+
```markdown
59+
This demonstrates `format="relative"` with `tense="past"` and `precision="day"`: <Time format="relative" tense="past" precision="day" datetime="2020-01-01T00:00:00Z" />.
60+
```
61+
62+
This demonstrates `format="relative"` with `tense="past"` and `precision="day"`: <Time format="relative" tense="past" precision="day" datetime="2020-01-01T00:00:00Z" />.
63+
64+
```markdown
65+
This demonstrates `now` (uses the current time at render): <Time format="datetime" now />.
66+
```
67+
68+
This demonstrates `now` (uses the current time at render): <Time format="datetime" now />.
69+
5270
## GFM
5371

5472
### Footnote
@@ -85,7 +103,11 @@ The references can be anywhere in the document, before or after their use:
85103

86104
### Strikethrough
87105

88-
`~one~` or `~~two~~` tildes: ~one~ ~~two~~
106+
`~~two~~` tildes: ~~two~~
107+
108+
### Subscript
109+
110+
`~one~` tilde: Subscript~one~
89111

90112
### Table
91113

@@ -1240,3 +1262,21 @@ Use SVG sprites for icons throughout your content:
12401262
Encourage readers to subscribe to your newsletter:
12411263

12421264
<Newsletter />
1265+
1266+
### Social Sharing Component
1267+
1268+
Add social sharing buttons for the article. The social image is auto-generated, and can be passed as an optional param to override the generated version.
1269+
1270+
```markdown
1271+
<Shares
1272+
title="Demo Page"
1273+
description="This is just a demo page to show how things work on the site"
1274+
url="/articles/demo"
1275+
/>
1276+
```
1277+
1278+
<Shares
1279+
title="Demo Page"
1280+
description="This is just a demo page to show how things work on the site"
1281+
url="/articles/demo"
1282+
/>

‎src/content/testimonials/english-first/index.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ publishDate: 2021-03-31
55
tags: []
66
---
77

8-
We have had a web presence since 2006. When advancements in web technology passed our skill levels we hired various developers, but there was always a certain amount of monitoring and adjustment required to achieve the proper result. Working with Kevin has been a refreshing change. Kevin is an authority on web development and is clearly in the upper percentile in terms of developer qualifications. Kevin's invaluable suggestions and improvements helped create a stable, cross platform site.
8+
We have had a web presence since 2006. When advancements in web technology passed our skill levels we hired various developers, but there was always a certain amount of monitoring and adjustment required to achieve the proper result. Working with Kevin has been a refreshing change.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getVisibleCharacterCount, stripFrontmatter } from '../index'
3+
4+
describe('TestimonialsLengthWarning', () => {
5+
describe('stripFrontmatter', () => {
6+
it('returns original string when no frontmatter exists', () => {
7+
expect(stripFrontmatter('Hello world')).toBe('Hello world')
8+
})
9+
10+
it('removes YAML frontmatter and trims body', () => {
11+
const raw = [
12+
'---',
13+
'name: Test',
14+
'---',
15+
'',
16+
'This is the body.',
17+
'',
18+
].join('\n')
19+
20+
expect(stripFrontmatter(raw)).toBe('This is the body.')
21+
})
22+
23+
it('returns original string when frontmatter is unterminated', () => {
24+
const raw = ['---', 'name: Test', 'This is still frontmatter'].join('\n')
25+
expect(stripFrontmatter(raw)).toBe(raw)
26+
})
27+
})
28+
29+
describe('getVisibleCharacterCount', () => {
30+
it('normalizes whitespace before counting', () => {
31+
const text = ' Hello\n\nworld\t '
32+
expect(getVisibleCharacterCount(text)).toBe('Hello world'.length)
33+
})
34+
35+
it('returns 0 for blank strings', () => {
36+
expect(getVisibleCharacterCount(' \n\n\t')).toBe(0)
37+
})
38+
})
39+
})

0 commit comments

Comments
 (0)