[{"data":1,"prerenderedAt":2522},["ShallowReactive",2],{"\u002Fprojects\u002Fblast-off-gmtk-game-jam-2026":3,"navigation":609},{"id":4,"title":5,"body":6,"date":592,"description":593,"embedCode":594,"extension":595,"featured":596,"githubLink":597,"image":598,"liveLink":599,"meta":600,"navigation":475,"path":601,"published":596,"seo":602,"stem":603,"technologies":604,"__hash__":608},"projects\u002Fprojects\u002Fblast-off-gmtk-game-jam-2026.md","Blast Off GMTK Game Jam 2026",{"type":7,"value":8,"toc":580},"minimark",[9,13,21,26,29,37,40,45,48,67,70,73,76,90,93,317,320,323,337,340,432,512,516,523,526,529,532,536,539,542,546,549,553,556,560,567,570,573,576],[10,11,12],"p",{},"For the past few years I've seen a lot youtube videos talking about the concepts of Game Jams and building tiny games under a strict time constraint. As someone whos played video games since I was a child I've always wanted to try to build one myself. For once I saw ahead of time that the 2026 GMTK game jam was coming up soon and I decided to join in for fun and see what I could get done in such a short timeframe. This was the first game I've ever made and decided to use the Pico 8 fantasy console for development as it comes with all the tools needed for coding, graphics, and music within a single application. I had played around a little with Pico 8 for an hour or two but never got very far. Just enough to know how to move sprites around when input is detected so this was a great chance to learn more.",[10,14,15,16,20],{},"The theme of this years Game Jam was ",[17,18,19],"em",{},"\"Count Down\""," . I gave myself the evening to try come up with any concepts I could think of that could realistically be built within my current knowledge. Some of these included racing style games with a race to a finish line before time runs out, a Mr. Driller style rhythm game where you dig downwards to a specific beat, and what ended up being the final concept, a sokoban style game where you need to move a group of rockets to their launchpads in time for their launch.",[22,23,25],"h2",{"id":24},"game-logic","Game Logic",[10,27,28],{},"This was the area I was most comfortable with coming from a web development background but it still had plenty of challenges to overcome. The first major hurdle to overcome was simply using the Pico 8 editor. You are severly limited in screen real estate when typing as all code is written within the Pico 8 application itself. This means using long variable and function names quickly causes you to need to scroll horizontally. To get around this you need to switch to short variable name, even single characters, and use comments when they are initialized to keep note of everything. There are some hacks to use external editors but I felt part of the fun and challenge was to stick to the built in editor.",[10,30,31],{},[32,33],"img",{"alt":34,"src":35,"title":36},"Screenshot showing the Pico 8 code editor","\u002Fimg\u002Fpico-editor.png","Pico 8 Code Editor",[10,38,39],{},"When it comes to actually coding the game loop Pico 8 offers built in functions called _draw() and _update() which handle the drawing of sprites and the games underlying logic respectively. Each function is called once per frame although if the system is under heavy load it is possible for the draw() to skip a frame of drawing the graphics to avoid any slowdown. There is also an init() function we can take advantage of to call our start level function the first time the game loads.",[41,42,44],"h3",{"id":43},"the-update-loop","The Update Loop",[10,46,47],{},"I wont go crazy into detail here as this page wouild get very long but we initially set up a set of variables to handle tracking the current status of the player and the state of the game.",[49,50,51,55,58,61,64],"ul",{},[52,53,54],"li",{},"Player for tracking the players current position",[52,56,57],{},"Levels to store all information about the level. This includes the map coordinates and the positions of the rockets and launchpads.",[52,59,60],{},"Current_Level to track what level we are on and allow us the level from the levels array",[52,62,63],{},"Rockets which is set to an empty array initially but will be set when launching each level",[52,65,66],{},"Launchpads which is handled the same as rockets",[10,68,69],{},"With these defined we then start each level by instantiating the rockets and launchpads to the required positions and setting the timers as needed. Since the update function is called every frame, which is 30 frames per second by default, the vast majority of our game logic will only actually need to be run when a player triggers an input in the game.",[10,71,72],{},"There is a built in btnp() function we can use to determine each of the possible inputs a player can make in the game. Our player can make five different types of inputs. They can move left, right, up, down, and press the X key\u002Fbutton to retry the current level they are on. Handling the retry is fairly simple as we can just call our start level function again without updating the current level variable.",[10,74,75],{},"For the movement options there was a few things that needed to happen",[49,77,78,81,84,87],{},[52,79,80],{},"If a player presses right we would need to update their position by increasing the players X coordinate by 8 (player.x = player.x+8)",[52,82,83],{},"But if the players new position cannot be moved to, the next tile is a wall or its a rocket that cannot be moved, then we need to not update their position",[52,85,86],{},"If the player tries to move to a tile that contains a rocket we then need to push the rocket in the same direction",[52,88,89],{},"But, once again, if the rocket cannot move to the next tile we need to cancel the movement of both the rocket and the player",[10,91,92],{},"My original plan was to allow the player to only push one rocket at a time. If they tried to push two rockets that were next to each other their movement would be blocked even if there was space on the opposite side. While developing I realised that it would be much easier to have a single move function that could be called recusively that any object, whether the player or a rocket, could be passed to. This move function would then return either true or false depending on if the object could move to the next tile or not.",[94,95,100],"pre",{"className":96,"code":97,"language":98,"meta":99,"style":99},"language-lua shiki shiki-themes github-dark github-dark monokai","\u002F\u002Fo=object\nfunction move(o)\n local newx = o.x\n local newy = o.y\n \n    if(btnp(0)) then\n     newx = o.x - 8\n     p.dir = true\n    elseif(btnp(1)) then\n        newx = o.x + 8\n        p.dir = false\n    elseif(btnp(2)) then\n     newy = o.y - 8\n    elseif(btnp(3)) then\n        newy = o.y + 8\n    end\n    \n    if(will_push(newx,newy)>0) then\n        local nr=will_push(newx,newy)\n        local moved=move(rocks[nr])\n        if(not moved) then\n            return false\n        end\n    end\n    \n    if(can_move(newx, newy)) then\n        o.x=newx\n        o.y=newy\n        \u002F\u002Fo.drawx=newx\n        \u002F\u002Fo.drawy=newy\n        \n        return true\n    end\n    \n    return false\nend\n","lua","",[101,102,103,111,117,123,129,135,141,147,153,159,165,171,177,183,189,195,201,207,213,219,225,231,237,243,248,253,259,265,271,277,283,289,295,300,305,311],"code",{"__ignoreMap":99},[104,105,108],"span",{"class":106,"line":107},"line",1,[104,109,110],{},"\u002F\u002Fo=object\n",[104,112,114],{"class":106,"line":113},2,[104,115,116],{},"function move(o)\n",[104,118,120],{"class":106,"line":119},3,[104,121,122],{}," local newx = o.x\n",[104,124,126],{"class":106,"line":125},4,[104,127,128],{}," local newy = o.y\n",[104,130,132],{"class":106,"line":131},5,[104,133,134],{}," \n",[104,136,138],{"class":106,"line":137},6,[104,139,140],{},"    if(btnp(0)) then\n",[104,142,144],{"class":106,"line":143},7,[104,145,146],{},"     newx = o.x - 8\n",[104,148,150],{"class":106,"line":149},8,[104,151,152],{},"     p.dir = true\n",[104,154,156],{"class":106,"line":155},9,[104,157,158],{},"    elseif(btnp(1)) then\n",[104,160,162],{"class":106,"line":161},10,[104,163,164],{},"        newx = o.x + 8\n",[104,166,168],{"class":106,"line":167},11,[104,169,170],{},"        p.dir = false\n",[104,172,174],{"class":106,"line":173},12,[104,175,176],{},"    elseif(btnp(2)) then\n",[104,178,180],{"class":106,"line":179},13,[104,181,182],{},"     newy = o.y - 8\n",[104,184,186],{"class":106,"line":185},14,[104,187,188],{},"    elseif(btnp(3)) then\n",[104,190,192],{"class":106,"line":191},15,[104,193,194],{},"        newy = o.y + 8\n",[104,196,198],{"class":106,"line":197},16,[104,199,200],{},"    end\n",[104,202,204],{"class":106,"line":203},17,[104,205,206],{},"    \n",[104,208,210],{"class":106,"line":209},18,[104,211,212],{},"    if(will_push(newx,newy)>0) then\n",[104,214,216],{"class":106,"line":215},19,[104,217,218],{},"        local nr=will_push(newx,newy)\n",[104,220,222],{"class":106,"line":221},20,[104,223,224],{},"        local moved=move(rocks[nr])\n",[104,226,228],{"class":106,"line":227},21,[104,229,230],{},"        if(not moved) then\n",[104,232,234],{"class":106,"line":233},22,[104,235,236],{},"            return false\n",[104,238,240],{"class":106,"line":239},23,[104,241,242],{},"        end\n",[104,244,246],{"class":106,"line":245},24,[104,247,200],{},[104,249,251],{"class":106,"line":250},25,[104,252,206],{},[104,254,256],{"class":106,"line":255},26,[104,257,258],{},"    if(can_move(newx, newy)) then\n",[104,260,262],{"class":106,"line":261},27,[104,263,264],{},"        o.x=newx\n",[104,266,268],{"class":106,"line":267},28,[104,269,270],{},"        o.y=newy\n",[104,272,274],{"class":106,"line":273},29,[104,275,276],{},"        \u002F\u002Fo.drawx=newx\n",[104,278,280],{"class":106,"line":279},30,[104,281,282],{},"        \u002F\u002Fo.drawy=newy\n",[104,284,286],{"class":106,"line":285},31,[104,287,288],{},"        \n",[104,290,292],{"class":106,"line":291},32,[104,293,294],{},"        return true\n",[104,296,298],{"class":106,"line":297},33,[104,299,200],{},[104,301,303],{"class":106,"line":302},34,[104,304,206],{},[104,306,308],{"class":106,"line":307},35,[104,309,310],{},"    return false\n",[104,312,314],{"class":106,"line":313},36,[104,315,316],{},"end\n",[10,318,319],{},"With the push mechanic defined mostly through this function, it became much quicker to build start building levels and it was funny how much of the level design was based around the mechanic of pushing multiple rockets which was only done as a way to reduce scope and stay on the required timeline.",[10,321,322],{},"Once movement was checked and handled for each frame the code would next check what needed to be updated within the level. After a successful input each of the following needed to be checked and handled.",[49,324,325,328,331,334],{},[52,326,327],{},"Is a rocket now positioned on a launchpad and should play the launch animation",[52,329,330],{},"Decrease the current timer on any rockets that have not reached a launchpad",[52,332,333],{},"Are there any rocket's timers that have reached zero and should explode and trigger the end of level state",[52,335,336],{},"Are all rockets launched and we trigger the win state and move to the next level",[10,338,339],{},"You can check out some of the code below to see how each of these was implemented",[94,341,343],{"className":96,"code":342,"language":98,"meta":99,"style":99},"function update_rocks()\n    for r in all(rocks) do\n        if(r.state ~= 1) then\n            if(is_rock_on_lp(r)) then\n                r.state=1\n                \u002F\u002Fr.x=-16\n                r.y=-16\n                sfx(02)\n            else\n                r.timer = r.timer - 1\n                if(r.timer\u003C0)then\n                    r.state=2\n                    sfx(03)\n                end\n            end\n        end\n    end\nend\n",[101,344,345,350,355,360,365,370,375,380,385,390,395,400,405,410,415,420,424,428],{"__ignoreMap":99},[104,346,347],{"class":106,"line":107},[104,348,349],{},"function update_rocks()\n",[104,351,352],{"class":106,"line":113},[104,353,354],{},"    for r in all(rocks) do\n",[104,356,357],{"class":106,"line":119},[104,358,359],{},"        if(r.state ~= 1) then\n",[104,361,362],{"class":106,"line":125},[104,363,364],{},"            if(is_rock_on_lp(r)) then\n",[104,366,367],{"class":106,"line":131},[104,368,369],{},"                r.state=1\n",[104,371,372],{"class":106,"line":137},[104,373,374],{},"                \u002F\u002Fr.x=-16\n",[104,376,377],{"class":106,"line":143},[104,378,379],{},"                r.y=-16\n",[104,381,382],{"class":106,"line":149},[104,383,384],{},"                sfx(02)\n",[104,386,387],{"class":106,"line":155},[104,388,389],{},"            else\n",[104,391,392],{"class":106,"line":161},[104,393,394],{},"                r.timer = r.timer - 1\n",[104,396,397],{"class":106,"line":167},[104,398,399],{},"                if(r.timer\u003C0)then\n",[104,401,402],{"class":106,"line":173},[104,403,404],{},"                    r.state=2\n",[104,406,407],{"class":106,"line":179},[104,408,409],{},"                    sfx(03)\n",[104,411,412],{"class":106,"line":185},[104,413,414],{},"                end\n",[104,416,417],{"class":106,"line":191},[104,418,419],{},"            end\n",[104,421,422],{"class":106,"line":197},[104,423,242],{},[104,425,426],{"class":106,"line":203},[104,427,200],{},[104,429,430],{"class":106,"line":209},[104,431,316],{},[94,433,435],{"className":96,"code":434,"language":98,"meta":99,"style":99},"function is_lev_fin()\n    for r in all(rocks) do\n        if(r.state ~= 1) then\n            return false\n        end\n    end\n    return true\nend\n\nfunction is_game_over()\n    for r in all(rocks) do\n        if(r.state == 2) then\n            return true\n        end\n    end\n    return false\nend\n",[101,436,437,442,446,450,454,458,462,467,471,477,482,486,491,496,500,504,508],{"__ignoreMap":99},[104,438,439],{"class":106,"line":107},[104,440,441],{},"function is_lev_fin()\n",[104,443,444],{"class":106,"line":113},[104,445,354],{},[104,447,448],{"class":106,"line":119},[104,449,359],{},[104,451,452],{"class":106,"line":125},[104,453,236],{},[104,455,456],{"class":106,"line":131},[104,457,242],{},[104,459,460],{"class":106,"line":137},[104,461,200],{},[104,463,464],{"class":106,"line":143},[104,465,466],{},"    return true\n",[104,468,469],{"class":106,"line":149},[104,470,316],{},[104,472,473],{"class":106,"line":155},[104,474,476],{"emptyLinePlaceholder":475},true,"\n",[104,478,479],{"class":106,"line":161},[104,480,481],{},"function is_game_over()\n",[104,483,484],{"class":106,"line":167},[104,485,354],{},[104,487,488],{"class":106,"line":173},[104,489,490],{},"        if(r.state == 2) then\n",[104,492,493],{"class":106,"line":179},[104,494,495],{},"            return true\n",[104,497,498],{"class":106,"line":185},[104,499,242],{},[104,501,502],{"class":106,"line":191},[104,503,200],{},[104,505,506],{"class":106,"line":197},[104,507,310],{},[104,509,510],{"class":106,"line":203},[104,511,316],{},[22,513,515],{"id":514},"art-and-sprites","Art and Sprites",[10,517,518],{},[32,519],{"alt":520,"src":521,"title":522},"Screenshot showing the Pico 8 sprite editor","\u002Fimg\u002Fpico-sprite.png","Pico 8 Sprite Editor",[10,524,525],{},"There may be a better way of handling both but making changes was difficult to picture as sprites are not always positioned next to each other on the sprite sheet if you don't plan well enough ahead.",[10,527,528],{},"For example, I started throwing together the splash screen by building an image of a rocket out of multiple sprites. The extra bits like the red legs to support it and the top where made of multiple different sprites. Part of the way through I realized it looked too wide and wanted to make it thinner. Shrinking it down meant I had to edit every sprite used to adjust them to the new width.",[10,530,531],{},"This happened again when I added a little light and shadow to try make it look less static. I had to create new sprites for each sprite that was affected and adjust current sprites just to make it look somewhat reasonable.",[41,533,535],{"id":534},"the-draw-loop","The Draw Loop",[10,537,538],{},"For animations I mostly stuck to 2 frame animations to keep things simple. I once watched a video about Super Mario 64 that showed they kept a constant random number generated to handle enemy animations. To emulate this concept I created a variable to track the current frame number up to 30. If we want to change an animation every second we can then just check if the number is 0. Two frame animations can easily be toggled by checking if the number is greater than 15 which will have them animate twice a second.",[10,540,541],{},"For more complex animations I needed to add a step variable to the object to keep track of what phase of the animation it is on. The only animation I had like this was the players walk cycle when moving between tiles. I added a step variable to the player object and incremented it every time the frame number was evenly divided by 10 (frame number % 10 == 0).",[22,543,545],{"id":544},"music-and-sound-effects","Music and Sound Effects",[10,547,548],{},"I expected music would be a big challenge as I have zero experience composing music of any type. But I have been trying on and off over the last few years to learn piano by myself at home which I hoped would give me an advantage and in some ways it did. To create the music I went over to the keyboard in my home office and started to play around with some basic melodies.",[41,550,552],{"id":551},"composition","Composition",[10,554,555],{},"To keep the music as simple as possible, every note used fell withing the C major scale for the main music loop (although I threw in a short F major tune for the title screen once I got a little more comfortable and had time at the end of the jam). Rolling up the basic triads of the scale produced a short looping melody that worked well enough for the game and helped the experience feel less empty. For the bass line I just kept it simple using CFAG notes at the start of each bar (Which are the chords for The Beatles Let it Be which is one of the few songs I can play on the piano).",[41,557,559],{"id":558},"using-the-pico-8-music-editor","Using the Pico 8 music editor",[10,561,562],{},[32,563],{"alt":564,"src":565,"title":566},"Screenshot showing the Pico 8 sound editor","\u002Fimg\u002Fpico-sound-1.png","Pico 8 Sound Editor",[10,568,569],{},"To convert these notes into music for the game I needed to get familiar to the sound effect and music tools within Pico 8. To build music we can choose up to 4 channels of audio to play at once. Each audio track is defined within the sound effects editor. Since there is both a bassline and a melody I needed to create two separate effects tracks and then play them at the same time as one final music track. This makes it difficult to hear the full version at once and if you want to tweak something small in the music you have to jump between editors.",[10,571,572],{},"The sound effects editor offers two different views. One allows you to create sound effects and alter the pitch by clicking with the mouse and the other allows you to use your keyboard as makeshift piano to input the notes. I found it much easier to use the keyboard to input the notes and use the surrounding buttons and instruments selections tools to change the octave and feel of the music.",[10,574,575],{},"I do feel that the sound editors are by far the most confusing for newcomers and are not very natural with all the switching between screens but if you get the hang of it there is a huge amount that can be done with it.",[577,578,579],"style",{},"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);}",{"title":99,"searchDepth":113,"depth":113,"links":581},[582,585,588],{"id":24,"depth":113,"text":25,"children":583},[584],{"id":43,"depth":119,"text":44},{"id":514,"depth":113,"text":515,"children":586},[587],{"id":534,"depth":119,"text":535},{"id":544,"depth":113,"text":545,"children":589},[590,591],{"id":551,"depth":119,"text":552},{"id":558,"depth":119,"text":559},"2026-08-12T10:15:00.000+00:00","My first Pico 8 video game built for the Game Maker's Toolkit Game Jam 2026.","\u003Ciframe frameborder=\"0\" src=\"https:\u002F\u002Fitch.io\u002Fembed-upload\u002F18528640?color=333333\" allowfullscreen=\"\" width=\"780\" height=\"700\">\u003Ca href=\"https:\u002F\u002Fhughie1991.itch.io\u002Fblast-off\">Play Blast off! on itch.io\u003C\u002Fa>\u003C\u002Fiframe>","md","true",null,"\u002Fimg\u002Fblast-off-2.png","https:\u002F\u002Fhughie1991.itch.io\u002Fblast-off",{},"\u002Fprojects\u002Fblast-off-gmtk-game-jam-2026",{"title":5,"description":593},"projects\u002Fblast-off-gmtk-game-jam-2026",[605,606,607],"Pico 8","Lua","GMTK Game Jam 2026","vuDJibR5Aecw3DSl-SaLVEvghafZtEruMN3bod1bfF0",[610,1030,1900],{"id":4,"title":5,"body":611,"date":592,"description":593,"embedCode":594,"extension":595,"featured":596,"githubLink":597,"image":598,"liveLink":599,"meta":1027,"navigation":475,"path":601,"published":596,"seo":1028,"stem":603,"technologies":1029,"__hash__":608},{"type":7,"value":612,"toc":1015},[613,615,619,621,623,627,629,631,633,645,647,649,651,661,663,811,813,815,825,827,903,975,977,981,983,985,987,989,991,993,995,997,999,1001,1003,1007,1009,1011,1013],[10,614,12],{},[10,616,15,617,20],{},[17,618,19],{},[22,620,25],{"id":24},[10,622,28],{},[10,624,625],{},[32,626],{"alt":34,"src":35,"title":36},[10,628,39],{},[41,630,44],{"id":43},[10,632,47],{},[49,634,635,637,639,641,643],{},[52,636,54],{},[52,638,57],{},[52,640,60],{},[52,642,63],{},[52,644,66],{},[10,646,69],{},[10,648,72],{},[10,650,75],{},[49,652,653,655,657,659],{},[52,654,80],{},[52,656,83],{},[52,658,86],{},[52,660,89],{},[10,662,92],{},[94,664,665],{"className":96,"code":97,"language":98,"meta":99,"style":99},[101,666,667,671,675,679,683,687,691,695,699,703,707,711,715,719,723,727,731,735,739,743,747,751,755,759,763,767,771,775,779,783,787,791,795,799,803,807],{"__ignoreMap":99},[104,668,669],{"class":106,"line":107},[104,670,110],{},[104,672,673],{"class":106,"line":113},[104,674,116],{},[104,676,677],{"class":106,"line":119},[104,678,122],{},[104,680,681],{"class":106,"line":125},[104,682,128],{},[104,684,685],{"class":106,"line":131},[104,686,134],{},[104,688,689],{"class":106,"line":137},[104,690,140],{},[104,692,693],{"class":106,"line":143},[104,694,146],{},[104,696,697],{"class":106,"line":149},[104,698,152],{},[104,700,701],{"class":106,"line":155},[104,702,158],{},[104,704,705],{"class":106,"line":161},[104,706,164],{},[104,708,709],{"class":106,"line":167},[104,710,170],{},[104,712,713],{"class":106,"line":173},[104,714,176],{},[104,716,717],{"class":106,"line":179},[104,718,182],{},[104,720,721],{"class":106,"line":185},[104,722,188],{},[104,724,725],{"class":106,"line":191},[104,726,194],{},[104,728,729],{"class":106,"line":197},[104,730,200],{},[104,732,733],{"class":106,"line":203},[104,734,206],{},[104,736,737],{"class":106,"line":209},[104,738,212],{},[104,740,741],{"class":106,"line":215},[104,742,218],{},[104,744,745],{"class":106,"line":221},[104,746,224],{},[104,748,749],{"class":106,"line":227},[104,750,230],{},[104,752,753],{"class":106,"line":233},[104,754,236],{},[104,756,757],{"class":106,"line":239},[104,758,242],{},[104,760,761],{"class":106,"line":245},[104,762,200],{},[104,764,765],{"class":106,"line":250},[104,766,206],{},[104,768,769],{"class":106,"line":255},[104,770,258],{},[104,772,773],{"class":106,"line":261},[104,774,264],{},[104,776,777],{"class":106,"line":267},[104,778,270],{},[104,780,781],{"class":106,"line":273},[104,782,276],{},[104,784,785],{"class":106,"line":279},[104,786,282],{},[104,788,789],{"class":106,"line":285},[104,790,288],{},[104,792,793],{"class":106,"line":291},[104,794,294],{},[104,796,797],{"class":106,"line":297},[104,798,200],{},[104,800,801],{"class":106,"line":302},[104,802,206],{},[104,804,805],{"class":106,"line":307},[104,806,310],{},[104,808,809],{"class":106,"line":313},[104,810,316],{},[10,812,319],{},[10,814,322],{},[49,816,817,819,821,823],{},[52,818,327],{},[52,820,330],{},[52,822,333],{},[52,824,336],{},[10,826,339],{},[94,828,829],{"className":96,"code":342,"language":98,"meta":99,"style":99},[101,830,831,835,839,843,847,851,855,859,863,867,871,875,879,883,887,891,895,899],{"__ignoreMap":99},[104,832,833],{"class":106,"line":107},[104,834,349],{},[104,836,837],{"class":106,"line":113},[104,838,354],{},[104,840,841],{"class":106,"line":119},[104,842,359],{},[104,844,845],{"class":106,"line":125},[104,846,364],{},[104,848,849],{"class":106,"line":131},[104,850,369],{},[104,852,853],{"class":106,"line":137},[104,854,374],{},[104,856,857],{"class":106,"line":143},[104,858,379],{},[104,860,861],{"class":106,"line":149},[104,862,384],{},[104,864,865],{"class":106,"line":155},[104,866,389],{},[104,868,869],{"class":106,"line":161},[104,870,394],{},[104,872,873],{"class":106,"line":167},[104,874,399],{},[104,876,877],{"class":106,"line":173},[104,878,404],{},[104,880,881],{"class":106,"line":179},[104,882,409],{},[104,884,885],{"class":106,"line":185},[104,886,414],{},[104,888,889],{"class":106,"line":191},[104,890,419],{},[104,892,893],{"class":106,"line":197},[104,894,242],{},[104,896,897],{"class":106,"line":203},[104,898,200],{},[104,900,901],{"class":106,"line":209},[104,902,316],{},[94,904,905],{"className":96,"code":434,"language":98,"meta":99,"style":99},[101,906,907,911,915,919,923,927,931,935,939,943,947,951,955,959,963,967,971],{"__ignoreMap":99},[104,908,909],{"class":106,"line":107},[104,910,441],{},[104,912,913],{"class":106,"line":113},[104,914,354],{},[104,916,917],{"class":106,"line":119},[104,918,359],{},[104,920,921],{"class":106,"line":125},[104,922,236],{},[104,924,925],{"class":106,"line":131},[104,926,242],{},[104,928,929],{"class":106,"line":137},[104,930,200],{},[104,932,933],{"class":106,"line":143},[104,934,466],{},[104,936,937],{"class":106,"line":149},[104,938,316],{},[104,940,941],{"class":106,"line":155},[104,942,476],{"emptyLinePlaceholder":475},[104,944,945],{"class":106,"line":161},[104,946,481],{},[104,948,949],{"class":106,"line":167},[104,950,354],{},[104,952,953],{"class":106,"line":173},[104,954,490],{},[104,956,957],{"class":106,"line":179},[104,958,495],{},[104,960,961],{"class":106,"line":185},[104,962,242],{},[104,964,965],{"class":106,"line":191},[104,966,200],{},[104,968,969],{"class":106,"line":197},[104,970,310],{},[104,972,973],{"class":106,"line":203},[104,974,316],{},[22,976,515],{"id":514},[10,978,979],{},[32,980],{"alt":520,"src":521,"title":522},[10,982,525],{},[10,984,528],{},[10,986,531],{},[41,988,535],{"id":534},[10,990,538],{},[10,992,541],{},[22,994,545],{"id":544},[10,996,548],{},[41,998,552],{"id":551},[10,1000,555],{},[41,1002,559],{"id":558},[10,1004,1005],{},[32,1006],{"alt":564,"src":565,"title":566},[10,1008,569],{},[10,1010,572],{},[10,1012,575],{},[577,1014,579],{},{"title":99,"searchDepth":113,"depth":113,"links":1016},[1017,1020,1023],{"id":24,"depth":113,"text":25,"children":1018},[1019],{"id":43,"depth":119,"text":44},{"id":514,"depth":113,"text":515,"children":1021},[1022],{"id":534,"depth":119,"text":535},{"id":544,"depth":113,"text":545,"children":1024},[1025,1026],{"id":551,"depth":119,"text":552},{"id":558,"depth":119,"text":559},{},{"title":5,"description":593},[605,606,607],{"id":1031,"title":1032,"body":1033,"date":1887,"description":1888,"embedCode":597,"extension":595,"featured":596,"githubLink":597,"image":1889,"liveLink":597,"meta":1890,"navigation":475,"path":1891,"published":596,"seo":1892,"stem":1893,"technologies":1894,"__hash__":1899},"projects\u002Fprojects\u002Fmy-seo-helper-technical-seo-site-analysis.md","My SEO Helper - Technical SEO site analysis",{"type":7,"value":1034,"toc":1877},[1035,1043,1046,1049,1052,1055,1059,1063,1075,1079,1090,1094,1117,1121,1124,1127,1130,1138,1141,1299,1557,1560,1563,1858,1861,1864,1874],[22,1036,1038,1039],{"id":1037},"project-case-study-technical-seo-site-management-and-maintenance","Project Case Study: ",[1040,1041,1042],"strong",{},"Technical SEO site management and maintenance",[10,1044,1045],{},"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.",[10,1047,1048],{},"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.",[10,1050,1051],{},"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.",[10,1053,1054],{},"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.",[22,1056,1058],{"id":1057},"tech-checks","Tech Checks",[41,1060,1062],{"id":1061},"robotstxt","Robots.txt",[1064,1065,1066,1069,1072],"ol",{},[52,1067,1068],{},"Should be exist and return a 200 response",[52,1070,1071],{},"Should contain a valid sitemap entry",[52,1073,1074],{},"Should no block any crawlers (for production URLs)",[41,1076,1078],{"id":1077},"sitemapxml","Sitemap.xml",[1064,1080,1081,1084,1087],{},[52,1082,1083],{},"Contains valid XML",[52,1085,1086],{},"Uses HTTPS for all URLs",[52,1088,1089],{},"Does not exceed size limits (50MB, 50,000 URLs)",[41,1091,1093],{"id":1092},"homepage","Homepage",[1064,1095,1096,1099,1102,1105,1108,1111,1114],{},[52,1097,1098],{},"Returns a successful response",[52,1100,1101],{},"Title tag",[52,1103,1104],{},"Meta description",[52,1106,1107],{},"Google PageSpeed Insights score > 90",[52,1109,1110],{},"Canonical tag detection",[52,1112,1113],{},"No-index meta tag identification",[52,1115,1116],{},"Heading hierarchy validation",[22,1118,1120],{"id":1119},"implementation","Implementation",[10,1122,1123],{},"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.",[10,1125,1126],{},"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.",[10,1128,1129],{},"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.",[94,1131,1136],{"className":1132,"code":1134,"language":1135},[1133],"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",[101,1137,1134],{"__ignoreMap":99},[10,1139,1140],{},"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.",[94,1142,1146],{"className":1143,"code":1144,"language":1145,"meta":99,"style":99},"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",[101,1147,1148,1189,1194,1199,1204,1209,1214,1219,1224,1229,1234,1239,1244,1249,1254,1259,1264,1269,1274,1279,1284,1289,1294],{"__ignoreMap":99},[104,1149,1150,1154,1158,1162,1166,1170,1174,1177,1180,1183,1186],{"class":106,"line":107},[104,1151,1153],{"class":1152},"s2d5f","const",[104,1155,1157],{"class":1156},"ssVjP"," CHECK_MAP",[104,1159,1161],{"class":1160},"setMH",":",[104,1163,1165],{"class":1164},"sZGI-"," Record",[104,1167,1169],{"class":1168},"s4SHQ","\u003C",[104,1171,1173],{"class":1172},"sPGPh","string",[104,1175,1176],{"class":1168},", ",[104,1178,1179],{"class":1164},"CheckFunction",[104,1181,1182],{"class":1168},"> ",[104,1184,1185],{"class":1160},"=",[104,1187,1188],{"class":1168}," {\n",[104,1190,1191],{"class":106,"line":113},[104,1192,1193],{"class":1168},"  robots_contains_sitemap: runRobotsContainsSitemapCheck,\n",[104,1195,1196],{"class":106,"line":119},[104,1197,1198],{"class":1168},"  robots_exists:           runRobotsExistsCheck,\n",[104,1200,1201],{"class":106,"line":125},[104,1202,1203],{"class":1168},"    robots_disallow_all:     runRobotsAllowsCrawlersCheck,\n",[104,1205,1206],{"class":106,"line":131},[104,1207,1208],{"class":1168},"  sitemap_exists:          runSitemapExistsCheck,\n",[104,1210,1211],{"class":106,"line":137},[104,1212,1213],{"class":1168},"  sitemap_valid_xml:       runSitemapValidXmlCheck,\n",[104,1215,1216],{"class":106,"line":143},[104,1217,1218],{"class":1168},"  sitemap_size:            runSitemapSizeCheck,\n",[104,1220,1221],{"class":106,"line":149},[104,1222,1223],{"class":1168},"  sitemap_https:           runSitemapHttpsCheck,\n",[104,1225,1226],{"class":106,"line":155},[104,1227,1228],{"class":1168},"    homepage_200:                run200Check,\n",[104,1230,1231],{"class":106,"line":161},[104,1232,1233],{"class":1168},"    homepage_canonical:        runCanonicalCheck,\n",[104,1235,1236],{"class":106,"line":167},[104,1237,1238],{"class":1168},"    homepage_noindex:        runNoIndexCheck,\n",[104,1240,1241],{"class":106,"line":173},[104,1242,1243],{"class":1168},"    homepage_pagespeed:      runPagespeedInsightsCheck,\n",[104,1245,1246],{"class":106,"line":179},[104,1247,1248],{"class":1168},"    page_200:                run200Check,\n",[104,1250,1251],{"class":106,"line":185},[104,1252,1253],{"class":1168},"    page_canonical:          runCanonicalCheck,\n",[104,1255,1256],{"class":106,"line":191},[104,1257,1258],{"class":1168},"    page_noindex:            runNoIndexCheck,\n",[104,1260,1261],{"class":106,"line":197},[104,1262,1263],{"class":1168},"    page_pagespeed:          runPagespeedInsightsCheck,\n",[104,1265,1266],{"class":106,"line":203},[104,1267,1268],{"class":1168},"    page_title_length:       runTitleLengthCheck,\n",[104,1270,1271],{"class":106,"line":209},[104,1272,1273],{"class":1168},"    page_meta_description_length: runMetaDescriptionLengthCheck,\n",[104,1275,1276],{"class":106,"line":215},[104,1277,1278],{"class":1168},"    page_h1:                 runH1Check,\n",[104,1280,1281],{"class":106,"line":221},[104,1282,1283],{"class":1168},"    page_heading_hierarchy:  runHeadingHierarchyCheck,\n",[104,1285,1286],{"class":106,"line":227},[104,1287,1288],{"class":1168},"    page_social_preview:     runSocialPreviewCheck,\n",[104,1290,1291],{"class":106,"line":233},[104,1292,1293],{"class":1168},"    page_structured_data:    runStructuredDataCheck,\n",[104,1295,1296],{"class":106,"line":239},[104,1297,1298],{"class":1168},"};\n",[94,1300,1302],{"className":1143,"code":1301,"language":1145,"meta":99,"style":99},"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",[101,1303,1304,1343,1363,1378,1383,1411,1415,1439,1456,1469,1473,1490,1495,1500,1505,1510,1515,1519,1544,1552],{"__ignoreMap":99},[104,1305,1306,1309,1312,1315,1319,1322,1326,1328,1331,1333,1336,1338,1340],{"class":106,"line":107},[104,1307,1308],{"class":1160},"export",[104,1310,1311],{"class":1160}," async",[104,1313,1314],{"class":1152}," function",[104,1316,1318],{"class":1317},"sTKsR"," runCheck",[104,1320,1321],{"class":1168},"(",[104,1323,1325],{"class":1324},"saAYD","pathId",[104,1327,1161],{"class":1160},[104,1329,1330],{"class":1172}," number",[104,1332,1176],{"class":1168},[104,1334,1335],{"class":1324},"checkId",[104,1337,1161],{"class":1160},[104,1339,1330],{"class":1172},[104,1341,1342],{"class":1168},") {\n",[104,1344,1345,1348,1351,1354,1357,1360],{"class":106,"line":113},[104,1346,1347],{"class":1152},"  const",[104,1349,1350],{"class":1156}," pathCheck",[104,1352,1353],{"class":1160}," =",[104,1355,1356],{"class":1160}," await",[104,1358,1359],{"class":1317}," getPathCheck",[104,1361,1362],{"class":1168},"(pathId, checkId);\n",[104,1364,1365,1367,1370,1372,1375],{"class":106,"line":119},[104,1366,1347],{"class":1152},[104,1368,1369],{"class":1156}," url",[104,1371,1353],{"class":1160},[104,1373,1374],{"class":1317}," buildUrl",[104,1376,1377],{"class":1168},"(pathCheck.path.site.url, pathCheck.path.path);\n",[104,1379,1380],{"class":106,"line":125},[104,1381,1382],{"class":1168},"  \n",[104,1384,1385,1387,1390,1393,1395,1398,1401,1403,1405,1408],{"class":106,"line":131},[104,1386,1347],{"class":1152},[104,1388,1389],{"class":1168}," { ",[104,1391,1392],{"class":1156},"response",[104,1394,1176],{"class":1168},[104,1396,1397],{"class":1156},"errors",[104,1399,1400],{"class":1168}," } ",[104,1402,1185],{"class":1160},[104,1404,1356],{"class":1160},[104,1406,1407],{"class":1317}," fetchPageContent",[104,1409,1410],{"class":1168},"(url);\n",[104,1412,1413],{"class":106,"line":137},[104,1414,1382],{"class":1168},[104,1416,1417,1419,1422,1424,1427,1430,1433,1437],{"class":106,"line":143},[104,1418,1347],{"class":1152},[104,1420,1421],{"class":1156}," result",[104,1423,1353],{"class":1160},[104,1425,1426],{"class":1168}," errors.",[104,1428,1429],{"class":1156},"length",[104,1431,1432],{"class":1160}," >",[104,1434,1436],{"class":1435},"siXTV"," 0",[104,1438,134],{"class":1168},[104,1440,1441,1444,1447,1449,1453],{"class":106,"line":149},[104,1442,1443],{"class":1160},"    ?",[104,1445,1446],{"class":1317}," createCheckResult",[104,1448,1321],{"class":1168},[104,1450,1452],{"class":1451},"sTjUT","\"FAILED\"",[104,1454,1455],{"class":1168},", errors)\n",[104,1457,1458,1461,1463,1466],{"class":106,"line":155},[104,1459,1460],{"class":1160},"    :",[104,1462,1356],{"class":1160},[104,1464,1465],{"class":1317}," executeCheck",[104,1467,1468],{"class":1168},"(pathCheck.check.slug, response.data, pathCheck.path.site.url, pathCheck.path);\n",[104,1470,1471],{"class":106,"line":161},[104,1472,134],{"class":1168},[104,1474,1475,1477,1480,1482,1484,1487],{"class":106,"line":167},[104,1476,1347],{"class":1152},[104,1478,1479],{"class":1156}," updatedPathCheck",[104,1481,1353],{"class":1160},[104,1483,1356],{"class":1160},[104,1485,1486],{"class":1317}," updatePathAndCheck",[104,1488,1489],{"class":1168},"(\n",[104,1491,1492],{"class":106,"line":173},[104,1493,1494],{"class":1168},"    pathId, \n",[104,1496,1497],{"class":106,"line":179},[104,1498,1499],{"class":1168},"    checkId, \n",[104,1501,1502],{"class":106,"line":185},[104,1503,1504],{"class":1168},"    response.status, \n",[104,1506,1507],{"class":106,"line":191},[104,1508,1509],{"class":1168},"    result\n",[104,1511,1512],{"class":106,"line":197},[104,1513,1514],{"class":1168},"  );\n",[104,1516,1517],{"class":106,"line":203},[104,1518,134],{"class":1168},[104,1520,1521,1524,1526,1529,1533,1535,1538,1541],{"class":106,"line":209},[104,1522,1523],{"class":1317},"  revalidatePath",[104,1525,1321],{"class":1168},[104,1527,1528],{"class":1451},"`paths\u002F",[104,1530,1532],{"class":1531},"skrme","${",[104,1534,1325],{"class":1168},[104,1536,1537],{"class":1531},"}",[104,1539,1540],{"class":1451},"`",[104,1542,1543],{"class":1168},");\n",[104,1545,1546,1549],{"class":106,"line":215},[104,1547,1548],{"class":1160},"  return",[104,1550,1551],{"class":1168}," updatedPathCheck;\n",[104,1553,1554],{"class":106,"line":221},[104,1555,1556],{"class":1168},"}\n",[10,1558,1559],{},"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.",[10,1561,1562],{},"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.",[94,1564,1566],{"className":1143,"code":1565,"language":1145,"meta":99,"style":99},"\"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",[101,1567,1568,1576,1580,1596,1610,1614,1660,1690,1694,1707,1718,1722,1731,1735,1745,1750,1754,1761,1772,1790,1803,1813,1818,1840,1850,1854],{"__ignoreMap":99},[104,1569,1570,1573],{"class":106,"line":107},[104,1571,1572],{"class":1451},"\"use client\"",[104,1574,1575],{"class":1168},";\n",[104,1577,1578],{"class":106,"line":113},[104,1579,476],{"emptyLinePlaceholder":475},[104,1581,1582,1585,1588,1591,1594],{"class":106,"line":119},[104,1583,1584],{"class":1160},"import",[104,1586,1587],{"class":1168}," { runCheck } ",[104,1589,1590],{"class":1160},"from",[104,1592,1593],{"class":1451}," '..\u002Factions'",[104,1595,1575],{"class":1168},[104,1597,1598,1600,1603,1605,1608],{"class":106,"line":125},[104,1599,1584],{"class":1160},[104,1601,1602],{"class":1168}," { useState } ",[104,1604,1590],{"class":1160},[104,1606,1607],{"class":1451}," 'react'",[104,1609,1575],{"class":1168},[104,1611,1612],{"class":106,"line":131},[104,1613,476],{"emptyLinePlaceholder":475},[104,1615,1616,1618,1621,1623,1626,1629,1631,1633,1635,1638,1640,1642,1645,1647,1649,1651,1653,1655,1657],{"class":106,"line":137},[104,1617,1308],{"class":1160},[104,1619,1620],{"class":1160}," default",[104,1622,1314],{"class":1152},[104,1624,1625],{"class":1317}," RunCheckButton",[104,1627,1628],{"class":1168},"({ ",[104,1630,1325],{"class":1324},[104,1632,1176],{"class":1168},[104,1634,1335],{"class":1324},[104,1636,1637],{"class":1168}," }",[104,1639,1161],{"class":1160},[104,1641,1389],{"class":1168},[104,1643,1325],{"class":1644},"ssgMC",[104,1646,1161],{"class":1160},[104,1648,1330],{"class":1172},[104,1650,1176],{"class":1168},[104,1652,1335],{"class":1644},[104,1654,1161],{"class":1160},[104,1656,1330],{"class":1172},[104,1658,1659],{"class":1168}," }) {\n",[104,1661,1662,1664,1667,1670,1672,1675,1678,1680,1683,1685,1688],{"class":106,"line":143},[104,1663,1347],{"class":1152},[104,1665,1666],{"class":1168}," [",[104,1668,1669],{"class":1156},"isPending",[104,1671,1176],{"class":1168},[104,1673,1674],{"class":1156},"setIsPending",[104,1676,1677],{"class":1168},"] ",[104,1679,1185],{"class":1160},[104,1681,1682],{"class":1317}," useState",[104,1684,1321],{"class":1168},[104,1686,1687],{"class":1435},"false",[104,1689,1543],{"class":1168},[104,1691,1692],{"class":106,"line":149},[104,1693,476],{"emptyLinePlaceholder":475},[104,1695,1696,1699,1701,1704],{"class":106,"line":155},[104,1697,1698],{"class":1160},"  async",[104,1700,1314],{"class":1152},[104,1702,1703],{"class":1317}," handleAction",[104,1705,1706],{"class":1168},"() {\n",[104,1708,1709,1712,1714,1716],{"class":106,"line":161},[104,1710,1711],{"class":1317},"    setIsPending",[104,1713,1321],{"class":1168},[104,1715,596],{"class":1435},[104,1717,1543],{"class":1168},[104,1719,1720],{"class":106,"line":167},[104,1721,206],{"class":1168},[104,1723,1724,1727,1729],{"class":106,"line":173},[104,1725,1726],{"class":1160},"    await",[104,1728,1318],{"class":1317},[104,1730,1362],{"class":1168},[104,1732,1733],{"class":106,"line":179},[104,1734,206],{"class":1168},[104,1736,1737,1739,1741,1743],{"class":106,"line":185},[104,1738,1711],{"class":1317},[104,1740,1321],{"class":1168},[104,1742,1687],{"class":1435},[104,1744,1543],{"class":1168},[104,1746,1747],{"class":106,"line":191},[104,1748,1749],{"class":1168},"  }\n",[104,1751,1752],{"class":106,"line":197},[104,1753,476],{"emptyLinePlaceholder":475},[104,1755,1756,1758],{"class":106,"line":203},[104,1757,1548],{"class":1160},[104,1759,1760],{"class":1168}," (\n",[104,1762,1763,1766,1770],{"class":106,"line":209},[104,1764,1765],{"class":1168},"    \u003C",[104,1767,1769],{"class":1768},"sCeOG","button",[104,1771,134],{"class":1168},[104,1773,1774,1777,1779,1783,1786,1788],{"class":106,"line":215},[104,1775,1776],{"class":1317},"      onClick",[104,1778,1185],{"class":1160},[104,1780,1782],{"class":1781},"shc6p","{",[104,1784,1785],{"class":1168},"handleAction",[104,1787,1537],{"class":1781},[104,1789,134],{"class":1168},[104,1791,1792,1795,1797,1799,1801],{"class":106,"line":221},[104,1793,1794],{"class":1317},"      disabled",[104,1796,1185],{"class":1160},[104,1798,1782],{"class":1781},[104,1800,1669],{"class":1168},[104,1802,1556],{"class":1781},[104,1804,1805,1808,1810],{"class":106,"line":227},[104,1806,1807],{"class":1317},"      className",[104,1809,1185],{"class":1160},[104,1811,1812],{"class":1451},"\"bg-black text-white px-4 py-2 rounded\"\n",[104,1814,1815],{"class":106,"line":233},[104,1816,1817],{"class":1168},"    >\n",[104,1819,1820,1823,1826,1829,1832,1835,1838],{"class":106,"line":239},[104,1821,1822],{"class":1781},"      {",[104,1824,1825],{"class":1168},"isPending ",[104,1827,1828],{"class":1160},"?",[104,1830,1831],{"class":1451}," \"Working...\"",[104,1833,1834],{"class":1160}," :",[104,1836,1837],{"class":1451}," \"Re-run Test\"",[104,1839,1556],{"class":1781},[104,1841,1842,1845,1847],{"class":106,"line":245},[104,1843,1844],{"class":1168},"    \u003C\u002F",[104,1846,1769],{"class":1768},[104,1848,1849],{"class":1168},">\n",[104,1851,1852],{"class":106,"line":250},[104,1853,1514],{"class":1168},[104,1855,1856],{"class":106,"line":255},[104,1857,1556],{"class":1168},[10,1859,1860],{},"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.",[10,1862,1863],{},"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.",[10,1865,1866,1867,1873],{},"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: ",[1868,1869,1870],"a",{"href":1870,"rel":1871},"https:\u002F\u002Fdev.to\u002Fbardaq\u002Flong-running-tasks-with-nextjs-a-journey-of-reinventing-the-wheel-1cjg",[1872],"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.",[577,1875,1876],{},"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":99,"searchDepth":113,"depth":113,"links":1878},[1879,1881,1886],{"id":1037,"depth":113,"text":1880},"Project Case Study: Technical SEO site management and maintenance",{"id":1057,"depth":113,"text":1058,"children":1882},[1883,1884,1885],{"id":1061,"depth":119,"text":1062},{"id":1077,"depth":119,"text":1078},{"id":1092,"depth":119,"text":1093},{"id":1119,"depth":113,"text":1120},"2026-04-09T10:44:00.000+01:00","Project to help keep track of the technical SEO health of various websites I manage","\u002Fimg\u002Fscreenshot-from-2026-04-09-10-58-38.png",{},"\u002Fprojects\u002Fmy-seo-helper-technical-seo-site-analysis",{"title":1032,"description":1888},"projects\u002Fmy-seo-helper-technical-seo-site-analysis",[1895,1896,1897,1898],"Next.js","Auth.js","Prisma","TypeScript","LWkpeeVYwyfOhBRxe46i4onCZP1Sh0pZNVoB19lEX_s",{"id":1901,"title":1902,"body":1903,"date":2510,"description":2511,"embedCode":597,"extension":595,"featured":596,"githubLink":597,"image":2512,"liveLink":2513,"meta":2514,"navigation":475,"path":2515,"published":596,"seo":2516,"stem":2517,"technologies":2518,"__hash__":2521},"projects\u002Fprojects\u002Fdarrens-tech-tutorials.md","Darren's Tech Tutorials",{"type":7,"value":1904,"toc":2499},[1905,1912,1916,1923,1929,1943,1946,1950,1976,1980,2080,2082,2086,2091,2101,2115,2119,2124,2261,2265,2268,2277,2287,2299,2433,2437,2440,2478,2480,2484,2490,2496],[22,1906,1908,1909],{"id":1907},"️-project-case-study-youtube-to-blog-static-site-generator","🏗️ Project Case Study: ",[1040,1910,1911],{},"YouTube-to-Blog Static Site Generator",[41,1913,1915],{"id":1914},"overview-problem-statement","🎯 Overview & Problem Statement",[10,1917,1918,1919,1922],{},"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 ",[1040,1920,1921],{},"copy-and-paste commands"," mentioned in the tutorials.",[10,1924,1925,1928],{},[1040,1926,1927],{},"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.",[10,1930,1931,1934,1935,1938,1939,1942],{},[1040,1932,1933],{},"Our Solution:"," Develop an automated pipeline that uses the YouTube Data API to retrieve video information and transcripts, then employs the ",[1040,1936,1937],{},"Gemini API"," to convert the raw transcript into a polished, structured markdown blog post. This content is then fed into a ",[1040,1940,1941],{},"Hugo"," static site generator to instantly build and deploy hundreds of fast-loading web pages.",[1944,1945],"hr",{},[41,1947,1949],{"id":1948},"key-project-goals","🚀 Key Project Goals",[49,1951,1952,1958,1964,1970],{},[52,1953,1954,1957],{},[1040,1955,1956],{},"Automation:"," Eliminate the manual process of blog post creation.",[52,1959,1960,1963],{},[1040,1961,1962],{},"Speed & Scale:"," Build hundreds of static, fast-loading pages instantly.",[52,1965,1966,1969],{},[1040,1967,1968],{},"SEO-Friendly:"," Ensure the new blog content is easily discoverable by search engines.",[52,1971,1972,1975],{},[1040,1973,1974],{},"Accessibility:"," Provide viewers with an easy-to-read, copyable alternative to watching the video.",[41,1977,1979],{"id":1978},"️-technology-stack","🛠️ Technology Stack",[1981,1982,1983,2000],"table",{},[1984,1985,1986],"thead",{},[1987,1988,1989,1994,1997],"tr",{},[1990,1991,1993],"th",{"align":1992},"left","Technology",[1990,1995,1996],{"align":1992},"Purpose",[1990,1998,1999],{"align":1992},"Key Feature Utilized",[2001,2002,2003,2016,2029,2041,2054,2067],"tbody",{},[1987,2004,2005,2010,2013],{},[2006,2007,2008],"td",{"align":1992},[1040,2009,1941],{},[2006,2011,2012],{"align":1992},"Static Site Generator (SSG)",[2006,2014,2015],{"align":1992},"Incredibly fast build times, Markdown-based content.",[1987,2017,2018,2023,2026],{},[2006,2019,2020],{"align":1992},[1040,2021,2022],{},"YouTube Data API",[2006,2024,2025],{"align":1992},"Data Source",[2006,2027,2028],{"align":1992},"Fetching Channel\u002FPlaylist\u002FVideo metadata.",[1987,2030,2031,2035,2038],{},[2006,2032,2033],{"align":1992},[1040,2034,1937],{},[2006,2036,2037],{"align":1992},"Content Generation",[2006,2039,2040],{"align":1992},"Converting raw video transcript into structured Markdown.",[1987,2042,2043,2048,2051],{},[2006,2044,2045],{"align":1992},[1040,2046,2047],{},"Node.js",[2006,2049,2050],{"align":1992},"Core Scripting Language",[2006,2052,2053],{"align":1992},"Orchestrating API calls and file system operations.",[1987,2055,2056,2061,2064],{},[2006,2057,2058],{"align":1992},[1040,2059,2060],{},"Cloudflare Pages",[2006,2062,2063],{"align":1992},"Hosting\u002FCI\u002FCD",[2006,2065,2066],{"align":1992},"Automated deployment and global CDN performance.",[1987,2068,2069,2074,2077],{},[2006,2070,2071],{"align":1992},[1040,2072,2073],{},"YouTube.js",[2006,2075,2076],{"align":1992},"Transcript Fetching",[2006,2078,2079],{"align":1992},"Accessing YouTube's internal API (InnerTube) for subtitles.",[1944,2081],{},[41,2083,2085],{"id":2084},"development-technical-breakdown","💻 Development & Technical Breakdown",[2087,2088,2090],"h4",{"id":2089},"_1-static-site-foundation-with-hugo","1. Static Site Foundation with Hugo",[10,2092,2093,2094,2097,2098,2100],{},"To meet the requirement for ",[1040,2095,2096],{},"speed and scalability",", a Static Site Generator (SSG) was the ideal choice. ",[1040,2099,1941],{}," was selected due to its reputation for being one of the fastest SSGs available.",[49,2102,2103,2109],{},[52,2104,2105,2108],{},[1040,2106,2107],{},"Benefit:"," The site is built from simple markdown files, resulting in pure HTML\u002FCSS\u002FJS, eliminating database lookups and ensuring blazing-fast load times.",[52,2110,2111,2114],{},[1040,2112,2113],{},"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.",[2087,2116,2118],{"id":2117},"_2-youtube-data-orchestration","2. YouTube Data Orchestration",[10,2120,2121,2122,1161],{},"The first hurdle was fetching all video data from the channel. This required a multi-step process using the ",[1040,2123,2022],{},[49,2125,2126,2187,2244],{},[52,2127,2128,2131,2132],{},[1040,2129,2130],{},"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.",[94,2133,2135],{"className":1143,"code":2134,"language":1145,"meta":99,"style":99},"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",[101,2136,2137,2154,2160,2178,2183],{"__ignoreMap":99},[104,2138,2139,2142,2144,2147,2149,2152],{"class":106,"line":107},[104,2140,2141],{"class":1160},"async",[104,2143,1314],{"class":1152},[104,2145,2146],{"class":1317}," getUploadsPlaylistId",[104,2148,1321],{"class":1168},[104,2150,2151],{"class":1324},"channelId",[104,2153,1342],{"class":1168},[104,2155,2156],{"class":106,"line":113},[104,2157,2159],{"class":2158},"s8-w5","  \u002F\u002F ... API call to channels.list ...\n",[104,2161,2162,2164,2167,2169,2172,2175],{"class":106,"line":119},[104,2163,1347],{"class":1152},[104,2165,2166],{"class":1156}," uploadsPlaylistId",[104,2168,1353],{"class":1160},[104,2170,2171],{"class":1168}," response.data.items[",[104,2173,2174],{"class":1435},"0",[104,2176,2177],{"class":1168},"]?.contentDetails.relatedPlaylists.uploads;\n",[104,2179,2180],{"class":106,"line":125},[104,2181,2182],{"class":2158},"  \u002F\u002F ... error handling ...\n",[104,2184,2185],{"class":106,"line":131},[104,2186,1556],{"class":1168},[52,2188,2189,2192,2193,2196,2197,2200,2201],{},[1040,2190,2191],{},"Step B: Paginated Video Retrieval","\nBecause YouTube limits API results per page, the video fetching function was built with a ",[1040,2194,2195],{},"pagination loop"," that checks for the ",[101,2198,2199],{},"nextPageToken"," and continues querying the API until all videos are processed.",[94,2202,2204],{"className":1143,"code":2203,"language":1145,"meta":99,"style":99},"do {\n  \u002F\u002F ... API call to playlistItems.list ...\n  \u002F\u002F ... process videos ...\n  nextPageToken = response.data.nextPageToken;\n} while (nextPageToken);\n",[101,2205,2206,2213,2218,2223,2233],{"__ignoreMap":99},[104,2207,2208,2211],{"class":106,"line":107},[104,2209,2210],{"class":1160},"do",[104,2212,1188],{"class":1168},[104,2214,2215],{"class":106,"line":113},[104,2216,2217],{"class":2158},"  \u002F\u002F ... API call to playlistItems.list ...\n",[104,2219,2220],{"class":106,"line":119},[104,2221,2222],{"class":2158},"  \u002F\u002F ... process videos ...\n",[104,2224,2225,2228,2230],{"class":106,"line":125},[104,2226,2227],{"class":1168},"  nextPageToken ",[104,2229,1185],{"class":1160},[104,2231,2232],{"class":1168}," response.data.nextPageToken;\n",[104,2234,2235,2238,2241],{"class":106,"line":131},[104,2236,2237],{"class":1168},"} ",[104,2239,2240],{"class":1160},"while",[104,2242,2243],{"class":1168}," (nextPageToken);\n",[52,2245,2246,2249,2250,2253,2254,2256,2257,2260],{},[1040,2247,2248],{},"Step C: Fetching the Raw Transcript","\nCrucially, the official YouTube Data API does ",[1040,2251,2252],{},"not"," provide direct access to the time-synced subtitle data. After testing multiple external libraries, ",[1040,2255,2073],{}," (which accesses YouTube's internal ",[1040,2258,2259],{},"InnerTube"," API) was chosen to reliably scrape the raw transcript text. This raw text forms the input for the AI conversion step.",[2087,2262,2264],{"id":2263},"_3-content-transformation-with-the-gemini-api","3. Content Transformation with the Gemini API",[10,2266,2267],{},"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.",[2269,2270,2271],"blockquote",{},[10,2272,2273,2276],{},[1040,2274,2275],{},"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.\"",[10,2278,2279,2282,2283,2286],{},[1040,2280,2281],{},"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 ",[1040,2284,2285],{},"retry mechanism using exponential backoff",".",[49,2288,2289,2296],{},[52,2290,2291,2292,2295],{},"If the API call fails, the script waits for a delay (",[101,2293,2294],{},"Math.pow(2, attempt) * 1000",") before trying again.",[52,2297,2298],{},"This significantly increases the reliability of the content generation pipeline.",[94,2300,2302],{"className":1143,"code":2301,"language":1145,"meta":99,"style":99},"\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",[101,2303,2304,2309,2342,2347,2362,2394,2419,2424,2429],{"__ignoreMap":99},[104,2305,2306],{"class":106,"line":107},[104,2307,2308],{"class":2158},"\u002F\u002F Retry loop with exponential backoff\n",[104,2310,2311,2314,2317,2320,2323,2325,2328,2331,2334,2337,2340],{"class":106,"line":113},[104,2312,2313],{"class":1160},"for",[104,2315,2316],{"class":1168}," (",[104,2318,2319],{"class":1152},"let",[104,2321,2322],{"class":1168}," attempt ",[104,2324,1185],{"class":1160},[104,2326,2327],{"class":1435}," 1",[104,2329,2330],{"class":1168},"; attempt ",[104,2332,2333],{"class":1160},"\u003C=",[104,2335,2336],{"class":1168}," maxRetries; attempt",[104,2338,2339],{"class":1160},"++",[104,2341,1342],{"class":1168},[104,2343,2344],{"class":106,"line":119},[104,2345,2346],{"class":2158},"    \u002F\u002F ... API call logic ...\n",[104,2348,2349,2352,2355,2358,2360],{"class":106,"line":125},[104,2350,2351],{"class":1160},"    if",[104,2353,2354],{"class":1168}," (attempt ",[104,2356,2357],{"class":1160},">",[104,2359,2327],{"class":1435},[104,2361,1342],{"class":1168},[104,2363,2364,2367,2370,2372,2375,2378,2380,2383,2386,2389,2392],{"class":106,"line":131},[104,2365,2366],{"class":1152},"        const",[104,2368,2369],{"class":1156}," delay",[104,2371,1353],{"class":1160},[104,2373,2374],{"class":1168}," Math.",[104,2376,2377],{"class":1317},"pow",[104,2379,1321],{"class":1168},[104,2381,2382],{"class":1435},"2",[104,2384,2385],{"class":1168},", attempt) ",[104,2387,2388],{"class":1160},"*",[104,2390,2391],{"class":1435}," 1000",[104,2393,1575],{"class":1168},[104,2395,2396,2399,2402,2405,2407,2410,2413,2416],{"class":106,"line":137},[104,2397,2398],{"class":1160},"        await",[104,2400,2401],{"class":1160}," new",[104,2403,2404],{"class":1172}," Promise",[104,2406,1321],{"class":1168},[104,2408,2409],{"class":1324},"resolve",[104,2411,2412],{"class":1152}," =>",[104,2414,2415],{"class":1317}," setTimeout",[104,2417,2418],{"class":1168},"(resolve, delay));\n",[104,2420,2421],{"class":106,"line":143},[104,2422,2423],{"class":1168},"    }\n",[104,2425,2426],{"class":106,"line":149},[104,2427,2428],{"class":2158},"    \u002F\u002F ... try\u002Fcatch block for API call ...\n",[104,2430,2431],{"class":106,"line":155},[104,2432,1556],{"class":1168},[41,2434,2436],{"id":2435},"results-impact","📈 Results & Impact",[10,2438,2439],{},"The automated YouTube-to-Blog generator achieved the following:",[49,2441,2442,2456,2466,2472],{},[52,2443,2444,2447,2448,2451,2452,2455],{},[1040,2445,2446],{},"95% Time Reduction:"," The time required to create a new blog post was reduced from ",[1040,2449,2450],{},"~1 hour of manual work"," (watching the video, writing, formatting) to ",[1040,2453,2454],{},"~5 minutes of automated processing"," per video.",[52,2457,2458,2461,2462,2465],{},[1040,2459,2460],{},"Instant Backlog Processing:"," The script successfully processed ",[1040,2463,2464],{},"200+ videos"," already on the channel, instantly creating a searchable, high-value content library for viewers.",[52,2467,2468,2471],{},[1040,2469,2470],{},"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.",[52,2473,2474,2477],{},[1040,2475,2476],{},"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.",[1944,2479],{},[41,2481,2483],{"id":2482},"conclusion-key-takeaways","💡 Conclusion & Key Takeaways",[10,2485,2486,2487,2489],{},"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 ",[1040,2488,1937],{}," proved to be the most critical component, allowing for the conversion of unstructured data (transcript) into highly structured, actionable content (markdown blog post).",[10,2491,2492,2495],{},[1040,2493,2494],{},"Project Takeaway:"," Thoughtful API integration, combined with robust error handling (like exponential backoff), is essential for building reliable, scalable automated content pipelines.",[577,2497,2498],{},"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":99,"searchDepth":113,"depth":113,"links":2500},[2501],{"id":1907,"depth":113,"text":2502,"children":2503},"🏗️ Project Case Study: YouTube-to-Blog Static Site Generator",[2504,2505,2506,2507,2508,2509],{"id":1914,"depth":119,"text":1915},{"id":1948,"depth":119,"text":1949},{"id":1978,"depth":119,"text":1979},{"id":2084,"depth":119,"text":2085},{"id":2435,"depth":119,"text":2436},{"id":2482,"depth":119,"text":2483},"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":1902,"description":2511},"projects\u002Fdarrens-tech-tutorials",[1941,2519,2520],"Gemini AI","YouTube","kt1fvtk1tx3v_L6HzBE5n-8LAnU4P-Sh_xhZ85-UD9c",1787136739827]