[{"data":1,"prerenderedAt":2787},["ShallowReactive",2],{"\u002Fprojects\u002Fmy-seo-helper-technical-seo-site-analysis":3,"navigation":917},{"id":4,"title":5,"body":6,"date":902,"description":903,"extension":904,"featured":725,"githubLink":905,"image":906,"liveLink":905,"meta":907,"navigation":587,"path":908,"seo":909,"stem":910,"technologies":911,"__hash__":916},"projects\u002Fprojects\u002Fmy-seo-helper-technical-seo-site-analysis.md","My SEO Helper - Technical SEO site analysis",{"type":7,"value":8,"toc":892},"minimark",[9,18,22,25,28,31,35,40,53,57,68,72,95,99,102,105,108,119,122,305,564,567,570,872,875,878,888],[10,11,13,14],"h2",{"id":12},"project-case-study-technical-seo-site-management-and-maintenance","Project Case Study: ",[15,16,17],"strong",{},"Technical SEO site management and maintenance",[19,20,21],"p",{},"Like many developers managing multiple websites, I've found it difficult to keep track of the general SEO health of websites after I've deployed them. On first deploy I do all the checks that the robots.txt and sitemap.xml are set up correctly but often never come back and verify everything is still good as pages are added and the site grows.",[19,23,24],{},"With a lot of SEO tools being expensive and having more focus on the non technical side, and free health check services focusing on pinging websites to check if they return a 200 response I finally decided to build myself a small dashboard I can add functionality to over time.",[19,26,27],{},"I decided to use Next.js to build my SEO helper to improve my knowledge of react and understand the usage of server components in their current state as a web development tool. This ended up being a great learning process as the stack differs quite a bit from a traditional MVC framework , or SPA flows I have been used to. I decided to use it as a full stack framework rather than building the frontend in next and using a separate backend. Initially I felt this was a good solution but as I've gotten further into the project I think I have learned more about the pros and cons of next as a full stack framework and where its main limitations apply.",[19,29,30],{},"But first I needed to outline the technical health checks I needed to do when pushing a new site live. The main three areas to cover where the robots.txt, sitemap.xml, and the homepage.",[10,32,34],{"id":33},"tech-checks","Tech Checks",[36,37,39],"h3",{"id":38},"robotstxt","Robots.txt",[41,42,43,47,50],"ol",{},[44,45,46],"li",{},"Should be exist and return a 200 response",[44,48,49],{},"Should contain a valid sitemap entry",[44,51,52],{},"Should no block any crawlers (for production URLs)",[36,54,56],{"id":55},"sitemapxml","Sitemap.xml",[41,58,59,62,65],{},[44,60,61],{},"Contains valid XML",[44,63,64],{},"Uses HTTPS for all URLs",[44,66,67],{},"Does not exceed size limits (50MB, 50,000 URLs)",[36,69,71],{"id":70},"homepage","Homepage",[41,73,74,77,80,83,86,89,92],{},[44,75,76],{},"Returns a successful response",[44,78,79],{},"Title tag",[44,81,82],{},"Meta description",[44,84,85],{},"Google PageSpeed Insights score > 90",[44,87,88],{},"Canonical tag detection",[44,90,91],{},"No-index meta tag identification",[44,93,94],{},"Heading hierarchy validation",[10,96,98],{"id":97},"implementation","Implementation",[19,100,101],{},"With the goal of covering those checks it was then time to move onto the implementation. Using next it was very simple to get up and running with a dashboard, authentication, and database. Auth.js was used with the prisma database adapter and all that was required was to update the migration file with the needed tables, configure Auth.js to use the database as the session store, and configure the database connection string. With the move toward a password-less future I set up google authentication through Auth.js for this project. If this was a publicly facing project I would add other providers like GitHub and support email authentication by using magic links but would still avoid usernames and passwords going forward. Note, to use the google provider it was required to create a secret within google cloud console and add it to the project.",[19,103,104],{},"For the database, prisma ORM was used to provide type safe code across all objects retrieved from the database. Defining the objects within the prisma schema and running the prisma generate command generates all the types needed for use throughout the project and allows for thorough typescript support throughout.",[19,106,107],{},"For our use case we needed users to be able to manage multiple sites they operate, configure the pages within those sites and evaluate aspects of those pages using the applications built in checks. This gives us Site, Path, Check, and PathCheck models with the PathCheck model storing the results of the test process.",[109,110,115],"pre",{"className":111,"code":113,"language":114},[112],"language-text","model Path {\n  id     Int     @id @default(autoincrement())\n  path String\n  type String\n  statusCode Int?\n  pagespeedScore Int?\n  site Site @relation(fields: [siteId], references: [id], onDelete: Cascade)\n  siteId Int\n  pathChecks PathCheck[]\n\n  @@unique([path, siteId])\n}\n\nmodel PathCheck {\n  path Path @relation(fields: [pathId], references: [id], onDelete: Cascade)\n  pathId Int\n  check Check @relation(fields: [checkId], references: [id])\n  checkId Int\n  status String\n  message String\n\n  @@id([pathId, checkId])\n}\n\nmodel Check {\n  id    Int     @id @default(autoincrement())\n  name String @unique\n  slug String @unique\n  pathChecks PathCheck[]\n  type String\n}\n","text",[116,117,113],"code",{"__ignoreMap":118},"",[19,120,121],{},"We then create a function to execute the checks against a specific path. We match each Check record from the database to a function using a simple map record.",[109,123,127],{"className":124,"code":125,"language":126,"meta":118,"style":118},"language-js shiki shiki-themes github-dark github-dark monokai","const CHECK_MAP: Record\u003Cstring, CheckFunction> = {\n  robots_contains_sitemap: runRobotsContainsSitemapCheck,\n  robots_exists:           runRobotsExistsCheck,\n    robots_disallow_all:     runRobotsAllowsCrawlersCheck,\n  sitemap_exists:          runSitemapExistsCheck,\n  sitemap_valid_xml:       runSitemapValidXmlCheck,\n  sitemap_size:            runSitemapSizeCheck,\n  sitemap_https:           runSitemapHttpsCheck,\n    homepage_200:                run200Check,\n    homepage_canonical:        runCanonicalCheck,\n    homepage_noindex:        runNoIndexCheck,\n    homepage_pagespeed:      runPagespeedInsightsCheck,\n    page_200:                run200Check,\n    page_canonical:          runCanonicalCheck,\n    page_noindex:            runNoIndexCheck,\n    page_pagespeed:          runPagespeedInsightsCheck,\n    page_title_length:       runTitleLengthCheck,\n    page_meta_description_length: runMetaDescriptionLengthCheck,\n    page_h1:                 runH1Check,\n    page_heading_hierarchy:  runHeadingHierarchyCheck,\n    page_social_preview:     runSocialPreviewCheck,\n    page_structured_data:    runStructuredDataCheck,\n};\n","js",[116,128,129,173,179,185,191,197,203,209,215,221,227,233,239,245,251,257,263,269,275,281,287,293,299],{"__ignoreMap":118},[130,131,134,138,142,146,150,154,158,161,164,167,170],"span",{"class":132,"line":133},"line",1,[130,135,137],{"class":136},"s2d5f","const",[130,139,141],{"class":140},"ssVjP"," CHECK_MAP",[130,143,145],{"class":144},"setMH",":",[130,147,149],{"class":148},"sZGI-"," Record",[130,151,153],{"class":152},"s4SHQ","\u003C",[130,155,157],{"class":156},"sPGPh","string",[130,159,160],{"class":152},", ",[130,162,163],{"class":148},"CheckFunction",[130,165,166],{"class":152},"> ",[130,168,169],{"class":144},"=",[130,171,172],{"class":152}," {\n",[130,174,176],{"class":132,"line":175},2,[130,177,178],{"class":152},"  robots_contains_sitemap: runRobotsContainsSitemapCheck,\n",[130,180,182],{"class":132,"line":181},3,[130,183,184],{"class":152},"  robots_exists:           runRobotsExistsCheck,\n",[130,186,188],{"class":132,"line":187},4,[130,189,190],{"class":152},"    robots_disallow_all:     runRobotsAllowsCrawlersCheck,\n",[130,192,194],{"class":132,"line":193},5,[130,195,196],{"class":152},"  sitemap_exists:          runSitemapExistsCheck,\n",[130,198,200],{"class":132,"line":199},6,[130,201,202],{"class":152},"  sitemap_valid_xml:       runSitemapValidXmlCheck,\n",[130,204,206],{"class":132,"line":205},7,[130,207,208],{"class":152},"  sitemap_size:            runSitemapSizeCheck,\n",[130,210,212],{"class":132,"line":211},8,[130,213,214],{"class":152},"  sitemap_https:           runSitemapHttpsCheck,\n",[130,216,218],{"class":132,"line":217},9,[130,219,220],{"class":152},"    homepage_200:                run200Check,\n",[130,222,224],{"class":132,"line":223},10,[130,225,226],{"class":152},"    homepage_canonical:        runCanonicalCheck,\n",[130,228,230],{"class":132,"line":229},11,[130,231,232],{"class":152},"    homepage_noindex:        runNoIndexCheck,\n",[130,234,236],{"class":132,"line":235},12,[130,237,238],{"class":152},"    homepage_pagespeed:      runPagespeedInsightsCheck,\n",[130,240,242],{"class":132,"line":241},13,[130,243,244],{"class":152},"    page_200:                run200Check,\n",[130,246,248],{"class":132,"line":247},14,[130,249,250],{"class":152},"    page_canonical:          runCanonicalCheck,\n",[130,252,254],{"class":132,"line":253},15,[130,255,256],{"class":152},"    page_noindex:            runNoIndexCheck,\n",[130,258,260],{"class":132,"line":259},16,[130,261,262],{"class":152},"    page_pagespeed:          runPagespeedInsightsCheck,\n",[130,264,266],{"class":132,"line":265},17,[130,267,268],{"class":152},"    page_title_length:       runTitleLengthCheck,\n",[130,270,272],{"class":132,"line":271},18,[130,273,274],{"class":152},"    page_meta_description_length: runMetaDescriptionLengthCheck,\n",[130,276,278],{"class":132,"line":277},19,[130,279,280],{"class":152},"    page_h1:                 runH1Check,\n",[130,282,284],{"class":132,"line":283},20,[130,285,286],{"class":152},"    page_heading_hierarchy:  runHeadingHierarchyCheck,\n",[130,288,290],{"class":132,"line":289},21,[130,291,292],{"class":152},"    page_social_preview:     runSocialPreviewCheck,\n",[130,294,296],{"class":132,"line":295},22,[130,297,298],{"class":152},"    page_structured_data:    runStructuredDataCheck,\n",[130,300,302],{"class":132,"line":301},23,[130,303,304],{"class":152},"};\n",[109,306,308],{"className":124,"code":307,"language":126,"meta":118,"style":118},"export async function runCheck(pathId: number, checkId: number) {\n  const pathCheck = await getPathCheck(pathId, checkId);\n  const url = buildUrl(pathCheck.path.site.url, pathCheck.path.path);\n  \n  const { response, errors } = await fetchPageContent(url);\n  \n  const result = errors.length > 0 \n    ? createCheckResult(\"FAILED\", errors)\n    : await executeCheck(pathCheck.check.slug, response.data, pathCheck.path.site.url, pathCheck.path);\n \n  const updatedPathCheck = await updatePathAndCheck(\n    pathId, \n    checkId, \n    response.status, \n    result\n  );\n \n  revalidatePath(`paths\u002F${pathId}`);\n  return updatedPathCheck;\n}\n",[116,309,310,349,369,384,389,417,421,446,463,476,480,497,502,507,512,517,522,526,551,559],{"__ignoreMap":118},[130,311,312,315,318,321,325,328,332,334,337,339,342,344,346],{"class":132,"line":133},[130,313,314],{"class":144},"export",[130,316,317],{"class":144}," async",[130,319,320],{"class":136}," function",[130,322,324],{"class":323},"sTKsR"," runCheck",[130,326,327],{"class":152},"(",[130,329,331],{"class":330},"saAYD","pathId",[130,333,145],{"class":144},[130,335,336],{"class":156}," number",[130,338,160],{"class":152},[130,340,341],{"class":330},"checkId",[130,343,145],{"class":144},[130,345,336],{"class":156},[130,347,348],{"class":152},") {\n",[130,350,351,354,357,360,363,366],{"class":132,"line":175},[130,352,353],{"class":136},"  const",[130,355,356],{"class":140}," pathCheck",[130,358,359],{"class":144}," =",[130,361,362],{"class":144}," await",[130,364,365],{"class":323}," getPathCheck",[130,367,368],{"class":152},"(pathId, checkId);\n",[130,370,371,373,376,378,381],{"class":132,"line":181},[130,372,353],{"class":136},[130,374,375],{"class":140}," url",[130,377,359],{"class":144},[130,379,380],{"class":323}," buildUrl",[130,382,383],{"class":152},"(pathCheck.path.site.url, pathCheck.path.path);\n",[130,385,386],{"class":132,"line":187},[130,387,388],{"class":152},"  \n",[130,390,391,393,396,399,401,404,407,409,411,414],{"class":132,"line":193},[130,392,353],{"class":136},[130,394,395],{"class":152}," { ",[130,397,398],{"class":140},"response",[130,400,160],{"class":152},[130,402,403],{"class":140},"errors",[130,405,406],{"class":152}," } ",[130,408,169],{"class":144},[130,410,362],{"class":144},[130,412,413],{"class":323}," fetchPageContent",[130,415,416],{"class":152},"(url);\n",[130,418,419],{"class":132,"line":199},[130,420,388],{"class":152},[130,422,423,425,428,430,433,436,439,443],{"class":132,"line":205},[130,424,353],{"class":136},[130,426,427],{"class":140}," result",[130,429,359],{"class":144},[130,431,432],{"class":152}," errors.",[130,434,435],{"class":140},"length",[130,437,438],{"class":144}," >",[130,440,442],{"class":441},"siXTV"," 0",[130,444,445],{"class":152}," \n",[130,447,448,451,454,456,460],{"class":132,"line":211},[130,449,450],{"class":144},"    ?",[130,452,453],{"class":323}," createCheckResult",[130,455,327],{"class":152},[130,457,459],{"class":458},"sTjUT","\"FAILED\"",[130,461,462],{"class":152},", errors)\n",[130,464,465,468,470,473],{"class":132,"line":217},[130,466,467],{"class":144},"    :",[130,469,362],{"class":144},[130,471,472],{"class":323}," executeCheck",[130,474,475],{"class":152},"(pathCheck.check.slug, response.data, pathCheck.path.site.url, pathCheck.path);\n",[130,477,478],{"class":132,"line":223},[130,479,445],{"class":152},[130,481,482,484,487,489,491,494],{"class":132,"line":229},[130,483,353],{"class":136},[130,485,486],{"class":140}," updatedPathCheck",[130,488,359],{"class":144},[130,490,362],{"class":144},[130,492,493],{"class":323}," updatePathAndCheck",[130,495,496],{"class":152},"(\n",[130,498,499],{"class":132,"line":235},[130,500,501],{"class":152},"    pathId, \n",[130,503,504],{"class":132,"line":241},[130,505,506],{"class":152},"    checkId, \n",[130,508,509],{"class":132,"line":247},[130,510,511],{"class":152},"    response.status, \n",[130,513,514],{"class":132,"line":253},[130,515,516],{"class":152},"    result\n",[130,518,519],{"class":132,"line":259},[130,520,521],{"class":152},"  );\n",[130,523,524],{"class":132,"line":265},[130,525,445],{"class":152},[130,527,528,531,533,536,540,542,545,548],{"class":132,"line":271},[130,529,530],{"class":323},"  revalidatePath",[130,532,327],{"class":152},[130,534,535],{"class":458},"`paths\u002F",[130,537,539],{"class":538},"skrme","${",[130,541,331],{"class":152},[130,543,544],{"class":538},"}",[130,546,547],{"class":458},"`",[130,549,550],{"class":152},");\n",[130,552,553,556],{"class":132,"line":277},[130,554,555],{"class":144},"  return",[130,557,558],{"class":152}," updatedPathCheck;\n",[130,560,561],{"class":132,"line":283},[130,562,563],{"class":152},"}\n",[19,565,566],{},"To allow the user to trigger these checks from the frontend we can use React server components, one of the best things I learned from using Next.js. It seems most frameworks regardless of language are trying to solve the problem of how to connect front and backend code. In PHP there is Livewire which re-renders a component on the backend as changes occur and sends the rendered HTML back to the frontend. While with server actions we are now calling our backend functions on the frontend just as if they were all running in the same environment. This helps minimise the tedium of setting up lots of single use API endpoints and implementing the HTTP request in each component as needed. There is a learning curve to the process though as you need to keep track of which code is server run and which is client run with the directives \"use server\" and \"use client\" but it doesn't take long to get used to it as it effectively comes down to if the user can interact with a component, e.g. click a button, then its a frontend client component and everything else is a background server component.",[19,568,569],{},"Client components can call server functions directly and its all handled in the background by react and next. For example, in My SEO Helper the user can run a test against a specific path on their site. Traditionally we could have an API endpoint and make a POST request manually but now we create can create a client component that calls the server action directly while the button continues to show a loading symbol.",[109,571,573],{"className":124,"code":572,"language":126,"meta":118,"style":118},"\"use client\";\n\nimport { runCheck } from '..\u002Factions';\nimport { useState } from 'react';\n\nexport default function RunCheckButton({ pathId, checkId }: { pathId: number, checkId: number }) {\n  const [isPending, setIsPending] = useState(false);\n\n  async function handleAction() {\n    setIsPending(true);\n    \n    await runCheck(pathId, checkId);\n    \n    setIsPending(false);\n  }\n\n  return (\n    \u003Cbutton \n      onClick={handleAction} \n      disabled={isPending}\n      className=\"bg-black text-white px-4 py-2 rounded\"\n    >\n      {isPending ? \"Working...\" : \"Re-run Test\"}\n    \u003C\u002Fbutton>\n  );\n}\n",[116,574,575,583,589,605,619,623,669,699,703,716,728,733,742,746,756,761,765,772,783,801,814,824,829,851,862,867],{"__ignoreMap":118},[130,576,577,580],{"class":132,"line":133},[130,578,579],{"class":458},"\"use client\"",[130,581,582],{"class":152},";\n",[130,584,585],{"class":132,"line":175},[130,586,588],{"emptyLinePlaceholder":587},true,"\n",[130,590,591,594,597,600,603],{"class":132,"line":181},[130,592,593],{"class":144},"import",[130,595,596],{"class":152}," { runCheck } ",[130,598,599],{"class":144},"from",[130,601,602],{"class":458}," '..\u002Factions'",[130,604,582],{"class":152},[130,606,607,609,612,614,617],{"class":132,"line":187},[130,608,593],{"class":144},[130,610,611],{"class":152}," { useState } ",[130,613,599],{"class":144},[130,615,616],{"class":458}," 'react'",[130,618,582],{"class":152},[130,620,621],{"class":132,"line":193},[130,622,588],{"emptyLinePlaceholder":587},[130,624,625,627,630,632,635,638,640,642,644,647,649,651,654,656,658,660,662,664,666],{"class":132,"line":199},[130,626,314],{"class":144},[130,628,629],{"class":144}," default",[130,631,320],{"class":136},[130,633,634],{"class":323}," RunCheckButton",[130,636,637],{"class":152},"({ ",[130,639,331],{"class":330},[130,641,160],{"class":152},[130,643,341],{"class":330},[130,645,646],{"class":152}," }",[130,648,145],{"class":144},[130,650,395],{"class":152},[130,652,331],{"class":653},"ssgMC",[130,655,145],{"class":144},[130,657,336],{"class":156},[130,659,160],{"class":152},[130,661,341],{"class":653},[130,663,145],{"class":144},[130,665,336],{"class":156},[130,667,668],{"class":152}," }) {\n",[130,670,671,673,676,679,681,684,687,689,692,694,697],{"class":132,"line":205},[130,672,353],{"class":136},[130,674,675],{"class":152}," [",[130,677,678],{"class":140},"isPending",[130,680,160],{"class":152},[130,682,683],{"class":140},"setIsPending",[130,685,686],{"class":152},"] ",[130,688,169],{"class":144},[130,690,691],{"class":323}," useState",[130,693,327],{"class":152},[130,695,696],{"class":441},"false",[130,698,550],{"class":152},[130,700,701],{"class":132,"line":211},[130,702,588],{"emptyLinePlaceholder":587},[130,704,705,708,710,713],{"class":132,"line":217},[130,706,707],{"class":144},"  async",[130,709,320],{"class":136},[130,711,712],{"class":323}," handleAction",[130,714,715],{"class":152},"() {\n",[130,717,718,721,723,726],{"class":132,"line":223},[130,719,720],{"class":323},"    setIsPending",[130,722,327],{"class":152},[130,724,725],{"class":441},"true",[130,727,550],{"class":152},[130,729,730],{"class":132,"line":229},[130,731,732],{"class":152},"    \n",[130,734,735,738,740],{"class":132,"line":235},[130,736,737],{"class":144},"    await",[130,739,324],{"class":323},[130,741,368],{"class":152},[130,743,744],{"class":132,"line":241},[130,745,732],{"class":152},[130,747,748,750,752,754],{"class":132,"line":247},[130,749,720],{"class":323},[130,751,327],{"class":152},[130,753,696],{"class":441},[130,755,550],{"class":152},[130,757,758],{"class":132,"line":253},[130,759,760],{"class":152},"  }\n",[130,762,763],{"class":132,"line":259},[130,764,588],{"emptyLinePlaceholder":587},[130,766,767,769],{"class":132,"line":265},[130,768,555],{"class":144},[130,770,771],{"class":152}," (\n",[130,773,774,777,781],{"class":132,"line":271},[130,775,776],{"class":152},"    \u003C",[130,778,780],{"class":779},"sCeOG","button",[130,782,445],{"class":152},[130,784,785,788,790,794,797,799],{"class":132,"line":277},[130,786,787],{"class":323},"      onClick",[130,789,169],{"class":144},[130,791,793],{"class":792},"shc6p","{",[130,795,796],{"class":152},"handleAction",[130,798,544],{"class":792},[130,800,445],{"class":152},[130,802,803,806,808,810,812],{"class":132,"line":283},[130,804,805],{"class":323},"      disabled",[130,807,169],{"class":144},[130,809,793],{"class":792},[130,811,678],{"class":152},[130,813,563],{"class":792},[130,815,816,819,821],{"class":132,"line":289},[130,817,818],{"class":323},"      className",[130,820,169],{"class":144},[130,822,823],{"class":458},"\"bg-black text-white px-4 py-2 rounded\"\n",[130,825,826],{"class":132,"line":295},[130,827,828],{"class":152},"    >\n",[130,830,831,834,837,840,843,846,849],{"class":132,"line":301},[130,832,833],{"class":792},"      {",[130,835,836],{"class":152},"isPending ",[130,838,839],{"class":144},"?",[130,841,842],{"class":458}," \"Working...\"",[130,844,845],{"class":144}," :",[130,847,848],{"class":458}," \"Re-run Test\"",[130,850,563],{"class":792},[130,852,854,857,859],{"class":132,"line":853},24,[130,855,856],{"class":152},"    \u003C\u002F",[130,858,780],{"class":779},[130,860,861],{"class":152},">\n",[130,863,865],{"class":132,"line":864},25,[130,866,521],{"class":152},[130,868,870],{"class":132,"line":869},26,[130,871,563],{"class":152},[19,873,874],{},"Even though this is a client component we can call the runCheck function (I call them checks on the backend because it felt weird to see the word 'test' in the code), which is in an action file that uses the \"use server\" directive and can access backend resources like the database directly.",[19,876,877],{},"Where I have struggled a little with next is that while its an open framework there is a tendency in the documentation to expect you to host on Vercel or push you to another paid service that you are looking to implement. Most of these services have free tiers and such but would be nicer if self hosted options were documented more clearly such as opennext.",[19,879,880,881,887],{},"As an MVP project things are looking good so far but as the project moves into the future for v1.5 I will be looking to add queuing and scheduling to the project and so far my research has shown there will be some challenges to overcome. Some of the above processes will start to run long as we add checks that will analyse the text in more detail and connect to other APIs, e.g. google search console, and the suggestions used for next for asynchronous jobs tend to be expensive third party services which are also more designed for simple jobs like queuing emails to be sent. I found this article which outlines a lot of the issues I've experienced while trying to prototype directly within next itself: ",[882,883,884],"a",{"href":884,"rel":885},"https:\u002F\u002Fdev.to\u002Fbardaq\u002Flong-running-tasks-with-nextjs-a-journey-of-reinventing-the-wheel-1cjg",[886],"nofollow",". But overall for v1.5 I plan on moving the scanning engine and check functionality to its own separate backend, leaning towards Express JS to reuse code, which will allow me to focus on using next for the frontend dashboard functionality it excels at.",[889,890,891],"style",{},"html pre.shiki code .s2d5f, html code.shiki .s2d5f{--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .ssVjP, html code.shiki .ssVjP{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html pre.shiki code .setMH, html code.shiki .setMH{--shiki-default:#F97583;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .sZGI-, html code.shiki .sZGI-{--shiki-default:#B392F0;--shiki-default-text-decoration:inherit;--shiki-dark:#B392F0;--shiki-dark-text-decoration:inherit;--shiki-sepia:#A6E22E;--shiki-sepia-text-decoration:underline}html pre.shiki code .s4SHQ, html code.shiki .s4SHQ{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .sPGPh, html code.shiki .sPGPh{--shiki-default:#79B8FF;--shiki-default-font-style:inherit;--shiki-dark:#79B8FF;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .sTKsR, html code.shiki .sTKsR{--shiki-default:#B392F0;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .saAYD, html code.shiki .saAYD{--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}html pre.shiki code .siXTV, html code.shiki .siXTV{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html pre.shiki code .sTjUT, html code.shiki .sTjUT{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .skrme, html code.shiki .skrme{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-sepia:#F92672}html pre.shiki code .ssgMC, html code.shiki .ssgMC{--shiki-default:#FFAB70;--shiki-dark:#FFAB70;--shiki-sepia:#F8F8F2}html pre.shiki code .sCeOG, html code.shiki .sCeOG{--shiki-default:#85E89D;--shiki-dark:#85E89D;--shiki-sepia:#F92672}html pre.shiki code .shc6p, html code.shiki .shc6p{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-sepia:#F92672}",{"title":118,"searchDepth":175,"depth":175,"links":893},[894,896,901],{"id":12,"depth":175,"text":895},"Project Case Study: Technical SEO site management and maintenance",{"id":33,"depth":175,"text":34,"children":897},[898,899,900],{"id":38,"depth":181,"text":39},{"id":55,"depth":181,"text":56},{"id":70,"depth":181,"text":71},{"id":97,"depth":175,"text":98},"2026-04-09T10:44:00.000+01:00","Project to help keep track of the technical SEO health of various websites I manage","md",null,"\u002Fimg\u002Fscreenshot-from-2026-04-09-10-58-38.png",{},"\u002Fprojects\u002Fmy-seo-helper-technical-seo-site-analysis",{"title":5,"description":903},"projects\u002Fmy-seo-helper-technical-seo-site-analysis",[912,913,914,915],"Next.js","Auth.js","Prisma","TypeScript","YdeJAbmYjL8NKlFEKlSNStlH3TUqmJDDAJTbo0T1hug",[918,1577,2201],{"id":4,"title":5,"body":919,"date":902,"description":903,"extension":904,"featured":725,"githubLink":905,"image":906,"liveLink":905,"meta":1574,"navigation":587,"path":908,"seo":1575,"stem":910,"technologies":1576,"__hash__":916},{"type":7,"value":920,"toc":1565},[921,925,927,929,931,933,935,937,945,947,955,957,973,975,977,979,981,986,988,1104,1302,1304,1306,1554,1556,1558,1563],[10,922,13,923],{"id":12},[15,924,17],{},[19,926,21],{},[19,928,24],{},[19,930,27],{},[19,932,30],{},[10,934,34],{"id":33},[36,936,39],{"id":38},[41,938,939,941,943],{},[44,940,46],{},[44,942,49],{},[44,944,52],{},[36,946,56],{"id":55},[41,948,949,951,953],{},[44,950,61],{},[44,952,64],{},[44,954,67],{},[36,956,71],{"id":70},[41,958,959,961,963,965,967,969,971],{},[44,960,76],{},[44,962,79],{},[44,964,82],{},[44,966,85],{},[44,968,88],{},[44,970,91],{},[44,972,94],{},[10,974,98],{"id":97},[19,976,101],{},[19,978,104],{},[19,980,107],{},[109,982,984],{"className":983,"code":113,"language":114},[112],[116,985,113],{"__ignoreMap":118},[19,987,121],{},[109,989,990],{"className":124,"code":125,"language":126,"meta":118,"style":118},[116,991,992,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052,1056,1060,1064,1068,1072,1076,1080,1084,1088,1092,1096,1100],{"__ignoreMap":118},[130,993,994,996,998,1000,1002,1004,1006,1008,1010,1012,1014],{"class":132,"line":133},[130,995,137],{"class":136},[130,997,141],{"class":140},[130,999,145],{"class":144},[130,1001,149],{"class":148},[130,1003,153],{"class":152},[130,1005,157],{"class":156},[130,1007,160],{"class":152},[130,1009,163],{"class":148},[130,1011,166],{"class":152},[130,1013,169],{"class":144},[130,1015,172],{"class":152},[130,1017,1018],{"class":132,"line":175},[130,1019,178],{"class":152},[130,1021,1022],{"class":132,"line":181},[130,1023,184],{"class":152},[130,1025,1026],{"class":132,"line":187},[130,1027,190],{"class":152},[130,1029,1030],{"class":132,"line":193},[130,1031,196],{"class":152},[130,1033,1034],{"class":132,"line":199},[130,1035,202],{"class":152},[130,1037,1038],{"class":132,"line":205},[130,1039,208],{"class":152},[130,1041,1042],{"class":132,"line":211},[130,1043,214],{"class":152},[130,1045,1046],{"class":132,"line":217},[130,1047,220],{"class":152},[130,1049,1050],{"class":132,"line":223},[130,1051,226],{"class":152},[130,1053,1054],{"class":132,"line":229},[130,1055,232],{"class":152},[130,1057,1058],{"class":132,"line":235},[130,1059,238],{"class":152},[130,1061,1062],{"class":132,"line":241},[130,1063,244],{"class":152},[130,1065,1066],{"class":132,"line":247},[130,1067,250],{"class":152},[130,1069,1070],{"class":132,"line":253},[130,1071,256],{"class":152},[130,1073,1074],{"class":132,"line":259},[130,1075,262],{"class":152},[130,1077,1078],{"class":132,"line":265},[130,1079,268],{"class":152},[130,1081,1082],{"class":132,"line":271},[130,1083,274],{"class":152},[130,1085,1086],{"class":132,"line":277},[130,1087,280],{"class":152},[130,1089,1090],{"class":132,"line":283},[130,1091,286],{"class":152},[130,1093,1094],{"class":132,"line":289},[130,1095,292],{"class":152},[130,1097,1098],{"class":132,"line":295},[130,1099,298],{"class":152},[130,1101,1102],{"class":132,"line":301},[130,1103,304],{"class":152},[109,1105,1106],{"className":124,"code":307,"language":126,"meta":118,"style":118},[116,1107,1108,1136,1150,1162,1166,1188,1192,1210,1222,1232,1236,1250,1254,1258,1262,1266,1270,1274,1292,1298],{"__ignoreMap":118},[130,1109,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128,1130,1132,1134],{"class":132,"line":133},[130,1111,314],{"class":144},[130,1113,317],{"class":144},[130,1115,320],{"class":136},[130,1117,324],{"class":323},[130,1119,327],{"class":152},[130,1121,331],{"class":330},[130,1123,145],{"class":144},[130,1125,336],{"class":156},[130,1127,160],{"class":152},[130,1129,341],{"class":330},[130,1131,145],{"class":144},[130,1133,336],{"class":156},[130,1135,348],{"class":152},[130,1137,1138,1140,1142,1144,1146,1148],{"class":132,"line":175},[130,1139,353],{"class":136},[130,1141,356],{"class":140},[130,1143,359],{"class":144},[130,1145,362],{"class":144},[130,1147,365],{"class":323},[130,1149,368],{"class":152},[130,1151,1152,1154,1156,1158,1160],{"class":132,"line":181},[130,1153,353],{"class":136},[130,1155,375],{"class":140},[130,1157,359],{"class":144},[130,1159,380],{"class":323},[130,1161,383],{"class":152},[130,1163,1164],{"class":132,"line":187},[130,1165,388],{"class":152},[130,1167,1168,1170,1172,1174,1176,1178,1180,1182,1184,1186],{"class":132,"line":193},[130,1169,353],{"class":136},[130,1171,395],{"class":152},[130,1173,398],{"class":140},[130,1175,160],{"class":152},[130,1177,403],{"class":140},[130,1179,406],{"class":152},[130,1181,169],{"class":144},[130,1183,362],{"class":144},[130,1185,413],{"class":323},[130,1187,416],{"class":152},[130,1189,1190],{"class":132,"line":199},[130,1191,388],{"class":152},[130,1193,1194,1196,1198,1200,1202,1204,1206,1208],{"class":132,"line":205},[130,1195,353],{"class":136},[130,1197,427],{"class":140},[130,1199,359],{"class":144},[130,1201,432],{"class":152},[130,1203,435],{"class":140},[130,1205,438],{"class":144},[130,1207,442],{"class":441},[130,1209,445],{"class":152},[130,1211,1212,1214,1216,1218,1220],{"class":132,"line":211},[130,1213,450],{"class":144},[130,1215,453],{"class":323},[130,1217,327],{"class":152},[130,1219,459],{"class":458},[130,1221,462],{"class":152},[130,1223,1224,1226,1228,1230],{"class":132,"line":217},[130,1225,467],{"class":144},[130,1227,362],{"class":144},[130,1229,472],{"class":323},[130,1231,475],{"class":152},[130,1233,1234],{"class":132,"line":223},[130,1235,445],{"class":152},[130,1237,1238,1240,1242,1244,1246,1248],{"class":132,"line":229},[130,1239,353],{"class":136},[130,1241,486],{"class":140},[130,1243,359],{"class":144},[130,1245,362],{"class":144},[130,1247,493],{"class":323},[130,1249,496],{"class":152},[130,1251,1252],{"class":132,"line":235},[130,1253,501],{"class":152},[130,1255,1256],{"class":132,"line":241},[130,1257,506],{"class":152},[130,1259,1260],{"class":132,"line":247},[130,1261,511],{"class":152},[130,1263,1264],{"class":132,"line":253},[130,1265,516],{"class":152},[130,1267,1268],{"class":132,"line":259},[130,1269,521],{"class":152},[130,1271,1272],{"class":132,"line":265},[130,1273,445],{"class":152},[130,1275,1276,1278,1280,1282,1284,1286,1288,1290],{"class":132,"line":271},[130,1277,530],{"class":323},[130,1279,327],{"class":152},[130,1281,535],{"class":458},[130,1283,539],{"class":538},[130,1285,331],{"class":152},[130,1287,544],{"class":538},[130,1289,547],{"class":458},[130,1291,550],{"class":152},[130,1293,1294,1296],{"class":132,"line":277},[130,1295,555],{"class":144},[130,1297,558],{"class":152},[130,1299,1300],{"class":132,"line":283},[130,1301,563],{"class":152},[19,1303,566],{},[19,1305,569],{},[109,1307,1308],{"className":124,"code":572,"language":126,"meta":118,"style":118},[116,1309,1310,1316,1320,1332,1344,1348,1388,1412,1416,1426,1436,1440,1448,1452,1462,1466,1470,1476,1484,1498,1510,1518,1522,1538,1546,1550],{"__ignoreMap":118},[130,1311,1312,1314],{"class":132,"line":133},[130,1313,579],{"class":458},[130,1315,582],{"class":152},[130,1317,1318],{"class":132,"line":175},[130,1319,588],{"emptyLinePlaceholder":587},[130,1321,1322,1324,1326,1328,1330],{"class":132,"line":181},[130,1323,593],{"class":144},[130,1325,596],{"class":152},[130,1327,599],{"class":144},[130,1329,602],{"class":458},[130,1331,582],{"class":152},[130,1333,1334,1336,1338,1340,1342],{"class":132,"line":187},[130,1335,593],{"class":144},[130,1337,611],{"class":152},[130,1339,599],{"class":144},[130,1341,616],{"class":458},[130,1343,582],{"class":152},[130,1345,1346],{"class":132,"line":193},[130,1347,588],{"emptyLinePlaceholder":587},[130,1349,1350,1352,1354,1356,1358,1360,1362,1364,1366,1368,1370,1372,1374,1376,1378,1380,1382,1384,1386],{"class":132,"line":199},[130,1351,314],{"class":144},[130,1353,629],{"class":144},[130,1355,320],{"class":136},[130,1357,634],{"class":323},[130,1359,637],{"class":152},[130,1361,331],{"class":330},[130,1363,160],{"class":152},[130,1365,341],{"class":330},[130,1367,646],{"class":152},[130,1369,145],{"class":144},[130,1371,395],{"class":152},[130,1373,331],{"class":653},[130,1375,145],{"class":144},[130,1377,336],{"class":156},[130,1379,160],{"class":152},[130,1381,341],{"class":653},[130,1383,145],{"class":144},[130,1385,336],{"class":156},[130,1387,668],{"class":152},[130,1389,1390,1392,1394,1396,1398,1400,1402,1404,1406,1408,1410],{"class":132,"line":205},[130,1391,353],{"class":136},[130,1393,675],{"class":152},[130,1395,678],{"class":140},[130,1397,160],{"class":152},[130,1399,683],{"class":140},[130,1401,686],{"class":152},[130,1403,169],{"class":144},[130,1405,691],{"class":323},[130,1407,327],{"class":152},[130,1409,696],{"class":441},[130,1411,550],{"class":152},[130,1413,1414],{"class":132,"line":211},[130,1415,588],{"emptyLinePlaceholder":587},[130,1417,1418,1420,1422,1424],{"class":132,"line":217},[130,1419,707],{"class":144},[130,1421,320],{"class":136},[130,1423,712],{"class":323},[130,1425,715],{"class":152},[130,1427,1428,1430,1432,1434],{"class":132,"line":223},[130,1429,720],{"class":323},[130,1431,327],{"class":152},[130,1433,725],{"class":441},[130,1435,550],{"class":152},[130,1437,1438],{"class":132,"line":229},[130,1439,732],{"class":152},[130,1441,1442,1444,1446],{"class":132,"line":235},[130,1443,737],{"class":144},[130,1445,324],{"class":323},[130,1447,368],{"class":152},[130,1449,1450],{"class":132,"line":241},[130,1451,732],{"class":152},[130,1453,1454,1456,1458,1460],{"class":132,"line":247},[130,1455,720],{"class":323},[130,1457,327],{"class":152},[130,1459,696],{"class":441},[130,1461,550],{"class":152},[130,1463,1464],{"class":132,"line":253},[130,1465,760],{"class":152},[130,1467,1468],{"class":132,"line":259},[130,1469,588],{"emptyLinePlaceholder":587},[130,1471,1472,1474],{"class":132,"line":265},[130,1473,555],{"class":144},[130,1475,771],{"class":152},[130,1477,1478,1480,1482],{"class":132,"line":271},[130,1479,776],{"class":152},[130,1481,780],{"class":779},[130,1483,445],{"class":152},[130,1485,1486,1488,1490,1492,1494,1496],{"class":132,"line":277},[130,1487,787],{"class":323},[130,1489,169],{"class":144},[130,1491,793],{"class":792},[130,1493,796],{"class":152},[130,1495,544],{"class":792},[130,1497,445],{"class":152},[130,1499,1500,1502,1504,1506,1508],{"class":132,"line":283},[130,1501,805],{"class":323},[130,1503,169],{"class":144},[130,1505,793],{"class":792},[130,1507,678],{"class":152},[130,1509,563],{"class":792},[130,1511,1512,1514,1516],{"class":132,"line":289},[130,1513,818],{"class":323},[130,1515,169],{"class":144},[130,1517,823],{"class":458},[130,1519,1520],{"class":132,"line":295},[130,1521,828],{"class":152},[130,1523,1524,1526,1528,1530,1532,1534,1536],{"class":132,"line":301},[130,1525,833],{"class":792},[130,1527,836],{"class":152},[130,1529,839],{"class":144},[130,1531,842],{"class":458},[130,1533,845],{"class":144},[130,1535,848],{"class":458},[130,1537,563],{"class":792},[130,1539,1540,1542,1544],{"class":132,"line":853},[130,1541,856],{"class":152},[130,1543,780],{"class":779},[130,1545,861],{"class":152},[130,1547,1548],{"class":132,"line":864},[130,1549,521],{"class":152},[130,1551,1552],{"class":132,"line":869},[130,1553,563],{"class":152},[19,1555,874],{},[19,1557,877],{},[19,1559,880,1560,887],{},[882,1561,884],{"href":884,"rel":1562},[886],[889,1564,891],{},{"title":118,"searchDepth":175,"depth":175,"links":1566},[1567,1568,1573],{"id":12,"depth":175,"text":895},{"id":33,"depth":175,"text":34,"children":1569},[1570,1571,1572],{"id":38,"depth":181,"text":39},{"id":55,"depth":181,"text":56},{"id":70,"depth":181,"text":71},{"id":97,"depth":175,"text":98},{},{"title":5,"description":903},[912,913,914,915],{"id":1578,"title":1579,"body":1580,"date":2188,"description":2189,"extension":904,"featured":725,"githubLink":2190,"image":2191,"liveLink":2192,"meta":2193,"navigation":587,"path":2194,"seo":2195,"stem":2196,"technologies":2197,"__hash__":2200},"projects\u002Fprojects\u002Fdarrens-tech-tutorials.md","Darren's Tech Tutorials",{"type":7,"value":1581,"toc":2177},[1582,1589,1593,1600,1606,1620,1623,1627,1654,1658,1758,1760,1764,1769,1779,1793,1797,1802,1939,1943,1946,1955,1965,1977,2111,2115,2118,2156,2158,2162,2168,2174],[10,1583,1585,1586],{"id":1584},"️-project-case-study-youtube-to-blog-static-site-generator","🏗️ Project Case Study: ",[15,1587,1588],{},"YouTube-to-Blog Static Site Generator",[36,1590,1592],{"id":1591},"overview-problem-statement","🎯 Overview & Problem Statement",[19,1594,1595,1596,1599],{},"Darren, a technical content creator, operates a successful YouTube channel featuring short, command-line-heavy tutorials. His goal was to provide a companion blog post for every video, allowing viewers to easily search, reference, and ",[15,1597,1598],{},"copy-and-paste commands"," mentioned in the tutorials.",[19,1601,1602,1605],{},[15,1603,1604],{},"The Challenge:"," Manually converting video transcripts into properly formatted, structured, and SEO-friendly blog posts was incredibly time-consuming, creating a significant bottleneck that limited content output.",[19,1607,1608,1611,1612,1615,1616,1619],{},[15,1609,1610],{},"Our Solution:"," Develop an automated pipeline that uses the YouTube Data API to retrieve video information and transcripts, then employs the ",[15,1613,1614],{},"Gemini API"," to convert the raw transcript into a polished, structured markdown blog post. This content is then fed into a ",[15,1617,1618],{},"Hugo"," static site generator to instantly build and deploy hundreds of fast-loading web pages.",[1621,1622],"hr",{},[36,1624,1626],{"id":1625},"key-project-goals","🚀 Key Project Goals",[1628,1629,1630,1636,1642,1648],"ul",{},[44,1631,1632,1635],{},[15,1633,1634],{},"Automation:"," Eliminate the manual process of blog post creation.",[44,1637,1638,1641],{},[15,1639,1640],{},"Speed & Scale:"," Build hundreds of static, fast-loading pages instantly.",[44,1643,1644,1647],{},[15,1645,1646],{},"SEO-Friendly:"," Ensure the new blog content is easily discoverable by search engines.",[44,1649,1650,1653],{},[15,1651,1652],{},"Accessibility:"," Provide viewers with an easy-to-read, copyable alternative to watching the video.",[36,1655,1657],{"id":1656},"️-technology-stack","🛠️ Technology Stack",[1659,1660,1661,1678],"table",{},[1662,1663,1664],"thead",{},[1665,1666,1667,1672,1675],"tr",{},[1668,1669,1671],"th",{"align":1670},"left","Technology",[1668,1673,1674],{"align":1670},"Purpose",[1668,1676,1677],{"align":1670},"Key Feature Utilized",[1679,1680,1681,1694,1707,1719,1732,1745],"tbody",{},[1665,1682,1683,1688,1691],{},[1684,1685,1686],"td",{"align":1670},[15,1687,1618],{},[1684,1689,1690],{"align":1670},"Static Site Generator (SSG)",[1684,1692,1693],{"align":1670},"Incredibly fast build times, Markdown-based content.",[1665,1695,1696,1701,1704],{},[1684,1697,1698],{"align":1670},[15,1699,1700],{},"YouTube Data API",[1684,1702,1703],{"align":1670},"Data Source",[1684,1705,1706],{"align":1670},"Fetching Channel\u002FPlaylist\u002FVideo metadata.",[1665,1708,1709,1713,1716],{},[1684,1710,1711],{"align":1670},[15,1712,1614],{},[1684,1714,1715],{"align":1670},"Content Generation",[1684,1717,1718],{"align":1670},"Converting raw video transcript into structured Markdown.",[1665,1720,1721,1726,1729],{},[1684,1722,1723],{"align":1670},[15,1724,1725],{},"Node.js",[1684,1727,1728],{"align":1670},"Core Scripting Language",[1684,1730,1731],{"align":1670},"Orchestrating API calls and file system operations.",[1665,1733,1734,1739,1742],{},[1684,1735,1736],{"align":1670},[15,1737,1738],{},"Cloudflare Pages",[1684,1740,1741],{"align":1670},"Hosting\u002FCI\u002FCD",[1684,1743,1744],{"align":1670},"Automated deployment and global CDN performance.",[1665,1746,1747,1752,1755],{},[1684,1748,1749],{"align":1670},[15,1750,1751],{},"YouTube.js",[1684,1753,1754],{"align":1670},"Transcript Fetching",[1684,1756,1757],{"align":1670},"Accessing YouTube's internal API (InnerTube) for subtitles.",[1621,1759],{},[36,1761,1763],{"id":1762},"development-technical-breakdown","💻 Development & Technical Breakdown",[1765,1766,1768],"h4",{"id":1767},"_1-static-site-foundation-with-hugo","1. Static Site Foundation with Hugo",[19,1770,1771,1772,1775,1776,1778],{},"To meet the requirement for ",[15,1773,1774],{},"speed and scalability",", a Static Site Generator (SSG) was the ideal choice. ",[15,1777,1618],{}," was selected due to its reputation for being one of the fastest SSGs available.",[1628,1780,1781,1787],{},[44,1782,1783,1786],{},[15,1784,1785],{},"Benefit:"," The site is built from simple markdown files, resulting in pure HTML\u002FCSS\u002FJS, eliminating database lookups and ensuring blazing-fast load times.",[44,1788,1789,1792],{},[15,1790,1791],{},"Workflow:"," The custom Node.js script generates new markdown files for each video, and a simple commit to the GitHub repository automatically triggers Cloudflare Pages to rebuild and deploy the entire site in seconds.",[1765,1794,1796],{"id":1795},"_2-youtube-data-orchestration","2. YouTube Data Orchestration",[19,1798,1799,1800,145],{},"The first hurdle was fetching all video data from the channel. This required a multi-step process using the ",[15,1801,1700],{},[1628,1803,1804,1865,1922],{},[44,1805,1806,1809,1810],{},[15,1807,1808],{},"Step A: Fetching the Uploads Playlist ID","\nThe core channel information is queried to find the unique ID for the default 'Uploads' playlist, which contains every public video on the channel.",[109,1811,1813],{"className":124,"code":1812,"language":126,"meta":118,"style":118},"async function getUploadsPlaylistId(channelId) {\n  \u002F\u002F ... API call to channels.list ...\n  const uploadsPlaylistId = response.data.items[0]?.contentDetails.relatedPlaylists.uploads;\n  \u002F\u002F ... error handling ...\n}\n",[116,1814,1815,1832,1838,1856,1861],{"__ignoreMap":118},[130,1816,1817,1820,1822,1825,1827,1830],{"class":132,"line":133},[130,1818,1819],{"class":144},"async",[130,1821,320],{"class":136},[130,1823,1824],{"class":323}," getUploadsPlaylistId",[130,1826,327],{"class":152},[130,1828,1829],{"class":330},"channelId",[130,1831,348],{"class":152},[130,1833,1834],{"class":132,"line":175},[130,1835,1837],{"class":1836},"s8-w5","  \u002F\u002F ... API call to channels.list ...\n",[130,1839,1840,1842,1845,1847,1850,1853],{"class":132,"line":181},[130,1841,353],{"class":136},[130,1843,1844],{"class":140}," uploadsPlaylistId",[130,1846,359],{"class":144},[130,1848,1849],{"class":152}," response.data.items[",[130,1851,1852],{"class":441},"0",[130,1854,1855],{"class":152},"]?.contentDetails.relatedPlaylists.uploads;\n",[130,1857,1858],{"class":132,"line":187},[130,1859,1860],{"class":1836},"  \u002F\u002F ... error handling ...\n",[130,1862,1863],{"class":132,"line":193},[130,1864,563],{"class":152},[44,1866,1867,1870,1871,1874,1875,1878,1879],{},[15,1868,1869],{},"Step B: Paginated Video Retrieval","\nBecause YouTube limits API results per page, the video fetching function was built with a ",[15,1872,1873],{},"pagination loop"," that checks for the ",[116,1876,1877],{},"nextPageToken"," and continues querying the API until all videos are processed.",[109,1880,1882],{"className":124,"code":1881,"language":126,"meta":118,"style":118},"do {\n  \u002F\u002F ... API call to playlistItems.list ...\n  \u002F\u002F ... process videos ...\n  nextPageToken = response.data.nextPageToken;\n} while (nextPageToken);\n",[116,1883,1884,1891,1896,1901,1911],{"__ignoreMap":118},[130,1885,1886,1889],{"class":132,"line":133},[130,1887,1888],{"class":144},"do",[130,1890,172],{"class":152},[130,1892,1893],{"class":132,"line":175},[130,1894,1895],{"class":1836},"  \u002F\u002F ... API call to playlistItems.list ...\n",[130,1897,1898],{"class":132,"line":181},[130,1899,1900],{"class":1836},"  \u002F\u002F ... process videos ...\n",[130,1902,1903,1906,1908],{"class":132,"line":187},[130,1904,1905],{"class":152},"  nextPageToken ",[130,1907,169],{"class":144},[130,1909,1910],{"class":152}," response.data.nextPageToken;\n",[130,1912,1913,1916,1919],{"class":132,"line":193},[130,1914,1915],{"class":152},"} ",[130,1917,1918],{"class":144},"while",[130,1920,1921],{"class":152}," (nextPageToken);\n",[44,1923,1924,1927,1928,1931,1932,1934,1935,1938],{},[15,1925,1926],{},"Step C: Fetching the Raw Transcript","\nCrucially, the official YouTube Data API does ",[15,1929,1930],{},"not"," provide direct access to the time-synced subtitle data. After testing multiple external libraries, ",[15,1933,1751],{}," (which accesses YouTube's internal ",[15,1936,1937],{},"InnerTube"," API) was chosen to reliably scrape the raw transcript text. This raw text forms the input for the AI conversion step.",[1765,1940,1942],{"id":1941},"_3-content-transformation-with-the-gemini-api","3. Content Transformation with the Gemini API",[19,1944,1945],{},"This step is the core of the automation solution. The raw, unstructured transcript is submitted to the Gemini API with a specific system instruction and prompt to ensure a high-quality, structured output.",[1947,1948,1949],"blockquote",{},[19,1950,1951,1954],{},[15,1952,1953],{},"Prompt Strategy:"," The system prompt instructs Gemini to act as a \"Technical Blogger\" and convert the raw transcript into a structured markdown document, explicitly formatting commands within code blocks. This is crucial for meeting the user requirement of \"easy to copy commands.\"",[19,1956,1957,1960,1961,1964],{},[15,1958,1959],{},"Robust API Handling:"," Given that generating content can sometimes take longer or  encounter temporary errors (such as model being overloaded at the time), the API call function was implemented with a ",[15,1962,1963],{},"retry mechanism using exponential backoff",".",[1628,1966,1967,1974],{},[44,1968,1969,1970,1973],{},"If the API call fails, the script waits for a delay (",[116,1971,1972],{},"Math.pow(2, attempt) * 1000",") before trying again.",[44,1975,1976],{},"This significantly increases the reliability of the content generation pipeline.",[109,1978,1980],{"className":124,"code":1979,"language":126,"meta":118,"style":118},"\u002F\u002F Retry loop with exponential backoff\nfor (let attempt = 1; attempt \u003C= maxRetries; attempt++) {\n    \u002F\u002F ... API call logic ...\n    if (attempt > 1) {\n        const delay = Math.pow(2, attempt) * 1000;\n        await new Promise(resolve => setTimeout(resolve, delay));\n    }\n    \u002F\u002F ... try\u002Fcatch block for API call ...\n}\n",[116,1981,1982,1987,2020,2025,2040,2072,2097,2102,2107],{"__ignoreMap":118},[130,1983,1984],{"class":132,"line":133},[130,1985,1986],{"class":1836},"\u002F\u002F Retry loop with exponential backoff\n",[130,1988,1989,1992,1995,1998,2001,2003,2006,2009,2012,2015,2018],{"class":132,"line":175},[130,1990,1991],{"class":144},"for",[130,1993,1994],{"class":152}," (",[130,1996,1997],{"class":136},"let",[130,1999,2000],{"class":152}," attempt ",[130,2002,169],{"class":144},[130,2004,2005],{"class":441}," 1",[130,2007,2008],{"class":152},"; attempt ",[130,2010,2011],{"class":144},"\u003C=",[130,2013,2014],{"class":152}," maxRetries; attempt",[130,2016,2017],{"class":144},"++",[130,2019,348],{"class":152},[130,2021,2022],{"class":132,"line":181},[130,2023,2024],{"class":1836},"    \u002F\u002F ... API call logic ...\n",[130,2026,2027,2030,2033,2036,2038],{"class":132,"line":187},[130,2028,2029],{"class":144},"    if",[130,2031,2032],{"class":152}," (attempt ",[130,2034,2035],{"class":144},">",[130,2037,2005],{"class":441},[130,2039,348],{"class":152},[130,2041,2042,2045,2048,2050,2053,2056,2058,2061,2064,2067,2070],{"class":132,"line":193},[130,2043,2044],{"class":136},"        const",[130,2046,2047],{"class":140}," delay",[130,2049,359],{"class":144},[130,2051,2052],{"class":152}," Math.",[130,2054,2055],{"class":323},"pow",[130,2057,327],{"class":152},[130,2059,2060],{"class":441},"2",[130,2062,2063],{"class":152},", attempt) ",[130,2065,2066],{"class":144},"*",[130,2068,2069],{"class":441}," 1000",[130,2071,582],{"class":152},[130,2073,2074,2077,2080,2083,2085,2088,2091,2094],{"class":132,"line":199},[130,2075,2076],{"class":144},"        await",[130,2078,2079],{"class":144}," new",[130,2081,2082],{"class":156}," Promise",[130,2084,327],{"class":152},[130,2086,2087],{"class":330},"resolve",[130,2089,2090],{"class":136}," =>",[130,2092,2093],{"class":323}," setTimeout",[130,2095,2096],{"class":152},"(resolve, delay));\n",[130,2098,2099],{"class":132,"line":205},[130,2100,2101],{"class":152},"    }\n",[130,2103,2104],{"class":132,"line":211},[130,2105,2106],{"class":1836},"    \u002F\u002F ... try\u002Fcatch block for API call ...\n",[130,2108,2109],{"class":132,"line":217},[130,2110,563],{"class":152},[36,2112,2114],{"id":2113},"results-impact","📈 Results & Impact",[19,2116,2117],{},"The automated YouTube-to-Blog generator achieved the following:",[1628,2119,2120,2134,2144,2150],{},[44,2121,2122,2125,2126,2129,2130,2133],{},[15,2123,2124],{},"95% Time Reduction:"," The time required to create a new blog post was reduced from ",[15,2127,2128],{},"~1 hour of manual work"," (watching the video, writing, formatting) to ",[15,2131,2132],{},"~5 minutes of automated processing"," per video.",[44,2135,2136,2139,2140,2143],{},[15,2137,2138],{},"Instant Backlog Processing:"," The script successfully processed ",[15,2141,2142],{},"200+ videos"," already on the channel, instantly creating a searchable, high-value content library for viewers.",[44,2145,2146,2149],{},[15,2147,2148],{},"Improved User Experience:"," Viewers can now quickly search for technical articles, copy code snippets, and refer back to tutorials without rewatching the video, directly addressing the initial problem statement.",[44,2151,2152,2155],{},[15,2153,2154],{},"Scalability:"," The framework is now in place to automatically generate a new blog post every time a new video is uploaded with minimal manual intervention.",[1621,2157],{},[36,2159,2161],{"id":2160},"conclusion-key-takeaways","💡 Conclusion & Key Takeaways",[19,2163,2164,2165,2167],{},"This project successfully leveraged the power of modern APIs and a static site generator to solve a significant content production bottleneck. The integration of the ",[15,2166,1614],{}," proved to be the most critical component, allowing for the conversion of unstructured data (transcript) into highly structured, actionable content (markdown blog post).",[19,2169,2170,2173],{},[15,2171,2172],{},"Project Takeaway:"," Thoughtful API integration, combined with robust error handling (like exponential backoff), is essential for building reliable, scalable automated content pipelines.",[889,2175,2176],{},"html pre.shiki code .setMH, html code.shiki .setMH{--shiki-default:#F97583;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .s2d5f, html code.shiki .s2d5f{--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sTKsR, html code.shiki .sTKsR{--shiki-default:#B392F0;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .s4SHQ, html code.shiki .s4SHQ{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .saAYD, html code.shiki .saAYD{--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}html pre.shiki code .s8-w5, html code.shiki .s8-w5{--shiki-default:#6A737D;--shiki-dark:#6A737D;--shiki-sepia:#88846F}html pre.shiki code .ssVjP, html code.shiki .ssVjP{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html pre.shiki code .siXTV, html code.shiki .siXTV{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .sPGPh, html code.shiki .sPGPh{--shiki-default:#79B8FF;--shiki-default-font-style:inherit;--shiki-dark:#79B8FF;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}",{"title":118,"searchDepth":175,"depth":175,"links":2178},[2179],{"id":1584,"depth":175,"text":2180,"children":2181},"🏗️ Project Case Study: YouTube-to-Blog Static Site Generator",[2182,2183,2184,2185,2186,2187],{"id":1591,"depth":181,"text":1592},{"id":1625,"depth":181,"text":1626},{"id":1656,"depth":181,"text":1657},{"id":1762,"depth":181,"text":1763},{"id":2113,"depth":181,"text":2114},{"id":2160,"depth":181,"text":2161},"2025-12-02T10:15:00.000+00:00","Automated content pipeline that converts YouTube video transcripts into fast, SEO-friendly Hugo blog posts using the Gemini AI API. This solution eliminated hours of manual labor by automatically fetching video data, generating structured Markdown, and deploying hundreds of articles instantly.","#","\u002Fimg\u002Fscreenshot-from-2025-12-02-10-16-37.png","https:\u002F\u002Fdarrenstechtutorials.com",{},"\u002Fprojects\u002Fdarrens-tech-tutorials",{"title":1579,"description":2189},"projects\u002Fdarrens-tech-tutorials",[1618,2198,2199],"Gemini AI","YouTube","aTVyNZJkGP4aOrFfH3_UBdCY9CFigdG7mQQ_H-Qs_l8",{"id":2202,"title":2203,"body":2204,"date":2774,"description":2775,"extension":904,"featured":725,"githubLink":2776,"image":2777,"liveLink":2778,"meta":2779,"navigation":587,"path":2780,"seo":2781,"stem":2782,"technologies":2783,"__hash__":2786},"projects\u002Fprojects\u002Fportfolio.md","My Portfolio",{"type":7,"value":2205,"toc":2764},[2206,2213,2217,2239,2243,2347,2349,2353,2356,2392,2396,2404,2406,2410,2421,2425,2428,2475,2479,2486,2502,2572,2578,2712,2715,2717,2721,2759,2761],[10,2207,2209,2210],{"id":2208},"project-case-study-high-performance-static-portfolio-with-serverless-cms-backend","🚀 Project Case Study: ",[15,2211,2212],{},"High-Performance Static Portfolio with Serverless CMS Backend",[36,2214,2216],{"id":2215},"project-summary","🎯 Project Summary",[19,2218,2219,2220,160,2223,2226,2227,2230,2231,2234,2235,2238],{},"A personal portfolio website developed to showcase expertise in the modern Jamstack ecosystem, focusing on ",[15,2221,2222],{},"performance",[15,2224,2225],{},"maintainability",", and ",[15,2228,2229],{},"self-hosted authentication",". The site uses Nuxt for a static build, the Nuxt Content module for simplified writing, and ",[15,2232,2233],{},"Decap CMS"," for content editing. A key technical challenge solved was implementing a fully self-hosted, serverless GitHub OAuth flow using ",[15,2236,2237],{},"Cloudflare Workers"," (via Cloudflare Pages Functions) to authenticate the CMS.",[36,2240,2242],{"id":2241},"️-technology-stack-key-modules","🛠️ Technology Stack & Key Modules",[1659,2244,2245,2257],{},[1662,2246,2247],{},[1665,2248,2249,2251,2254],{},[1668,2250,1671],{},[1668,2252,2253],{},"Role",[1668,2255,2256],{},"Value Proposition",[1679,2258,2259,2272,2284,2297,2309,2322,2335],{},[1665,2260,2261,2266,2269],{},[1684,2262,2263],{},[15,2264,2265],{},"Nuxt 3",[1684,2267,2268],{},"Frontend Framework \u002F SSG",[1684,2270,2271],{},"Provides a robust, full-stack foundation for a static-generated site.",[1665,2273,2274,2278,2281],{},[1684,2275,2276],{},[15,2277,1738],{},[1684,2279,2280],{},"Hosting \u002F CI\u002FCD",[1684,2282,2283],{},"Fast, globally distributed static hosting with built-in serverless functions.",[1665,2285,2286,2291,2294],{},[1684,2287,2288],{},[15,2289,2290],{},"Nuxt Content",[1684,2292,2293],{},"Content Management",[1684,2295,2296],{},"Markdown-based content module integrated directly into the Nuxt application.",[1665,2298,2299,2303,2306],{},[1684,2300,2301],{},[15,2302,2233],{},[1684,2304,2305],{},"Headless CMS",[1684,2307,2308],{},"Provides a visual, web-based editor for Markdown content (self-hosted).",[1665,2310,2311,2316,2319],{},[1684,2312,2313],{},[15,2314,2315],{},"Cloudflare Functions",[1684,2317,2318],{},"Backend \u002F Security",[1684,2320,2321],{},"Serverless environment used to implement custom GitHub OAuth authentication.",[1665,2323,2324,2329,2332],{},[1684,2325,2326],{},[15,2327,2328],{},"Nuxt Image",[1684,2330,2331],{},"Performance",[1684,2333,2334],{},"Ensures images are optimally sized and served to minimize load times.",[1665,2336,2337,2342,2344],{},[1684,2338,2339],{},[15,2340,2341],{},"Nuxt Scripts",[1684,2343,2331],{},[1684,2345,2346],{},"Efficiently manages the loading of third-party scripts (e.g., Google Tag Manager).",[1621,2348],{},[36,2350,2352],{"id":2351},"performance-optimization-focus","📈 Performance & Optimization Focus",[19,2354,2355],{},"A core objective was achieving exceptional performance scores, leveraging the advantages of static site generation (SSG) and strategic Nuxt modules.",[1628,2357,2358,2364,2373,2382],{},[44,2359,2360,2363],{},[15,2361,2362],{},"Static Site Generation (SSG):"," By deploying static files built during the CI\u002FCD pipeline, the site eliminates server-side processing overhead on page requests, ensuring extremely fast Time to First Byte (TTFB).",[44,2365,2366,2369,2370,2372],{},[15,2367,2368],{},"Image Optimization:"," The ",[15,2371,2328],{}," module was implemented to automatically generate different image formats and sizes, serving the smallest possible image based on the user's device and viewport.",[44,2374,2375,2378,2379,2381],{},[15,2376,2377],{},"Third-Party Script Loading:"," ",[15,2380,2341],{}," was used to ensure scripts like Google Tag Manager are loaded non-render-blocking, preserving a high performance score.",[44,2383,2384,2387,2388,2391],{},[15,2385,2386],{},"Zero-Cost Email Solution:"," Cloudflare's free email forwarding service was used, combined with a manual ",[15,2389,2390],{},"SPF record update"," in the DNS settings, to allow sending email via a verified address without the need for a separate, costly email server.",[1765,2393,2395],{"id":2394},"performance-metrics-google-pagespeed-insights","Performance Metrics (Google PageSpeed Insights)",[19,2397,2398],{},[2399,2400],"img",{"alt":2401,"src":2402,"title":2403},"Screenshot showing a score of 100 in each Google PageSpeed Insights Category","\u002Fimg\u002Finsights.png","Google PageSpeed Insights Results",[1621,2405],{},[36,2407,2409],{"id":2408},"technical-deep-dive-self-hosted-serverless-authentication","💻 Technical Deep Dive: Self-Hosted Serverless Authentication",[19,2411,2412,2413,2416,2417,2420],{},"The biggest technical challenge was implementing a fully self-hosted solution for the Decap CMS authentication, avoiding reliance on third-party backends like Netlify Identity. This required learning and implementing ",[15,2414,2415],{},"GitHub OAuth 2.0"," using ",[15,2418,2419],{},"Cloudflare Pages Functions"," (built on Workers).",[1765,2422,2424],{"id":2423},"challenge-implementing-oauth-using-cloudflare-workers","Challenge: Implementing OAuth using Cloudflare Workers",[19,2426,2427],{},"The solution required creating an API endpoint that lives within the static site's deployment environment.",[41,2429,2430,2448,2461],{},[44,2431,2432,2435,2436,2439,2440,2443,2444,2447],{},[15,2433,2434],{},"Cloudflare Pages Functions:"," Backend logic was implemented by placing TypeScript files (",[116,2437,2438],{},".ts",") within a ",[116,2441,2442],{},"functions"," directory, automatically creating serverless API routes (e.g., ",[116,2445,2446],{},"\u002Fdecap\u002Fauth",").",[44,2449,2450,2453,2454,2457,2458,1964],{},[15,2451,2452],{},"GitHub OAuth Application:"," An OAuth App was registered in GitHub to obtain the necessary ",[116,2455,2456],{},"Client ID"," and ",[116,2459,2460],{},"Secret Key",[44,2462,2463,2369,2466,2457,2468,2470,2471,2474],{},[15,2464,2465],{},"Secure Secret Management:",[116,2467,2456],{},[116,2469,2460],{}," were securely stored as environment variables (",[15,2472,2473],{},"Secrets",") within the Cloudflare Pages project settings, ensuring they are not exposed in the client-side code.",[1765,2476,2478],{"id":2477},"key-implementation-steps-auth-request","Key Implementation Steps (Auth Request)",[19,2480,2481,2482,2485],{},"To simplify the OAuth process, an ",[116,2483,2484],{},"OAuthClient"," class was created to handle configuration and token exchange. The first function handled the initial request:",[1628,2487,2488,2493,2499],{},[44,2489,2490,2491,2447],{},"The Cloudflare function is triggered at the custom endpoint (e.g., ",[116,2492,2446],{},[44,2494,2495,2496,2498],{},"It generates the GitHub authorization URL using the stored ",[116,2497,2456],{}," and scope requirements.",[44,2500,2501],{},"It redirects the user's browser to GitHub to authorize the application.",[109,2503,2507],{"className":2504,"code":2505,"language":2506,"meta":118,"style":118},"language-typescript shiki shiki-themes github-dark github-dark monokai","\u002F\u002F \u002Ffunctions\u002Fdecap\u002Fauth.ts\nexport function onRequest(context) {\n    const url = new URL(context.request.url);\n    \u002F\u002F handleAuth utilizes the environment variables (context.env) \n    \u002F\u002F to build and return the redirect to GitHub.\n    return handleAuth(url, context.env); \n}\n","typescript",[116,2508,2509,2514,2530,2547,2552,2557,2568],{"__ignoreMap":118},[130,2510,2511],{"class":132,"line":133},[130,2512,2513],{"class":1836},"\u002F\u002F \u002Ffunctions\u002Fdecap\u002Fauth.ts\n",[130,2515,2516,2518,2520,2523,2525,2528],{"class":132,"line":175},[130,2517,314],{"class":144},[130,2519,320],{"class":136},[130,2521,2522],{"class":323}," onRequest",[130,2524,327],{"class":152},[130,2526,2527],{"class":330},"context",[130,2529,348],{"class":152},[130,2531,2532,2535,2537,2539,2541,2544],{"class":132,"line":181},[130,2533,2534],{"class":136},"    const",[130,2536,375],{"class":140},[130,2538,359],{"class":144},[130,2540,2079],{"class":144},[130,2542,2543],{"class":323}," URL",[130,2545,2546],{"class":152},"(context.request.url);\n",[130,2548,2549],{"class":132,"line":187},[130,2550,2551],{"class":1836},"    \u002F\u002F handleAuth utilizes the environment variables (context.env) \n",[130,2553,2554],{"class":132,"line":193},[130,2555,2556],{"class":1836},"    \u002F\u002F to build and return the redirect to GitHub.\n",[130,2558,2559,2562,2565],{"class":132,"line":199},[130,2560,2561],{"class":144},"    return",[130,2563,2564],{"class":323}," handleAuth",[130,2566,2567],{"class":152},"(url, context.env); \n",[130,2569,2570],{"class":132,"line":205},[130,2571,563],{"class":152},[19,2573,2574,2575,2577],{},"The ",[116,2576,2484],{}," class was central to abstracting the logic for generating the authorization URL and, crucially, handling the subsequent token exchange in the callback function.",[109,2579,2581],{"className":2504,"code":2580,"language":2506,"meta":118,"style":118},"\u002F\u002F OAuthClient authorization method snippet\nauthorizeURL(options: { redirect_uri: string; scope: string; state: string }) {\n    const { clientConfig } = this;\n    const { tokenHost, authorizePath } = clientConfig.target;\n    \u002F\u002F Constructs the final URL used to redirect the user to GitHub\n    return `${tokenHost}${authorizePath}?response_type=code&client_id=${clientConfig.id}&redirect_uri=${redirect_uri}&scope=${scope}&state=${state}`;\n}\n",[116,2582,2583,2588,2596,2615,2636,2641,2708],{"__ignoreMap":118},[130,2584,2585],{"class":132,"line":133},[130,2586,2587],{"class":1836},"\u002F\u002F OAuthClient authorization method snippet\n",[130,2589,2590,2593],{"class":132,"line":175},[130,2591,2592],{"class":323},"authorizeURL",[130,2594,2595],{"class":152},"(options: { redirect_uri: string; scope: string; state: string }) {\n",[130,2597,2598,2600,2602,2605,2607,2609,2613],{"class":132,"line":181},[130,2599,2534],{"class":136},[130,2601,395],{"class":152},[130,2603,2604],{"class":140},"clientConfig",[130,2606,406],{"class":152},[130,2608,169],{"class":144},[130,2610,2612],{"class":2611},"skksa"," this",[130,2614,582],{"class":152},[130,2616,2617,2619,2621,2624,2626,2629,2631,2633],{"class":132,"line":187},[130,2618,2534],{"class":136},[130,2620,395],{"class":152},[130,2622,2623],{"class":140},"tokenHost",[130,2625,160],{"class":152},[130,2627,2628],{"class":140},"authorizePath",[130,2630,406],{"class":152},[130,2632,169],{"class":144},[130,2634,2635],{"class":152}," clientConfig.target;\n",[130,2637,2638],{"class":132,"line":193},[130,2639,2640],{"class":1836},"    \u002F\u002F Constructs the final URL used to redirect the user to GitHub\n",[130,2642,2643,2645,2648,2650,2652,2655,2657,2659,2662,2664,2666,2669,2672,2674,2677,2679,2682,2684,2687,2689,2692,2694,2697,2699,2702,2704,2706],{"class":132,"line":199},[130,2644,2561],{"class":144},[130,2646,2647],{"class":458}," `",[130,2649,539],{"class":538},[130,2651,2623],{"class":152},[130,2653,2654],{"class":538},"}${",[130,2656,2628],{"class":152},[130,2658,544],{"class":538},[130,2660,2661],{"class":458},"?response_type=code&client_id=",[130,2663,539],{"class":538},[130,2665,2604],{"class":152},[130,2667,1964],{"class":2668},"sK5FR",[130,2670,2671],{"class":152},"id",[130,2673,544],{"class":538},[130,2675,2676],{"class":458},"&redirect_uri=",[130,2678,539],{"class":538},[130,2680,2681],{"class":152},"redirect_uri",[130,2683,544],{"class":538},[130,2685,2686],{"class":458},"&scope=",[130,2688,539],{"class":538},[130,2690,2691],{"class":152},"scope",[130,2693,544],{"class":538},[130,2695,2696],{"class":458},"&state=",[130,2698,539],{"class":538},[130,2700,2701],{"class":152},"state",[130,2703,544],{"class":538},[130,2705,547],{"class":458},[130,2707,582],{"class":152},[130,2709,2710],{"class":132,"line":205},[130,2711,563],{"class":152},[19,2713,2714],{},"This successful implementation allows the Decap CMS to fully authenticate against GitHub, enabling secure content editing without any additional, external server or service cost.",[1621,2716],{},[36,2718,2720],{"id":2719},"skills-demonstrated","⭐ Skills Demonstrated",[1628,2722,2723,2729,2735,2741,2747,2753],{},[44,2724,2725,2728],{},[15,2726,2727],{},"Front-End Development:"," Nuxt 3, Component Architecture, Content Management (Nuxt Content).",[44,2730,2731,2734],{},[15,2732,2733],{},"Performance Optimization:"," Static Site Generation (SSG), Image Optimization (Nuxt Image), Script Management (Nuxt Scripts), High PageSpeed Scores.",[44,2736,2737,2740],{},[15,2738,2739],{},"Serverless Architecture:"," Cloudflare Pages Functions (Workers), Serverless API Endpoint Creation.",[44,2742,2743,2746],{},[15,2744,2745],{},"Security & Authentication:"," Implementation of GitHub OAuth 2.0 Flow, Secure Secret Management (Cloudflare Variables and Secrets).",[44,2748,2749,2752],{},[15,2750,2751],{},"DevOps\u002FCI\u002FCD:"," Automated deployment via GitHub and Cloudflare Pages.",[44,2754,2755,2758],{},[15,2756,2757],{},"Networking:"," DNS Configuration (SPF records), Email Forwarding.",[1621,2760],{},[889,2762,2763],{},"html pre.shiki code .s8-w5, html code.shiki .s8-w5{--shiki-default:#6A737D;--shiki-dark:#6A737D;--shiki-sepia:#88846F}html pre.shiki code .setMH, html code.shiki .setMH{--shiki-default:#F97583;--shiki-dark:#F97583;--shiki-sepia:#F92672}html pre.shiki code .s2d5f, html code.shiki .s2d5f{--shiki-default:#F97583;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic}html pre.shiki code .sTKsR, html code.shiki .sTKsR{--shiki-default:#B392F0;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E}html pre.shiki code .s4SHQ, html code.shiki .s4SHQ{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2}html pre.shiki code .saAYD, html code.shiki .saAYD{--shiki-default:#FFAB70;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit;--shiki-sepia:#FD971F;--shiki-sepia-font-style:italic}html pre.shiki code .ssVjP, html code.shiki .ssVjP{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-sepia:#F8F8F2}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html pre.shiki code .skksa, html code.shiki .skksa{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-sepia:#FD971F}html pre.shiki code .sTjUT, html code.shiki .sTjUT{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74}html pre.shiki code .skrme, html code.shiki .skrme{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-sepia:#F92672}html pre.shiki code .sK5FR, html code.shiki .sK5FR{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-sepia:#F8F8F2}",{"title":118,"searchDepth":175,"depth":175,"links":2765},[2766],{"id":2208,"depth":175,"text":2767,"children":2768},"🚀 Project Case Study: High-Performance Static Portfolio with Serverless CMS Backend",[2769,2770,2771,2772,2773],{"id":2215,"depth":181,"text":2216},{"id":2241,"depth":181,"text":2242},{"id":2351,"depth":181,"text":2352},{"id":2408,"depth":181,"text":2409},{"id":2719,"depth":181,"text":2720},"2025-10-22","A personal portfolio website developed to showcase expertise in the modern Jamstack ecosystem, focusing on performance, maintainability, and self-hosted authentication. The site uses Nuxt for a static build, the Nuxt Content module for simplified writing, and Decap CMS for content editing. A key technical challenge solved was implementing a fully self-hosted, serverless GitHub OAuth flow using Cloudflare Workers (via Cloudflare Pages Functions) to authenticate the CMS.","https:\u002F\u002Fgithub.com\u002Fhughie1991\u002Fportfolio-v2","\u002Fimg\u002Fportfolio-screenshot.png","https:\u002F\u002Fhughthomasoneill.com",{},"\u002Fprojects\u002Fportfolio",{"title":2203,"description":2775},"projects\u002Fportfolio",[2784,2290,2233,2785,1738],"Nuxt 4","Tailwind CSS","gLliVC2sDAOS7DqZAxbJbaAA5qG8aln_m9lDjscEL6g",1775823021351]