Skip to content

Commit 9f133c6

Browse files
davestewartclaude
andauthored
v3.0.1: fixes from CodeRabbit review of #108 (#111)
- walkMeta only skips identity/AST keys at the document root, so nested fields like `download.path` are rewritten - Keep query strings on generated srcset candidates - Live reload: split srcset on commas, skip src busting on srcset-only elements, and reload media elements when a <source> changes Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 535133f commit 9f133c6

9 files changed

Lines changed: 65 additions & 26 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
### v3.0.1
4+
5+
- Rewrite nested frontmatter and data fields named `path`, `id`, `stem` or `extension` (previously skipped at any depth, not just the document root)
6+
- Keep query strings on generated `srcset` candidates, not just `src`
7+
- Live reload: fix busting authored `srcset`s without spaces or descriptors, don't add an empty `src` to srcset-only elements, and reload `<video>` / `<audio>` when a `<source>` changes
8+
39
### v3.0.0
410

511
Nuxt Content 3 support. The module now targets `@nuxt/content` v3 only; for Nuxt Content 2 keep using `nuxt-content-assets@1`.

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nuxt-content-assets",
3-
"version": "3.0.0",
3+
"version": "3.0.1",
44
"description": "Enable locally-located assets in Nuxt Content",
55
"repository": {
66
"type": "git",

‎src/build/assets.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ export function makeAssetIndex (publicPath: string, srcset: SrcsetOptions | fals
5959
if (!asset) {
6060
return
6161
}
62-
const resolved: ResolvedAsset = { ...asset }
63-
if (srcset && isImage(absSrc)) {
64-
Object.assign(resolved, getSrcset(absSrc, asset, path => assets.get(path), srcset))
65-
}
62+
// keep any query on src and srcset candidates
6663
const query = parseQuery(value)
67-
if (query) {
68-
resolved.srcAttr += query
64+
const withQuery = (asset: AssetConfig): AssetConfig => ({ ...asset, srcAttr: asset.srcAttr + query })
65+
const resolved: ResolvedAsset = withQuery(asset)
66+
if (srcset && isImage(absSrc)) {
67+
Object.assign(resolved, getSrcset(absSrc, resolved, (path) => {
68+
const variant = assets.get(path)
69+
return variant && withQuery(variant)
70+
}, srcset))
6971
}
7072
return resolved
7173
}

‎src/runtime/plugin.client.ts‎

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ export default defineNuxtPlugin(() => {
1818
const time = String(Date.now())
1919
const selector = `:is(img, video, source, embed, iframe):where([src*="${src}"], [srcset*="${src}"])`
2020

21+
// media elements only pick up a changed <source> when reloaded
22+
const media = new Set<HTMLMediaElement>()
23+
2124
document.querySelectorAll(selector).forEach((el: any) => {
2225
// dim if deleted
2326
el.style.opacity = isUpdate ? '1' : '0.2'
@@ -40,25 +43,40 @@ export default defineNuxtPlugin(() => {
4043
}
4144

4245
// bust src (nuxt image serves from /_ipx/..., so keep whatever path is there)
43-
const [path, query] = String(el.getAttribute('src') || '').split('?')
44-
const params = new URLSearchParams(query)
45-
params.set('time', time)
46-
if (width && height && params.get('width')) {
47-
params.set('width', String(width))
48-
params.set('height', String(height))
46+
const srcAttr = el.getAttribute('src')
47+
if (srcAttr) {
48+
const [path, query] = srcAttr.split('?')
49+
const params = new URLSearchParams(query)
50+
params.set('time', time)
51+
if (width && height && params.get('width')) {
52+
params.set('width', String(width))
53+
params.set('height', String(height))
54+
}
55+
el.setAttribute('src', `${path}?${params.toString()}`)
4956
}
50-
el.setAttribute('src', `${path}?${params.toString()}`)
5157

5258
// bust srcset
5359
const srcset = el.getAttribute('srcset')
5460
if (srcset) {
55-
el.setAttribute('srcset', srcset.replace(/(\S+)(\s+\S+)?/g, (_match: string, url: string, descriptor = '') => {
56-
const [base, q] = url.split('?')
57-
const p = new URLSearchParams(q)
58-
p.set('time', time)
59-
return `${base}?${p.toString()}${descriptor}`
60-
}))
61+
el.setAttribute('srcset', srcset
62+
.split(',')
63+
.map((candidate: string) => candidate.trim())
64+
.filter(Boolean)
65+
.map((candidate: string) => {
66+
const [url, ...descriptors] = candidate.split(/\s+/)
67+
const [base, q] = url.split('?')
68+
const p = new URLSearchParams(q)
69+
p.set('time', time)
70+
return [`${base}?${p.toString()}`, ...descriptors].join(' ')
71+
})
72+
.join(', '))
73+
}
74+
75+
if (el.tagName === 'SOURCE' && el.parentElement instanceof HTMLMediaElement) {
76+
media.add(el.parentElement)
6177
}
6278
})
79+
80+
media.forEach(el => el.load())
6381
})
6482
})

‎src/runtime/utils/content.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ export interface ElementView {
1515
const nonMetaKeys = ['body', 'excerpt', 'rawbody', '__metadata', 'id', 'path', 'stem', 'extension']
1616

1717
/**
18-
* Walk parsed content fields (frontmatter and schema fields), skipping ASTs and identity fields
18+
* Walk parsed content fields (frontmatter and schema fields), skipping root-level ASTs and identity fields
1919
*/
2020
export function walkMeta (content: Record<string, any>, callback: WalkCallback) {
21-
walk(content, callback, (value, key) => !(typeof key === 'string' && nonMetaKeys.includes(key)))
21+
walk(content, callback, (value, key, parent) => !(parent === content && typeof key === 'string' && nonMetaKeys.includes(key)))
2222
}
2323

2424
/**

‎src/runtime/utils/object.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export type Walkable = { [key: string | number]: any }
22

3-
export type WalkFilter = (value: any, key?: string | number) => boolean | undefined
3+
export type WalkFilter = (value: any, key?: string | number, parent?: Walkable) => boolean | undefined
44

55
export type WalkCallback = (value: any, parent: Walkable, key: string | number) => void
66

@@ -15,7 +15,7 @@ export function walk (node: any, callback: WalkCallback, filter?: WalkFilter): v
1515
function visit (node: any, callback: WalkCallback, parent: Walkable, key: string | number) {
1616
// filter
1717
if (filter) {
18-
const result = filter(node, key)
18+
const result = filter(node, key, parent)
1919
if (result === false) {
2020
return
2121
}

‎test/unit/content.spec.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ describe('content', () => {
4545
expect(values).toEqual(['Title', 'image.jpg', 'a.jpg', 'b.jpg', 2, 'og.jpg'])
4646
})
4747

48+
it('should only skip identity keys at the root', () => {
49+
const values: any[] = []
50+
walkMeta({ path: '/', download: { path: 'file.pdf', id: 'a.jpg' }, links: [{ stem: 'b.jpg' }] }, value => values.push(value))
51+
expect(values).toEqual(['file.pdf', 'a.jpg', 'b.jpg'])
52+
})
53+
4854
it('should walk minimark bodies including headings but not code', () => {
4955
const tags: string[] = []
5056
walkBody(content.body, node => tags.push(`${node.tag}:${node.props.src || node.props.href}`))

‎test/unit/process.spec.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ describe('process', () => {
6464
sizes: '(max-width: 400px) 100vw, 400px',
6565
})
6666
expect(p2[2][1].src).toBe('/posts/image.png?x=1')
67+
expect(p2[2][1].srcset).toBe('/posts/image.png?x=1 400w, /posts/image@2x.png?x=1 800w')
6768
expect(p2[2][1].style).toBe('color: red; aspect-ratio: 400/300;')
6869
expect(p3[2][1]).toEqual({ href: '/posts/file.pdf', target: '_blank' })
6970
expect(video[1]).toEqual({ src: '/posts/media/video.mp4' })
@@ -86,6 +87,12 @@ describe('process', () => {
8687
expect(content.image).toBe('/posts/image.png')
8788
})
8889

90+
it('should rewrite nested frontmatter keys that share identity key names', () => {
91+
const content = { path: '/posts', download: { path: 'file.pdf' } }
92+
processContent('/c/posts/index.md', content, makeIndex(), [])
93+
expect(content).toEqual({ path: '/posts', download: { path: '/posts/file.pdf' } })
94+
})
95+
8996
it('should keep an authored srcset', () => {
9097
const content = { body: { type: 'minimark', value: [['img', { src: 'image.png', srcSet: 'custom.png 1x' }]] } }
9198
processContent('/c/posts/index.md', content, makeIndex(), [])

0 commit comments

Comments
 (0)