Skip to content

Commit 127ad59

Browse files
committed
refactor: reworked nuxt module with validation, dedupe, and more
1 parent 7835ef2 commit 127ad59

File tree

5 files changed

+151
-38
lines changed

5 files changed

+151
-38
lines changed

TODOs.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
# TODOs
22

33
<!-- TODO: A todo within the TODOs!!! This is a temporary file to track work since there are so many of us mucking about on this! REMOVE BEFORE FINALIZING THE PR!!!! -->
4+
<!-- TODO: hackmd - https://hackmd.io/@e7pGbT7hTrONe4H59uAIPg/ByMdCwoIbg -->
45

56
---
67

78
## In Flight
89

9-
- Blog list UI - fix posts width
10-
- Blog post UI - (Started - needs more polish)
10+
- Blog list/post UI - (Started - needs more polish)
1111
- OAuth
1212
- Standard site push - Mock PDS push for now
1313
- constellation - bsky API
14-
- Docs Run them locally with `pnpm dev:docs`.
14+
- Docs - Run them locally with `pnpm dev:docs`.
1515

1616
---
1717

@@ -22,7 +22,7 @@
2222
- bsky posts stuff - Bluesky comments
2323
- How does i18n deal with dynamic values? $t('blog.post.title'),
2424
- blog publishing for https://bsky.app/profile/npmx.dev - cli/actions pipeline
25-
- site.standard.publication lexicon - decales it's a blog on atproto can be manual setup
25+
- site.standard.publication lexicon - declares it's a blog on atproto can be manual setup
2626
- site.standard.document - publishes everytime there's a new blog entry.
2727
- Proposed: the pipeline takes login pds, handle, and app_password as secrets. Checks to see if one has already been published for that blog post. If so does not push it. If it hasn't then it creates the atproto record when deploying/building by logging in. check if an article has been created by parsing the createdate of the blog post to a TID, do a getRecord. if it's there dont publish the new one. If it isnt send it up. I wrote about tid concept here
2828
https://marvins-guide.leaflet.pub/3mckm76mfws2h

app/pages/blog/test-fail.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
author: 'Daniel Roe'
3+
title: 'TEST FAIL'
4+
tags: ['OpenSource', 'Nuxt']
5+
excerpt: 'My first post'
6+
date: '2026-01-28'
7+
slug: 'first-post'
8+
description: 'I was made to test this nuxt module'
9+
draft: false
10+
---
11+
12+
# TEST FAIL

modules/standard-site-sync.ts

Lines changed: 120 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,128 @@
1-
import { defineNuxtModule, useNuxt } from 'nuxt/kit'
1+
import { readFileSync } from 'node:fs'
2+
import { createHash } from 'node:crypto'
3+
import { defineNuxtModule, useNuxt, createResolver } from 'nuxt/kit'
4+
import { safeParse } from 'valibot'
25
import * as site from '../shared/types/lexicons/site'
6+
import { BlogPostSchema } from '../shared/schemas/blog'
7+
import { NPMX_SITE } from '../shared/utils/constants'
38

4-
const PUBLICATION_SITE = 'https://npmx.dev'
9+
const syncedDocuments = new Map<string, string>()
510

611
export default defineNuxtModule({
7-
meta: {
8-
name: 'standard-site-sync',
9-
},
10-
setup() {
12+
meta: { name: 'standard-site-sync' },
13+
async setup() {
1114
const nuxt = useNuxt()
12-
if (nuxt.options._prepare) {
13-
return
14-
}
15-
nuxt.hook('content:file:afterParse', ctx => {
16-
const { content } = ctx
17-
18-
const document = site.standard.document.$build({
19-
site: PUBLICATION_SITE,
20-
path: content.path as string,
21-
title: content.title as string,
22-
description: (content.excerpt || content.description) as string | undefined,
23-
tags: content.tags as string[] | undefined,
24-
publishedAt: new Date(content.date as string).toISOString(),
25-
})
26-
27-
// TODO: Mock PDS push
28-
console.log('[standard-site-sync] Would push:', JSON.stringify(document, null, 2))
15+
const { resolve } = createResolver(import.meta.url)
16+
const contentDir = resolve('../app/pages/blog')
17+
18+
if (nuxt.options._prepare) return
19+
20+
nuxt.hook('build:before', async () => {
21+
const glob = await import('fast-glob').then(m => m.default)
22+
const files = await glob(`${contentDir}/**/*.md`)
23+
24+
for (const file of files) {
25+
await syncFile(file, NPMX_SITE)
26+
}
27+
})
28+
29+
nuxt.hook('builder:watch', async (_event, path) => {
30+
if (path.endsWith('.md')) {
31+
await syncFile(resolve(nuxt.options.rootDir, path), NPMX_SITE)
32+
}
2933
})
3034
},
3135
})
36+
37+
// TODO: Placeholder algo, can likely be simplified
38+
function parseBasicFrontmatter(fileContent: string): Record<string, any> {
39+
const match = fileContent.match(/^---\r?\n([\s\S]+?)\r?\n---/)
40+
if (!match) return {}
41+
42+
const obj: Record<string, any> = {}
43+
const lines = match[1]?.split('\n')
44+
45+
if (!lines) return {}
46+
47+
for (const line of lines) {
48+
const [key, ...valParts] = line.split(':')
49+
if (key && valParts.length) {
50+
let value = valParts.join(':').trim()
51+
52+
// Remove surrounding quotes
53+
value = value.replace(/^["']|["']$/g, '')
54+
55+
// Handle Booleans
56+
if (value === 'true') {
57+
obj[key.trim()] = true
58+
continue
59+
}
60+
if (value === 'false') {
61+
obj[key.trim()] = false
62+
continue
63+
}
64+
65+
// Handle basic array [tag1, tag2]
66+
if (value.startsWith('[') && value.endsWith(']')) {
67+
obj[key.trim()] = value
68+
.slice(1, -1)
69+
.split(',')
70+
.map(s => s.trim().replace(/^["']|["']$/g, ''))
71+
} else {
72+
obj[key.trim()] = value
73+
}
74+
}
75+
}
76+
return obj
77+
}
78+
79+
const syncFile = async (filePath: string, siteUrl: string) => {
80+
try {
81+
const fileContent = readFileSync(filePath, 'utf-8')
82+
const frontmatter = parseBasicFrontmatter(fileContent)
83+
84+
// Schema expects 'path' & frontmatter provides 'slug'
85+
if (frontmatter.slug) {
86+
frontmatter.path = `/blog/${frontmatter.slug}`
87+
}
88+
89+
const result = safeParse(BlogPostSchema, frontmatter)
90+
if (!result.success) {
91+
console.warn(`[standard-site-sync] Validation failed for ${filePath}`, result.issues)
92+
return
93+
}
94+
95+
const data = result.output
96+
97+
// filter drafts
98+
if (data.draft) {
99+
if (process.env.DEBUG === 'true') {
100+
console.debug(`[standard-site-sync] Skipping draft: ${data.path}`)
101+
}
102+
return
103+
}
104+
105+
const hash = createHash('sha1').update(JSON.stringify(data)).digest('hex')
106+
107+
if (syncedDocuments.get(data.path) === hash) {
108+
return
109+
}
110+
111+
// TODO: Review later
112+
const document = site.standard.document.$build({
113+
site: siteUrl as `${string}:${string}`,
114+
path: data.path,
115+
title: data.title,
116+
description: data.description ?? data.excerpt,
117+
tags: data.tags,
118+
publishedAt: new Date(data.date).toISOString(),
119+
})
120+
121+
console.log('[standard-site-sync] Pushing:', JSON.stringify(document, null, 2))
122+
// TODO: Real PDS push
123+
124+
syncedDocuments.set(data.path, hash)
125+
} catch (error) {
126+
console.error(`[standard-site-sync] Error in ${filePath}:`, error)
127+
}
128+
}

shared/schemas/blog.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1-
import * as v from 'valibot'
1+
import { object, string, optional, array, boolean, pipe, isoDate } from 'valibot'
2+
import type { InferOutput } from 'valibot'
23

3-
export const BlogPostSchema = v.object({
4-
author: v.string(),
5-
title: v.string(),
6-
date: v.string(),
7-
description: v.string(),
8-
slug: v.string(),
9-
excerpt: v.optional(v.string()),
10-
tags: v.optional(v.array(v.string())),
11-
draft: v.optional(v.boolean()),
4+
export const BlogPostSchema = object({
5+
author: string(),
6+
title: string(),
7+
date: pipe(string(), isoDate()),
8+
description: string(),
9+
path: string(),
10+
excerpt: optional(string()),
11+
tags: optional(array(string())),
12+
draft: optional(boolean()),
1213
})
1314

1415
/**
1516
* Inferred type for blog post frontmatter
1617
*/
1718
/** @public */
18-
export type BlogPostFrontmatter = v.InferOutput<typeof BlogPostSchema>
19+
export type BlogPostFrontmatter = InferOutput<typeof BlogPostSchema>

shared/utils/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ export const CACHE_MAX_AGE_ONE_DAY = 60 * 60 * 24
55
export const CACHE_MAX_AGE_ONE_YEAR = 60 * 60 * 24 * 365
66

77
// API Strings
8+
// // shared/utils/constants.ts
9+
export const NPMX_SITE: `${string}:${string}` = 'https://npmx.dev'
10+
// export const NPMX_SITE = 'https://npmx.dev'
811
export const NPM_REGISTRY = 'https://registry.npmjs.org'
912
export const ERROR_PACKAGE_ANALYSIS_FAILED = 'Failed to analyze package.'
1013
export const ERROR_PACKAGE_VERSION_AND_FILE_FAILED = 'Version and file path are required.'

0 commit comments

Comments
 (0)