Code for my personal website

feat: migrate to ghost cms

+179 -295
+2
package.json
··· 21 21 "@fontsource/inter": "^5.1.0", 22 22 "@fontsource/lora": "^5.1.0", 23 23 "@tailwindcss/typography": "^0.5.15", 24 + "@tryghost/content-api": "^1.11.21", 24 25 "astro": "^4.16.13", 25 26 "canvas-confetti": "^1.9.3", 26 27 "clsx": "^2.1.1", ··· 31 32 }, 32 33 "devDependencies": { 33 34 "@types/canvas-confetti": "^1.6.4", 35 + "@types/tryghost__content-api": "^1.3.17", 34 36 "@typescript-eslint/eslint-plugin": "^8.5.0", 35 37 "@typescript-eslint/parser": "^8.5.0", 36 38 "eslint": "^9.10.0",
+79
pnpm-lock.yaml
··· 29 29 '@tailwindcss/typography': 30 30 specifier: ^0.5.15 31 31 version: 0.5.15(tailwindcss@3.4.11) 32 + '@tryghost/content-api': 33 + specifier: ^1.11.21 34 + version: 1.11.21 32 35 astro: 33 36 specifier: ^4.16.13 34 37 version: 4.16.13(typescript@5.6.2) ··· 55 58 '@types/canvas-confetti': 56 59 specifier: ^1.6.4 57 60 version: 1.6.4 61 + '@types/tryghost__content-api': 62 + specifier: ^1.3.17 63 + version: 1.3.17 58 64 '@typescript-eslint/eslint-plugin': 59 65 specifier: ^8.5.0 60 66 version: 8.5.0(@typescript-eslint/parser@8.5.0)(eslint@9.10.0)(typescript@5.6.2) ··· 1262 1268 tailwindcss: 3.4.11 1263 1269 dev: false 1264 1270 1271 + /@tryghost/content-api@1.11.21: 1272 + resolution: {integrity: sha512-ozJqEMHDUO7D0SGxPbUnG+RvwBbzC3zmdGOW8cFvkcKzrhe7uOAmVKyq7/J3kRAM2QthTlmiDpqp7NEo9ZLlKg==} 1273 + dependencies: 1274 + axios: 1.7.7 1275 + transitivePeerDependencies: 1276 + - debug 1277 + dev: false 1278 + 1265 1279 /@types/acorn@4.0.6: 1266 1280 resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} 1267 1281 dependencies: ··· 1356 1370 dependencies: 1357 1371 '@types/node': 17.0.45 1358 1372 dev: false 1373 + 1374 + /@types/tryghost__content-api@1.3.17: 1375 + resolution: {integrity: sha512-4DASYoK0hP1+XDyLS/8IZevalQRJuPmyPmfxdT1hnYRjxnJkgusATeDc/7QXA2izMZ/+cWkgdZDeTN2cBW+EoA==} 1376 + dev: true 1359 1377 1360 1378 /@types/unist@2.0.11: 1361 1379 resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} ··· 1858 1876 synckit: 0.9.1 1859 1877 dev: true 1860 1878 1879 + /asynckit@0.4.0: 1880 + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 1881 + dev: false 1882 + 1861 1883 /autoprefixer@10.4.20(postcss@8.4.47): 1862 1884 resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} 1863 1885 engines: {node: ^10 || ^12 || >=14} ··· 1886 1908 engines: {node: '>=4'} 1887 1909 dev: true 1888 1910 1911 + /axios@1.7.7: 1912 + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} 1913 + dependencies: 1914 + follow-redirects: 1.15.9 1915 + form-data: 4.0.1 1916 + proxy-from-env: 1.1.0 1917 + transitivePeerDependencies: 1918 + - debug 1919 + dev: false 1920 + 1889 1921 /axobject-query@4.1.0: 1890 1922 resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} 1891 1923 engines: {node: '>= 0.4'} ··· 2120 2152 color-string: 1.9.1 2121 2153 dev: false 2122 2154 2155 + /combined-stream@1.0.8: 2156 + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 2157 + engines: {node: '>= 0.8'} 2158 + dependencies: 2159 + delayed-stream: 1.0.0 2160 + dev: false 2161 + 2123 2162 /comma-separated-tokens@2.0.3: 2124 2163 resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} 2125 2164 dev: false ··· 2253 2292 object-keys: 1.1.1 2254 2293 dev: true 2255 2294 2295 + /delayed-stream@1.0.0: 2296 + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 2297 + engines: {node: '>=0.4.0'} 2298 + dev: false 2299 + 2256 2300 /dequal@2.0.3: 2257 2301 resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 2258 2302 engines: {node: '>=6'} ··· 2862 2906 engines: {node: '>=8'} 2863 2907 dev: false 2864 2908 2909 + /follow-redirects@1.15.9: 2910 + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} 2911 + engines: {node: '>=4.0'} 2912 + peerDependencies: 2913 + debug: '*' 2914 + peerDependenciesMeta: 2915 + debug: 2916 + optional: true 2917 + dev: false 2918 + 2865 2919 /for-each@0.3.3: 2866 2920 resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} 2867 2921 dependencies: ··· 2874 2928 dependencies: 2875 2929 cross-spawn: 7.0.3 2876 2930 signal-exit: 4.1.0 2931 + dev: false 2932 + 2933 + /form-data@4.0.1: 2934 + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} 2935 + engines: {node: '>= 6'} 2936 + dependencies: 2937 + asynckit: 0.4.0 2938 + combined-stream: 1.0.8 2939 + mime-types: 2.1.35 2877 2940 dev: false 2878 2941 2879 2942 /fraction.js@4.3.7: ··· 4330 4393 braces: 3.0.3 4331 4394 picomatch: 2.3.1 4332 4395 4396 + /mime-db@1.52.0: 4397 + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 4398 + engines: {node: '>= 0.6'} 4399 + dev: false 4400 + 4401 + /mime-types@2.1.35: 4402 + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 4403 + engines: {node: '>= 0.6'} 4404 + dependencies: 4405 + mime-db: 1.52.0 4406 + dev: false 4407 + 4333 4408 /mimic-function@5.0.1: 4334 4409 resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} 4335 4410 engines: {node: '>=18'} ··· 4801 4876 4802 4877 /property-information@6.5.0: 4803 4878 resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} 4879 + dev: false 4880 + 4881 + /proxy-from-env@1.1.0: 4882 + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 4804 4883 dev: false 4805 4884 4806 4885 /punycode@2.3.1:
+7 -5
src/components/NavCard.astro
··· 1 1 --- 2 - import type { CollectionEntry } from 'astro:content'; 2 + import type { PostOrPage } from '@tryghost/content-api'; 3 3 4 4 type Props = { 5 - entry: CollectionEntry<'blog'> | CollectionEntry<'projects'>; 5 + entry: PostOrPage; 6 6 }; 7 7 8 8 const { entry } = Astro.props; 9 9 --- 10 10 11 11 <a 12 - href={`/${entry.collection}/${entry.slug}`} 12 + href={entry.tags && entry.tags.length > 0 13 + ? `/${entry.tags[0].name}/${entry.slug}` 14 + : '#'} 13 15 class='relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20 14 16 hover:bg-lime-500/30 dark:hover:bg-lime-300/5 15 17 hover:text-black dark:hover:text-white ··· 19 21 > 20 22 <div class='flex flex-col flex-1 truncate'> 21 23 <div class='font-semibold flex gap-2'> 22 - {entry.data.title} 24 + {entry.title} 23 25 </div> 24 26 <div class='text-sm'> 25 - {entry.data.description} 27 + {entry.excerpt} 26 28 </div> 27 29 </div> 28 30 <svg
-57
src/content/blog/learnings-over-the-past-year/index.mdx
··· 1 - --- 2 - title: 'Learnings over the past year' 3 - description: 'Things I have come to realize and learn in the last 1 year.' 4 - date: 'Oct 10 2024' 5 - draft: true 6 - --- 7 - 8 - May of 2022 is when I finished my university education in the field of Computer Systems Engineering. 9 - I was taught a whole bunch of things - networking, working with FPGAs, and a whole bunch more. The most interesting of those subjects was application development. The primary language was Java (not Spring Boot, just plain old Java) and well, I wasn't very fond of it. 10 - 11 - As soon as I graduated, I found out about web applications and got hooked. Picked up the basics of HTML, CSS, and JS in a couple of weeks, and built a dummy website to put my newfound knowledge to use. It was OVERLOADED with animations that added nothing, but it was all so new and shiny 😆 12 - 13 - Got my first full-time job at a startup based in Oman (my place of birth and residence from 2000 to 2023). Long story short, I picked up React, Next, Express, and even some AI fine-tuning in the one year that I worked there. It was a good run, but I couldn't continue working there due to a bunch of reasons. 14 - 15 - ## Välkommen till Sverige! 16 - 17 - That translates to "Welcome to Sweden!". December 16, 2023 is when I landed in Copenhagen and headed to Malmö for my new and shiny job as a Fullstack Developer at Curamet. I already had a project waiting for me, and it was at IKEA. I was excited, but I kept thinking - "what kind of tech does IKEA have, that it needs to hire consultants?" I mean it's a retail company right? 18 - 19 - Well, apart from "logistical" reasons, turns out IKEA, even though not a tech giant, has a crap ton of tech things going on across the company. My project was with the "Customer Rewards" team which, as the name suggests, is responsible for the tech behind giving rewards to customers in return for loyalty. 20 - 21 - The 2 senior engineers and my manager, over the past 9 months have exposed me to a plethora of things I never even thought would exist in software engineering. All I knew up until I came to Sweden, was how to build CRUD applications. Even if I was under pressure at the startup, I was always in my comfort zone, doing the things I know and nothing else. 22 - 23 - Since day 1 of my project assignment, I was exposed to things I couldn't even begin to understand. What the hell is Domain Driven Design? Why are there `fixup!` commits in open PRs and what do they mean? Why is the code composed like that? It was a lot to take in, and I was lost in the mayhem. 24 - 25 - ## Turn it up to 11! 26 - 27 - import TurnIt from './turn-to-11.astro'; 28 - 29 - <TurnIt /> 30 - 31 - Being lost was not gonna help me. I needed to start being annoying now if I was to make good use of my time. I started with a seemigly simple task - upgrade some of our services to node v20. How hard could that be? Right? 32 - 33 - Well, the upgrade itself was simple. I just whipped up a bash script to do the "steps" for me. But then I realized, I had no idea how to make sure the upgrade didn't break anything. "Well once I raise the PR, someone will verify the changes. I can get some input from them!" - thankfully I did lol. 34 - 35 - Things were a little quiet over the next few weeks, with me just upgrading the services, and verifying the changes. But then my manager told us about this new application we need to build - a finance reporting system for all the rewards our system gives to customers. From a high level, it seemed pretty straight-forward since we store events for everything. But then we got into a long design discussion. It was fascinating, and it kinda scared me. I was a new joiner and knew maybe 1% of how the system worked. 36 - 37 - If it wasn't for the absolutely amazing team, I think I still would have been getting onboarded. 38 - 39 - This new application for finance reporting needed to interact with other parts of our system. Parts that I had never interacted with before. In the 4-5 month stretch that I was hyper-focused on to finish this new app, I feel like I learnt more about the system that if I would have directly worked on those "other" parts. Not only that, I understood why the team does certain things in a certain way. I learnt how to use Terraform (which was surprisingly SO MUCH more simpler than I imagined lol), I learnt the nuances of working with GCP's Cloud Run, Secret Manager, and all the other things and how Terraform behaved. I finally understood what CQRS meant. I realized why Domain Driven Design was important, and how it was leveraged in our complex composition of services. I understood why fixup commits are so so so much nicer than a new commit. Everything started to piece itself together. 40 - 41 - Most of this work I did was very backend-heavy - I was not and still am not complaining, something about writing business logic hits _the spot_ for me. The app we built for finance reporting was called "the best tool we have used so far" by the consumers. It was an amazing feeling to say the least. 42 - 43 - ## 11 is where I thrive 44 - 45 - Over the past year, I have come to realize that **11** is where I thrive. I haven't had a single moment since joining this team, where I have felt at ease (and I mean that in a good way). I'm constantly learning new things whether it's related to work, or just tech in general. 46 - 47 - But you might be wondering - did I _really_ learn anything new? I know for a fact I did. There are so many things I would do differently at the startup if I had the knowledge I have now. Command Query Responsibility Segregation, Domain Driven Design, Event Driven Architecture, Event Modelling, nuances of the serverless ecosystem, and so much more that is simply a lot to mention here 😆 48 - 49 - ## The next 1 year 50 - 51 - I really do not know what I will be doing a year from now, but I know for a fact that if I don't get to turn it up to 11, I will eventually just lose interest in whatever it is I will be doing. I'm not a veteran in the field, I'm a rookie that wants to find his footing, and 11 is where I will find it. Not 9, not 10, but 11. 52 - 53 - Thank you for reading my first attempt at a proper blog post. I don't have comments on this site yet, but if you want to talk about what I wrote here, or just anything at all, feel free to reach out to me! 54 - 55 - import Socials from '../../../components/Socials.astro'; 56 - 57 - <Socials />
-26
src/content/blog/learnings-over-the-past-year/socials.astro
··· 1 - --- 2 - import Link from '@/components/Link.astro'; 3 - import { SITE, SOCIALS } from '@/config'; 4 - --- 5 - 6 - <ul class='flex flex-wrap gap-2 p-0'> 7 - { 8 - SOCIALS.map((SOCIAL) => ( 9 - <li class='flex gap-x-2 text-nowrap'> 10 - <Link 11 - href={SOCIAL.HREF} 12 - external 13 - aria-label={`${SITE.NAME} on ${SOCIAL.NAME}`} 14 - > 15 - {SOCIAL.NAME} 16 - </Link> 17 - {'/'} 18 - </li> 19 - )) 20 - } 21 - <li class='line-clamp-1'> 22 - <Link href={`mailto:${SITE.EMAIL}`} aria-label={`Email ${SITE.NAME}`}> 23 - {SITE.EMAIL} 24 - </Link> 25 - </li> 26 - </ul>
-21
src/content/blog/learnings-over-the-past-year/turn-to-11.astro
··· 1 - --- 2 - import { Image } from 'astro:assets'; 3 - --- 4 - 5 - <div style='width:100%;height:0;padding-bottom:56%;position:relative;'> 6 - <Image 7 - src='https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExMnY5bDNzdnJvaDhlbTNueDRiOTNyN284ejh3NzBhOWdjejM4NWJqayZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/3o6EhWZRnnQNI3gg9y/giphy.webp' 8 - alt='Turn it up to 11 GIF' 9 - width={720} 10 - height={405} 11 - style='position:absolute' 12 - class='giphy-embed' 13 - /> 14 - </div> 15 - <p> 16 - <a 17 - class='text-sm italic' 18 - href='https://giphy.com/gifs/cchq-comic-con-comic-con-hq-3o6EhWZRnnQNI3gg9y' 19 - >via GIPHY</a 20 - > 21 - </p>
+1 -23
src/content/config.ts
··· 1 1 import { defineCollection, z } from 'astro:content'; 2 2 3 - const blog = defineCollection({ 4 - type: 'content', 5 - schema: z.object({ 6 - title: z.string(), 7 - description: z.string(), 8 - date: z.coerce.date(), 9 - draft: z.boolean() 10 - }) 11 - }); 12 - 13 3 const work = defineCollection({ 14 4 type: 'content', 15 5 schema: z.object({ ··· 20 10 }) 21 11 }); 22 12 23 - const projects = defineCollection({ 24 - type: 'content', 25 - schema: z.object({ 26 - title: z.string(), 27 - description: z.string(), 28 - date: z.coerce.date(), 29 - draft: z.boolean(), 30 - demoURL: z.string().optional(), 31 - repoURL: z.string().optional() 32 - }) 33 - }); 34 - 35 - const collections = { blog, work, projects }; 13 + const collections = { work }; 36 14 37 15 export default collections;
-80
src/content/projects/notting/index.mdx
··· 1 - --- 2 - title: 'Notting' 3 - description: 'A note-taking app built to learn about the Ports and Adapters pattern.' 4 - date: 'Sep 14 2024' 5 - repoURL: 'https://github.com/safwanyp/notting' 6 - --- 7 - 8 - import Link from '../../../components/Link.astro'; 9 - 10 - ### What is Notting? 11 - 12 - Notting is an app I am working on in my free time to learn and put into practice, my knowledge of 13 - the Ports and Adapters pattern in software engineering. 14 - 15 - If I were to briefly explain what the pattern specifies, I would say this - It's a way of separating 16 - an application's core functionality (i.e. business logic) from the technologies it uses. I will try and 17 - make this clear in this post. 18 - 19 - ### What does Notting do? 20 - 21 - Nothing complex. It allows a user to write notes, save them, edit them, and of course view them. 22 - 23 - ### Why build something that already exists? 24 - 25 - Good question and as I said in the beginning, it's just something for me to practice the new things 26 - I'm trying to learn - but that's not all. 27 - 28 - I'm reading the book Hexagonal Architecture Explained by Alistair Cockburn and Juan Manuel Garrido de Paz 29 - on the Apple Books app. I like to take notes of my inferences, questions, and other things when reading 30 - books like this. Doing this in the Books app was okay at first, but got really annoying as I kept reading. 31 - 32 - Hence I decided to start putting my knowledge into actual code, and thought - why not build an app that lets 33 - me do what I want exactly, while also doing it with my newly gained knowledge? 34 - 35 - And here we are. 36 - 37 - ### How is the Ports and Adapters pattern implemented in Notting? 38 - 39 - The whole idea of the pattern is that whatever technology you use should be independent of the core 40 - logic/functionality of the application. There are times when I like to do stuff in my terminal, and other times 41 - in my code editor. But even the code editor changes from time to time. (Right now, I'm trying to move from 42 - VS Code to Zed. I like VS Code because it has everything to help me work efficiently, but has its annoyances). 43 - 44 - Another thing to note is that I wnat to be able to switch between storage providers to persist my notes. 45 - Maybe AWS S3, or GCS, or even Git. I don't know what I will go with. 46 - 47 - To make these "switches" as easily as possible, Ports and Adapters will come in handy. 48 - 49 - The following diagram is something I whipped up to illustrate this implementation. 50 - ![Image illustrating the Ports and Adapters implementation in the Notting app](/images/notting-pna_diagram.webp) 51 - 52 - The app's core logic is defined inside the app itself. To get the app to do something for me (the user), I will 53 - need to go through one of the driver ports, which are on the left-hand side. Let's take the "For Interacting with the App" 54 - port for now. 55 - 56 - This port has multiple adapters (or implementations), which allow me to use different methods to do the same thing - interact 57 - with the app. If I decide to use the CLI adapter for the port, I will have to execute commands in a CLI tool to save a note (as an example). 58 - If I use the Web App adapter, I will have to use a web app to do the same thing. 59 - 60 - If it isn't clear already, the whole point is that I can use WHATEVER method I want, as long the method conforms to the constaints 61 - defined by the port. This statement is true for all the ports and adapters in the app, regardless of whether they are driver (left-hand side) 62 - or driven (right-hand side) ports. 63 - 64 - Now let's take the "For Saving Notes" driven port (right-hand side) as an example. I mentioned earlier that maybe 65 - I want to store my notes in S3, or Git, or something else. To make this possible, I create a port for saving notes, that defines 66 - the constraints that the adapter (S3, Git, etc.) needs to satisfy. As long as these constraints are satisfied, I can seamlessly 67 - switch between providers. 68 - 69 - I will not go into the actual code since that will require a much longer post, but the repo is linked at the top of this post 70 - for anyone interested. I have done my best to seperate the commits in the repo, so that it's easy to see how this switching 71 - is possible. 72 - 73 - > 💡 As of Sept 22 2024, the app is still a work in progress. I will update this post if anything does change! 74 - 75 - ### Some other thoughts 76 - 77 - The app is a work in progress. I aim to make the repository as easy to parse as possible, so anyone can simply clone and run it. 78 - The ultimate goal is to have a way to self-host it, so that I will no longer have to worry about my note-taking problems lol 79 - 80 - If you have any questions, please feel free to reach out to me via any of the contact methods listed on my <Link href='/#contact'>home page</Link> or just a start a discussion/issue on the repo linked at the beginning of this post!
+3
src/env.d.ts
··· 1 1 /// <reference path="../.astro/types.d.ts" /> 2 + interface ImportMetaEnv { 3 + readonly CONTENT_API_KEY: string; 4 + }
+9
src/lib/ghost.ts
··· 1 + import GhostContentAPI from '@tryghost/content-api'; 2 + 3 + const ghost = new GhostContentAPI({ 4 + url: 'https://ghost.safwanyp.com', 5 + key: import.meta.env.CONTENT_API_KEY, 6 + version: 'v5.0' 7 + }); 8 + 9 + export { ghost };
+14 -15
src/pages/blog/[...slug].astro
··· 1 1 --- 2 - import { type CollectionEntry, getCollection } from 'astro:content'; 3 2 import Layout from '@/layouts/Layout.astro'; 4 3 import Container from '@/components/Container.astro'; 5 4 import DateComponent from '@/components/Date.astro'; 6 5 import { readingTime } from '@/utils'; 7 6 import Back from '@/components/Back.astro'; 7 + import { ghost } from '@/lib/ghost'; 8 + import type { PostOrPage } from '@tryghost/content-api'; 8 9 9 10 export async function getStaticPaths() { 10 - const posts = (await getCollection('blog')) 11 - .filter((post) => !post.data.draft) 12 - .sort( 13 - (a, b) => 14 - new Date(b.data.date as string).valueOf() - 15 - new Date(a.data.date).valueOf() 16 - ); 11 + const posts = await ghost.posts.browse({ 12 + limit: 'all', 13 + order: 'published_at DESC', 14 + filter: 'tag:blog', 15 + include: 'tags' 16 + }); 17 17 18 18 return posts.map((post) => ({ 19 19 params: { slug: post.slug }, ··· 21 21 })); 22 22 } 23 23 24 - type Props = CollectionEntry<'blog'>; 24 + type Props = PostOrPage; 25 25 26 26 const post = Astro.props; 27 - const { Content } = await post.render(); 28 27 --- 29 28 30 - <Layout title={post.data.title} description={post.data.description}> 29 + <Layout title={String(post.title)} description={String(post.excerpt)}> 31 30 <Container> 32 31 <div class='animate'> 33 32 <Back href='/blog'> Back to blog </Back> ··· 35 34 <div class='space-y-1 my-10'> 36 35 <div class='animate flex items-center gap-1.5'> 37 36 <div class='font-base text-sm'> 38 - <DateComponent date={new Date(post.data.date)} /> 37 + <DateComponent date={new Date(post.published_at!)} /> 39 38 </div> 40 39 &bull; 41 40 <div class='font-base text-sm'> 42 - {readingTime(post.body)} 41 + {readingTime(post.html!)} 43 42 </div> 44 43 </div> 45 44 <div class='animate text-2xl font-semibold text-black dark:text-white'> 46 - {post.data.title} 45 + {post.title} 47 46 </div> 48 47 </div> 49 48 <article class='animate'> 50 - <Content /> 49 + <Fragment set:html={post.html} /> 51 50 </article> 52 51 </Container> 53 52 </Layout>
+10 -10
src/pages/blog/index.astro
··· 1 1 --- 2 - import { type CollectionEntry, getCollection } from 'astro:content'; 3 2 import Layout from '@/layouts/Layout.astro'; 4 3 import Container from '@/components/Container.astro'; 5 4 import NavCard from '@/components/NavCard.astro'; 6 5 import { BLOG } from '@/config'; 6 + import { ghost } from '@/lib/ghost'; 7 + import type { PostOrPage } from '@tryghost/content-api'; 7 8 8 - const data = (await getCollection('blog')) 9 - .filter((post) => !post.data.draft) 10 - .sort( 11 - (a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf() 12 - ); 9 + const data = await ghost.posts.browse({ 10 + limit: 'all', 11 + order: 'published_at DESC', 12 + filter: 'tag:blog', 13 + include: 'tags' 14 + }); 13 15 14 - type Acc = { 15 - [year: string]: CollectionEntry<'blog'>[]; 16 - }; 16 + type Acc = Record<string, PostOrPage[]>; 17 17 18 18 const posts = data.reduce((acc: Acc, post) => { 19 - const year = new Date(post.data.date).getFullYear().toString(); 19 + const year = new Date(post.published_at!).getFullYear().toString(); 20 20 if (!acc[year]) { 21 21 acc[year] = []; 22 22 }
+13 -8
src/pages/index.astro
··· 7 7 import { SITE, HOME } from '@/config'; 8 8 import { dateRange } from '@/utils'; 9 9 import Socials from '@/components/Socials.astro'; 10 + import { ghost } from '@/lib/ghost'; 10 11 11 - const blog = (await getCollection('blog')) 12 - .filter((post) => !post.data.draft) 13 - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) 14 - .slice(0, SITE.NUM_POSTS_ON_HOMEPAGE); 12 + const blog = await ghost.posts.browse({ 13 + limit: SITE.NUM_POSTS_ON_HOMEPAGE, 14 + order: 'published_at DESC', 15 + filter: 'tag:blog', 16 + include: 'tags' 17 + }); 15 18 16 - const projects = (await getCollection('projects')) 17 - .filter((project) => !project.data.draft) 18 - .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) 19 - .slice(0, SITE.NUM_PROJECTS_ON_HOMEPAGE); 19 + const projects = await ghost.posts.browse({ 20 + limit: SITE.NUM_PROJECTS_ON_HOMEPAGE, 21 + order: 'published_at DESC', 22 + filter: 'tag:projects', 23 + include: 'tags' 24 + }); 20 25 21 26 const allwork = (await getCollection('work')) 22 27 .sort(
+15 -32
src/pages/projects/[...slug].astro
··· 1 1 --- 2 - import { type CollectionEntry, getCollection } from 'astro:content'; 3 2 import Layout from '@/layouts/Layout.astro'; 4 3 import Container from '@/components/Container.astro'; 5 4 import DateComponent from '@/components/Date.astro'; 6 5 import { readingTime } from '@/utils'; 7 6 import Back from '@/components/Back.astro'; 8 - import Link from '@/components/Link.astro'; 7 + import { ghost } from '@/lib/ghost'; 8 + import type { PostOrPage } from '@tryghost/content-api'; 9 9 10 10 export async function getStaticPaths() { 11 - const projects = (await getCollection('projects')) 12 - .filter((post) => !post.data.draft) 13 - .sort( 14 - (a, b) => 15 - new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf() 16 - ); 11 + const projects = await ghost.posts.browse({ 12 + limit: 'all', 13 + order: 'published_at DESC', 14 + filter: 'tag:projects', 15 + include: 'tags' 16 + }); 17 + 17 18 return projects.map((project) => ({ 18 19 params: { slug: project.slug }, 19 20 props: project 20 21 })); 21 22 } 22 - type Props = CollectionEntry<'projects'>; 23 + type Props = PostOrPage; 23 24 24 25 const project = Astro.props; 25 - const { Content } = await project.render(); 26 26 --- 27 27 28 - <Layout title={project.data.title} description={project.data.description}> 28 + <Layout title={String(project.title)} description={String(project.excerpt)}> 29 29 <Container> 30 30 <div class='animate'> 31 31 <Back href='/projects'> Back to projects </Back> ··· 33 33 <div class='space-y-1 my-10'> 34 34 <div class='animate flex items-center gap-1.5'> 35 35 <div class='font-base text-sm'> 36 - <DateComponent date={new Date(project.data.date)} /> 36 + <DateComponent date={new Date(project.published_at!)} /> 37 37 </div> 38 38 &bull; 39 39 <div class='font-base text-sm'> 40 - {readingTime(project.body)} 40 + {readingTime(project.html!)} 41 41 </div> 42 42 </div> 43 43 <div class='animate text-2xl font-semibold text-black dark:text-white'> 44 - {project.data.title} 44 + {project.title} 45 45 </div> 46 - { 47 - (project.data.demoURL || project.data.repoURL) && ( 48 - <nav class='animate flex gap-1'> 49 - {project.data.demoURL && ( 50 - <Link href={project.data.demoURL} external> 51 - demo 52 - </Link> 53 - )} 54 - {project.data.demoURL && project.data.repoURL && <span>/</span>} 55 - {project.data.repoURL && ( 56 - <Link href={project.data.repoURL} external> 57 - repo 58 - </Link> 59 - )} 60 - </nav> 61 - ) 62 - } 63 46 </div> 64 47 <article class='animate'> 65 - <Content /> 48 + <Fragment set:html={project.html} /> 66 49 </article> 67 50 </Container> 68 51 </Layout>
+7 -6
src/pages/projects/index.astro
··· 1 1 --- 2 - import { getCollection } from 'astro:content'; 3 2 import Layout from '@/layouts/Layout.astro'; 4 3 import Container from '@/components/Container.astro'; 5 4 import NavCard from '@/components/NavCard.astro'; 6 5 import { PROJECTS } from '@/config'; 6 + import { ghost } from '@/lib/ghost'; 7 7 8 - const projects = (await getCollection('projects')) 9 - .filter((project) => !project.data.draft) 10 - .sort( 11 - (a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf() 12 - ); 8 + const projects = await ghost.posts.browse({ 9 + limit: 'all', 10 + order: 'published_at DESC', 11 + filter: 'tag:projects', 12 + include: 'tags' 13 + }); 13 14 --- 14 15 15 16 <Layout title={PROJECTS.TITLE} description={PROJECTS.DESCRIPTION}>
+19 -12
src/pages/rss.xml.ts
··· 1 1 import rss from '@astrojs/rss'; 2 - import { getCollection } from 'astro:content'; 3 2 import { HOME } from '@/config'; 3 + import { ghost } from '@/lib/ghost'; 4 4 5 5 type Context = { 6 6 site: string; 7 7 }; 8 8 9 9 export async function GET(context: Context) { 10 - const blog = (await getCollection('blog')).filter( 11 - (post: { data: { draft: boolean } }) => post.data.draft 12 - ); 10 + const blog = await ghost.posts.browse({ 11 + limit: 'all', 12 + order: 'published_at DESC', 13 + filter: 'tag:blog', 14 + include: 'tags' 15 + }); 13 16 14 - const projects = (await getCollection('projects')).filter( 15 - (project: { data: { draft: boolean } }) => project.data.draft 16 - ); 17 + const projects = await ghost.posts.browse({ 18 + limit: 'all', 19 + order: 'published_at DESC', 20 + filter: 'tag:projects', 21 + include: 'tags' 22 + }); 17 23 18 24 const items = [...blog, ...projects].sort( 19 - (a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf() 25 + (a, b) => 26 + new Date(b.published_at!).valueOf() - new Date(a.published_at!).valueOf() 20 27 ); 21 28 22 29 return rss({ ··· 24 31 description: HOME.DESCRIPTION, 25 32 site: context.site, 26 33 items: items.map((item) => ({ 27 - title: item.data.title, 28 - description: item.data.description, 29 - pubDate: item.data.date, 30 - link: `/${item.collection}/${item.slug}/` 34 + title: String(item.title), 35 + description: String(item.excerpt), 36 + pubDate: new Date(item.published_at!), 37 + link: `/${item.tags ? item.tags[0].name : ''}/${item.slug}/` 31 38 })) 32 39 }); 33 40 }