September 23rd, 2024 × #Svelte#Rust#Podcasting
Syntax Assistant Desktop App
Scott and Wes discuss a Hack Week project where they built a desktop app to automate the Syntax podcast publishing workflow using Svelte, Rust and other technologies.
- Welcome to Hack Week desktop app project
- Desktop app to automate Syntax publishing process
- Debugging desktop apps vs websites
- Idea originated from past conversations
- Used FigJam for initial brainstorming
- Looked at various Syntax publishing facets to automate
- Frontend built with SvelteKit, backend with Rust
- Runs as desktop app with custom icon and dock integration
- Show notes generated automatically from video file metadata
- Real-time data syncing using IndexedDB database
- Added import/export database functions
- Reasons for building as desktop app vs website
- Transcript generation would enable more automation
- Difficulties getting app signed and notarized
- Wrap up and conclusion
Transcript
Scott Tolinski
Welcome to Syntax on this Monday Hasty Treat. We're talking all about my Hack Week project that I did with CJ at Sanity. We built a desktop app to help publish syntax that basically automated a ton of steps that we need to do to to publish this show.
Desktop app to automate Syntax publishing process
Scott Tolinski
So it was a really cool experience. We built it with a lot of neat tech, and I'm gonna talk you through a little bit about what we did, why we chose to do it, some of the tech we used, and all that good stuff. My name is Scott Tolinski. I'm a developer from Denver, and with me as always is Wes. What's up, Wes? Hey. I am excited to hear about your project and all the mostly, like, the tech decisions you chose to make a desktop application
Debugging desktop apps vs websites
Wes Bos
with web languages, I guess, you could say. Yeah. Totally. Mostly?
Scott Tolinski
Yeah. And you know what? We could have really used Sentry during the publishing phase of this because let me tell you, man, debugging production desktop apps is a different experience than debugging a website. I mean, when you the moment you don't have your inspect or get into those dev tools, you're coming through, like, Apple logs that I had to, like, set up to write even myself. I had to write the code that wrote the Apple logs.
Scott Tolinski
So it's like, now I'm scrolling through these massive amount of logs. It would have been really nice to have Sentry on this thing. So, maybe next time, I I can figure out how to get Sentry going on this if I have more time.
Scott Tolinski
And that would save me a lot of time because it helps you find and fix all of your bugs. So head on over to century.comforward/syntax, and it make it 2 months for free if you want to save yourself the pain of trying to debug your production software
Wes Bos
in a very painful way. Alright. So let's hear. Let's start with the idea.
Idea originated from past conversations
Wes Bos
With Hack Week every year, there's always like this, like, what should I do this year? You know? There's always a couple ideas floating around, and this is something you had been talking about for a while. Right? Yeah. Yeah. In fact, even,
Scott Tolinski
for for those of you, I will be sharing screen on this. I'm not we are going to talk through it. I'm not going to rely on that screen sharing. So in case you're worried that it's gonna be a visual episode, it's not. But if you are on YouTube, you will get a little bit extra here. So, basically, the way we started off, believe it or not, we started off in FigJam because if you haven't used FigJam before, it's part of Figma for brainstorming.
Scott Tolinski
And it allows you to do all kinds of things from, like, dropping stickers on, to leaving comments, thumbing up, or, like, sticky notes. And we used it for, like, the what are the mind mapping kind of software? That's kind of what we used it for. Yeah. I love mind mapping software.
Used FigJam for initial brainstorming
Scott Tolinski
Yeah. So we started off, and we just had the idea of a production assistant. We went from there. We had all these different facets of syntax publishing. Right? Whether that is the creative side of publishing, your your thumbnails, your titles, the publishing side of things, which JS, like, actually pushing go and uploading to YouTube or creating a PR so that way it gets to go into our website. The way that it works is that we have markdown files. Each one of those markdown files imports show.
Looked at various Syntax publishing facets to automate
Scott Tolinski
So you need to do a PR to add a show to the website. If you've ever been to the website and the show's not there, it's usually because, like, there Wes an issue with the PR or something like that.
Scott Tolinski
There's also the whole show notes authoring process. When Wes work on our show notes, they have to do a few things. They Node to have chapters that work on YouTube. So when you upload to YouTube, you get that nice click through the chapters. They have to actually be able to be exported as HTML.
Scott Tolinski
They have to be exported as markdown itself for our website along with front matter and all that stuff where we have metadata about the show. And they have to be able to be exported as text in a way that works with our our YouTube setup, like I said, with the chapter. So you have text, HTML, and markdown all from the same source, which is a big goes out on RSS feed. The text goes into the YouTube, and then the markdown gets parsed and displayed on the website. Right? Correct. Yeah. So you have all these formats and that can get easy to be messed up. We're we're doing that all kind of manually right now. And so Randy has, like, a giant long list of steps. Alright. I gotta do this. I gotta copy this. I gotta move this over here. I gotta update this here. I gotta generate a time stamp. I gotta do all this, then create a PR, then upload to YouTube, then upload Tolinski and all this stuff. So you can see how these, like, kind of sequential steps are easier to automate.
Scott Tolinski
In addition, he also has to run an FFmpeg script that takes the MP 4 that has metadata chapters embedded.
Scott Tolinski
And what that does, when you take that MP 4 and we need to create an MP 3 that has that same metadata chapter embedded. So that way, when you're listening in your podcast player, you get the chapters
Wes Bos
Wes we use DaVinci Resolve to edit the thing and export it as a m p four.
Wes Bos
Yes. But DaVinci Resolve does not have the ability to embed metadata of the chapters into the m p three. So we wrote Randy like a FFmpeg script, and we actually did a show on this type of thing just to, like, make that step easier. Right? But there's that's just one part of this whole thing. And with anything, a a lot of people have said they've they've written scripts to fix this stuff because mistakes happen when there's, like, a human click. It's called ClickOps. I love that that name. You know? When when a human has to follow a bunch of steps, that's when mistakes start happening.
Scott Tolinski
Yes. Yeah. For real. And so, I mean, a Scott of it just kinda seemed right for being able to automate overall. Mhmm. And I think that's where we we just sort of looked at everything and we're like, alright. What can we do? What can we attempt to do? And so we wrote it all out in a big, you know, mind map and basically gave thumbs up to the stuff that we wanted to attack.
Frontend built with SvelteKit, backend with Rust
Scott Tolinski
And, you know, we only had 4 days to get to this. So, yeah. So we just kind of started there. Tech wise, we decided to go with Yarn and Svelte five for the the basics of everything here. Now we also use a database, which is IndexedDB via Dexi. Dexi is just a package that makes working with IndexedDB a little bit easier. Mhmm. So, like, nothing crazy there.
Scott Tolinski
But we chose Svelte five because pretty much everything I'm doing these days is in Svelte five. It does use a SvelteKit, and that actually creates itself via the the Tori, like, wizard, the the Tori getting started. Right? You do a new Tori app. It asks you what language you want to use, what front end framework you want to use, and that adds everything so that way it works correctly. You know? Yeah. It has to be like a static site. Pages have to be prerendered. There's, like, a a lot of caveats there. So it's a desktop application.
Wes Bos
The UI of the application is running in Svelte five and SvelteKit.
Wes Bos
And then the back end, which is the we'll talk about this, I guess, but, like, the FFmpeg, the processing,
Runs as desktop app with custom icon and dock integration
Scott Tolinski
that stuff runs server side via Rust? Yes. Yeah. There's a lot of interesting stuff there. And and, if you're looking on YouTube right now, it does have a an icon. It opens up as an app. It lives in my dock. It's a straight up app.
Scott Tolinski
And because of that, I had to get into signing and publishing and all that stuff, which was a a trip itself. So as far as tech goes, Wes. We we do use, Svelte five for the UI, Tori for the the entire creation of an application.
Scott Tolinski
And then we went into Rustland only for a few things, but they were kind of, like, heavier operations. We have FFmpeg, which is running as a sidecar.
Scott Tolinski
So the user doesn't have to install it themselves. It comes with the application.
Scott Tolinski
So FFmpeg and FF Probe to get data and stuff like that. There there's a lot here. So the app, which I'll show you on YouTube, again, I'm just gonna talk through it. The app itself basically allows you to create a new project. From creating a new project, it has you drop in an m p 4. And drop in an m p 4, and that m p 4, as we mentioned, has metadata associated with it. And since we have that metadata associated with it, what that is able to do is generate the show notes that we described automatically from the metadata. So when you create the chapters in DaVinci Resolve, that's writing to some basic data. So when we drop this in here, CJ was able to use FF probe to, gain information about this, including that chapter information.
Show notes generated automatically from video file metadata
Scott Tolinski
And then we were able to just send that chapter information back to the UI, run some easy peasy, you know, JavaScript on it to convert it to markdown in the format that we needed. And then from there, copying JS HTML text or markdown is all just super easy because you're just passing it through a quick little transformer.
Scott Tolinski
This this, actually this UI, which is markdown it's a markdown editor. This is, I believe, inked MD, which is not something we had used before. It was nice. What what CJ did is he looked at the tool that we are currently using for markdown, which was Dillinger.
Scott Tolinski
And he just went to their repo and found out what is the markdown tech they were using. So that way we knew that the transformation was going to be the exact same as what we were using already.
Scott Tolinski
Mhmm. So that way Wes didn't have any variation there. So, again, what we do is we have, you know, a couple of markdown fields here. They're all getting parsed into a global store inside of Svelte, and that global store is taking care of all of the transformations we might have. There's also some interesting stuff in here, like, we have templates. You know, like, when you add front matter and metadata to an episode, it's kind of complex sometimes with all of the stuff we need for our guest information. So I wanted to make it so you have a one click to add a guest. Right? That way, they don't have to guess on the indentation or the fields that exist. Here are all the fields that exist. And, likewise, one of the things that, Randy has to do is he has to generate an epoch time stamp for each episode. He had to go to another website to copy and paste a date. So I just added in a input type date that automatically creates that time stamp for us automatically directly in app. So the goal here Wes, like, alright. We have all these different steps ESLint all these different places. How can we possibly remove all of those steps? So we just kinda went down the list like that and found Yeah. Individual steps.
Scott Tolinski
Again, likewise, if you update the front matter, the Node, title and everything changes for the file Node, so everything happens in real time. And the coolest part about all of this is every single time we make a change, whether that is writing markdown, changing a title, changing a show number, any of this stuff. You make any change whatsoever.
Real-time data syncing using IndexedDB database
Scott Tolinski
And what happens is it runs through a DeXy function to save it to the database. And again, since the database itself is and I'll I'll show you this a little bit in code here. I'll talk through it. But the the cool thing is is basically, anytime we update anything, we're just running a function here that will yeah. We just run a single function and make sure our data JS in the right format, and then it puts it into the database. That's it. But since it's writing to an IndexedDB, it's so incredibly fast that we can run both a save to the database and then sync the database to the UI without ever having to worry about speed or slow down or waiting for that data to come back. Yeah. It's a reactive database. Like, the whole database is reactive. Totally. And it and if, you're you're looking at it, it is JS simple as literally writing local db projects Scott put. Like, that's it. And you just put your data, then it's in the the database. So Mhmm. We did have to do some serialization stuff for dates. Other than that, nothing too crazy.
Scott Tolinski
Another cool thing that we did was we added an import and export all data function. So you can import and export to essentially backup your data at any given point. We didn't wanna have, you know, worrying about storing this data offline and syncing. So we said, alright. If you wanna back up your data, you just click export. And all that does is it takes the Dexi database.
Added import/export database functions
Scott Tolinski
It converts it to JSON and spits it out as a JSON file, and then it downloads that file. And, likewise, the import just accepts a JSON file, populates the Dexi database. That's all you need. Right? Beautiful. So it it's so pretty simple. And we ended up using Rust for a handful of really interesting things on this. So for 1, for instance, we added open and finder buttons, which will again just open your finder. This is a Mac app, so it opens finder to find that exact file at any given point because the app doesn't care where the files live. So you just click open and finder. That's actually written in Rust. That's a a nice little quick and easy Rust function. Other interesting things we did in Rust were Wes ran the FFmpeg, which again will generate the MP 3 that happens automatically when you drop the file in. So you drop the file in, generates the pnpm 3, pulls in the show notes automatically, and that's all happening behind the scenes using Rust functions that we're invoking from the UI. So it's as simple as, like, an in and it's as simple as a invoke function with the name of the Wes function in there, and then that begins the process. Now these things don't block unless you want them to block. So we have them non blocking so they just kind of chug along in the background. Mhmm. Can I so I have a question?
Reasons for building as desktop app vs website
Wes Bos
Sure. And this is more of a question for the people listening are probably asking this right now, not not necessarily me, but why is this not just a website instead of a desktop application?
Scott Tolinski
Yeah. Desktop app gives us some a number of things. It it lets us work easier with the local file system. Now you can use the file system API, and that's totally fine. But one, this allows us to work with the local file system. 2, we get to do all this processing in its own its own, its own process on in Rust. Right? So we can just Yeah. Throw all that stuff to Rust, so it's gonna be a little bit faster. We don't have to worry about running these things within the ESLint or or a worker or something like that. Yeah. I'm curious what that would look like if if we did wanna make this. Like like, this seems like it would be pretty cool being part of
Wes Bos
the syntax, like CMS. You know? Like Yeah. Totally. Because, like, you're imagine you just edit the the show notes just right from the UI and then click save, and then it just goes right to to the website, and then it'll push itself, update the RSS feed, you know, update any any YouTube that needs to happen. But, like, the thing that would be missing there would be the the FFmpeg.
Wes Bos
Now you can run FFmpeg either in the browser via WASM, or you can run it, like, in Node. But if you were to run it in node, then you would have to upload the entire m p four. Right? This is instant where it runs, obviously, locally. Right?
Scott Tolinski
Yeah. Yeah. And it it's pretty dang fast overall. I I'm sure you could probably pull this off as a, as a website too. I think that might be an interesting an interesting thing to do. We also added a couple of small AI features. We had more, we had more planned. In fact, let me even show you if I what I do is I wanna update the title. What's a good podcast title, Wes?
Wes Bos
JavaScript is dying.
Scott Tolinski
Okay.
Scott Tolinski
Okay. So if we have a title, one of the creative things we added was just, using Anthropic.
Scott Tolinski
We just I said, hey. You're a web dev podcast.
Scott Tolinski
You Node? Generate me 10 increasingly clickbaity titles given this current episode.
Scott Tolinski
So we had a clickbait titles generator, so that way if we ever wanted to what's so funny is that, like, the number 10 on the clickbaity, it's just almost always exposed.
Scott Tolinski
Like, we did Node of the React survey, and it was like, Mark Zuckerberg exposed in the latest React survey. This one says exposed the web development conspiracy that's killing JavaScript.
Scott Tolinski
So I made a little, clickbait to non clickbait little graph here and just, you know, just in case we wanna ideate or maybe get some ideas here. I don't think we'll ever go in the 10 out of 10, in terms of clickbait. But yeah.
Wes Bos
You know what would be really cool is if this also involved the, the transcript generation. Because right now, the transcript generation happens Yes. After it's published, and then they go out and generate it. But there's a lot that would be helpful if the transcript happened immediately because then you can generate titles based on that. You can generate descriptions.
Wes Bos
You can also, like, tag. Sometimes we have issues with, like, tagging who the guest is on the transcript.
Wes Bos
Having that transcript earlier on in the creation of this, I think, would be really helpful.
Transcript generation would enable more automation
Scott Tolinski
Totally. Yeah. In fact, the auto gen TypeScript was one of the things on our list that we just couldn't get to. Because you you think all of this sounds pretty interesting and fun, and it it is. The Yeah. The thing that was the hardest out of all of this, and, unfortunately, this was just a thing CJ was tasked with, I feel bad because it wasn't me, was the OAuth flow. So with this, in this app, not only can it automatically create a pull request given the notes. Right? So it takes all the the notes. It will automatically create a pull request with the markdown notes.
Scott Tolinski
That that that in itself is great. To do that, you need to log in to GitHub. And to do that, you need OAuth.
Scott Tolinski
Now the weird thing about how Tori works is if you think about it, what do you need in an OAuth flow? You need, like, a return URL. Right? And so the return URL ended up being this kind of weird Tori internal URL.
Scott Tolinski
And we kind of even had to fake it a little bit. There's some there's some funny trickiness. In fact, I have the return URL being, like a syntax .fm page that doesn't exist at some point. So that way, it's, like, kind of taking you to a page, then this site, because it's in this window, accepts all incoming page Wes, and we just check to see what that request is and then kind of hijack it. So there's some extreme trickery going on there, and I would say it took CJ an entire day. There is a way you can do this with deep links, but we found that deep links were a little bit tough on a desktop application. A deep link, if you don't know, is a it's it's kind of like a URL for an actual app. Anytime you've ever, like, clicked on a Reddit link and it's like, open this an app, and it actually, hopefully, takes you to the right place, not so much with Reddit that often, but that that's being done with a a deep link. Right? So That's not
Wes Bos
crazy tech. Yeah. Can we talk about Bos? Like, you go to a website, and it's like, you must open this in the app to enjoy it. So you're like, alright. Fine. Open open the app. And it brings you to the App Store for an app you already have installed.
Wes Bos
Can we fix this, please?
Scott Tolinski
I feel like it's I feel like it's the app developers, because it's almost always, like I don't know. The apps that I have that aren't as good. But, yes, that always drives me absolutely nuts.
Scott Tolinski
Yeah. So that I mean, there's just so many cool things to go. I feel like I could keep going on and on. I mean, even the login stuff, like so for instance, when we when we do the login to GitHub, you you run a Torrey command that that you first, you get the window and you evaluate. You replace the window dot location.
Scott Tolinski
And then it it's like, man, it was so hacky to get this going. But once CJ got it, I I was able to do log in to YouTube easily enough. And there's all this kind of trickery stuff where we wanna, like, hide the window really quickly. Once it's done, you don't wanna show the window. The window kind of exists, but it's hidden from the start when the application starts. There's all all these kind of, like, crazier steps that we had to give you. I mean yeah. Does it pop open like a browser and ask you to log in, or does it kick it off to your default browser?
Wes Bos
It just pops open a window within Tori here. I always wonder about that because, like, it's so sketchy when these websites open up this little thing. It's like log in with Google. I'm like, how do I how do I know that this is there's no URL bar to trust.
Wes Bos
You know? I always like it when they kick me off to an external URL. It's I often think about the the Sanity uses Okta for logging ESLint things. And often, it it goes back and forth to the browser, like, 11 times. So it's like sign in to your Google account. Oh, but sign in to this. Oh, but open a desktop app. Oh, you know, like, I do not envy the person that has to debug that workflow.
Scott Tolinski
Yeah. For real.
Scott Tolinski
So, yeah, if you're looking at this on YouTube right now, I made this all the c g or the CSS, I made this straight up from scratch here. No frameworks or anything. Just got going with it. Svelte adds some nice little animations in here, some nice little page fades. Just overall, it feels like an actual desktop app. And I thought that was a a really fun thing to do. The publishing, believe it or not, was some of the hardest stuff because the moment that you get ready to publish, you don't realize 1, that the build sure. If the Svelte app builds, that's fine. That does not mean your app's going to work. Because what ended up happening is is as we we we talked in our our Tori Node, and Tori's pretty locked down. So what I found, it was like playing whack a mole with content restriction stuff in terms of like, alright. Now you need permissions for that. Now you need permissions for this. Now you need to tell, you know, that I can actually even hit GitHub from this thing. So by default, the cool thing about Tori is that it's super duper locked down. That just makes trying to finish up if you're not familiar with all of the little things. You're constantly kinda going back and forth between alright. I need to do this. I need to do that. There's also some, like, weird programming stuff with, like, the window size. Like, I always wanted the window size to be, like I think it's, like, 70% of the the total browser width. And then because it was creating it always really little tiny little window, and you had to always drag it. It was also always having, like, a title bar. So it's like the amount of work we had to do to get rid of the title bar was tough. I had to bring in, like, a full plug in for that. Oh, man. I had to do math to generate the window size, to calculate the window size based on the monitor size. Like so nothing nothing's necessarily easy in this. But at the end of the day, it's a pretty polished little app that we could just send a DMG to and have that that working really nicely. And the again, one of the hardest parts is after everything was done, I was like, yes. Finally got this building. The app is finally working. And then I I get it to our editor. You know, it's version 1. And he's like, oh, it says macOS says the application is damaged.
Scott Tolinski
That's because you need to sign your apps. Luckily, I have a Apple developer account. I just used my own personal Apple developer account for this just because I wanted to to get it out. I had to create a certificate, download their certificate, do it all whole bunch of stuff in Xcode. I mean, I was like getting into Mac OS stuff to sign this. And once I got it signed, it all worked just fine.
Wes Bos
But if you haven't done that process before of signing an application with the SDK it, and it still didn't work fine. Like, I had to go into my privacy settings and allow unidentified developer.
Difficulties getting app signed and notarized
Wes Bos
Yep. Like, that's why the guy we had on from Tori the other day, he's building, what, ToriCloud or something like that. Because, like, that whole process of signing everything is such a pain in the butt. It's such a pain. And and I know even, like, the export flow,
Scott Tolinski
when you have, like, updates and stuff is gonna be a pain. But, man, the experience is really cool. I'm gonna show you is this the debug? Yeah. This is the release. Wes, I even made a check this out. I even made a nice I love that. Saw this. Yeah. The drag to install. I got to make it. Yes. I made a custom drag to install that had a nice little background, which is actually kinda tough to make sure that this was inside it. Like, the logo was inside of the square and all that stuff. But, you know, sometimes you you get a custom little drag to install inside the DMG, and it's a nice experience. So, I thought I'd button it up with all that.
Wes Bos
And, theoretically, you could make this a Windows application as well, right, if we are to be so sad as to hire somebody who runs Windows?
Scott Tolinski
Yes. The theoretically, there is a number of macOS specific hacks in here, but I think they would be fairly trivial to get going. I think some of those things are more along the lines of, like, assuming things were installed at a certain location. For instance, f f probe, for some reason, the side the sidecar wouldn't work, so the user needs to install f f probe on their machine.
Scott Tolinski
But it was in production.
Scott Tolinski
It was not finding f f probe. This is one of the ones I had to dive through the debug logs in. In production, for some reason, would not find FF probe even though it found it just fine in development.
Wes Bos
Like, I I don't know why. Oh, man. Paths, shells. It's probably like a child process that is like a a shell that doesn't have the the full path available to it. Like, having stuff not in the path correctly is the bane of my existence.
Scott Tolinski
Yes. And you know what? I thought I had everything in the path correctly. So what I had to do this is, like, 9th hour stuff. I had to write a function that, I'm trying to find it here. I had to find a function that guessed where the FF probe may be located.
Scott Tolinski
So the first thing it did is check your bin, then it checks local bin, then it checks Homebrew, then it checks program files, FFmpeg.
Scott Tolinski
And if it's not in one of those, then it's like, hey. You probably don't have it installed. Go install it, for you. So yeah. It it was yeah. That would that was the whole thing. 1st, check to see if it's in path. If it is in path, then return that path. Otherwise, here are some common paths. Run through them. Check to see if it's in any of those.
Scott Tolinski
Otherwise, you can set an e n v variable with a a hard coded path. And that's all just to give people the option. But this was yeah. This was an end of the line function here to just get this working.
Wes Bos
And the idea with the idea with sidecar is that there's, like, a Rust version of FFmpeg that's already been bundled up that will just work anywhere Rust runs?
Scott Tolinski
Yes. In the past, I've had it so that you just run the command and you just have the user install FF Probe FFmpeg on their own. Yeah. Yeah. I think it's just easier if you get it all bundled up. And by all means, FF Probe should be bundled up in this as well, but I think it might be buggy, or we just couldn't get it working or whatever. Funky with it. Yeah. Yeah.
Wes Bos
Oh, well, we had our we had our fair share share of WASM FFmpeg issues as well deploying in production as we needed it to, like, copy paste into our Vercel serverless function because Vercel was not bundling it with the with the actual function. So there's it's annoying, but it's well worth including your own so you don't have to ask the user where it is or try to find it.
Scott Tolinski
Yeah. Yeah. And, I mean, it it's just a lot. And I'll show you really quick. Here's what the logs look like when you're in production. These are what the logs look like, and they're infinite. So, like, anything you could possibly want it, you know, it's all in here. And sometimes sometimes there's some actual relevant information here if you can find it. That was like it it for those of you not watching, it is a massive dump of just text drinks. It's just like just I mean, if you ever looked at those Apple logs and try to decipher it, it's it's crazy to to figure this out. And this is all mostly stuff that I'm logging, but Tory JS also logging a fair amount of information. So trying to find out, like, what was going wrong at any ESLint. This is actually where, like, chat g p t came in or or Claude came in really handy. I was able to give it a massive amount of these logs and just say, parse this out for me. Like, where is the error in this? I can't even see the error in this. There's so many logs. I cannot see the error. And then, luckily, it would almost always find it for me. So, that that's a good use case for an LLM in this type of thing because, again, I'm not asking it for its opinion on something. I'm just saying, hey. The error JS in here. I know you can find it. Beautiful. Well, I'm excited to see where this goes. I think it's, gonna be a super handy tool for our whole publishing process. Take a lot of Totally. Like, speed us up JS well as make the
Wes Bos
actual show notes and and things like that much better.
Scott Tolinski
Yeah. And check it out, Wes. I Node a light mode for you. The whole thing works light and dark mode. Oh, god.
Scott Tolinski
So and this JS, I mean, this is like a what? A 4 day project. And look at how nice the light mode looks. I even have a custom, menu in here and everything. So light mode, dark mode, that works, and that's all through if you wanna go and learn how I did a light mode and dark mode this easily, I did it using the techniques exactly that we talked about in the effortless light and dark mode episode we did on syntax just a little while ago. So if you wanna learn those techniques, this was super duper easy to get working light and dark mode on this machine. So that's really all I have about this unless you have any extra questions or any anything else. No. Let's wrap it up. Thanks, everybody, for Tingling. We'll catch you later.