feat: implement open graph (#5340)

* chore: add open graph images

* fix: open graph function

* fix: open graph query params

* fix: remove guide id

* fix: generate images on build time

* fix: external author image

* fix: special character issue

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
pull/5373/head
Arik Chakma 8 months ago committed by GitHub
parent ec9d2d4c74
commit d0bd4d6faf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      package.json
  2. 279
      pnpm-lock.yaml
  3. BIN
      public/fonts/BalsamiqSans-Regular.ttf
  4. 3
      public/images/graph.svg
  5. BIN
      public/og-images/best-practices/api-security.png
  6. BIN
      public/og-images/best-practices/aws.png
  7. BIN
      public/og-images/best-practices/code-review.png
  8. BIN
      public/og-images/best-practices/frontend-performance.png
  9. BIN
      public/og-images/guides/asymptotic-notation.png
  10. BIN
      public/og-images/guides/avoid-render-blocking-javascript-with-async-defer.png
  11. BIN
      public/og-images/guides/backend-developer-skills.png
  12. BIN
      public/og-images/guides/backend-languages.png
  13. BIN
      public/og-images/guides/basic-authentication.png
  14. BIN
      public/og-images/guides/basics-of-authentication.png
  15. BIN
      public/og-images/guides/big-o-notation.png
  16. BIN
      public/og-images/guides/character-encodings.png
  17. BIN
      public/og-images/guides/ci-cd.png
  18. BIN
      public/og-images/guides/consistency-patterns-in-distributed-systems.png
  19. BIN
      public/og-images/guides/design-patterns-for-humans.png
  20. BIN
      public/og-images/guides/dhcp-in-one-picture.png
  21. BIN
      public/og-images/guides/dns-in-one-picture.png
  22. BIN
      public/og-images/guides/free-resources-to-learn-llms.png
  23. BIN
      public/og-images/guides/history-of-javascript.png
  24. BIN
      public/og-images/guides/how-to-setup-a-jump-server.png
  25. BIN
      public/og-images/guides/http-basic-authentication.png
  26. BIN
      public/og-images/guides/http-caching.png
  27. BIN
      public/og-images/guides/introduction-to-llms.png
  28. BIN
      public/og-images/guides/journey-to-http2.png
  29. BIN
      public/og-images/guides/jwt-authentication.png
  30. BIN
      public/og-images/guides/levels-of-seniority.png
  31. BIN
      public/og-images/guides/oauth.png
  32. BIN
      public/og-images/guides/proxy-servers.png
  33. BIN
      public/og-images/guides/random-numbers.png
  34. BIN
      public/og-images/guides/scaling-databases.png
  35. BIN
      public/og-images/guides/session-authentication.png
  36. BIN
      public/og-images/guides/session-based-authentication.png
  37. BIN
      public/og-images/guides/setup-and-auto-renew-ssl-certificates.png
  38. BIN
      public/og-images/guides/single-command-database-setup.png
  39. BIN
      public/og-images/guides/ssl-tls-https-ssh.png
  40. BIN
      public/og-images/guides/sso.png
  41. BIN
      public/og-images/guides/token-authentication.png
  42. BIN
      public/og-images/guides/torrent-client.png
  43. BIN
      public/og-images/guides/unfamiliar-codebase.png
  44. BIN
      public/og-images/guides/what-are-web-vitals.png
  45. BIN
      public/og-images/guides/what-is-internet.png
  46. BIN
      public/og-images/guides/what-is-sli-slo-sla.png
  47. BIN
      public/og-images/guides/why-build-it-and-they-will-come-wont-work-anymore.png
  48. BIN
      public/og-images/roadmaps/ai-data-scientist.png
  49. BIN
      public/og-images/roadmaps/android.png
  50. BIN
      public/og-images/roadmaps/angular.png
  51. BIN
      public/og-images/roadmaps/aspnet-core.png
  52. BIN
      public/og-images/roadmaps/aws.png
  53. BIN
      public/og-images/roadmaps/backend.png
  54. BIN
      public/og-images/roadmaps/blockchain.png
  55. BIN
      public/og-images/roadmaps/code-review.png
  56. BIN
      public/og-images/roadmaps/computer-science.png
  57. BIN
      public/og-images/roadmaps/cpp.png
  58. BIN
      public/og-images/roadmaps/cyber-security.png
  59. BIN
      public/og-images/roadmaps/datastructures-and-algorithms.png
  60. BIN
      public/og-images/roadmaps/design-system.png
  61. BIN
      public/og-images/roadmaps/devops.png
  62. BIN
      public/og-images/roadmaps/docker.png
  63. BIN
      public/og-images/roadmaps/flutter.png
  64. BIN
      public/og-images/roadmaps/frontend.png
  65. BIN
      public/og-images/roadmaps/full-stack.png
  66. BIN
      public/og-images/roadmaps/game-developer.png
  67. BIN
      public/og-images/roadmaps/golang.png
  68. BIN
      public/og-images/roadmaps/graphql.png
  69. BIN
      public/og-images/roadmaps/java.png
  70. BIN
      public/og-images/roadmaps/javascript.png
  71. BIN
      public/og-images/roadmaps/kubernetes.png
  72. BIN
      public/og-images/roadmaps/mlops.png
  73. BIN
      public/og-images/roadmaps/mongodb.png
  74. BIN
      public/og-images/roadmaps/nodejs.png
  75. BIN
      public/og-images/roadmaps/postgresql-dba.png
  76. BIN
      public/og-images/roadmaps/prompt-engineering.png
  77. BIN
      public/og-images/roadmaps/python.png
  78. BIN
      public/og-images/roadmaps/qa.png
  79. BIN
      public/og-images/roadmaps/react-native.png
  80. BIN
      public/og-images/roadmaps/react.png
  81. BIN
      public/og-images/roadmaps/rust.png
  82. BIN
      public/og-images/roadmaps/server-side-game-developer.png
  83. BIN
      public/og-images/roadmaps/software-architect.png
  84. BIN
      public/og-images/roadmaps/software-design-architecture.png
  85. BIN
      public/og-images/roadmaps/spring-boot.png
  86. BIN
      public/og-images/roadmaps/sql.png
  87. BIN
      public/og-images/roadmaps/system-design.png
  88. BIN
      public/og-images/roadmaps/technical-writer.png
  89. BIN
      public/og-images/roadmaps/typescript.png
  90. BIN
      public/og-images/roadmaps/ux-design.png
  91. BIN
      public/og-images/roadmaps/vue.png
  92. 554
      scripts/generate-og-images.mjs
  93. 1
      src/lib/author.ts
  94. 38
      src/lib/best-pratice.ts
  95. 12
      src/lib/image.ts
  96. 8
      src/lib/open-graph.ts
  97. 11
      src/pages/[roadmapId]/index.astro
  98. 2
      src/pages/authors/[authorId].astro
  99. 28
      src/pages/authors/[authorId].json.ts
  100. 17
      src/pages/backend/developer-skills.astro
  101. Some files were not shown because too many files have changed in this diff Show More

@ -19,6 +19,7 @@
"generate-renderer": "sh scripts/generate-renderer.sh", "generate-renderer": "sh scripts/generate-renderer.sh",
"best-practice-dirs": "node scripts/best-practice-dirs.cjs", "best-practice-dirs": "node scripts/best-practice-dirs.cjs",
"best-practice-content": "node scripts/best-practice-content.cjs", "best-practice-content": "node scripts/best-practice-content.cjs",
"generate:og": "node ./scripts/generate-og-images.mjs",
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"dependencies": { "dependencies": {
@ -27,6 +28,7 @@
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@fingerprintjs/fingerprintjs": "^4.2.2", "@fingerprintjs/fingerprintjs": "^4.2.2",
"@nanostores/react": "^0.7.1", "@nanostores/react": "^0.7.1",
"@resvg/resvg-js": "^2.6.0",
"@types/react": "^18.2.56", "@types/react": "^18.2.56",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.19",
"astro": "^4.4.0", "astro": "^4.4.0",
@ -34,6 +36,9 @@
"clsx": "^2.1.0", "clsx": "^2.1.0",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"dracula-prism": "^2.1.16", "dracula-prism": "^2.1.16",
"gray-matter": "^4.0.3",
"htm": "^3.1.1",
"image-size": "^1.1.1",
"jose": "^5.2.2", "jose": "^5.2.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"lucide-react": "^0.334.0", "lucide-react": "^0.334.0",
@ -49,6 +54,9 @@
"rehype-external-links": "^3.0.0", "rehype-external-links": "^3.0.0",
"remark-parse": "^11.0.0", "remark-parse": "^11.0.0",
"roadmap-renderer": "^1.0.6", "roadmap-renderer": "^1.0.6",
"satori": "^0.10.13",
"satori-html": "^0.3.2",
"sharp": "^0.33.2",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"tailwind-merge": "^2.2.1", "tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",

@ -20,6 +20,9 @@ dependencies:
'@nanostores/react': '@nanostores/react':
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1(nanostores@0.9.5)(react@18.2.0) version: 0.7.1(nanostores@0.9.5)(react@18.2.0)
'@resvg/resvg-js':
specifier: ^2.6.0
version: 2.6.0
'@types/react': '@types/react':
specifier: ^18.2.56 specifier: ^18.2.56
version: 18.2.59 version: 18.2.59
@ -41,6 +44,15 @@ dependencies:
dracula-prism: dracula-prism:
specifier: ^2.1.16 specifier: ^2.1.16
version: 2.1.16 version: 2.1.16
gray-matter:
specifier: ^4.0.3
version: 4.0.3
htm:
specifier: ^3.1.1
version: 3.1.1
image-size:
specifier: ^1.1.1
version: 1.1.1
jose: jose:
specifier: ^5.2.2 specifier: ^5.2.2
version: 5.2.2 version: 5.2.2
@ -86,6 +98,15 @@ dependencies:
roadmap-renderer: roadmap-renderer:
specifier: ^1.0.6 specifier: ^1.0.6
version: 1.0.6 version: 1.0.6
satori:
specifier: ^0.10.13
version: 0.10.13
satori-html:
specifier: ^0.3.2
version: 0.3.2
sharp:
specifier: ^0.33.2
version: 0.33.2
slugify: slugify:
specifier: ^1.6.6 specifier: ^1.6.6
version: 1.6.6 version: 1.6.6
@ -1222,6 +1243,132 @@ packages:
- immer - immer
dev: false dev: false
/@resvg/resvg-js-android-arm-eabi@2.6.0:
resolution: {integrity: sha512-lJnZ/2P5aMocrFMW7HWhVne5gH82I8xH6zsfH75MYr4+/JOaVcGCTEQ06XFohGMdYRP3v05SSPLPvTM/RHjxfA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-android-arm64@2.6.0:
resolution: {integrity: sha512-N527f529bjMwYWShZYfBD60dXA4Fux+D695QsHQ93BDYZSHUoOh1CUGUyICevnTxs7VgEl98XpArmUWBZQVMfQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-darwin-arm64@2.6.0:
resolution: {integrity: sha512-MabUKLVayEwlPo0mIqAmMt+qESN8LltCvv5+GLgVga1avpUrkxj/fkU1TKm8kQegutUjbP/B0QuMuUr0uhF8ew==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-darwin-x64@2.6.0:
resolution: {integrity: sha512-zrFetdnSw/suXjmyxSjfDV7i61hahv6DDG6kM7BYN2yJ3Es5+BZtqYZTcIWogPJedYKmzN1YTMWGd/3f0ubFiA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-linux-arm-gnueabihf@2.6.0:
resolution: {integrity: sha512-sH4gxXt7v7dGwjGyzLwn7SFGvwZG6DQqLaZ11MmzbCwd9Zosy1TnmrMJfn6TJ7RHezmQMgBPi18bl55FZ1AT4A==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-linux-arm64-gnu@2.6.0:
resolution: {integrity: sha512-fCyMncqCJtrlANADIduYF4IfnWQ295UKib7DAxFXQhBsM9PLDTpizr0qemZcCNadcwSVHnAIzL4tliZhCM8P6A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-linux-arm64-musl@2.6.0:
resolution: {integrity: sha512-ouLjTgBQHQyxLht4FdMPTvuY8xzJigM9EM2Tlu0llWkN1mKyTQrvYWi6TA6XnKdzDJHy7ZLpWpjZi7F5+Pg+Vg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-linux-x64-gnu@2.6.0:
resolution: {integrity: sha512-n3zC8DWsvxC1AwxpKFclIPapDFibs5XdIRoV/mcIlxlh0vseW1F49b97F33BtJQRmlntsqqN6GMMqx8byB7B+Q==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-linux-x64-musl@2.6.0:
resolution: {integrity: sha512-n4tasK1HOlAxdTEROgYA1aCfsEKk0UOFDNd/AQTTZlTmCbHKXPq+O8npaaKlwXquxlVK8vrkcWbksbiGqbCAcw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-win32-arm64-msvc@2.6.0:
resolution: {integrity: sha512-X2+EoBJFwDI5LDVb51Sk7ldnVLitMGr9WwU/i21i3fAeAXZb3hM16k67DeTy16OYkT2dk/RfU1tP1wG+rWbz2Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-win32-ia32-msvc@2.6.0:
resolution: {integrity: sha512-L7oevWjQoUgK5W1fCKn0euSVemhDXVhrjtwqpc7MwBKKimYeiOshO1Li1pa8bBt5PESahenhWgdB6lav9O0fEg==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js-win32-x64-msvc@2.6.0:
resolution: {integrity: sha512-8lJlghb+Unki5AyKgsnFbRJwkEj9r1NpwyuBG8yEJiG1W9eEGl03R3I7bsVa3haof/3J1NlWf0rzSa1G++A2iw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@resvg/resvg-js@2.6.0:
resolution: {integrity: sha512-Tf3YpbBKcQn991KKcw/vg7vZf98v01seSv6CVxZBbRkL/xyjnoYB6KgrFL6zskT1A4dWC/vg77KyNOW+ePaNlA==}
engines: {node: '>= 10'}
optionalDependencies:
'@resvg/resvg-js-android-arm-eabi': 2.6.0
'@resvg/resvg-js-android-arm64': 2.6.0
'@resvg/resvg-js-darwin-arm64': 2.6.0
'@resvg/resvg-js-darwin-x64': 2.6.0
'@resvg/resvg-js-linux-arm-gnueabihf': 2.6.0
'@resvg/resvg-js-linux-arm64-gnu': 2.6.0
'@resvg/resvg-js-linux-arm64-musl': 2.6.0
'@resvg/resvg-js-linux-x64-gnu': 2.6.0
'@resvg/resvg-js-linux-x64-musl': 2.6.0
'@resvg/resvg-js-win32-arm64-msvc': 2.6.0
'@resvg/resvg-js-win32-ia32-msvc': 2.6.0
'@resvg/resvg-js-win32-x64-msvc': 2.6.0
dev: false
/@rollup/rollup-android-arm-eabi@4.9.6: /@rollup/rollup-android-arm-eabi@4.9.6:
resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==} resolution: {integrity: sha512-MVNXSSYN6QXOulbHpLMKYi60ppyO13W9my1qogeiAqtjb2yR4LSmfU2+POvDkLzhjYLXz9Rf9+9a3zFHW1Lecg==}
cpu: [arm] cpu: [arm]
@ -1326,6 +1473,15 @@ packages:
dev: false dev: false
optional: true optional: true
/@shuding/opentype.js@1.4.0-beta.0:
resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==}
engines: {node: '>= 8.0.0'}
hasBin: true
dependencies:
fflate: 0.7.4
string.prototype.codepointat: 0.2.1
dev: false
/@sigstore/bundle@1.1.0: /@sigstore/bundle@1.1.0:
resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==} resolution: {integrity: sha512-PFutXEy0SmQxYI4texPw3dd2KewuNqv7OuK1ZFtY2fM754yhvG2KdgwIhRnoEE2uHdtdGNQ8s0lb94dW9sELog==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -2047,6 +2203,11 @@ packages:
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
dev: false dev: false
/base64-js@0.0.8:
resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==}
engines: {node: '>= 0.4'}
dev: false
/base64-js@1.5.1: /base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: false dev: false
@ -2223,6 +2384,10 @@ packages:
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dev: false dev: false
/camelize@1.0.1:
resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
dev: false
/caniuse-lite@1.0.30001579: /caniuse-lite@1.0.30001579:
resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==} resolution: {integrity: sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA==}
dev: false dev: false
@ -2364,6 +2529,7 @@ packages:
/color-string@1.9.1: /color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
requiresBuild: true
dependencies: dependencies:
color-name: 1.1.4 color-name: 1.1.4
simple-swizzle: 0.2.2 simple-swizzle: 0.2.2
@ -2477,6 +2643,19 @@ packages:
type-fest: 1.4.0 type-fest: 1.4.0
dev: false dev: false
/css-background-parser@0.1.0:
resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==}
dev: false
/css-box-shadow@1.0.0-3:
resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==}
dev: false
/css-color-keywords@1.0.0:
resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
engines: {node: '>=4'}
dev: false
/css-select@5.1.0: /css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies: dependencies:
@ -2487,6 +2666,14 @@ packages:
nth-check: 2.1.1 nth-check: 2.1.1
dev: false dev: false
/css-to-react-native@3.2.0:
resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==}
dependencies:
camelize: 1.0.1
css-color-keywords: 1.0.0
postcss-value-parser: 4.2.0
dev: false
/css-tree@2.2.1: /css-tree@2.2.1:
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
@ -2854,6 +3041,10 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: false dev: false
/escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/escape-string-regexp@1.0.5: /escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
@ -2946,6 +3137,10 @@ packages:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
/fflate@0.7.4:
resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
dev: false
/filename-reserved-regex@2.0.0: /filename-reserved-regex@2.0.0:
resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -3402,6 +3597,11 @@ packages:
hasBin: true hasBin: true
dev: false dev: false
/hex-rgb@4.3.0:
resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
engines: {node: '>=6'}
dev: false
/hosted-git-info@5.2.1: /hosted-git-info@5.2.1:
resolution: {integrity: sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==} resolution: {integrity: sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@ -3416,6 +3616,10 @@ packages:
lru-cache: 7.18.3 lru-cache: 7.18.3
dev: false dev: false
/htm@3.1.1:
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
dev: false
/html-escaper@3.0.3: /html-escaper@3.0.3:
resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
dev: false dev: false
@ -3506,6 +3710,14 @@ packages:
engines: {node: '>= 4'} engines: {node: '>= 4'}
dev: false dev: false
/image-size@1.1.1:
resolution: {integrity: sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==}
engines: {node: '>=16.x'}
hasBin: true
dependencies:
queue: 6.0.2
dev: false
/import-lazy@4.0.0: /import-lazy@4.0.0:
resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -3563,6 +3775,7 @@ packages:
/is-arrayish@0.3.2: /is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
requiresBuild: true
dev: false dev: false
/is-binary-path@2.1.0: /is-binary-path@2.1.0:
@ -3916,6 +4129,13 @@ packages:
resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==} resolution: {integrity: sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==}
engines: {node: '>=14'} engines: {node: '>=14'}
/linebreak@1.1.0:
resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==}
dependencies:
base64-js: 0.0.8
unicode-trie: 2.0.0
dev: false
/lines-and-columns@1.2.4: /lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
@ -5116,6 +5336,10 @@ packages:
- supports-color - supports-color
dev: false dev: false
/pako@0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
dev: false
/param-case@3.0.4: /param-case@3.0.4:
resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==}
dependencies: dependencies:
@ -5123,6 +5347,13 @@ packages:
tslib: 2.6.2 tslib: 2.6.2
dev: false dev: false
/parse-css-color@0.2.1:
resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==}
dependencies:
color-name: 1.1.4
hex-rgb: 4.3.0
dev: false
/parse-github-url@1.0.2: /parse-github-url@1.0.2:
resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==} resolution: {integrity: sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -5505,6 +5736,12 @@ packages:
dev: false dev: false
optional: true optional: true
/queue@6.0.2:
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
dependencies:
inherits: 2.0.4
dev: false
/quick-lru@5.1.1: /quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -5886,6 +6123,28 @@ packages:
suf-log: 2.5.3 suf-log: 2.5.3
dev: true dev: true
/satori-html@0.3.2:
resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==}
dependencies:
ultrahtml: 1.5.2
dev: false
/satori@0.10.13:
resolution: {integrity: sha512-klCwkVYMQ/ZN5inJLHzrUmGwoRfsdP7idB5hfpJ1jfiJk1ErDitK8Hkc6Kll1+Ox2WtqEuGecSZLnmup3CGzvQ==}
engines: {node: '>=16'}
dependencies:
'@shuding/opentype.js': 1.4.0-beta.0
css-background-parser: 0.1.0
css-box-shadow: 1.0.0-3
css-to-react-native: 3.2.0
emoji-regex: 10.3.0
escape-html: 1.0.3
linebreak: 1.1.0
parse-css-color: 0.2.1
postcss-value-parser: 4.2.0
yoga-wasm-web: 0.3.3
dev: false
/sax@1.3.0: /sax@1.3.0:
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
dev: false dev: false
@ -6041,6 +6300,7 @@ packages:
/simple-swizzle@0.2.2: /simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
requiresBuild: true
dependencies: dependencies:
is-arrayish: 0.3.2 is-arrayish: 0.3.2
dev: false dev: false
@ -6211,6 +6471,10 @@ packages:
strip-ansi: 7.1.0 strip-ansi: 7.1.0
dev: false dev: false
/string.prototype.codepointat@0.2.1:
resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==}
dev: false
/string_decoder@1.3.0: /string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies: dependencies:
@ -6427,6 +6691,10 @@ packages:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
/tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
dev: false
/to-fast-properties@2.0.0: /to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -6529,6 +6797,13 @@ packages:
resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==} resolution: {integrity: sha512-akOOQ/Yln8a2sgcLj4U0Jmx0R5jpIg2IUyRrWOzmEbjBtGzBdHtSeFKgoEcoH4KYIG/Pb8GQ/BwtYm0GCq1Sqg==}
dev: false dev: false
/unicode-trie@2.0.0:
resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==}
dependencies:
pako: 0.2.9
tiny-inflate: 1.0.3
dev: false
/unified@10.1.2: /unified@10.1.2:
resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==} resolution: {integrity: sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==}
dependencies: dependencies:
@ -6945,6 +7220,10 @@ packages:
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
dev: false dev: false
/yoga-wasm-web@0.3.3:
resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==}
dev: false
/zod@3.22.4: /zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false dev: false

@ -0,0 +1,3 @@
<svg width="46" height="27" viewBox="0 0 46 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M43.354 0.9C42.184 0.9 41.2371 1.84684 41.2371 3.01686C41.2371 3.30867 41.3062 3.57708 41.4117 3.82435L33.4085 15.0163C33.38 15.0161 33.3514 15.0167 33.3248 15.0172C33.3051 15.0176 33.2864 15.018 33.2697 15.018C32.8703 15.018 32.484 15.1223 32.161 15.3186L25.2976 11.9024C25.1995 10.8219 24.2903 9.97585 23.1854 9.97585C22.0154 9.97585 21.0686 10.9227 21.0686 12.0927C21.0686 12.1865 21.0799 12.2794 21.0925 12.3656L13.8077 18.1561C13.5852 18.0783 13.3472 18.0433 13.1011 18.0433C12.0622 18.0433 11.2066 18.7882 11.0265 19.7732L4.26122 22.5041C3.91213 22.2447 3.48642 22.077 3.01686 22.077C1.84684 22.077 0.9 23.0238 0.9 24.1938C0.9 25.3639 1.84684 26.3107 3.01686 26.3107C4.06426 26.3107 4.92372 25.5497 5.0923 24.5492L11.8566 21.8497C12.2057 22.1092 12.6315 22.277 13.1011 22.277C14.2711 22.277 15.218 21.3301 15.218 20.1601C15.218 20.0663 15.2067 19.9735 15.194 19.8873L22.4789 14.0968C22.7013 14.1746 22.9393 14.2096 23.1854 14.2096C23.5848 14.2096 23.9711 14.1053 24.2941 13.909L31.1575 17.3252C31.2556 18.4057 32.1649 19.2517 33.2697 19.2517C34.4397 19.2517 35.3866 18.3049 35.3866 17.1348C35.3866 16.843 35.3175 16.5746 35.2119 16.3273L43.2151 5.13536C43.2437 5.13561 43.2723 5.13503 43.2989 5.13449C43.3186 5.13409 43.3373 5.13371 43.354 5.13371C44.524 5.13371 45.4708 4.18687 45.4708 3.01686C45.4708 1.84684 44.524 0.9 43.354 0.9Z" fill="black" stroke="black" stroke-width="0.2" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

@ -0,0 +1,554 @@
import path from 'node:path';
import fs from 'node:fs/promises';
import matter from 'gray-matter';
import { html } from 'satori-html';
import satori from 'satori';
import sharp from 'sharp';
import imageSize from 'image-size';
import { Resvg } from '@resvg/resvg-js';
const ALL_ROADMAP_DIR = path.join(process.cwd(), '/src/data/roadmaps');
const ALL_BEST_PRACTICE_DIR = path.join(
process.cwd(),
'/src/data/best-practices',
);
const ALL_GUIDE_DIR = path.join(process.cwd(), '/src/data/guides');
const ALl_AUTHOR_DIR = path.join(process.cwd(), '/src/data/authors');
const ALL_ROADMAP_IMAGE_DIR = path.join(process.cwd(), '/public/roadmaps');
const ALL_BEST_PRACTICE_IMAGE_DIR = path.join(
process.cwd(),
'/public/best-practices',
);
const ALL_AUTHOR_IMAGE_DIR = path.join(process.cwd(), '/public');
const alreadyGeneratedImages = await fs.readdir(
path.join(process.cwd(), '/public/og-images'),
{
recursive: true,
},
);
async function getAllRoadmaps() {
const allRoadmapDirNames = await fs.readdir(ALL_ROADMAP_DIR);
const allRoadmapFrontmatter = await Promise.all(
allRoadmapDirNames.map(async (roadmapDirName) => {
const roadmapDirPath = path.join(
ALL_ROADMAP_DIR,
roadmapDirName,
`${roadmapDirName}.md`,
);
const markdown = await fs.readFile(roadmapDirPath, 'utf8');
const { data } = matter(markdown);
return {
id: roadmapDirName,
title: data?.briefTitle,
description: data?.briefDescription,
};
}),
);
return allRoadmapFrontmatter;
}
async function getAllBestPractices() {
const allBestPracticeDirNames = await fs.readdir(ALL_BEST_PRACTICE_DIR);
const allBestPracticeFrontmatter = await Promise.all(
allBestPracticeDirNames.map(async (bestPracticeDirName) => {
const bestPracticeDirPath = path.join(
ALL_BEST_PRACTICE_DIR,
bestPracticeDirName,
`${bestPracticeDirName}.md`,
);
const markdown = await fs.readFile(bestPracticeDirPath, 'utf8');
const { data } = matter(markdown);
return {
id: bestPracticeDirName,
title: data?.briefTitle,
description: data?.briefDescription,
};
}),
);
return allBestPracticeFrontmatter;
}
async function getAllGuides() {
const allGuideDirNames = await fs.readdir(ALL_GUIDE_DIR);
const allGuideFrontmatter = await Promise.all(
allGuideDirNames.map(async (guideDirName) => {
const guideDirPath = path.join(ALL_GUIDE_DIR, guideDirName);
const markdown = await fs.readFile(guideDirPath, 'utf8');
const { data } = matter(markdown);
return {
id: guideDirName?.replace('.md', ''),
title: data?.title,
description: data?.description,
authorId: data?.authorId,
};
}),
);
return allGuideFrontmatter;
}
async function getAllAuthors() {
const allAuthorDirNames = await fs.readdir(ALl_AUTHOR_DIR);
const allAuthorFrontmatter = await Promise.all(
allAuthorDirNames.map(async (authorDirName) => {
const authorDirPath = path.join(ALl_AUTHOR_DIR, authorDirName);
const markdown = await fs.readFile(authorDirPath, 'utf8');
const { data } = matter(markdown);
return {
id: authorDirName?.replace('.md', ''),
name: data?.name,
imageUrl: data?.imageUrl,
};
}),
);
return allAuthorFrontmatter;
}
async function getAllRoadmapImageIds() {
const allRoadmapImageDirNames = await fs.readdir(ALL_ROADMAP_IMAGE_DIR);
return allRoadmapImageDirNames?.reduce((acc, image) => {
acc[image.replace(/(\.[^.]*)$/, '')] = image;
return acc;
}, {});
}
async function getAllBestPracticeImageIds() {
const allBestPracticeImageDirNames = await fs.readdir(
ALL_BEST_PRACTICE_IMAGE_DIR,
);
return allBestPracticeImageDirNames?.reduce((acc, image) => {
acc[image.replace(/(\.[^.]*)$/, '')] = image;
return acc;
}, {});
}
async function generateResourceOpenGraph() {
const allRoadmaps = (await getAllRoadmaps()).filter(
(roadmap) => !alreadyGeneratedImages.includes(`roadmaps/${roadmap.id}.png`),
);
const allBestPractices = (await getAllBestPractices()).filter(
(bestPractice) =>
!alreadyGeneratedImages.includes(`best-practices/${bestPractice.id}.png`),
);
const allRoadmapImageIds = await getAllRoadmapImageIds();
const allBestPracticeImageIds = await getAllBestPracticeImageIds();
const resources = [];
allRoadmaps.forEach((roadmap) => {
const hasImage = allRoadmapImageIds?.[roadmap.id];
resources.push({
type: 'roadmaps',
id: roadmap.id,
title: roadmap.title,
description: roadmap.description,
image: hasImage
? path.join(ALL_ROADMAP_IMAGE_DIR, allRoadmapImageIds[roadmap.id])
: null,
});
});
allBestPractices.forEach((bestPractice) => {
const hasImage = allBestPracticeImageIds?.[bestPractice.id];
resources.push({
type: 'best-practices',
id: bestPractice.id,
title: bestPractice.title,
description: bestPractice.description,
image: hasImage
? path.join(
ALL_BEST_PRACTICE_IMAGE_DIR,
allBestPracticeImageIds[bestPractice.id],
)
: null,
});
});
for (const resource of resources) {
if (!resource.image) {
let template = getRoadmapDefaultTemplate(resource);
if (
hasSpecialCharacters(resource.title) ||
hasSpecialCharacters(resource.description)
) {
// For some reason special characters are not being rendered properly
// https://github.com/natemoo-re/satori-html/issues/20
// So we need to unescape the html
template = JSON.parse(unescapeHtml(JSON.stringify(template)));
}
await generateOpenGraph(
template,
resource.type,
resource.id + '.png',
'resvg',
);
} else {
const image = await fs.readFile(resource.image);
const dimensions = imageSize(image);
const widthRatio = 1200 / dimensions.width;
let width = dimensions.width * widthRatio * 0.85;
let height = dimensions.height * widthRatio * 0.85;
let template = getRoadmapImageTemplate({
...resource,
image: `data:image/${dimensions.type};base64,${image.toString('base64')}`,
width,
height,
});
if (
hasSpecialCharacters(resource.title) ||
hasSpecialCharacters(resource.description)
) {
// For some reason special characters are not being rendered properly
// https://github.com/natemoo-re/satori-html/issues/20
// So we need to unescape the html
template = JSON.parse(unescapeHtml(JSON.stringify(template)));
}
await generateOpenGraph(template, resource.type, resource.id + '.png');
}
}
}
async function generateGuideOpenGraph() {
const allGuides = (await getAllGuides()).filter(
(guide) => !alreadyGeneratedImages.includes(`guides/${guide.id}.png`),
);
const allAuthors = await getAllAuthors();
for (const guide of allGuides) {
const author = allAuthors.find((author) => author.id === guide.authorId);
const image =
author?.imageUrl || 'https://roadmap.sh/images/default-avatar.png';
const isExternalImage = image?.startsWith('http');
let authorImageExtention = '';
let authorAvatar;
if (!isExternalImage) {
authorAvatar = await fs.readFile(path.join(ALL_AUTHOR_IMAGE_DIR, image));
authorImageExtention = image?.split('.')[1];
}
const template = getGuideTemplate({
...guide,
authorName: author.name,
authorAvatar: isExternalImage
? image
: `data:image/${authorImageExtention};base64,${authorAvatar.toString('base64')}`,
});
if (
hasSpecialCharacters(guide.title) ||
hasSpecialCharacters(guide.description)
) {
// For some reason special characters are not being rendered properly
// https://github.com/natemoo-re/satori-html/issues/20
// So we need to unescape the html
template = JSON.parse(unescapeHtml(JSON.stringify(template)));
}
await generateOpenGraph(template, 'guides', guide.id + '.png');
}
}
async function generateOpenGraph(
htmlString,
type,
fileName,
renderer = 'sharp',
) {
console.log('Started 🚀', `${type}/${fileName}`);
const svg = await satori(htmlString, {
width: 1200,
height: 630,
fonts: [
{
name: 'balsamiq',
data: await fs.readFile(
path.join(process.cwd(), '/public/fonts/BalsamiqSans-Regular.ttf'),
),
weight: 400,
style: 'normal',
},
],
});
await fs.mkdir(path.join(process.cwd(), '/public/og-images/' + type), {
recursive: true,
});
// It will be used to generate the default image
// for some reasone sharp is not working with this
// FIXME: Investigate why sharp is not working with this
if (renderer === 'resvg') {
const resvg = new Resvg(svg, {
fitTo: {
mode: 'width',
value: 2500,
},
});
const pngData = resvg.render();
const pngBuffer = pngData.asPng();
await fs.writeFile(
path.join(process.cwd(), '/public/og-images/' + `${type}/${fileName}`),
pngBuffer,
);
} else {
await sharp(Buffer.from(svg), { density: 150 })
.png()
.toFile(
path.join(process.cwd(), '/public/og-images/' + `${type}/${fileName}`),
);
}
console.log('Completed ✅', `${type}/${fileName}`);
}
await generateResourceOpenGraph();
await generateGuideOpenGraph();
function getRoadmapDefaultTemplate({ title, description }) {
return html`<div tw="bg-white relative flex flex-col h-full w-full">
<div
tw="absolute flex top-[90px] left-0 w-full h-px bg-black opacity-5"
></div>
<div tw="absolute flex top-0 left-0 w-full h-[18px] bg-black"></div>
<div tw="absolute flex bottom-0 left-0 w-full h-[18px] bg-black"></div>
<div
tw="absolute flex bottom-[90px] left-0 w-full h-px bg-black opacity-5"
></div>
<div
tw="absolute flex top-0 left-[90px] h-full w-px bg-black opacity-5"
></div>
<div
tw="absolute flex top-0 right-[90px] h-full w-px bg-black opacity-5"
></div>
<div tw="flex flex-col px-[100px] py-[90px] h-full">
<div tw="flex justify-between flex-col p-[30px] h-full">
<div tw="flex flex-col">
<div tw="text-[70px] leading-[70px] tracking-tight">${title}</div>
<div
tw="mt-[16px] text-[30px] leading-[36px] tracking-tight opacity-80"
>
${description}
</div>
</div>
<div tw="flex flex-col">
<div tw="flex items-center mt-2.5">
<div
tw="flex items-center justify-center w-[40px] h-[40px] mr-[24px]"
>
<svg
width="46"
height="27"
viewBox="0 0 46 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M43.354 0.9C42.184 0.9 41.2371 1.84684 41.2371 3.01686C41.2371 3.30867 41.3062 3.57708 41.4117 3.82435L33.4085 15.0163C33.38 15.0161 33.3514 15.0167 33.3248 15.0172C33.3051 15.0176 33.2864 15.018 33.2697 15.018C32.8703 15.018 32.484 15.1223 32.161 15.3186L25.2976 11.9024C25.1995 10.8219 24.2903 9.97585 23.1854 9.97585C22.0154 9.97585 21.0686 10.9227 21.0686 12.0927C21.0686 12.1865 21.0799 12.2794 21.0925 12.3656L13.8077 18.1561C13.5852 18.0783 13.3472 18.0433 13.1011 18.0433C12.0622 18.0433 11.2066 18.7882 11.0265 19.7732L4.26122 22.5041C3.91213 22.2447 3.48642 22.077 3.01686 22.077C1.84684 22.077 0.9 23.0238 0.9 24.1938C0.9 25.3639 1.84684 26.3107 3.01686 26.3107C4.06426 26.3107 4.92372 25.5497 5.0923 24.5492L11.8566 21.8497C12.2057 22.1092 12.6315 22.277 13.1011 22.277C14.2711 22.277 15.218 21.3301 15.218 20.1601C15.218 20.0663 15.2067 19.9735 15.194 19.8873L22.4789 14.0968C22.7013 14.1746 22.9393 14.2096 23.1854 14.2096C23.5848 14.2096 23.9711 14.1053 24.2941 13.909L31.1575 17.3252C31.2556 18.4057 32.1649 19.2517 33.2697 19.2517C34.4397 19.2517 35.3866 18.3049 35.3866 17.1348C35.3866 16.843 35.3175 16.5746 35.2119 16.3273L43.2151 5.13536C43.2437 5.13561 43.2723 5.13503 43.2989 5.13449C43.3186 5.13409 43.3373 5.13371 43.354 5.13371C44.524 5.13371 45.4708 4.18687 45.4708 3.01686C45.4708 1.84684 44.524 0.9 43.354 0.9Z"
fill="black"
stroke="black"
stroke-width="0.2"
/>
</svg>
</div>
<div tw="text-[30px] flex leading-[30px]">
6th most starred GitHub project
</div>
</div>
<div tw="flex items-center mt-2.5">
<div
tw="flex items-center justify-center w-[40px] h-[40px] mr-[24px]"
>
<svg
width="40"
height="27"
viewBox="0 0 40 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M24.8419 21.5546V23.347H37.3473V22.3072C37.3473 21.803 37.1644 21.3086 36.7814 20.9808C35.797 20.1382 34.0544 19.1021 31.4735 19.1021C28.1305 19.1021 25.8107 20.618 24.8419 21.5546ZM22.7297 19.8874C23.9917 18.5206 27.0669 16.4008 31.4735 16.4008C35.9173 16.4008 38.5374 18.7892 39.5092 19.9307C39.8516 20.3328 40 20.825 40 21.2875V26.0483H31.0946H22.1892V21.2978C22.1892 20.8197 22.349 20.2997 22.7297 19.8874Z"
fill="black"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.30026 21.0084C2.86588 21.3329 2.65267 21.8607 2.65267 22.4029V23.347H15.1581V21.5229C14.3747 20.6776 12.4668 19.1021 9.28433 19.1021C6.53917 19.1021 4.48401 20.1243 3.30026 21.0084ZM0.540477 19.8874C1.80253 18.5206 4.87765 16.4008 9.28433 16.4008C13.7281 16.4008 16.3482 18.7892 17.32 19.9307C17.6624 20.3328 17.8108 20.825 17.8108 21.2875V26.0483H8.90538H0V21.2978C0 20.8197 0.15977 20.2997 0.540477 19.8874Z"
fill="black"
/>
<rect
x="10.6122"
y="16.4008"
width="17.3655"
height="7.718"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.8062 19.6515C11.3801 19.9868 11.1665 20.5126 11.1665 21.0548V22.5365H27.4235V20.9495C27.4235 20.4454 27.2397 19.9534 26.8651 19.616C25.6227 18.4973 23.3035 17.0182 19.7876 17.0182C16.0572 17.0182 13.307 18.4702 11.8062 19.6515ZM8.42064 18.0391C10.0613 16.2623 14.059 13.5065 19.7876 13.5065C25.5645 13.5065 28.9707 16.6115 30.2341 18.0954C30.6791 18.6181 30.872 19.258 30.872 19.8592V26.0482H19.295H7.71802V19.8727C7.71802 19.2511 7.92572 18.5751 8.42064 18.0391Z"
fill="black"
/>
<circle
cx="20.2598"
cy="5.7885"
r="4.0385"
stroke="black"
stroke-width="3.5"
/>
<circle
cx="31.8367"
cy="9.64748"
r="3.07375"
stroke="black"
stroke-width="3.5"
/>
<circle
cx="8.68276"
cy="9.64748"
r="3.07375"
stroke="black"
stroke-width="3.5"
/>
</svg>
</div>
<div tw="text-[30px] flex leading-[30px]">
Created and maintained by community
</div>
</div>
<div tw="flex items-center mt-2.5">
<div
tw="flex items-center justify-center w-[40px] h-[40px] mr-[24px]"
>
<svg
width="38"
height="38"
viewBox="0 0 38 38"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 33.155C21.375 33.155 23.3541 34.8334 25.3333 34.8334C30.0833 34.8334 34.8333 22.1667 34.8333 15.485C34.7793 13.4342 33.9169 11.4878 32.434 10.0701C30.951 8.65243 28.9678 7.87839 26.9166 7.9167C23.4016 7.9167 20.5833 10.1967 19 11.0834C17.4166 10.1967 14.5983 7.9167 11.0833 7.9167C9.0309 7.8742 7.04532 8.64686 5.56147 10.0654C4.07761 11.484 3.21646 13.4328 3.16663 15.485C3.16663 22.1667 7.91663 34.8334 12.6666 34.8334C14.6458 34.8334 16.625 33.155 19 33.155Z"
stroke="black"
stroke-width="3.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M15.8334 3.16699C17.4167 3.95866 19 6.33366 19 11.0837"
stroke="black"
stroke-width="3.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div tw="text-[30px] flex leading-[30px]">Up-to-date roadmap</div>
</div>
</div>
</div>
</div>
</div> `;
}
function getRoadmapImageTemplate({ title, description, image, height, width }) {
return html`<div tw="bg-white relative flex flex-col h-full w-full">
<div tw="absolute flex top-0 left-0 w-full h-[18px] bg-black"></div>
<div tw="flex flex-col px-[90px] pt-[90px]">
<div tw="flex flex-col pb-0">
<div tw="text-[70px] leading-[70px] tracking-tight">
${title?.replace('&', `{"&"}`)}
</div>
<div
tw="mt-[16px] text-[30px] leading-[36px] tracking-tight opacity-80"
>
${description}
</div>
</div>
</div>
<img
src="${image}"
width="${width}"
height="${height}"
tw="mx-auto mt-[36px]"
/>
</div> `;
}
function getGuideTemplate({ title, description, authorName, authorAvatar }) {
return html`<div tw="bg-white relative flex flex-col h-full w-full">
<div
tw="absolute flex top-[90px] left-0 w-full h-px bg-black opacity-5"
></div>
<div tw="absolute flex top-0 left-0 w-full h-[18px] bg-black"></div>
<div tw="absolute flex bottom-0 left-0 w-full h-[18px] bg-black"></div>
<div
tw="absolute flex bottom-[90px] left-0 w-full h-px bg-black opacity-5"
></div>
<div
tw="absolute flex top-0 left-[90px] h-full w-px bg-black opacity-5"
></div>
<div
tw="absolute flex top-0 right-[90px] h-full w-px bg-black opacity-5"
></div>
<div tw="flex flex-col px-[100px] py-[90px] h-full">
<div tw="flex justify-center flex-col p-[30px] h-full">
<div tw="flex flex-col">
<div tw="flex items-center">
<img
src="${authorAvatar}"
width="30"
height="30"
tw="rounded-full"
/>
<div tw="text-[20px] leading-[20px] tracking-tight ml-3">
${authorName}
</div>
</div>
<div tw="mt-6 text-[48px] leading-tight tracking-tight">${title}</div>
<div tw="mt-3 text-[24px] leading-[30px] tracking-tight opacity-80">
${description}
</div>
</div>
</div>
</div>
</div> `;
}
function unescapeHtml(html) {
return html
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'");
}
function hasSpecialCharacters(str) {
return /[&<>"]/.test(str);
}

@ -38,7 +38,6 @@ export async function getAuthorIds() {
}, },
); );
console.log(Object.keys(authorFiles));
return Object.keys(authorFiles).map(authorPathToId); return Object.keys(authorFiles).map(authorPathToId);
} }

@ -48,7 +48,7 @@ export async function getBestPracticeIds() {
'/src/data/best-practices/*/*.md', '/src/data/best-practices/*/*.md',
{ {
eager: true, eager: true,
} },
); );
return Object.keys(bestPracticeFiles).map(bestPracticePathToId); return Object.keys(bestPracticeFiles).map(bestPracticePathToId);
@ -64,7 +64,7 @@ export async function getAllBestPractices(): Promise<BestPracticeFileType[]> {
'/src/data/best-practices/*/*.md', '/src/data/best-practices/*/*.md',
{ {
eager: true, eager: true,
} },
); );
const bestPracticeFiles = Object.values(bestPracticeFilesMap); const bestPracticeFiles = Object.values(bestPracticeFilesMap);
@ -74,6 +74,38 @@ export async function getAllBestPractices(): Promise<BestPracticeFileType[]> {
})); }));
return bestPracticeItems.sort( return bestPracticeItems.sort(
(a, b) => a.frontmatter.order - b.frontmatter.order (a, b) => a.frontmatter.order - b.frontmatter.order,
); );
} }
/**
* Gets the best practice file by ID
*
* @param id - Best practice file ID
* @returns BestPracticeFileType
*/
export async function getBestPracticeById(
id: string,
): Promise<BestPracticeFileType | null> {
const bestPracticeFilesMap = import.meta.glob<BestPracticeFileType>(
'/src/data/best-practices/*/*.md',
{
eager: true,
},
);
const bestPracticeFiles = Object.values(bestPracticeFilesMap);
const bestPracticeFile = bestPracticeFiles.find(
(bestPracticeFile) => bestPracticePathToId(bestPracticeFile.file) === id,
);
if (!bestPracticeFile) {
throw new Error(`Best practice with ID ${id} not found`);
}
return {
...bestPracticeFile,
id: bestPracticePathToId(bestPracticeFile.file),
};
}

@ -0,0 +1,12 @@
import imageSize from 'image-size';
import { readFile } from 'node:fs/promises';
export async function getLocalImageDimensions(path: string) {
try {
const imageBuffer = await readFile(path);
return imageSize(imageBuffer);
} catch (error) {
console.error(error, (error as Error)?.stack);
return null;
}
}

@ -0,0 +1,8 @@
type RoadmapOpenGraphQuery = {
group: 'roadmaps' | 'guides' | 'best-practices';
resourceId: string;
};
export function getOpenGraphImageUrl(params: RoadmapOpenGraphQuery) {
return `${import.meta.env.DEV ? 'http://localhost:3000' : 'https://roadmap.sh'}/og-images/${params.group}/${params.resourceId}.png`;
}

@ -13,6 +13,7 @@ import {
generateArticleSchema, generateArticleSchema,
generateFAQSchema, generateFAQSchema,
} from '../../lib/jsonld-schema'; } from '../../lib/jsonld-schema';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
export async function getStaticPaths() { export async function getStaticPaths() {
@ -55,14 +56,20 @@ if (roadmapData.schema) {
if (roadmapFAQs.length) { if (roadmapFAQs.length) {
jsonLdSchema.push(generateFAQSchema(roadmapFAQs)); jsonLdSchema.push(generateFAQSchema(roadmapFAQs));
} }
const ogImageUrl =
roadmapData?.seo?.ogImageUrl ||
getOpenGraphImageUrl({
group: 'roadmaps',
resourceId: roadmapId,
});
--- ---
<BaseLayout <BaseLayout
permalink={`/${roadmapId}`} permalink={`/${roadmapId}`}
title={roadmapData?.seo?.title} title={roadmapData?.seo?.title}
briefTitle={roadmapData.briefTitle} briefTitle={roadmapData.briefTitle}
ogImageUrl={roadmapData?.seo?.ogImageUrl || ogImageUrl={ogImageUrl}
'https://roadmap.sh/images/og-img.png'}
description={roadmapData.seo.description} description={roadmapData.seo.description}
keywords={roadmapData.seo.keywords} keywords={roadmapData.seo.keywords}
noIndex={roadmapData.isUpcoming} noIndex={roadmapData.isUpcoming}

@ -30,7 +30,7 @@ const videos = await getVideosByAuthor(authorId);
permalink={`/author/${author.id}`} permalink={`/author/${author.id}`}
title={`${author.frontmatter.name} - Author at roadmap.sh`} title={`${author.frontmatter.name} - Author at roadmap.sh`}
briefTitle={author.frontmatter.name} briefTitle={author.frontmatter.name}
ogImageUrl={`https://roadmap.sh/${author.frontmatter.imageUrl}`} ogImageUrl={`https://roadmap.sh/${authorFrontmatter.imageUrl}`}
description={`${author.frontmatter.name} has written ${guides.length} articles on roadmap.sh on a variety of topics.`} description={`${author.frontmatter.name} has written ${guides.length} articles on roadmap.sh on a variety of topics.`}
noIndex={false} noIndex={false}
jsonLd={[ jsonLd={[

@ -0,0 +1,28 @@
import type { APIRoute } from 'astro';
import { getAuthorById, getAuthorIds } from '../../lib/author';
export async function getStaticPaths() {
const authorIds = await getAuthorIds();
return await Promise.all(
authorIds.map(async (authorId) => {
const authorDetails = await getAuthorById(authorId);
return {
params: { authorId },
props: {
authorDetails: authorDetails?.frontmatter || {},
},
};
}),
);
}
export const GET: APIRoute = async function ({ params, request, props }) {
return new Response(JSON.stringify(props.authorDetails), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
};

@ -3,18 +3,25 @@ import GuideHeader from '../../components/GuideHeader.astro';
import MarkdownFile from '../../components/MarkdownFile.astro'; import MarkdownFile from '../../components/MarkdownFile.astro';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { getGuideById } from '../../lib/guide'; import { getGuideById } from '../../lib/guide';
import { getOpenGraphImageUrl } from '../../lib/open-graph';
const guideId = 'backend-developer-skills'; const guideId = 'backend-developer-skills';
const guide = await getGuideById(guideId); const guide = await getGuideById(guideId);
const { frontmatter: guideData } = guide; const { frontmatter: guideData } = guide!;
const ogImageUrl = getOpenGraphImageUrl({
group: 'guides',
resourceId: guideId,
});
--- ---
<BaseLayout <BaseLayout
title={guideData.seo.title} title={guideData.seo.title}
description={guideData.seo.description} description={guideData.seo.description}
permalink={`/backend/${guideId}`} permalink={`/backend/${guideId}`}
canonicalUrl={guideData.canonicalUrl} canonicalUrl={guideData.canonicalUrl}
ogImageUrl={ogImageUrl}
> >
<GuideHeader guide={guide} /> <GuideHeader guide={guide} />

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save