Pārlūkot izejas kodu

feat: author/posts/gallery seed, local media, Docker media mount
- Extended the seed script (added authros, posts and gallery data)
- Cache uploads per filename (ensureMedia) and clear posts/gallery/authors
before reseeding
- docker-compose: bind ./media to payload container; CI=true on seed service
- Remove @payloadcms/storage-vercel-blob; media stays on disk (./media)

YusufSyam 1 mēnesi atpakaļ
vecāks
revīzija
515f723fb1

+ 1 - 2
.env.example

@@ -2,8 +2,7 @@
 # Inside Docker Compose network only: host can be the service name "postgres".
 DATABASE_URL=postgresql://postgres:password123@127.0.0.1:5432/postgres
 PAYLOAD_SECRET=your_payload_secret
-NODE_ENV='development'
-BLOB_READ_WRITE_TOKEN=your_vercel_blob_token
+NODE_ENV=development
 
 # Contact endpoint configuration
 CONTACT_TO_EMAIL=team@example.com

+ 5 - 0
docker-compose.yml

@@ -7,6 +7,10 @@ services:
     restart: unless-stopped
     ports:
       - '3000:3000'
+    # Seed (and local uploads) write files to ./media on the host. The app image has no project
+    # checkout — without this mount, DB rows point to paths like /app/media/*.png that do not exist.
+    volumes:
+      - ./media:/app/media
     depends_on:
       postgres:
         condition: service_healthy
@@ -48,6 +52,7 @@ services:
       - .env
     environment:
       NODE_ENV: development
+      CI: "true"
     depends_on:
       postgres:
         condition: service_healthy

+ 0 - 1
package.json

@@ -22,7 +22,6 @@
     "@payloadcms/db-postgres": "3.70.0",
     "@payloadcms/next": "3.70.0",
     "@payloadcms/richtext-lexical": "3.70.0",
-    "@payloadcms/storage-vercel-blob": "3.70.0",
     "@payloadcms/ui": "3.70.0",
     "cross-env": "^7.0.3",
     "dotenv": "16.4.7",

+ 8 - 205
pnpm-lock.yaml

@@ -17,9 +17,6 @@ importers:
       '@payloadcms/richtext-lexical':
         specifier: 3.70.0
         version: 3.70.0(@faceless-ui/modal@3.0.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@payloadcms/next@3.70.0(@types/react@19.2.1)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)(yjs@13.6.29)
-      '@payloadcms/storage-vercel-blob':
-        specifier: 3.70.0
-        version: 3.70.0(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)
       '@payloadcms/ui':
         specifier: 3.70.0
         version: 3.70.0(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)
@@ -819,10 +816,6 @@ packages:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
 
-  '@fastify/busboy@2.1.1':
-    resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
-    engines: {node: '>=14'}
-
   '@floating-ui/core@1.7.3':
     resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
 
@@ -1350,13 +1343,6 @@ packages:
       next: ^15.4.10
       payload: 3.70.0
 
-  '@payloadcms/plugin-cloud-storage@3.70.0':
-    resolution: {integrity: sha512-4yLLrqEG0QNCm+cBAWxdXQYEyBryWJuXevpTBUp7f1CW9KbObZH72NSGhuBG09ZOL6T6lYiMOcg/UYX7H8IgqA==}
-    peerDependencies:
-      payload: 3.70.0
-      react: ^19.0.1 || ^19.1.2 || ^19.2.1
-      react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
-
   '@payloadcms/richtext-lexical@3.70.0':
     resolution: {integrity: sha512-2f6toXSqpsVnMcf5AO9EcNztN0aUoQLmWe0onLXXNHyGDB32rPgWnxxOjrRxKjAUVZKt+x+5S9zPMqVg4X415g==}
     engines: {node: ^18.20.2 || >=20.9.0}
@@ -1368,12 +1354,6 @@ packages:
       react: ^19.0.1 || ^19.1.2 || ^19.2.1
       react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1
 
-  '@payloadcms/storage-vercel-blob@3.70.0':
-    resolution: {integrity: sha512-uq039Ihf000epPXwiU3RN6IoJ4IQTXE03rvkkF9mMawF0wJOt9emwyUAjNH6oNMGBjFX42rW/DzvZKNDsEDA+Q==}
-    engines: {node: ^18.20.2 || >=20.9.0}
-    peerDependencies:
-      payload: 3.70.0
-
   '@payloadcms/translations@3.70.0':
     resolution: {integrity: sha512-b8C1Wg7zknUpfe6T2C13/bjrx8i4oRLCaHEJ6LuuVRMxeDyH4hPrxivyjZFlEIajzb7+HjwpdeWfa8IoiqvEpg==}
 
@@ -1819,10 +1799,6 @@ packages:
     cpu: [x64]
     os: [win32]
 
-  '@vercel/blob@0.22.3':
-    resolution: {integrity: sha512-l0t2KhbOO/I8ZNOl9zypYf1NE0837aO4/CPQNGR/RAxtj8FpdYKjhyUADUXj2gERLQmnhun+teaVs/G7vZJ/TQ==}
-    engines: {node: '>=16.14'}
-
   '@vitejs/plugin-react@4.5.2':
     resolution: {integrity: sha512-QNVT3/Lxx99nMQWJWF7K4N6apUEuT0KlZA3mx/mVaoGj3smm/8rc8ezz15J1pcbcjDK0V15rpHetVfya08r76Q==}
     engines: {node: ^14.18.0 || >=16.0.0}
@@ -1955,9 +1931,6 @@ packages:
     resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
     engines: {node: '>= 0.4'}
 
-  async-retry@1.3.3:
-    resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==}
-
   atomic-sleep@1.0.0:
     resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
     engines: {node: '>=8.0.0'}
@@ -2017,10 +1990,6 @@ packages:
     resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
     engines: {node: '>=10.16.0'}
 
-  bytes@3.1.2:
-    resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
-    engines: {node: '>= 0.8'}
-
   cac@6.7.14:
     resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
     engines: {node: '>=8'}
@@ -2231,10 +2200,6 @@ packages:
     resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
     engines: {node: '>=6'}
 
-  detect-file@1.0.0:
-    resolution: {integrity: sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==}
-    engines: {node: '>=0.10.0'}
-
   detect-libc@2.1.2:
     resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
     engines: {node: '>=8'}
@@ -2566,10 +2531,6 @@ packages:
     resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
     engines: {node: '>=0.10.0'}
 
-  expand-tilde@2.0.2:
-    resolution: {integrity: sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==}
-    engines: {node: '>=0.10.0'}
-
   expect-type@1.3.0:
     resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
     engines: {node: '>=12.0.0'}
@@ -2620,9 +2581,6 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
-  find-node-modules@2.1.3:
-    resolution: {integrity: sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==}
-
   find-root@1.1.0:
     resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==}
 
@@ -2630,10 +2588,6 @@ packages:
     resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
     engines: {node: '>=10'}
 
-  findup-sync@4.0.0:
-    resolution: {integrity: sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==}
-    engines: {node: '>= 8'}
-
   flat-cache@4.0.1:
     resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
     engines: {node: '>=16'}
@@ -2702,14 +2656,6 @@ packages:
     resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
     engines: {node: '>=10.13.0'}
 
-  global-modules@1.0.0:
-    resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==}
-    engines: {node: '>=0.10.0'}
-
-  global-prefix@1.0.2:
-    resolution: {integrity: sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==}
-    engines: {node: '>=0.10.0'}
-
   globals@14.0.0:
     resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
     engines: {node: '>=18'}
@@ -2777,10 +2723,6 @@ packages:
   hoist-non-react-statics@3.3.2:
     resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
 
-  homedir-polyfill@1.0.3:
-    resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
-    engines: {node: '>=0.10.0'}
-
   html-encoding-sniffer@4.0.0:
     resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
     engines: {node: '>=18'}
@@ -2828,9 +2770,6 @@ packages:
     resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
     engines: {node: '>=0.8.19'}
 
-  ini@1.3.8:
-    resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
-
   internal-slot@1.1.0:
     resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
     engines: {node: '>= 0.4'}
@@ -2874,10 +2813,6 @@ packages:
   is-buffer@1.1.6:
     resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
 
-  is-buffer@2.0.5:
-    resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
-    engines: {node: '>=4'}
-
   is-bun-module@2.0.0:
     resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==}
 
@@ -2974,10 +2909,6 @@ packages:
     resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
     engines: {node: '>= 0.4'}
 
-  is-windows@1.0.2:
-    resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
-    engines: {node: '>=0.10.0'}
-
   isarray@2.0.5:
     resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
 
@@ -3155,9 +3086,6 @@ packages:
     resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
     engines: {node: '>= 8'}
 
-  merge@2.1.1:
-    resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
-
   micromark-core-commonmark@2.0.3:
     resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
 
@@ -3370,10 +3298,6 @@ packages:
     resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
     engines: {node: '>=8'}
 
-  parse-passwd@1.0.0:
-    resolution: {integrity: sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==}
-    engines: {node: '>=0.10.0'}
-
   parse5@7.3.0:
     resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
 
@@ -3662,10 +3586,6 @@ packages:
     resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
     engines: {node: '>=0.10.0'}
 
-  resolve-dir@1.0.1:
-    resolution: {integrity: sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==}
-    engines: {node: '>=0.10.0'}
-
   resolve-from@4.0.0:
     resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
     engines: {node: '>=4'}
@@ -3682,10 +3602,6 @@ packages:
     resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
     hasBin: true
 
-  retry@0.13.1:
-    resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
-    engines: {node: '>= 4'}
-
   reusify@1.1.0:
     resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -4074,10 +3990,6 @@ packages:
   undici-types@6.21.0:
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
 
-  undici@5.29.0:
-    resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
-    engines: {node: '>=14.0'}
-
   undici@7.10.0:
     resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
     engines: {node: '>=20.18.1'}
@@ -4256,10 +4168,6 @@ packages:
     resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
     engines: {node: '>= 0.4'}
 
-  which@1.3.1:
-    resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
-    hasBin: true
-
   which@2.0.2:
     resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
     engines: {node: '>= 8'}
@@ -4887,8 +4795,6 @@ snapshots:
       react: 19.2.1
       react-dom: 19.2.1(react@19.2.1)
 
-  '@fastify/busboy@2.1.1': {}
-
   '@floating-ui/core@1.7.3':
     dependencies:
       '@floating-ui/utils': 0.2.10
@@ -5461,21 +5367,6 @@ snapshots:
       - supports-color
       - typescript
 
-  '@payloadcms/plugin-cloud-storage@3.70.0(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)':
-    dependencies:
-      '@payloadcms/ui': 3.70.0(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)
-      find-node-modules: 2.1.3
-      payload: 3.70.0(graphql@16.12.0)(typescript@5.7.3)
-      range-parser: 1.2.1
-      react: 19.2.1
-      react-dom: 19.2.1(react@19.2.1)
-    transitivePeerDependencies:
-      - '@types/react'
-      - monaco-editor
-      - next
-      - supports-color
-      - typescript
-
   '@payloadcms/richtext-lexical@3.70.0(@faceless-ui/modal@3.0.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(@payloadcms/next@3.70.0(@types/react@19.2.1)(graphql@16.12.0)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3))(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)(yjs@13.6.29)':
     dependencies:
       '@faceless-ui/modal': 3.0.0(react-dom@19.2.1(react@19.2.1))(react@19.2.1)
@@ -5520,20 +5411,6 @@ snapshots:
       - typescript
       - yjs
 
-  '@payloadcms/storage-vercel-blob@3.70.0(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)':
-    dependencies:
-      '@payloadcms/plugin-cloud-storage': 3.70.0(@types/react@19.2.1)(monaco-editor@0.55.1)(next@15.4.10(@babel/core@7.28.6)(@playwright/test@1.56.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(sass@1.77.4))(payload@3.70.0(graphql@16.12.0)(typescript@5.7.3))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.7.3)
-      '@vercel/blob': 0.22.3
-      payload: 3.70.0(graphql@16.12.0)(typescript@5.7.3)
-    transitivePeerDependencies:
-      - '@types/react'
-      - monaco-editor
-      - next
-      - react
-      - react-dom
-      - supports-color
-      - typescript
-
   '@payloadcms/translations@3.70.0':
     dependencies:
       date-fns: 4.1.0
@@ -5939,13 +5816,6 @@ snapshots:
   '@unrs/resolver-binding-win32-x64-msvc@1.11.1':
     optional: true
 
-  '@vercel/blob@0.22.3':
-    dependencies:
-      async-retry: 1.3.3
-      bytes: 3.1.2
-      is-buffer: 2.0.5
-      undici: 5.29.0
-
   '@vitejs/plugin-react@4.5.2(vite@7.3.1(@types/node@22.19.6)(sass@1.77.4)(tsx@4.21.0))':
     dependencies:
       '@babel/core': 7.28.6
@@ -6122,10 +5992,6 @@ snapshots:
 
   async-function@1.0.0: {}
 
-  async-retry@1.3.3:
-    dependencies:
-      retry: 0.13.1
-
   atomic-sleep@1.0.0: {}
 
   available-typed-arrays@1.0.7:
@@ -6179,8 +6045,6 @@ snapshots:
     dependencies:
       streamsearch: 1.1.0
 
-  bytes@3.1.2: {}
-
   cac@6.7.14: {}
 
   call-bind-apply-helpers@1.0.2:
@@ -6379,8 +6243,6 @@ snapshots:
 
   dequal@2.0.3: {}
 
-  detect-file@1.0.0: {}
-
   detect-libc@2.1.2: {}
 
   devlop@1.1.0:
@@ -6645,8 +6507,8 @@ snapshots:
       '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.7.3)
       eslint: 9.39.2
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2)
-      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
+      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2)
+      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
       eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2)
       eslint-plugin-react: 7.37.5(eslint@9.39.2)
       eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2)
@@ -6665,7 +6527,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2):
+  eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2):
     dependencies:
       '@nolyfill/is-core-module': 1.0.39
       debug: 4.4.3
@@ -6676,22 +6538,22 @@ snapshots:
       tinyglobby: 0.2.15
       unrs-resolver: 1.11.1
     optionalDependencies:
-      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
+      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2):
+  eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2):
     dependencies:
       debug: 3.2.7
     optionalDependencies:
       '@typescript-eslint/parser': 8.53.0(eslint@9.39.2)(typescript@5.7.3)
       eslint: 9.39.2
       eslint-import-resolver-node: 0.3.9
-      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2)
+      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2)
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2):
+  eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.9
@@ -6702,7 +6564,7 @@ snapshots:
       doctrine: 2.1.0
       eslint: 9.39.2
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
+      eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2)(typescript@5.7.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
       hasown: 2.0.2
       is-core-module: 2.16.1
       is-glob: 4.0.3
@@ -6842,10 +6704,6 @@ snapshots:
 
   esutils@2.0.3: {}
 
-  expand-tilde@2.0.2:
-    dependencies:
-      homedir-polyfill: 1.0.3
-
   expect-type@1.3.0: {}
 
   fast-copy@3.0.2: {}
@@ -6890,11 +6748,6 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
-  find-node-modules@2.1.3:
-    dependencies:
-      findup-sync: 4.0.0
-      merge: 2.1.1
-
   find-root@1.1.0: {}
 
   find-up@5.0.0:
@@ -6902,13 +6755,6 @@ snapshots:
       locate-path: 6.0.0
       path-exists: 4.0.0
 
-  findup-sync@4.0.0:
-    dependencies:
-      detect-file: 1.0.0
-      is-glob: 4.0.3
-      micromatch: 4.0.8
-      resolve-dir: 1.0.1
-
   flat-cache@4.0.1:
     dependencies:
       flatted: 3.3.3
@@ -6987,20 +6833,6 @@ snapshots:
     dependencies:
       is-glob: 4.0.3
 
-  global-modules@1.0.0:
-    dependencies:
-      global-prefix: 1.0.2
-      is-windows: 1.0.2
-      resolve-dir: 1.0.1
-
-  global-prefix@1.0.2:
-    dependencies:
-      expand-tilde: 2.0.2
-      homedir-polyfill: 1.0.3
-      ini: 1.3.8
-      is-windows: 1.0.2
-      which: 1.3.1
-
   globals@14.0.0: {}
 
   globalthis@1.0.4:
@@ -7055,10 +6887,6 @@ snapshots:
     dependencies:
       react-is: 16.13.1
 
-  homedir-polyfill@1.0.3:
-    dependencies:
-      parse-passwd: 1.0.0
-
   html-encoding-sniffer@4.0.0:
     dependencies:
       whatwg-encoding: 3.1.1
@@ -7100,8 +6928,6 @@ snapshots:
 
   imurmurhash@0.1.4: {}
 
-  ini@1.3.8: {}
-
   internal-slot@1.1.0:
     dependencies:
       es-errors: 1.3.0
@@ -7150,8 +6976,6 @@ snapshots:
 
   is-buffer@1.1.6: {}
 
-  is-buffer@2.0.5: {}
-
   is-bun-module@2.0.0:
     dependencies:
       semver: 7.7.3
@@ -7247,8 +7071,6 @@ snapshots:
       call-bound: 1.0.4
       get-intrinsic: 1.3.0
 
-  is-windows@1.0.2: {}
-
   isarray@2.0.5: {}
 
   isexe@2.0.0: {}
@@ -7464,8 +7286,6 @@ snapshots:
 
   merge2@1.4.1: {}
 
-  merge@2.1.1: {}
-
   micromark-core-commonmark@2.0.3:
     dependencies:
       decode-named-character-reference: 1.2.0
@@ -7792,8 +7612,6 @@ snapshots:
       json-parse-even-better-errors: 2.3.1
       lines-and-columns: 1.2.4
 
-  parse-passwd@1.0.0: {}
-
   parse5@7.3.0:
     dependencies:
       entities: 6.0.1
@@ -8118,11 +7936,6 @@ snapshots:
 
   require-from-string@2.0.2: {}
 
-  resolve-dir@1.0.1:
-    dependencies:
-      expand-tilde: 2.0.2
-      global-modules: 1.0.0
-
   resolve-from@4.0.0: {}
 
   resolve-pkg-maps@1.0.0: {}
@@ -8139,8 +7952,6 @@ snapshots:
       path-parse: 1.0.7
       supports-preserve-symlinks-flag: 1.0.0
 
-  retry@0.13.1: {}
-
   reusify@1.1.0: {}
 
   rollup@4.55.1:
@@ -8630,10 +8441,6 @@ snapshots:
 
   undici-types@6.21.0: {}
 
-  undici@5.29.0:
-    dependencies:
-      '@fastify/busboy': 2.1.1
-
   undici@7.10.0: {}
 
   unist-util-is@6.0.1:
@@ -8862,10 +8669,6 @@ snapshots:
       gopd: 1.2.0
       has-tostringtag: 1.0.2
 
-  which@1.3.1:
-    dependencies:
-      isexe: 2.0.0
-
   which@2.0.2:
     dependencies:
       isexe: 2.0.0

+ 0 - 2
src/app/(payload)/admin/importMap.js

@@ -22,7 +22,6 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
 import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
 import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
-import { VercelBlobClientUploadHandler as VercelBlobClientUploadHandler_16c82c5e25f430251a3e3ba57219ff4e } from '@payloadcms/storage-vercel-blob/client'
 import { CollectionCards as CollectionCards_ab83ff7e88da8d3530831f296ec4756a } from '@payloadcms/ui/rsc'
 
 export const importMap = {
@@ -50,6 +49,5 @@ export const importMap = {
   "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
   "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
   "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
-  "@payloadcms/storage-vercel-blob/client#VercelBlobClientUploadHandler": VercelBlobClientUploadHandler_16c82c5e25f430251a3e3ba57219ff4e,
   "@payloadcms/ui/rsc#CollectionCards": CollectionCards_ab83ff7e88da8d3530831f296ec4756a
 }

+ 2 - 11
src/payload.config.ts

@@ -1,5 +1,4 @@
 import { postgresAdapter } from '@payloadcms/db-postgres'
-import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob'
 import { lexicalEditor } from '@payloadcms/richtext-lexical'
 import path from 'path'
 import { buildConfig } from 'payload'
@@ -17,6 +16,8 @@ import { Gallery } from './collections/Gallery'
 const filename = fileURLToPath(import.meta.url)
 const dirname = path.dirname(filename)
 
+/** Upload collection `media` uses Payload default local storage (`./media` relative to the app cwd). */
+
 export default buildConfig({
   admin: {
     user: Users.slug,
@@ -39,14 +40,4 @@ export default buildConfig({
   cors: '*',
   // Rate limiting is implemented in src/middleware.ts
   // Configuration: 500 requests per 15 minutes, trustProxy: true
-  plugins: [
-    vercelBlobStorage({
-      // enabled: true,
-      enabled: process.env.NODE_ENV === 'test',
-      collections: {
-        media: true, // Aktifkan untuk collection 'media'
-      },
-      token: process.env.BLOB_READ_WRITE_TOKEN || '', // Nanti didapat dari dashboard Vercel
-    }),
-  ],
 })

+ 255 - 42
src/scripts/seed.ts

@@ -1,5 +1,6 @@
 import 'dotenv/config'
 import { readFileSync, statSync } from 'fs'
+import { convertMarkdownToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
 import { getPayload } from 'payload'
 import path from 'path'
 import { fileURLToPath } from 'url'
@@ -9,11 +10,14 @@ import config from '../payload.config'
 const filename = fileURLToPath(import.meta.url)
 const dirname = path.dirname(filename)
 
+/** One upload per filename; reused by clients, careers, posts, gallery, and author. */
+const mediaByFile = new Map<string, number>()
+
 // Data Careers
 const careers = [
     {
         name: 'Code Warrior',
-        imageFile: 'news1.png', // Asumsi file ini ada di folder seed-assets
+        imageFile: 'news1.png',
         requirements: [
             'Resourceful, communicative, fast learner, innovative, and good team player',
             'Hands on experience with Java EE',
@@ -144,6 +148,141 @@ const clients = [
     },
 ]
 
+const postsSeed = [
+    {
+        type: 'news' as const,
+        category: 'Teknologi',
+        title: 'Perbankan Indonesia Genjot Adopsi Cloud dan Keamanan Siber',
+        imageFile: 'news1.png',
+        excerpt:
+            'Lembaga keuangan mempercepat modernisasi core banking sambil memperketat tata kelola risiko siber dan kepatuhan data.',
+        publishedDate: '2025-02-10T02:00:00.000Z',
+        markdown: `## Ringkasan
+
+**Jakarta** — Transformasi digital di sektor perbankan nasional masuk babak baru: institusi tidak hanya memigrasikan beban kerja ke *cloud*, tetapi juga merancang ulang arsitektur keamanan agar konsisten dengan regulasi dan ekspektasi nasabah.
+
+## Poin Utama
+
+- Peningkatan investasi pada observabilitas dan deteksi ancaman berbasis perilaku
+- Standarisasi pipeline rilis untuk mengurangi *drift* konfigurasi antar lingkungan
+- Kolaborasi lintas divisi antara IT, risiko, dan kepatuhan
+
+## Analisis
+
+Menurut praktisi industri, tantangan terbesar bukan pada alat, melainkan pada disiplin proses: dokumentasi perubahan, uji regresi otomatis, dan mekanisme *rollback* yang dapat diaudit.
+
+> "Keamanan bukan fitur tambahan; ia harus menjadi bagian dari definisi selesainya sebuah fitur," demikian ringkasan dari diskusi panel internal.
+
+## Tautan terkait
+
+Baca juga [panduan baseline keamanan aplikasi](https://owasp.org) untuk referensi praktik umum.
+
+---
+
+*Narasi ini disusun untuk keperluan demonstrasi konten editorial pada lingkungan pengembangan.*`,
+    },
+    {
+        type: 'blog' as const,
+        category: 'Opini',
+        title: 'Membangun Budaya DevSecOps di Tim Produksi',
+        imageFile: 'news2.jpeg',
+        excerpt:
+            'Integrasi keamanan sejak awal siklus pengembangan membutuhkan kejelasan peran, metrik, dan ruang aman untuk eksperimen.',
+        publishedDate: '2025-02-18T04:30:00.000Z',
+        markdown: `## Mengapa budaya lebih menentukan daripada alat
+
+Tim yang sehat memisahkan **akuntabilitas** dari **salah-sasaran**: temuan kerentanan menjadi input perencanaan, bukan ajang mencari kambing hitam.
+
+### Tiga praktik yang sering terabaikan
+
+1. *Threat modeling* ringkas di awal fitur — cukup satu halaman, asal konsisten
+2. *Security champions* per squad — jembatan antara dev dan tim keamanan
+3. Metrik yang masuk akal: MTTD/MTTR insiden, bukan hanya jumlah temuan
+
+## Studi kasus fiktif
+
+Sebuah tim backend mengurangi insiden konfigurasi dengan menerapkan *policy-as-code* pada pipeline CI/CD. Hasilnya, perubahan infrastruktur dapat ditinjau seperti kode aplikasi.
+
+---
+
+Paragraf penutup: DevSecOps berhasil ketika insinyur merasa **dibantu**, bukan diawasi.`,
+    },
+    {
+        type: 'news' as const,
+        category: 'Perusahaan',
+        title: 'Hanoman Perluas Kolaborasi dengan Mitra Strategis Sektor Publik',
+        imageFile: 'news3.jpeg',
+        excerpt:
+            'Inisiatif baru menekankan interoperabilitas sistem dan peningkatan layanan berbasis data bagi masyarakat dan pelaku usaha.',
+        publishedDate: '2025-03-01T01:15:00.000Z',
+        markdown: `## Siaran Pers (contoh)
+
+**Jakarta** — Kemitraan strategis ini fokus pada tiga garis besar: integrasi layanan, peningkatan kualitas data, dan penguatan kapasitas sumber daya manusia di lapangan.
+
+### Rencana aksi
+
+- Fase persiapan: penyelarasan *data dictionary* dan hak akses
+- Fase implementasi: pilot terbatas dengan *feedback loop* mingguan
+- Fase stabilisasi: dokumentasi operasional dan transfer pengetahuan
+
+## Kutipan
+
+> Kolaborasi berkelanjutan membutuhkan transparansi target dan metrik keberhasilan yang disepakati bersama.
+
+### Daftar pihak (ilustrasi)
+
+- Tim program dan manajemen risiko
+- Unit teknis lapangan
+- Mitra ekosistem dan penyedia infrastruktur
+
+---
+
+Informasi lebih lanjut akan diumumkan melalui kanal resmi perusahaan.`,
+    },
+    {
+        type: 'blog' as const,
+        category: 'Analisis',
+        title: 'Tren AI di Ruang Kerja: Antara Efisiensi dan Tata Kelola Data',
+        imageFile: 'news4.png',
+        excerpt:
+            'Penerapan asisten AI menuntut kebijakan penggunaan yang jelas, audit log, dan kesadaran privasi di seluruh lini organisasi.',
+        publishedDate: '2025-03-12T03:45:00.000Z',
+        markdown: `## Lanskap saat ini
+
+Organisasi bereksperimen dengan **asisten penulisan**, **ringkasan dokumen**, dan **klasifikasi tiket** dukungan. Manfaatnya nyata, namun risiko kebocoran data sensitif ikut meningkat.
+
+## Checklist tata kelola (ringkas)
+
+- [ ] Klasifikasi data: apa yang boleh masuk ke model publik vs internal
+- [ ] Log penggunaan: siapa, kapan, konteks permintaan
+- [ ] Pelatihan staf: penggunaan yang etis dan kebijakan sanksi ringan
+
+## Opini
+
+> AI paling aman ketika diposisikan sebagai *co-pilot* dengan batasan teknis dan organisasi yang eksplisit.
+
+### Baca juga
+
+Referensi umum: [Prinsip AI yang manusiawi](https://www.unesco.org/en/artificial-intelligence/recommendation-ethics) (contoh tautan eksternal).
+
+---
+
+*Artikel ini bersifat ilustratif untuk pengujian tampilan rich text.*`,
+    },
+]
+
+/** Gallery memakai media yang sama dengan unggahan clients/careers (sudah di-cache). */
+const gallerySeed = [
+    { imageFile: 'news1.png', caption: 'Sesi kolaborasi tim teknologi dan bisnis' },
+    { imageFile: 'news2.jpeg', caption: 'Workshop pengalaman pengguna dan desain produk' },
+    { imageFile: 'news3.jpeg', caption: 'Kunjungan lapangan bersama mitra strategis' },
+    { imageFile: 'news4.png', caption: 'Pembahasan arsitektur dan tata kelola rilis' },
+    { imageFile: 'btn.png', caption: 'Logo mitra: layanan perbankan nasional' },
+    { imageFile: 'bii.png', caption: 'Kolaborasi di sektor jasa keuangan' },
+    { imageFile: 'tsel.png', caption: 'Solusi untuk pelanggan korporat dan industri' },
+    { imageFile: 'hpm.jpg', caption: 'Kemitraan di sektor manufaktur dan distribusi' },
+]
+
 /**
  * Generate slug from name
  */
@@ -155,7 +294,7 @@ function generateSlug(name: string): string {
 }
 
 /**
- * Upload image file to Media collection
+ * Upload image file to Media collection (no cache — use ensureMedia instead).
  */
 async function uploadImage(
     payload: Awaited<ReturnType<typeof getPayload>>,
@@ -164,7 +303,6 @@ async function uploadImage(
     const seedAssetsPath = path.resolve(dirname, '../seed-assets', filename)
 
     try {
-        // Check if file exists
         if (!statSync(seedAssetsPath).isFile()) {
             throw new Error(`File not found: ${seedAssetsPath}`)
         }
@@ -172,7 +310,6 @@ async function uploadImage(
         const fileBuffer = readFileSync(seedAssetsPath)
         const fileExtension = path.extname(filename).slice(1).toLowerCase()
 
-        // Determine MIME type
         const mimeTypes: Record<string, string> = {
             jpg: 'image/jpeg',
             jpeg: 'image/jpeg',
@@ -198,7 +335,6 @@ async function uploadImage(
             },
         })
 
-        // Convert ID to number (Payload uses number IDs for PostgreSQL)
         return typeof media.id === 'number' ? media.id : Number(media.id)
     } catch (error) {
         console.error(`Error uploading ${filename}:`, error)
@@ -206,6 +342,32 @@ async function uploadImage(
     }
 }
 
+async function ensureMedia(
+    payload: Awaited<ReturnType<typeof getPayload>>,
+    filename: string,
+): Promise<number> {
+    const existing = mediaByFile.get(filename)
+    if (existing !== undefined) {
+        return existing
+    }
+    const id = await uploadImage(payload, filename)
+    mediaByFile.set(filename, id)
+    return id
+}
+
+async function lexicalFromMarkdown(
+    sanitizedConfig: Awaited<typeof config>,
+    markdown: string,
+): Promise<ReturnType<typeof convertMarkdownToLexical>> {
+    const editorConfig = await editorConfigFactory.default({
+        config: sanitizedConfig,
+    })
+    return convertMarkdownToLexical({
+        editorConfig,
+        markdown,
+    })
+}
+
 /**
  * Main seeding function
  */
@@ -216,39 +378,30 @@ async function seed() {
         const payloadConfig = await config
         const payload = await getPayload({ config: payloadConfig })
 
-        // Reset collections (optional - uncomment if you want to clear existing data)
         console.log('🗑️  Clearing existing data...')
-        try {
-            const existingCareers = await payload.find({
-                collection: 'careers',
-                limit: 1000,
-            })
-            for (const career of existingCareers.docs) {
-                await payload.delete({
-                    collection: 'careers',
-                    id: career.id,
+        const deleteCollection = async (slug: 'posts' | 'gallery' | 'careers' | 'clients' | 'authors') => {
+            try {
+                const res = await payload.find({
+                    collection: slug,
+                    limit: 1000,
                 })
+                for (const doc of res.docs) {
+                    await payload.delete({
+                        collection: slug,
+                        id: doc.id,
+                    })
+                }
+                console.log(`   ✓ Deleted ${res.docs.length} existing ${slug}`)
+            } catch {
+                console.log(`   ⚠ No existing ${slug} to delete`)
             }
-            console.log(`   ✓ Deleted ${existingCareers.docs.length} existing careers`)
-        } catch (error) {
-            console.log('   ⚠ No existing careers to delete')
         }
 
-        try {
-            const existingClients = await payload.find({
-                collection: 'clients',
-                limit: 1000,
-            })
-            for (const client of existingClients.docs) {
-                await payload.delete({
-                    collection: 'clients',
-                    id: client.id,
-                })
-            }
-            console.log(`   ✓ Deleted ${existingClients.docs.length} existing clients`)
-        } catch (error) {
-            console.log('   ⚠ No existing clients to delete')
-        }
+        await deleteCollection('posts')
+        await deleteCollection('gallery')
+        await deleteCollection('careers')
+        await deleteCollection('clients')
+        await deleteCollection('authors')
 
         try {
             const existingMedia = await payload.find({
@@ -262,13 +415,14 @@ async function seed() {
                 })
             }
             console.log(`   ✓ Deleted ${existingMedia.docs.length} existing media files`)
-        } catch (error) {
+        } catch {
             console.log('   ⚠ No existing media to delete')
         }
 
+        mediaByFile.clear()
+
         console.log('\n📤 Seeding Clients...\n')
 
-        // Seed Clients
         for (const clientData of clients) {
             try {
                 if (!clientData.imageFile) {
@@ -277,7 +431,7 @@ async function seed() {
                 }
 
                 console.log(`   📤 Uploading image for ${clientData.name}...`)
-                const mediaId = await uploadImage(payload, clientData.imageFile)
+                const mediaId = await ensureMedia(payload, clientData.imageFile)
 
                 console.log(`   ✅ Creating client: ${clientData.name}`)
                 await payload.create({
@@ -302,14 +456,13 @@ async function seed() {
 
         console.log('📤 Seeding Careers...\n')
 
-        // Seed Careers
         for (const careerData of careers) {
             try {
                 let mediaId: number | undefined
 
                 if (careerData.imageFile) {
-                    console.log(`   📤 Uploading image for ${careerData.name}...`)
-                    mediaId = await uploadImage(payload, careerData.imageFile)
+                    console.log(`   📤 Ensuring media for ${careerData.name}...`)
+                    mediaId = await ensureMedia(payload, careerData.imageFile)
                 }
 
                 const slug = generateSlug(careerData.name)
@@ -342,7 +495,69 @@ async function seed() {
             }
         }
 
-        console.log('✨ Seed process completed successfully!')
+        console.log('📤 Seeding Author...\n')
+
+        const authorImageId = await ensureMedia(payload, 'yusuf.jpeg')
+        const authorDoc = await payload.create({
+            collection: 'authors',
+            data: {
+                name: 'muh yusuf syam',
+                image: authorImageId,
+                description:
+                    'Pengembang perangkat lunak dan kontributor teknis; tertarik pada arsitektur web, DX, dan kolaborasi lintas tim.',
+                socialMediaLink: 'https://www.linkedin.com/in/muh-yusuf-syam/',
+            },
+        })
+        const authorId = typeof authorDoc.id === 'number' ? authorDoc.id : Number(authorDoc.id)
+        console.log(`   ✓ Author created: muh yusuf syam (id: ${authorId})\n`)
+
+        console.log('📤 Seeding Posts...\n')
+
+        for (const post of postsSeed) {
+            try {
+                const featuredId = await ensureMedia(payload, post.imageFile)
+                const slug = generateSlug(post.title)
+                const content = await lexicalFromMarkdown(payloadConfig, post.markdown)
+
+                await payload.create({
+                    collection: 'posts',
+                    data: {
+                        type: post.type,
+                        category: post.category,
+                        title: post.title,
+                        slug,
+                        image: featuredId,
+                        excerpt: post.excerpt,
+                        publishedDate: post.publishedDate,
+                        content,
+                        author: authorId,
+                    },
+                })
+                console.log(`   ✓ Post: ${post.title}`)
+            } catch (error) {
+                console.error(`   ✗ Error seeding post "${post.title}":`, error)
+            }
+        }
+
+        console.log('\n📤 Seeding Gallery...\n')
+
+        for (const item of gallerySeed) {
+            try {
+                const imageId = await ensureMedia(payload, item.imageFile)
+                await payload.create({
+                    collection: 'gallery',
+                    data: {
+                        image: imageId,
+                        caption: item.caption,
+                    },
+                })
+                console.log(`   ✓ Gallery: ${item.caption.slice(0, 48)}…`)
+            } catch (error) {
+                console.error(`   ✗ Error seeding gallery "${item.caption}":`, error)
+            }
+        }
+
+        console.log('\n✨ Seed process completed successfully!')
         process.exit(0)
     } catch (error) {
         console.error('❌ Seed process failed:', error)
@@ -350,9 +565,7 @@ async function seed() {
     }
 }
 
-// Run seed
 seed().catch((error) => {
     console.error('❌ Seed process failed:', error)
     process.exit(1)
 })
-

BIN
src/seed-assets/yusuf.jpeg