diff --git a/README.md b/README.md index 0f21b52e..5324a068 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,24 @@ pnpm preview pnpm generate ``` +## Setting up Ghost ECM local instance + +```bash +docker compose up -d +``` + +This will shoot up a Ghost instance on port 2368. You can access the ghost admin dashboard through `http://localhost:2368/ghost`. +You'll be prompted to make an admin account. After that: + +1. Go to settings -> integrations. +2. Make a custom integration to generate a Content API Key and URL. +3. Update `.env` file with the API URL and Content API key. + +```bash +NUXT_PUBLIC_GHOST_URL= +NUXT_PUBLIC_GHOST_KEY= +``` + ## Code style and linting - [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - [ESLint v9](https://eslint.org/) diff --git a/app.config.ts b/app.config.ts index 47d946cf..de9ef945 100644 --- a/app.config.ts +++ b/app.config.ts @@ -64,12 +64,6 @@ export default defineAppConfig({ href: 'https://github.com/storacha', icon: 'i-simple-icons:github', }, - { - name: 'Medium', - description: 'Read our blog', - href: 'https://medium.com/@storacha', - icon: 'i-simple-icons:medium', - }, { name: 'YouTube', description: 'Watch our demos', diff --git a/components/Blog/Card.vue b/components/Blog/Card.vue index 1ce19726..9b0b6186 100644 --- a/components/Blog/Card.vue +++ b/components/Blog/Card.vue @@ -6,22 +6,31 @@ defineProps<{ item: Item showSnippet?: boolean }>() + +// Change the path as required +const fallbackImage = '/img/fallback.webp' + +function handleImageError(event: Event) { + const target = event.target as HTMLImageElement + target.src = fallbackImage +} diff --git a/composables/useGhostClient.ts b/composables/useGhostClient.ts new file mode 100644 index 00000000..8939693b --- /dev/null +++ b/composables/useGhostClient.ts @@ -0,0 +1,15 @@ +import GhostContentAPI from '@tryghost/content-api' + +export function useGhostClient() { + const config = useRuntimeConfig() + const { url, key, version } = config.public.ghost + + // Create API instance with site credentials + const api = new GhostContentAPI({ + url, + key, + version, + }) + + return { api } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..1098fc2b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +services: + + ghost: + image: ghost:5-alpine + restart: always + ports: + - 2368:2368 + environment: + # Configuration: https://ghost.org/docs/config + database__client: mysql + database__connection__host: db + database__connection__user: root + database__connection__password: example + database__connection__database: ghost + security__staffDeviceVerification: false + url: http://localhost:2368 + volumes: + - ghost:/var/lib/ghost/content + + db: + image: mysql:8.0 + restart: always + environment: + MYSQL_ROOT_PASSWORD: example + volumes: + - db:/var/lib/mysql + +volumes: + ghost: + db: diff --git a/nuxt.config.ts b/nuxt.config.ts index 92f44721..d861e5ee 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -89,9 +89,13 @@ export default defineNuxtConfig({ runtimeConfig: { // public runtime config public: { - // feed URL used for /api/blog - blogFeedUrl: 'https://medium.com/feed/@storacha', consoleUrl: import.meta.env.NUXT_PUBLIC_CONSOLE_URL || 'https://console.storacha.network', + // Ghost CMS settings used for /blog + ghost: { + url: import.meta.env.NUXT_PUBLIC_GHOST_URL || 'http://localhost:2368', // TODO: update this to your Ghost CMS URL + key: import.meta.env.NUXT_PUBLIC_GHOST_KEY, + version: 'v5.0', + }, }, }, diff --git a/package.json b/package.json index 167a9d89..bec31b6f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nuxtjs/fontaine": "^0.4.3", "@nuxtjs/plausible": "^1.0.3", "@nuxtjs/seo": "^2.0.0-rc.23", + "@tryghost/content-api": "^1.11.26", "@unocss/nuxt": "^0.63.6", "@unocss/preset-icons": "^0.63.6", "@vueuse/core": "^11.2.0", @@ -38,6 +39,7 @@ "devDependencies": { "@antfu/eslint-config": "^3.8.0", "@nuxt/eslint": "^0.6.1", + "@types/tryghost__content-api": "^1.3.17", "@unocss/eslint-config": "^0.63.6", "eslint": "^9.13.0", "nuxt": "^3.13.2", diff --git a/pages/blog.vue b/pages/blog.vue deleted file mode 100644 index fc8058d7..00000000 --- a/pages/blog.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/pages/blog/[slug].vue b/pages/blog/[slug].vue new file mode 100644 index 00000000..4f5ab728 --- /dev/null +++ b/pages/blog/[slug].vue @@ -0,0 +1,61 @@ + + + + + diff --git a/pages/blog/index.vue b/pages/blog/index.vue new file mode 100644 index 00000000..a022e693 --- /dev/null +++ b/pages/blog/index.vue @@ -0,0 +1,33 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae7092d2..4f851717 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 1.8.1(ioredis@5.4.1)(magicast@0.3.5)(rollup@4.24.3)(webpack-sources@3.2.3) '@nuxt/scripts': specifier: ^0.9.5 - version: 0.9.5(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(fuse.js@7.0.0)(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1)) + version: 0.9.5(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(axios@1.9.0)(fuse.js@7.0.0)(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1)) '@nuxtjs/fontaine': specifier: ^0.4.3 version: 0.4.3(magicast@0.3.5)(rollup@4.24.3)(webpack-sources@3.2.3) @@ -35,6 +35,9 @@ importers: '@nuxtjs/seo': specifier: ^2.0.0-rc.23 version: 2.0.0-rc.23(h3@1.13.0)(magicast@0.3.5)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3) + '@tryghost/content-api': + specifier: ^1.11.26 + version: 1.11.26 '@unocss/nuxt': specifier: ^0.63.6 version: 0.63.6(magicast@0.3.5)(postcss@8.4.47)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1)) @@ -66,6 +69,9 @@ importers: '@nuxt/eslint': specifier: ^0.6.1 version: 0.6.1(eslint@9.13.0(jiti@2.4.0))(magicast@0.3.5)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(webpack-sources@3.2.3) + '@types/tryghost__content-api': + specifier: ^1.3.17 + version: 1.3.17 '@unocss/eslint-config': specifier: ^0.63.6 version: 0.63.6(eslint@9.13.0(jiti@2.4.0))(typescript@5.6.3) @@ -1767,6 +1773,9 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 + '@tryghost/content-api@1.11.26': + resolution: {integrity: sha512-BhTLERo2onwxnSVcxsZKvbBpRjxuBA1gLge2VCoqIHBrt3HfML79+rjKt6rDoenwFh2BvDBxlY1HTVkNvpKOpQ==} + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} @@ -1813,6 +1822,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/tryghost__content-api@1.3.17': + resolution: {integrity: sha512-4DASYoK0hP1+XDyLS/8IZevalQRJuPmyPmfxdT1hnYRjxnJkgusATeDc/7QXA2izMZ/+cWkgdZDeTN2cBW+EoA==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -2486,6 +2498,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -2493,6 +2508,9 @@ packages: peerDependencies: postcss: ^8.1.0 + axios@1.9.0: + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==} + b4a@1.6.7: resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} @@ -2603,6 +2621,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -2731,6 +2753,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -2990,6 +3016,10 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + delegates@1.0.0: resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} @@ -3062,6 +3092,10 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -3127,9 +3161,25 @@ packages: errx@0.1.0: resolution: {integrity: sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -3497,6 +3547,15 @@ packages: focus-trap@7.6.0: resolution: {integrity: sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + fontaine@0.5.0: resolution: {integrity: sha512-vPDSWKhVAfTx4hRKT777+N6Szh2pAosAuzLpbppZ6O3UdD/1m6OlHjNcC3vIbgkRTIcLjzySLHXzPeLO2rE8cA==} @@ -3507,6 +3566,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.3: + resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + engines: {node: '>= 6'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -3553,9 +3616,17 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-port-please@3.1.2: resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} @@ -3642,6 +3713,10 @@ packages: resolution: {integrity: sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -3668,6 +3743,14 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} @@ -4139,6 +4222,10 @@ packages: marky@1.2.5: resolution: {integrity: sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.1: resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} @@ -4941,6 +5028,9 @@ packages: protocols@2.0.1: resolution: {integrity: sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@3.0.2: resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} @@ -7168,7 +7258,7 @@ snapshots: - supports-color - webpack-sources - '@nuxt/devtools-ui-kit@1.6.0(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(fuse.js@7.0.0)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1))': + '@nuxt/devtools-ui-kit@1.6.0(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(axios@1.9.0)(fuse.js@7.0.0)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1))': dependencies: '@iconify-json/carbon': 1.2.4 '@iconify-json/logos': 1.2.3 @@ -7184,7 +7274,7 @@ snapshots: '@unocss/preset-mini': 0.62.4 '@unocss/reset': 0.62.4 '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.6.3)) - '@vueuse/integrations': 11.2.0(focus-trap@7.6.0)(fuse.js@7.0.0)(vue@3.5.12(typescript@5.6.3)) + '@vueuse/integrations': 11.2.0(axios@1.9.0)(focus-trap@7.6.0)(fuse.js@7.0.0)(vue@3.5.12(typescript@5.6.3)) '@vueuse/nuxt': 11.2.0(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(rollup@4.24.3)(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3) defu: 6.1.4 focus-trap: 7.6.0 @@ -7415,10 +7505,10 @@ snapshots: - supports-color - webpack-sources - '@nuxt/scripts@0.9.5(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(fuse.js@7.0.0)(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1))': + '@nuxt/scripts@0.9.5(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(axios@1.9.0)(fuse.js@7.0.0)(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1))': dependencies: '@nuxt/devtools-kit': 1.6.0(magicast@0.3.5)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(webpack-sources@3.2.3) - '@nuxt/devtools-ui-kit': 1.6.0(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(fuse.js@7.0.0)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1)) + '@nuxt/devtools-ui-kit': 1.6.0(@nuxt/devtools@1.6.0(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3))(@unocss/webpack@0.63.6(rollup@4.24.3)(webpack@5.96.1(esbuild@0.23.1)))(@vue/compiler-core@3.5.12)(axios@1.9.0)(fuse.js@7.0.0)(magicast@0.3.5)(nuxt@3.13.2(@parcel/watcher@2.4.1)(@types/node@22.8.6)(eslint@9.13.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.24.3)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3))(webpack-sources@3.2.3))(postcss@8.4.47)(rollup@4.24.3)(vite@5.4.10(@types/node@22.8.6)(terser@5.36.0))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)(webpack@5.96.1(esbuild@0.23.1)) '@nuxt/kit': 3.13.2(magicast@0.3.5)(rollup@4.24.3)(webpack-sources@3.2.3) '@stripe/stripe-js': 4.9.0 '@types/google.maps': 3.58.1 @@ -8046,6 +8136,12 @@ snapshots: '@tanstack/virtual-core': 3.10.8 vue: 3.5.12(typescript@5.6.3) + '@tryghost/content-api@1.11.26': + dependencies: + axios: 1.9.0 + transitivePeerDependencies: + - debug + '@trysound/sax@0.2.0': {} '@types/debug@4.1.12': @@ -8094,6 +8190,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/tryghost__content-api@1.3.17': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -8898,12 +8996,13 @@ snapshots: '@unhead/vue': 1.11.10(vue@3.5.12(typescript@5.6.3)) vue: 3.5.12(typescript@5.6.3) - '@vueuse/integrations@11.2.0(focus-trap@7.6.0)(fuse.js@7.0.0)(vue@3.5.12(typescript@5.6.3))': + '@vueuse/integrations@11.2.0(axios@1.9.0)(focus-trap@7.6.0)(fuse.js@7.0.0)(vue@3.5.12(typescript@5.6.3))': dependencies: '@vueuse/core': 11.2.0(vue@3.5.12(typescript@5.6.3)) '@vueuse/shared': 11.2.0(vue@3.5.12(typescript@5.6.3)) vue-demi: 0.14.10(vue@3.5.12(typescript@5.6.3)) optionalDependencies: + axios: 1.9.0 focus-trap: 7.6.0 fuse.js: 7.0.0 transitivePeerDependencies: @@ -9144,6 +9243,8 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + autoprefixer@10.4.20(postcss@8.4.47): dependencies: browserslist: 4.24.2 @@ -9154,6 +9255,14 @@ snapshots: postcss: 8.4.47 postcss-value-parser: 4.2.0 + axios@1.9.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.3 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + b4a@1.6.7: {} bail@2.0.2: {} @@ -9282,6 +9391,11 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + callsites@3.1.0: {} camelize@1.0.1: {} @@ -9428,6 +9542,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@2.20.3: {} @@ -9652,6 +9770,8 @@ snapshots: defu@6.1.4: {} + delayed-stream@1.0.0: {} + delegates@1.0.0: {} denque@2.1.0: {} @@ -9708,6 +9828,12 @@ snapshots: dotenv@16.4.5: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -9769,8 +9895,23 @@ snapshots: errx@0.1.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.5.4: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + esbuild@0.17.19: optionalDependencies: '@esbuild/android-arm': 0.17.19 @@ -10342,6 +10483,8 @@ snapshots: dependencies: tabbable: 6.2.0 + follow-redirects@1.15.9: {} + fontaine@0.5.0(webpack-sources@3.2.3): dependencies: '@capsizecss/metrics': 2.2.0 @@ -10372,6 +10515,14 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + fraction.js@4.3.7: {} fresh@0.5.2: {} @@ -10414,8 +10565,26 @@ snapshots: get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-port-please@3.1.2: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-source@2.0.12: dependencies: data-uri-to-buffer: 2.0.2 @@ -10520,6 +10689,8 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.1.0 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -10551,6 +10722,12 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + has-unicode@2.0.1: {} hash-sum@2.0.0: {} @@ -11086,6 +11263,8 @@ snapshots: marky@1.2.5: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.1: dependencies: '@types/mdast': 4.0.4 @@ -12333,6 +12512,8 @@ snapshots: protocols@2.0.1: {} + proxy-from-env@1.1.0: {} + pump@3.0.2: dependencies: end-of-stream: 1.4.4 diff --git a/public/img/fallback.webp b/public/img/fallback.webp new file mode 100644 index 00000000..f0ce3560 Binary files /dev/null and b/public/img/fallback.webp differ diff --git a/server/api/blog.get.ts b/server/api/blog.get.ts deleted file mode 100644 index 00a666be..00000000 --- a/server/api/blog.get.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { XMLParser } from 'fast-xml-parser' -import type { Feed } from '~/types/blog' - -async function getFeed(feedUrl: string) { - const rss = await $fetch(feedUrl) - return rss -} - -async function fetchPosts(url: string): Promise { - const rss = await getFeed(url) - const root = new XMLParser().parse(rss) - const { channel } = root.rss - - const regex = / { - const images = Array.from(String(post['content:encoded']) - .matchAll(regex)).map(match => match[1]).filter(Boolean) - let snippet = post['content:encoded'].replace(/(<([^>]+)>)/g, '') - if (snippet.length > 200) { - snippet = `${snippet.slice(0, 200)}...` - } - return { - title: post.title, - snippet, - pubDate: post.pubDate, - isoDate: post.isoDate, - link: post.link, - images, - } - }), - } -} - -export default defineCachedEventHandler(async (event) => { - const config = useRuntimeConfig(event) - try { - const feedUrl = config.public.blogFeedUrl - if (!feedUrl) - throw createError({ message: 'Please add a valid blogFeedUrl to your public runtime config' }) - const posts = await fetchPosts(feedUrl) - return posts - } - catch (e: any) { - console.error('failed to get blog posts:', e) - // throw a generic error - throw createError({ status: 500, message: `Failed to fetch posts: ${e.message}` }) - } -}, { maxAge: 60 * 60 }) // cache API response for 60 minutes diff --git a/server/api/blog/[slug].get.ts b/server/api/blog/[slug].get.ts new file mode 100644 index 00000000..d495532e --- /dev/null +++ b/server/api/blog/[slug].get.ts @@ -0,0 +1,46 @@ +import GhostContentAPI from '@tryghost/content-api' + +export default defineCachedEventHandler(async (event) => { + const slug = getRouterParam(event, 'slug') + if (!slug) { + throw createError({ + status: 400, + message: 'Missing slug parameter', + }) + } + + try { + const config = useRuntimeConfig() + const { url, key, version } = config.public.ghost + + // Create API instance with site credentials + const api = new GhostContentAPI({ + url, + key, + version, + }) + + // Get the specific post by slug + const post = await api.posts.read({ slug }, { include: ['tags'] }) + + // Process the content + const content = post.html || '' + + return { + title: post.title, + content, + snippet: post.excerpt || '', + pubDate: post.published_at || '', + isoDate: post.published_at || '', + slug: post.slug, + images: post.feature_image ? [post.feature_image] : [], + } + } + catch (error: any) { + console.error(`Failed to get post with slug ${slug}:`, error) + throw createError({ + status: error.response?.status || 500, + message: `Failed to fetch post: ${error.message}`, + }) + } +}) diff --git a/server/api/blog/index.get.ts b/server/api/blog/index.get.ts new file mode 100644 index 00000000..b238140c --- /dev/null +++ b/server/api/blog/index.get.ts @@ -0,0 +1,56 @@ +import GhostContentAPI from '@tryghost/content-api' +import type { Feed, Item } from '~/types/blog' + +async function fetchGhostPosts(): Promise { + const config = useRuntimeConfig() + const { url, key, version } = config.public.ghost + + // Create API instance with site credentials + const api = new GhostContentAPI({ + url, + key, + version, + }) + + // Get all posts with their featured image + const posts = await api.posts.browse({ + limit: 'all', + include: ['tags'], + fields: ['id', 'title', 'slug', 'feature_image', 'published_at', 'html', 'excerpt'], + }) + + // Transform Ghost posts to match our Item interface + const items: Item[] = posts.map((post) => { + // Extract the first paragraph as a snippet if excerpt is not available + let snippet = post.excerpt || '' + if (!snippet && post.html) { + snippet = post.html.replace(/(<([^>]+)>)/g, '') + if (snippet.length > 200) { + snippet = `${snippet.slice(0, 200)}...` + } + } + + return { + title: post.title || '', + snippet, + pubDate: post.published_at || '', + isoDate: post.published_at || '', + link: `/blog/${post.slug}`, + images: post.feature_image ? [post.feature_image] : [], + } + }) + + return { items } +} + +export default defineCachedEventHandler(async (_event) => { + try { + const posts = await fetchGhostPosts() + return posts + } + catch (e: any) { + console.error('failed to get blog posts:', e) + // throw a generic error + throw createError({ status: 500, message: `Failed to fetch posts: ${e.message}` }) + } +})