Notes

A public collection of notes and learnings that might help and inspire others.

Table of Contents   (Skip)

#28   Batch-replace a string in many folders and update GIT

Posted:

Needed to replace Skypack with another CDN across many projects.

This script automates the taks and should serve as a starting point for many similar use cases:

#!/bin/bash

for D in *; do
if [ -d "${D}" ]; then
cd ${D}

echo "processing ${D}..."

( shopt -s globstar dotglob
sed -i -- 's/cdn[.]skypack[.]dev/esm.sh/g' **/*.js
sed -i -- 's/cdn[.]skypack[.]dev/esm.sh/g' **/*.html
)

echo "updating git..."

git add --all
git commit -a -m "replace skypack"
git push

echo "${D} processed!"

cd ..
fi
done

#27   Azure SWA: Function host is not running.

Posted:

An Azure Static Web App (SWA) last deployed in late August 2022 showed the message “Function host is not running.” after a recent deployment.

Local testing with Azure’s SWA CLI also showed errors. Further investigation showed that the “Minimum extension version” in host.json had to be changed:

"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
}

Reason: EOL for extended support of the runtime version for functions on December 13, 2022.

Source: Azure Functions Docs

#26   11ty: 'draft' status in front matter

Posted:

My site didn’t have anay status for content yet and I felt like unpublishing some older posts. Now (ab)using ‘draft’ status for that, this is how (in .eleventy.js):

const publishedContent = (item) => !item.data.draft

//...

config.addCollection('blog', async (collection) => {
return collection.getFilteredByGlob('./src/blog/*.md').filter(publishedContent)
})

Source: giustino.blog/how-to-drafts-eleventy

#25   Get OneDrive ‘driveId’ of a private Teams channel

Posted:

URL decode what’s displayed in your browser’s address bar when viewing the root page of your Teams channel’s documents library in SharePoint. We need channelId and groupId:

https://your_company-my.sharepoint.com/personal/THIS_IS_YOU/_layouts/15/onedrive.aspx?id=/sites/TEAMS_CHANNEL_NAME_FULL/Shared Documents/CHANNEL_NAME&listurl=https://your_company.sharepoint.com/sites/TEAMS_CHANNEL_NAME_FULL/Shared Documents&remoteItem={"mp":{"webAbsoluteUrl":"https://your_company.sharepoint.com/sites/TEAM_NAME","listFullUrl":"https://your_company.sharepoint.com/sites/TEAM_NAME/Shared Documents","rootFolder":"","channelId":"CHANNEL_ID_","groupId":"GROUP_GUID","displayName":"CHANNEL_NAME"},"rsi":{"listFullUrl":"https://your_company.sharepoint.com/sites/TEAMS_CHANNEL_NAME_FULL/Shared Documents","rootFolder":"/sites/TEAMS_CHANNEL_NAME_FULL/Shared Documents/CHANNEL_NAME","webAbsoluteUrl":"https://your_company.sharepoint.com/sites/CHANNEL_NAME_FULL"}}

Head to MS Graph Explorer at: developer.microsoft.com/en-us/graph/graph-explorer#

Use the endpoint at /v1.0/teams/{teamId}/channels/{channelId}/filesFolder with the two ids previously obtained from the URL.

NB: you might have to set some permissions for the request to go through.

Eventually, the request should lead to a response that contains the driveId we were looking for:

"parentReference": {
"driveId": "b!...yourDriveId",
"driveType": "documentLibrary"
},

#24   Vite: HTTPS on localhost (basic SSL)

Posted:

Had to look this up today, here’s how to do it.

First install a Vite plugin:

npm install -D @vitejs/plugin-basic-ssl

Then use it in vite.config.ts:

import basicSsl from '@vitejs/plugin-basic-ssl'

export default {
plugins: [
basicSsl()
]
}

NB: works with Astro too, simply create the Vite config file in case it’s not there yet.

#23   Remove SentinelOne Browser Extension

Posted:

I don’t tolerate spyware, no matter where it comes from. Here’s what you can do if you’re on Windows and got S1 forced into your browser/s:

  1. Open regedit
  2. Navigate to HKLM\SOFTWARE\Policies\[Vendor]\[Browser]
  3. Get rid of whatever keys you find in ExtensionInstallForcelist
  4. Close your browser/s
  5. Clean out your user profile’s extensions folder:
    Edge: [LocalUserProfile]\AppData\Local\Microsoft\Edge\User Data\[EdgeProfile]\Extensions\[S1ExtensionID]\
    Firefox: [LocalUserProfile]\AppData\Roaming\Mozilla\Firefox\Profiles\[FFProfile]\extensions\sentinelone_visibility@sentinelone.com.xpi

NB: survives reboots, will have to see whether or not those policies are ever getting reapplied.

#22   Restore WSL2 distro after user profile migration

Posted:

Recently got my work notebook’s user profile changed to a different one. WSL was still installed and working, but didn’t recognize my Manjaro installation anymore (despite of it being in the exact same physical location on a separate non-OS partition)

This is what I did to get it running again:

  1. Set WSL2 as default: wsl --set-default-version 2 (this is crucial)
  2. Install a clean new instance
  3. Make sure it’s installed properly, then run wsl —shutdown
  4. Remove the newly created vhdx file and replace with your old one
  5. Run something like Manjaro.exe config --default-user <username> to make your previous user the default user
  6. Set Windows Terminal as WSL’s default: {InstanceName}.exe config --default-term wt
  7. Restore your WT settings (i.e. from your old user profile, a path like C:\Users\user.name\AppData\Local\Packages\Microsoft.WindowsTerminal_123xyz\LocalState)

NB: really pleasant surprise to find everything just as I’d left it after having figured out to above steps :)

#21   SSH: no matching host key type found. Their offer: ssh-rsa

Posted:

Got this error when trying to use sftp in a Linux terminal.

Resolution:

Edit /etc/ssh/ssh_config, add those lines:

HostKeyAlgorithms ssh-rsa,ssh-dss
PubkeyAcceptedKeyTypes ssh-rsa,ssh-dss

NB: might break some other stuff (GitLab in my case), so be careful and remember you added those line.

#20   Vue: 'Array.push()' used in Vuex mutation not triggering 'watch()'

Posted:

I was doing this in Vuex:

ADD_RECIPE_USER(state, value) {
state.userRecipes.push(value)
}

It was working fine, but a watch() effect used in a Vue component didn’t catch the update while going back and forth between routes made the updated data appear as if it was always there.

After adding a bunch of cheap console.log() ‘breakpoints’, I eventually got to the bottom of it and changed my code to this:

ADD_RECIPE_USER(state, value) {
state.userRecipes = [...state.userRecipes, value]
}

Conclusion: watch() paired with Vuex getters seems to require the state.key = newValue assignment.

Seems a bit weird, but might have been mentioned in the docs somewhere.

#19   Windows 10: process 'System' causing constant high disk activity

Posted:

Symptoms:

  • Fresh Windows 10 installation on a new device (build 19043)
  • Drive C:\ BitLocker encryted
  • Drive D:\ not encrypted but on the same hardware as drive C:\ (Intel SSD)
  • Constant disk activity of > 50% by “System” process, starting right after boot
  • Read/write to HardDiskVolume4 (D:\ in my case)

Resolution:

Turned on BitLocker for drive D:\ as well.

No more constant high disk activity after reboots, system feels more responsive in general.

NB: found this when systematically stopping running services due to the lack of a better approach.

#18   Netlify: secure HTTP headers

Posted:

Sensible default config that returns an A+ in Mozilla Observatory:

[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; img-src *; frame-ancestors 'none'"
Referrer-Policy = "same-origin"
Strict-Transport-Security = "max-age=63072000; includeSubdomains; preload"
X-Content-Type-Options = "nosniff"
X-Frame-Options = "DENY"
X-XSS-Protection = "1; mode=block"

NB: adjust CSP as necessary when working on sites that actually load scripts.

#17   Vue: limiting the number of array items inside 'v-for'

Posted:

Didn’t know that this could work, but it does:

<Component v-for="(item, index) in list.slice(0, 20)" ... />

Use case: getting the first 20 items of an array.

If the array is shorter, all items will be displayed.

#16   JavaScript: native Date formatting

Posted:

I needed to format a date from new Date() recently and using any libs (like date-fns or moment.js) and was not an option; doing that would have been overkill for the small project I was working on.

After a bit of research, I came across the Date.prototype.toLocaleDateString method and implemented it like this:

const getDate = () => {
let date = new Date();

let dateOptions = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
};

return date.toLocaleDateString('en-US', dateOptions);
}

Result: Tuesday, April 13, 2021

Source: Natively Format JavaScript Dates and Times

#15   CSS: scroll-behavior

Posted:

I was aware of the existence of the scroll-behavior attribute, but I never really used it so far (‘thanks’ to jQuery, scrolling libs or framework internals handling it themselves).

When added to a site (like this one), it’ll make all your anchor-based navigation feel buttery smooth - who wouldn’t want that?

*,
*::before,
*::after
{
scroll-behavior: smooth;
/* etc. */
}

And that’s it. This one line covers everything: from simple “back to top” links to anchor-tag navigation across pages - they’ll all be smooth now.

Browser support: caniuse/css-scroll-behavior

PS: you can try how it feels right away; just click on any of the #-prefixed anchors in front of the TIL item titles on this page.

#14   GIT: SSH with multiple identities for the same host

Posted:

Had some issues working with 2 different accounts at GitHub recently.

Initial git clone with the desired identity (specified explicitly) sets up the repository correctly:

git clone -c core.sshCommand="/usr/bin/ssh -i /home/me/.ssh/id_rsa_foo" git@github.com:me/repo.git

GIT config should then contain this automatically:

[core]
...
sshcommand = /usr/bin/ssh -i /home/me/.ssh/id_rsa_foo

Source: stackoverflow.com/a/41947805

#13   Get mobile browsers to display the correct Favicon

Posted:

Mobile browsers were driving me crazy for a while - they kept choosing the one icon with a solid background (that apple touch thing) over all the other icons.

Looks like the issue was caused by the fact that the lazily copy/pasted code from a very well known Favicon generator did not account for the larger icons in the <head>. When I added the bigger icons there, mobile Chromium-based browsers started correctly displaying the desired icon.

For future reference:

<link rel="manifest" href="/site.webmanifest">
<meta name="theme-color" content="#012B48">

<link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png">
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">

<link rel="apple-touch-icon" sizes="180x180" href="/apple-180x180-solid.png">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">

<meta name="masapplication-config" content="/browserconfig.xml">

Other findings during the process of researching Favicon details:

  • keep favicon.ico in the site root, no need to reference it in the <head>
  • unless “Add to Homescreen” is used, the icons in site.webmanifest don’t seem to matter
  • mobile browser icon caching is wonky, always test on multiple devices and/or with freshly installed browsers

Good read on this topic: the-essentials-of-favicons

#12   Fauna DB: response size and pagination

Posted:

Maybe I’m lazy, but I didn’t really feel like implementing proper pagination for DB query results that hardly ever reach the 2 digit KB numbers.

To make sure you’ll get all the queried DB rows in the response, Faunas’s Paginate() function needs a size parameter:

exports.handler = (event, context, callback) => {
// init stuff
return client.query(q.Paginate(q.Match(q.Index('indexName'), 'term'), { size: 500 }))
.then((response) => {
// the rest of your code
}).catch((error) => {
// the rest of your code
})
}

Keep in mind that you’ll only get 64 results if this parameter is not specified!

#11   Fauna DB: EPIPE error

Posted:

Quick note on an eror I got from Fauna DB recently:

FetchError: request to https://db.fauna.com/ failed, reason: write EPIPE

Apparently that happens due to a known issue related to AWS Lambda connections (which are also used to execute Netlify functions).

To fix that “frozen execution context”, simply change the function to this syntax:

exports.handler = (event, context, callback) => {
const client = new faunadb.Client({
secret: process.env.FAUNA_SECRET
});
const q = faunadb.query;

// the rest of your code
}

#10   Vue3: handle page refresh (F5) in your application

Posted:

I recently noticed that there was an issue with cross device data in WATCH3R. Adding/removing list items on one device would not update the app’s (cached) state on another device. Logging out and back in would resolve this issue, but refreshing the page wouldn’t.

After a bit of research, I found out that there wasn’t really a “best practice” on how to handle page refresh in Vue, so I simply tried some things that seemed promising.

What I ended up with is the following:

const updateList = () => {
if (mode.value) { // double check 'mode' here, just in case
store.dispatch('list/readList', mode.value);
}
}

onMounted(() => {
setTimeout(updateList, 250) // timeout is required here; otherwise 'route.meta.mode' is undefined
})

I placed this code directly in App.vue, which is the entrypoint of my application and gets re-loaded on page refresh. route.meta.mode controls which list’s data to load - I decided against re-loading all list data, as that wouldn’t be in line with the app’s caching strategy.

12/2021 - Refer to the following commits on how to block page refresh when handling unsaved changes:

#9   GoTrue Admin methods

Posted:

Something I noticed trying to get a list of my app’s users from Netlify’s Identity service.

It’s not a big challenge to write the necessary function (see: GitHub readme), but what it doesn’t mention (clearly enough…) is that this admin function needs to be deployed to your site to work.

I struggled for a bit, looking for mistakes that weren’t there while receiving 401 authorization errors…

NB: deploy the admin function/s, keep the UI to use them offline and disable the function when not needed anymore (I resorted to renaming it function.js.bak just in case I need it again in the future).

Update Dec. 3rd 2020: I created a pull request and the readme (see link above) was updated.

#8   Vue3: <select> default value

Posted:

Cost me more time than I anticipated, esp. due to the fact that it only happened for production builds.

So, in order for a <select> (with an Object-based v-model) to properly display its default value in Vue 3 (3.0.2), this had to be done:

<select ... v-model.lazy="selected" @change="update(selected)">
<option disabled :value="{}" :selected="selected === {}">Select Something...</option>
<option v-for="(item, index) in list" :key="index" :value="item"></option>
</select>

The :selected="selected === {}" was necessary, as the <select> would otherwise display as a blank box once the v-for rendered <option> elements came in.

Again, this only happened in production, local dev builds did not behave that way which made it extremely frustrating to debug.

Probably worthy of further investigation and a report to the Vue team, but I don’t have time to try and get a reproducible example done at the moment.

#7   Vue3: "click outside" directive

Posted:

Directives are quite different in Vue 3 - see: Vue 3 docs

Here’s what I came up with for a simple “close an element (modal etc.) when a click outside of this element registers” directive. Clicks on elements within the target element and elements with the class click-outside-ignore will be ignored.

The function/method to close the element is registered as binding.value, so when using it on a component, it should look like this: <Component v-click-outside="closeComponent" />

let handleOutsideClick = null;

app.directive('click-outside', {
beforeMount(el, binding, vnode) {
handleOutsideClick = (e) => {
e.stopPropagation();
if(!el.contains(e.target) && !e.target.classList.contains('click-outside-ignore')) {
binding.value();
}
}
document.addEventListener('click', handleOutsideClick);
document.addEventListener('touchstart', handleOutsideClick);
},
beforeUnmount() {
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener('touchstart', handleOutsideClick);
}
});

#6   Hugo: RSS feed duplication

Posted:

Checking Google Search Console earlier this week made me notice that Hugo created duplicates of my RSS feed in strange places like /tags/feed.xml and /tags/tagname/feed.xml.

After a quick look at the Hugo documentation, I found out that this behaviour seems to be the default for pages of the kind taxonomy and term - seems I forgot about that or never even noticed it in the first place…

Conclusion: I had to change the [outputs] section of my config.toml from this:

[outputs]
home = ["HTML","RSS"]
section = ["HTML"]

To this:

[outputs]
home = ["HTML","RSS"]
section = ["HTML"]
taxonomy = ["HTML"]
term = ["HTML"]

#5   Android: use ADB to uninstall bloatware

Posted:

I recently got the Android 10 update and had to factory reset my phone thanks to the phone’s language settings not sticking anymore. Factory reset unearthed all the bloatware again that I had gotten rid of years ago, so I also had to do that again.

Here’s a quick reminder of how to do that:

  1. Get ADB platform tools for your OS
  2. On your phone: enable developer mode and USB debudding
  3. Connect phone
  4. Test if connected successfully and allow debugging (will be prompted): adb devices
  5. adb shell to get into the phone
  6. List packages with pm list packages | grep 'pkg.name.etc' or search for them like this: pm list packages -f TESTPKG
  7. Uninstall bloatware like this: pm uninstall -k --user 0 com.android.google.youtube
  8. See bloatware disappear 😁

Based on this guide at xda: How to Uninstall Carrier/OEM Bloatware Without Root Access

#4   Shell Script: launch VS Code for a specific folder/repository

Posted:

I wanted to have an easy way to launch VS Code for specific repositories and came across the code PATH-TO-REPO command.

This little shell script will open a specific folder/repository based on an argument given to it or based on a directory listing of the /home/user/Repos folder:

#!/bin/bash

# get param
repo=$1

if [ -z "$repo" ]
then
# check first
ls /home/USER/Repos/

# ask user for input
echo -n "which repo: "
read repo
else
echo "opening "$repo
fi

code /home/USER/Repos/$repo

I’ve also configured an alias for that script; simply typing lcode REPO-NAME now opens VS Code for that folder/repository.

#3   Vue2: handle component's click event in its parent

Posted:

Use case: a component creates nothing but a <button> with a slot that handles its display state internally but hasn’t got any actual functionality; the handler method is defined in the parent.

In this case, @click.native has to be used to call the method defined in the parent:

// in parent component

<cButton @click.native="theMethod(praram)">Button Name</cButton>

#2   Manjaro Linux mount script: BitLocker encrypted external drive

Posted:

Got a new external USB 3.0 drive and set up BitLocker encryption. Manjaro (Dolphin) couldn’t really do anything with it, kept prompting for the pwd but didn’t mount it.

I ended up writing my own mount script that works well so far:

#!/bin/bash

# check first
lsblk -f

# ask user for input
echo -n "input details (fstype bitlocker): "
read device
read -sp 'Magic powder: ' mgk

# mount according to user input
sudo fusermount -u /media/data/drive/ # unmount in case it's still mounted for some reason
sudo dislocker -V /dev/$device -u$mgk -- /media/data/drive
sudo mount -o loop /media/data/drive/dislocker-file /media/data/drive-mnt

echo mounted drive

It lists connected block devices first (lsblk), so it’s not hardcoded to sda/sdb in case that ever changes. Need selection of whatever mentions BitLocker, then asks for the encryption key/pwd and proceeds to mount the drive with dislocker. Folders used in /media/data/ need to exist before the script is executed.

#1   Manjaro Linux: .service file error when setting up KeeWeb

Posted:

Installed KeeWeb on Manjaro and got the following error:

The name org.freedesktop.secrets was not provided by any .service file

Resolution: had to install a package called gnome-keyring.