[{"data":1,"prerenderedAt":699},["ShallowReactive",2],{"nav-pages":3,"footer-socials":160,"$nLR9CPt1-i":183,"projects-portfolio-website":204},[4,127],{"id":5,"title":6,"body":7,"crumbs":115,"description":119,"extension":120,"icon":117,"meta":121,"navigation":122,"path":123,"seo":124,"stem":125,"__hash__":126},"pages\u002Fpages\u002F1.index.md","Home",{"type":8,"value":9,"toc":109},"minimark",[10,15,19,24,27,61,64,96,99,103],[11,12,14],"h1",{"id":13},"my-portfolio","My Portfolio",[16,17,18],"p",{},"Hi there, welcome to my portfolio website.",[20,21,23],"h2",{"id":22},"who-am-i","Who am I?",[16,25,26],{},"I'm George, a software engineer with an interest in self-hosting, programming, and hardware.",[16,28,29,30,37,38,43,44,49,50,55,56,60],{},"I studied BSC Computer Science at ",[31,32,36],"a",{"href":33,"rel":34},"https:\u002F\u002Fwww.cardiff.ac.uk\u002F",[35],"nofollow","Cardiff University",", the projects I did during my time there can be found on my ",[31,39,42],{"href":40,"rel":41},"https:\u002F\u002Fcode.ggrainger.uk\u002Fggrainger?tab=repositories",[35],"Forgejo"," or ",[31,45,48],{"href":46,"rel":47},"https:\u002F\u002Fgithub.com\u002Frandomman552",[35],"GitHub"," though I have migrated most of these to my self-hosted Forgejo instance. For my dissertation I created a website to show sensor information in Cardiff University's new Abacws building, which you can see the code for ",[31,51,54],{"href":52,"rel":53},"https:\u002F\u002Fcode.ggrainger.uk\u002Fggrainger\u002FAbacws-Data-Vis",[35],"here"," or my deployment of ",[31,57,54],{"href":58,"rel":59},"https:\u002F\u002Fabacws.ggrainger.uk\u002F",[35],".",[16,62,63],{},"In my spare time I maintain a Linux server in my house, on which I self-host a couple of things including:",[65,66,67,76,88],"ul",{},[68,69,70,71],"li",{},"My own personal cloud storage using ",[31,72,75],{"href":73,"rel":74},"https:\u002F\u002Fnextcloud.com\u002F",[35],"NextCloud",[68,77,78,79,83,84],{},"My ",[31,80,82],{"href":40,"rel":81},[35],"code"," using ",[31,85,42],{"href":86,"rel":87},"https:\u002F\u002Fforgejo.org\u002F",[35],[68,89,90,95],{},[31,91,94],{"href":92,"rel":93},"https:\u002F\u002Fheadscale.net\u002Fstable\u002F",[35],"HeadScale"," for easy, self-hosted VPN access",[16,97,98],{},"In my spare time, I also write some code for Arduino's here and there, the connection between software and hardware is always fun to play with. I work for a company based in North Yorkshire that makes robotic automation systems, so I've got to work with some really cool hardware.",[20,100,102],{"id":101},"what-is-this-site-for","What is this site for?",[16,104,105,106,60],{},"I wanted a place to write about interesting projects I've worked on, here it is. You can check them out ",[31,107,54],{"href":108},"\u002Fprojects",{"title":110,"searchDepth":111,"depth":111,"links":112},"",2,[113,114],{"id":22,"depth":111,"text":23},{"id":101,"depth":111,"text":102},[116],{"name":6,"icon":117,"href":118},"material-symbols:home-rounded","\u002F","Welcome to George's portfolio website","md",{},true,"\u002Fpages",{"title":6,"description":119},"pages\u002F1.index","iJZaQtML6AUZWO7oOxOQo8ckoA7ZFdLQwhAci4MVMik",{"id":128,"title":129,"body":130,"crumbs":150,"description":154,"extension":120,"icon":153,"meta":155,"navigation":122,"path":156,"seo":157,"stem":158,"__hash__":159},"pages\u002Fpages\u002F2.projects.md","Projects",{"type":8,"value":131,"toc":148},[132,135,138,145],[11,133,129],{"id":134},"projects",[16,136,137],{},"My recent projects can be found below.",[16,139,140,141,144],{},"Can't find anything that interests you? Check out my ",[31,142,42],{"href":40,"rel":143},[35]," for some other things I've made that didn't make the cut (or that I haven't finished).",[146,147],"card-project-grid",{},{"title":110,"searchDepth":111,"depth":111,"links":149},[],[151,152],{"name":6,"icon":117,"href":118},{"name":129,"icon":153,"href":108},"material-symbols:deployed-code","View my projects here",{},"\u002Fpages\u002Fprojects",{"title":129,"description":154},"pages\u002F2.projects","ZvgQ8iafkO8Vo_2oMkyd_n-BCCp-tAooB16GjR_rLf4",[161,169,175],{"id":162,"extension":163,"icon":164,"link":165,"meta":166,"name":42,"stem":167,"__hash__":168},"socials\u002Fsocials\u002Fforgejo.yml","yml","devicon-plain:forgejo","https:\u002F\u002Fcode.ggrainger.uk\u002Fggrainger",{},"socials\u002Fforgejo","eLo2tedWGBfxJY_VmYmV0uFkIDhRuQvm4fmj_5QD7f0",{"id":170,"extension":163,"icon":171,"link":46,"meta":172,"name":48,"stem":173,"__hash__":174},"socials\u002Fsocials\u002Fgithub.yml","mdi:github-box",{},"socials\u002Fgithub","umA4vGSgNhkainXfqvtesFnwEcchjMuYOaVvJl409do",{"id":176,"extension":163,"icon":177,"link":178,"meta":179,"name":180,"stem":181,"__hash__":182},"socials\u002Fsocials\u002Flinkedin.yml","mdi:linkedin","https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fgeorge-grainger552\u002F",{},"LinkedIn","socials\u002Flinkedin","bTHOxI-WAMN6H2Ub1ipKl89w3nM34aSFOrB4ziV6Nhc",{"id":128,"title":129,"body":184,"crumbs":199,"description":154,"extension":120,"icon":153,"meta":202,"navigation":122,"path":156,"seo":203,"stem":158,"__hash__":159},{"type":8,"value":185,"toc":197},[186,188,190,195],[11,187,129],{"id":134},[16,189,137],{},[16,191,140,192,144],{},[31,193,42],{"href":40,"rel":194},[35],[146,196],{},{"title":110,"searchDepth":111,"depth":111,"links":198},[],[200,201],{"name":6,"icon":117,"href":118},{"name":129,"icon":153,"href":108},{},{"title":129,"description":154},{"id":205,"title":206,"body":207,"coverImage":689,"description":690,"extension":120,"liveDemo":691,"meta":692,"navigation":122,"ogImage":691,"path":693,"publishedAt":694,"seo":695,"sourceCode":696,"stem":697,"__hash__":698},"projects\u002Fprojects\u002Fportfolio-website.md","Portfolio Website",{"type":8,"value":208,"toc":677},[209,213,216,219,233,237,252,255,370,374,379,392,395,446,453,462,467,508,518,522,525,528,567,571,580,589,609,613,620,628,659,662,666],[20,210,212],{"id":211},"project-goals","Project Goals",[16,214,215],{},"In this project, I aimed to create a simple portfolio website (this one) for me to showcase my work.",[16,217,218],{},"I had a few requirements in mind for this:",[65,220,221,224,227,230],{},[68,222,223],{},"Minimalistic and responsive design",[68,225,226],{},"Statically hosted",[68,228,229],{},"Simple styling",[68,231,232],{},"Dark mode (the most important feature)",[20,234,236],{"id":235},"architecture","Architecture",[16,238,239,240,245,246,251],{},"To start with, I needed to decide what I was going to use to build the site.\nI've worked with ",[31,241,244],{"href":242,"rel":243},"https:\u002F\u002Fnuxt.com\u002F",[35],"Nuxt.js"," and used ",[31,247,250],{"href":248,"rel":249},"https:\u002F\u002Fstrapi.io\u002F",[35],"Strapi"," as a backend CMS for a previous project,\nbut I found Strapi to be a heavy and unwieldy product that doesn't do anything particularly hard.\nAs I'm going to be the only person making edits to the site, I can use something that requires more technical knowledge and is simpler.",[16,253,254],{},"I decided on:",[65,256,257,269,288,307,331,344,357],{},[68,258,259,263,264],{},[31,260,262],{"href":242,"rel":261},[35],"Nuxt"," for the framework.\n",[65,265,266],{},[68,267,268],{},"Full stack framework for building SSR and SSG applications.",[68,270,271,276,277],{},[31,272,275],{"href":273,"rel":274},"https:\u002F\u002Fcontent.nuxt.com\u002F",[35],"Nuxt Content"," for content management.\n",[65,278,279,282,285],{},[68,280,281],{},"Much simpler than Strapi (opinion warning).",[68,283,284],{},"Git based CMS, data is stored alongside code.",[68,286,287],{},"Markdown based.",[68,289,290,295,296],{},[31,291,294],{"href":292,"rel":293},"https:\u002F\u002Fnuxtseo.com\u002F",[35],"Nuxt SEO"," for SEO utilities.\n",[65,297,298,301,304],{},[68,299,300],{},"Automatically generates OG images for link previews.",[68,302,303],{},"Automatically handles sitemap generation.",[68,305,306],{},"Automatically handles robots.txt generation.",[68,308,309,314,315],{},[31,310,313],{"href":311,"rel":312},"https:\u002F\u002Fnuxt.com\u002Fmodules\u002Ficon",[35],"Nuxt Icon"," for icons.\n",[65,316,317],{},[68,318,319,320,325,326,60],{},"Large ",[31,321,324],{"href":322,"rel":323},"https:\u002F\u002Ficones.js.org\u002F",[35],"library"," of icons from ",[31,327,330],{"href":328,"rel":329},"https:\u002F\u002Ficonify.design\u002F",[35],"Iconify",[68,332,333,338,339],{},[31,334,337],{"href":335,"rel":336},"https:\u002F\u002Fimage.nuxt.com\u002F",[35],"Nuxt Image"," for image optimization.\n",[65,340,341],{},[68,342,343],{},"Optimizes images and generates srcsets.",[68,345,346,351,352],{},[31,347,350],{"href":348,"rel":349},"https:\u002F\u002Ftailwindcss.com\u002F",[35],"TailwindCSS"," for styling.\n",[65,353,354],{},[68,355,356],{},"Utility based CSS framework.",[68,358,359,364,365],{},[31,360,363],{"href":361,"rel":362},"https:\u002F\u002Fpages.cloudflare.com\u002F",[35],"Cloudflare Pages"," for hosting.\n",[65,366,367],{},[68,368,369],{},"Free hosting for static sites, with a global CDN.",[20,371,373],{"id":372},"project-structure","Project Structure",[375,376,378],"h3",{"id":377},"styling","Styling",[16,380,381,382,387,388,391],{},"Styling is done using TailwindCSS, tailwind provides a series of extensible utility classes that can be applied to elements to style them.\nThis differs from a more traditional CSS framework like ",[31,383,386],{"href":384,"rel":385},"https:\u002F\u002Fgetbootstrap.com\u002F",[35],"Bootstrap",", which takes a component based approach.\nIn a component based framework, you style elements using a component class such as ",[82,389,390],{},"btn"," which then styles that component like a button.\nWith a utility based framework, you compose utility classes together to style an element.",[16,393,394],{},"Both approaches have their pros and cons.",[65,396,397,421],{},[68,398,399,400],{},"Component based:\n",[65,401,402,413],{},[68,403,404,405],{},"Pros:\n",[65,406,407,410],{},[68,408,409],{},"Simple to use, just add a class to an element.",[68,411,412],{},"Easy to understand how an element will look.",[68,414,415,416],{},"Cons:\n",[65,417,418],{},[68,419,420],{},"Limited styling options, you are limited to the components provided by the framework.",[68,422,423,424],{},"Utility based:\n",[65,425,426,436],{},[68,427,404,428],{},[65,429,430,433],{},[68,431,432],{},"More flexible.",[68,434,435],{},"Can compose utility classes together to create any style you want.",[68,437,415,438],{},[65,439,440,443],{},[68,441,442],{},"Long class names on elements.",[68,444,445],{},"Harder to figure out exactly how an element will appear.",[16,447,448,449,452],{},"I find the drawbacks of the utility based approach can be mitigated by taking a component based approach to building the site.\nE.g. you can build an ",[82,450,451],{},"XButton"," component that uses utility classes for styling, but as it is encapsulated into a component it is easy to re-use across an application.",[16,454,455,456,461],{},"I designed a few components using tailwind to re-use throughout the site, this can be found on the ",[31,457,460],{"href":458,"title":459},"\u002Fdev","SUPER SECRET PAGE","dev"," pages deployed with this site.",[463,464,466],"h4",{"id":465},"dark-mode","Dark Mode",[16,468,469,470,473,474,479,480,485,486,491,492,495,496,498,499,502,503,60],{},"Dark mode is natively supported by TailwindCSS, and can be enabled by simply adding a ",[82,471,472],{},"dark"," (with some ",[31,475,478],{"href":476,"rel":477},"https:\u002F\u002Ftailwindcss.com\u002Fdocs\u002Fdark-mode#toggling-dark-mode-manually",[35],"configuration",") class to the body of the page.\nI used the library ",[31,481,484],{"href":482,"rel":483},"https:\u002F\u002Fvueuse.org\u002F",[35],"vueuse"," to check the user's preferred color scheme and allow switching between light and dark mode.\nThey have a handy ",[31,487,490],{"href":488,"rel":489},"https:\u002F\u002Fvueuse.org\u002Fcore\u002FuseColorMode\u002F",[35],"utility"," called ",[82,493,494],{},"useColorMode"," that handles this for you using local storage and media queries.\nYou just need to handle the addition and removal of the ",[82,497,472],{}," class on the body yourself, which I handled with a ",[82,500,501],{},"DarkModeSwitch"," ",[31,504,507],{"href":505,"rel":506},"https:\u002F\u002Fcode.ggrainger.uk\u002Fggrainger\u002FWebsite\u002Fsrc\u002Fbranch\u002Fmaster\u002Fapp\u002Fcomponents\u002FDarkModeSwitch.vue",[35],"component",[16,509,510,511,514,515,517],{},"You can do this yourself instead of relying on an external library, all you need to do is check the user's preferred color scheme using the ",[82,512,513],{},"window.matchMedia"," API and store the preference in local storage.\nThen you can add or remove the ",[82,516,472],{}," class on the body based on the stored preference.",[375,519,521],{"id":520},"content","Content",[16,523,524],{},"Nuxt Content allows you to configure multiple content sources, with different queryable fields.\nAt runtime, each of these sources gets loaded into an SQLite database, which can then be queried from the application.\nThis allows for easy filtering and sorting of content.",[16,526,527],{},"For this site, I have divided the content into the following collections.\nI will probably end up expanding this list in the future, but for now this is sufficient.",[65,529,530,548,559],{},[68,531,532,533],{},"Pages\n",[65,534,535,538],{},[68,536,537],{},"Pages to render on the site and show in the navigation bar.",[68,539,540,541,544,545,547],{},"Includes the ",[31,542,543],{"href":118},"home"," and ",[31,546,134],{"href":108}," pages.",[68,549,550,551],{},"Projects\n",[65,552,553],{},[68,554,555,556,558],{},"Projects to showcase on the ",[31,557,134],{"href":108}," page (like this one).",[68,560,561,562],{},"Socials\n",[65,563,564],{},[68,565,566],{},"Social links to show in the footer.",[375,568,570],{"id":569},"pages","Pages",[16,572,573,574,576,577,579],{},"Nuxt uses file based routing, where the file structure of the ",[82,575,569],{}," directory is used to generate routes for the site.\nThe ",[82,578,569],{}," directory is very simple for now, with only 3 templates in use.",[581,582,587],"pre",{"className":583,"code":585,"language":586},[584],"language-text","app\u002F\n└── pages\u002F\n    ├── projects\u002F\n    │   └── [slug].vue\n    ├── [...path].vue\n    └── dev.vue\n","text",[82,588,585],{"__ignoreMap":110},[16,590,591,594,595,598,599,601,602,605,606,608],{},[82,592,593],{},"dev.vue"," page is a development page used for testing components.\nThe ",[82,596,597],{},"[...path].vue"," page is a catch all that renders the matching page from the ",[82,600,569],{}," collection,\nand the ",[82,603,604],{},"projects\u002F[slug].vue"," page is used to render the matching project from the ",[82,607,134],{}," collection.",[375,610,612],{"id":611},"deployment","Deployment",[16,614,615],{},[616,617],"img",{"alt":618,"src":619},"Diagram of deployment infrastructure","\u002Fimages\u002Fprojects\u002Fportfolio-website\u002Fdeployment.png",[16,621,622,623,627],{},"I host my code on my own self-hosted ",[31,624,42],{"href":625,"rel":626},"https:\u002F\u002Fcode.ggrainger.uk\u002F",[35]," instance, which has CI\u002FCD support using Forgejo actions.\nWhen code is pushed to the repository, the site is built statically and then deployed to one of the following environments.",[65,629,630,646],{},[68,631,632,633],{},"Staging:\n",[65,634,635,643],{},[68,636,637,638,60],{},"Deployed to ",[31,639,642],{"href":640,"rel":641},"https:\u002F\u002Fcodeberg.org\u002Fgit-pages\u002Fgit-pages",[35],"git pages",[68,644,645],{},"Open source equivalent to GitHub pages.",[68,647,648,649],{},"Production:\n",[65,650,651,656],{},[68,652,637,653,60],{},[31,654,363],{"href":361,"rel":655},[35],[68,657,658],{},"Cloud hosted, so downtime on my server will not take the website down.",[16,660,661],{},"At the moment, these two environments are always updated together when I push to the repository.\nLong term, it would be better to have a separate staging branch or a manual trigger for production deployments.\nBut for my purposes, this is good enough.",[20,663,665],{"id":664},"conclusion","Conclusion",[16,667,668,669,673,674,676],{},"Well there you have it, some background information on how this site was built.\nYou can check out the source code for this site ",[31,670,54],{"href":671,"rel":672},"https:\u002F\u002Fcode.ggrainger.uk\u002Fggrainger\u002FWebsite",[35],".\nMore ",[31,675,134],{"href":108}," to come soon (hopefully)!",{"title":110,"searchDepth":111,"depth":111,"links":678},[679,680,681,688],{"id":211,"depth":111,"text":212},{"id":235,"depth":111,"text":236},{"id":372,"depth":111,"text":373,"children":682},[683,685,686,687],{"id":377,"depth":684,"text":378},3,{"id":520,"depth":684,"text":521},{"id":569,"depth":684,"text":570},{"id":611,"depth":684,"text":612},{"id":664,"depth":111,"text":665},"\u002Fimages\u002Fprojects\u002Fportfolio-website\u002Fcover.png","Building and deploying a portfolio website using Nuxt, TailwindCSS and Nuxt Content.",null,{},"\u002Fprojects\u002Fportfolio-website","2026-07-04 23:22:00",{"title":206,"description":690},"https:\u002F\u002Fcode.ggrainger.uk\u002Fggrainger\u002Fportfolio-website","projects\u002Fportfolio-website","OuwwvjdGpecRASVMy1_KB357WC2_C_L9t0wVa22GnNs",1785882542391]