Skip to content

Commit 6cbc061

Browse files
committed
feat: match import pages media by filename
1 parent 2352add commit 6cbc061

3 files changed

Lines changed: 150 additions & 219 deletions

File tree

public/sitemap.xml

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
3-
<url><loc>http://localhost:3000</loc><lastmod>2026-05-06T16:06:29.743Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en"/><xhtml:link rel="alternate" hreflang="de" href="http://localhost:3000/de"/></url>
4-
<url><loc>http://localhost:3000/robots.txt</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/robots.txt"/><xhtml:link rel="alternate" hreflang="de" href="http://localhost:3000/de/robots.txt"/></url>
5-
<url><loc>http://localhost:3000/en</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>1</priority></url>
6-
<url><loc>http://localhost:3000/en/about-us</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
7-
<url><loc>http://localhost:3000/en/actions</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
8-
<url><loc>http://localhost:3000/en/blog</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
9-
<url><loc>http://localhost:3000/en/community-edition</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
10-
<url><loc>http://localhost:3000/en/contact</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
11-
<url><loc>http://localhost:3000/en/enterprise-edition</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
12-
<url><loc>http://localhost:3000/en/features</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
13-
<url><loc>http://localhost:3000/en/jobs</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
14-
<url><loc>http://localhost:3000/en/legal-notice</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
15-
<url><loc>http://localhost:3000/en/privacy</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
16-
<url><loc>http://localhost:3000/en/subscription</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
17-
<url><loc>http://localhost:3000/en/terms</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
18-
<url><loc>http://localhost:3000/de</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>1</priority></url>
19-
<url><loc>http://localhost:3000/de/about-us</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
20-
<url><loc>http://localhost:3000/de/actions</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
21-
<url><loc>http://localhost:3000/de/blog</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
22-
<url><loc>http://localhost:3000/de/community-edition</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
23-
<url><loc>http://localhost:3000/de/contact</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
24-
<url><loc>http://localhost:3000/de/enterprise-edition</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
25-
<url><loc>http://localhost:3000/de/features</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
26-
<url><loc>http://localhost:3000/de/jobs</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
27-
<url><loc>http://localhost:3000/de/legal-notice</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
28-
<url><loc>http://localhost:3000/de/privacy</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
29-
<url><loc>http://localhost:3000/de/subscription</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
30-
<url><loc>http://localhost:3000/de/terms</loc><lastmod>2026-05-06T16:06:29.744Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
3+
<url><loc>http://localhost:3000</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en"/><xhtml:link rel="alternate" hreflang="de" href="http://localhost:3000/de"/></url>
4+
<url><loc>http://localhost:3000/robots.txt</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority><xhtml:link rel="alternate" hreflang="en" href="http://localhost:3000/en/robots.txt"/><xhtml:link rel="alternate" hreflang="de" href="http://localhost:3000/de/robots.txt"/></url>
5+
<url><loc>http://localhost:3000/en</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>1</priority></url>
6+
<url><loc>http://localhost:3000/en/about-us</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
7+
<url><loc>http://localhost:3000/en/actions</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
8+
<url><loc>http://localhost:3000/en/blog</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
9+
<url><loc>http://localhost:3000/en/community-edition</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
10+
<url><loc>http://localhost:3000/en/contact</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
11+
<url><loc>http://localhost:3000/en/enterprise-edition</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
12+
<url><loc>http://localhost:3000/en/features</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
13+
<url><loc>http://localhost:3000/en/jobs</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
14+
<url><loc>http://localhost:3000/en/legal-notice</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
15+
<url><loc>http://localhost:3000/en/privacy</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
16+
<url><loc>http://localhost:3000/en/subscription</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
17+
<url><loc>http://localhost:3000/en/terms</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
18+
<url><loc>http://localhost:3000/de</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>1</priority></url>
19+
<url><loc>http://localhost:3000/de/about-us</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
20+
<url><loc>http://localhost:3000/de/actions</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
21+
<url><loc>http://localhost:3000/de/blog</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
22+
<url><loc>http://localhost:3000/de/community-edition</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
23+
<url><loc>http://localhost:3000/de/contact</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
24+
<url><loc>http://localhost:3000/de/enterprise-edition</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
25+
<url><loc>http://localhost:3000/de/features</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
26+
<url><loc>http://localhost:3000/de/jobs</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
27+
<url><loc>http://localhost:3000/de/legal-notice</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
28+
<url><loc>http://localhost:3000/de/privacy</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
29+
<url><loc>http://localhost:3000/de/subscription</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
30+
<url><loc>http://localhost:3000/de/terms</loc><lastmod>2026-05-06T18:07:33.874Z</lastmod><changefreq>weekly</changefreq><priority>0.7</priority></url>
3131
</urlset>

scripts/import-pages.ts

Lines changed: 35 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,218 +1,68 @@
1-
import fs from 'node:fs/promises'
2-
import path from 'node:path'
3-
import { fileURLToPath } from 'node:url'
4-
import nextEnv from '@next/env'
5-
import { getPayload } from 'payload'
6-
import type { Payload } from 'payload'
7-
8-
type ImportPage = {
9-
id?: number | string
10-
slug?: string
11-
title?: unknown
12-
layout?: unknown
13-
[key: string]: unknown
14-
}
15-
16-
type ImportLocale = 'en' | 'de'
1+
import fs from "node:fs/promises"
2+
import path from "node:path"
3+
import { fileURLToPath } from "node:url"
4+
import nextEnv from "@next/env"
5+
import { getPayload } from "payload"
6+
import type { Payload } from "payload"
7+
import { importPages, normalizeImportLocale, normalizePageImportDocs } from "../src/lib/importPages"
178

189
const filename = fileURLToPath(import.meta.url)
1910
const dirname = path.dirname(filename)
2011

2112
const args = process.argv.slice(2)
22-
const positional = args.filter((arg) => !arg.startsWith('--'))
23-
const inputPath = path.resolve(process.cwd(), positional[0] ?? 'pages.json')
24-
const localeArg = args.find((arg) => arg.startsWith('--locale='))
25-
const localeInput = localeArg?.split('=')[1] || process.env.PAYLOAD_IMPORT_LOCALE || 'en'
26-
const dryRun = args.includes('--dry-run')
27-
28-
const systemFields = new Set(['id', 'createdAt', 'updatedAt'])
29-
const supportedLocales = new Set(['en', 'de'])
30-
31-
if (!supportedLocales.has(localeInput)) {
32-
throw new Error(`Unsupported locale "${localeInput}". Expected "en" or "de".`)
33-
}
34-
35-
const locale = localeInput as ImportLocale
13+
const positional = args.filter((arg) => !arg.startsWith("--"))
14+
const inputPath = path.resolve(process.cwd(), positional[0] ?? "pages.json")
15+
const localeArg = args.find((arg) => arg.startsWith("--locale="))
16+
const locale = normalizeImportLocale(localeArg?.split("=")[1] || process.env.PAYLOAD_IMPORT_LOCALE || "en")
17+
const dryRun = args.includes("--dry-run")
3618

3719
const toErrorDetails = (error: unknown) => {
3820
if (error instanceof Error) {
3921
return {
4022
message: error.message,
4123
name: error.name,
4224
stack: error.stack,
43-
...(typeof error === 'object' ? error : {}),
25+
...(typeof error === "object" ? error : {}),
4426
}
4527
}
4628

4729
return error
4830
}
4931

50-
const normalizeDocs = (raw: unknown): ImportPage[] => {
51-
if (Array.isArray(raw)) {
52-
return raw as ImportPage[]
53-
}
54-
55-
if (
56-
raw &&
57-
typeof raw === 'object' &&
58-
'docs' in raw &&
59-
Array.isArray((raw as { docs?: unknown }).docs)
60-
) {
61-
return (raw as { docs: ImportPage[] }).docs
62-
}
63-
64-
throw new Error('Expected pages.json to contain an array or an object with a docs array.')
65-
}
66-
67-
const removeSystemFields = (doc: ImportPage) => {
68-
return Object.fromEntries(Object.entries(doc).filter(([key]) => !systemFields.has(key)))
69-
}
70-
71-
const isRecord = (value: unknown): value is Record<string, unknown> => {
72-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
73-
}
74-
75-
const isPayloadRelationshipObject = (value: Record<string, unknown>) => {
76-
return (
77-
'id' in value &&
78-
!('blockType' in value) &&
79-
(
80-
'relationTo' in value ||
81-
'filename' in value ||
82-
'mimeType' in value ||
83-
'filesize' in value
84-
)
85-
)
86-
}
87-
88-
const isLocalizedValue = (value: Record<string, unknown>) => {
89-
const keys = Object.keys(value)
90-
91-
return keys.length > 0 && keys.every((key) => supportedLocales.has(key))
92-
}
93-
94-
const normalizeRelationships = (value: unknown): unknown => {
95-
if (Array.isArray(value)) {
96-
return value.map((item) => normalizeRelationships(item))
97-
}
98-
99-
if (!isRecord(value)) {
100-
return value
101-
}
102-
103-
if (isPayloadRelationshipObject(value)) {
104-
return value.id
105-
}
106-
107-
if (isLocalizedValue(value)) {
108-
return normalizeRelationships(value[locale] ?? value.en ?? value.de)
109-
}
110-
111-
return Object.fromEntries(
112-
Object.entries(value).map(([key, nestedValue]) => [key, normalizeRelationships(nestedValue)]),
113-
)
114-
}
115-
11632
const main = async () => {
11733
nextEnv.loadEnvConfig(process.cwd())
11834

119-
const { default: config } = await import('../src/payload.config')
120-
const raw = await fs.readFile(inputPath, 'utf8')
121-
const docs = normalizeDocs(JSON.parse(raw))
35+
const { default: config } = await import("../src/payload.config")
36+
const raw = await fs.readFile(inputPath, "utf8")
37+
const docs = normalizePageImportDocs(JSON.parse(raw))
12238
let payload: Payload | undefined
123-
const results: Array<{ id?: number | string; slug?: string; status: string; error?: unknown }> = []
12439

12540
try {
12641
payload = await getPayload({ config })
42+
const result = await importPages({
43+
docs,
44+
dryRun,
45+
locale,
46+
payload,
47+
})
48+
49+
console.log(
50+
JSON.stringify(
51+
{
52+
file: path.relative(path.resolve(dirname, ".."), inputPath),
53+
...result,
54+
},
55+
null,
56+
2,
57+
),
58+
)
12759

128-
for (const doc of docs) {
129-
if (!doc.slug || typeof doc.slug !== 'string') {
130-
results.push({
131-
id: doc.id,
132-
status: 'rejected',
133-
error: 'Missing required string field: slug',
134-
})
135-
continue
136-
}
137-
138-
const data = normalizeRelationships(removeSystemFields(doc)) as any
139-
140-
try {
141-
const existing = await payload.find({
142-
collection: 'pages',
143-
depth: 0,
144-
limit: 1,
145-
locale,
146-
overrideAccess: true,
147-
where: {
148-
slug: {
149-
equals: doc.slug,
150-
},
151-
},
152-
})
153-
154-
if (dryRun) {
155-
results.push({
156-
id: existing.docs[0]?.id ?? doc.id,
157-
slug: doc.slug,
158-
status: existing.totalDocs > 0 ? 'would-update' : 'would-create',
159-
})
160-
continue
161-
}
162-
163-
if (existing.docs[0]) {
164-
const updated = await payload.update({
165-
id: existing.docs[0].id,
166-
collection: 'pages',
167-
data,
168-
locale,
169-
overrideAccess: true,
170-
})
171-
172-
results.push({ id: updated.id, slug: doc.slug, status: 'updated' })
173-
} else {
174-
const created = await payload.create({
175-
collection: 'pages',
176-
data,
177-
locale,
178-
overrideAccess: true,
179-
})
180-
181-
results.push({ id: created.id, slug: doc.slug, status: 'created' })
182-
}
183-
} catch (error) {
184-
results.push({
185-
id: doc.id,
186-
slug: doc.slug,
187-
status: 'rejected',
188-
error: toErrorDetails(error),
189-
})
190-
}
60+
if (result.rejected > 0) {
61+
process.exitCode = 1
19162
}
19263
} finally {
19364
await payload?.db?.destroy?.()
19465
}
195-
196-
const rejected = results.filter((result) => result.status === 'rejected')
197-
198-
console.log(
199-
JSON.stringify(
200-
{
201-
file: path.relative(path.resolve(dirname, '..'), inputPath),
202-
locale,
203-
dryRun,
204-
total: results.length,
205-
rejected: rejected.length,
206-
results,
207-
},
208-
null,
209-
2,
210-
),
211-
)
212-
213-
if (rejected.length > 0) {
214-
process.exitCode = 1
215-
}
21666
}
21767

21868
main().catch((error) => {

0 commit comments

Comments
 (0)