Content collections are the best way to manage content in any Astro project. Using the Content layer API we can define a collection to provide automatic TypeScript type-safety for all of our content.
Let’s say we want to build our personal blog using Astro; using a collection we can turn a bunch of Markdown files (or MDX, Markdoc, YAML, or JSON files) stored locally in our project as the source of our blog content.
NOTE
This API allows us to load local content (aka files in our filesystem), but there are also third-party loaders (our create our own custom loader) to fetch the content from remote sources (think headless CMS).
To define a collection, we must create a src/content.config.ts file in your project, which Astro will use to configure your content collections (we can define many collections).
This file used to go in src/content/config.ts (the old location, Astro API moves fast), but apparently, now it has to be placed in src/content.config.ts. The glob function allows us to do two things:
Set the folder for our collection, using base (relative to project root).
Exclude some folder/files from being parsed, using the pattern option (!_**/*.md won’t match folders starting with un underscore)
NOTE
We are using TypeScript file (*.ts) to configure our collection, but it’s also possible to use JavaScript (with the .js extension) to define our collection; or even a Michael Jackson file (.mjs).
Keep your Astro source private and publish only the static build
GitHub Pages for private respositories is not supported on GitHub Free (read here) and I didn’t like the idea of making the whole blog’s source code and post drafts publicly available. So I had to come up with a solution.
NOTE
I’m aware that Cloudfare Pages it’s free, faster than GitHub Pages, and supports private repos. But I also had the comments section tied to the public repo so. Maybe next time…
The idea is simply using a two-repository strategy:
Private repo - Where we keep our source code with drafts, work-in-progress posts, and all our Astro source files.
Public repo - Just the contents of the dist/ folder, for GitHub Pages deployment.
By deployment target, I mean the public repo from where we will will serve the site to the world, using GitHub Pages.
NOTE
Remember, our private repo is where we write posts, keep drafts, and build the Astro project. The public repo is just where we push the final static output.
To publish when changes are pushed to a specific branch.
Or you can write a GitHub Actions workflow to publish your site.
We’ll be using the first option; just click on the Settings tab of your repo, and once there, find the Pages slot in the left sidebar. Mine looked like this:
What this means is that whenever we push changes to the master branch (root folder /), our GitHub pages will be published.
Before building our site, we need to tell Astro the final public URL where our site will be published. This is controlled by two settings in astro.config.ts:
base is the path under that domain where your site will live.
Since we will be serving the page from the URL of the public repo, we need to add it to our astro.config.ts:
exportdefaultdefineConfig({
site:'https://<username>.github.io',
base:'/',
// more stuff...
})
If your public repo is the special GitHub Pages repo named <username>.github.io, then the site is served from the root:
https://<username>.github.io/.
TIP
In my case, since it was my personal page, I used the https://<username>.github.io URL. But it doesn’t have to be, any public repo URL will do, e.g. https://github.com/<username>/<repo-name>.
But if you’re publishing to a repo named foo, then you should use something like:
Not much to say here, whenever you add a new post, and are ready to publish, you need to build right? Let’s assume you have the following build script in the package.json of your private repo:
"build": "astro build"
We just have to change to the root of your private repo, and run:
Terminal window
npmrunbuild
In the case of an Astro project, we should end up with a dist folder containing the artifacts of our build.
Before start writing the action, we need to create a GitHub Personal Access Token tied to the user that has push access to the public repo (myself).
In order to do that we have to click on our user logo (upper right corner), and once the sidebar opens up, click on Settings → Developer Settings → Personal access tokens. I selected Tokens (classic), then Generate new token, and Generate new token (classic).
NOTE
Make sure you do not over-scope the token. For your use, just public_repo is enough to gives the token access to public repositories, which is enough for your workflow to push the built dist/ files to.
Give it a descriptive description (under Note); I named mine Deploy code-blue to lifeBalance.github.io. Once the token has been generated, you are redirected to a new page; you should copy the token name GH_PAGES_TOKEN.
WARNING
Make sure to copy your token now as you will not be able to see it again.
If for some reason you didn’t copy the token, just regenerate it again, no big deal.
Now we need to add the PAT as a secret (e.g., GH_PAGES_TOKEN) in the private repo:
In our private repo, we go to Settings → Secrets and variables → Actions
Click New repository secret.
Name it GH_PAGES_TOKEN (or whatever you name it in the *.yml file).
Paste your copied PAT.
That’s it — now your GitHub Actions workflow will automatically have access to it through ${{ secrets.GH_PAGES_TOKEN }} — no manual shell exports needed ever again.
If your GitHub Pro subscription expires and you can no longer publish GitHub Pages from a private repo, you do not need to delete your old GitHub Pages workflow. In my case, I kept .github/workflows/astro.yml around and disabled it.
Go to the Actions tab.
Find the Deploy Astro site to Pages workflow. That is the workflow name from astro.yml.
Open any previous run of that workflow.
Click the ••• menu in the top-right corner.
Click Disable workflow.
IMPORTANT
This only disables astro.yml. It does not affect deploy.yml. The file stays in the repo, but GitHub will not run it unless you enable it again.
If you later re-enable GitHub Pro and want to deploy directly from the private repo again:
Disable deploy.yml, the workflow that pushes the build to the public repo.
Re-enable astro.yml.
In the private repo, configure GitHub Pages to use GitHub Actions as the source.
If you’re gonna be writing apps for iOS or macOS, most probably you should be installing Xcode, but if that’s not the case, probably it’s enough to install the command line tools:
Terminal window
xcode-select–-install
NOTE
Trying to run an unknown command such as git, will also cause the system to prompt us to install the command line tools.
To verify that the command line tools have been installed, you can run:
Terminal window
xcode-select-p
The output of the command above should be the location of the **command line tools in our system, in my case /Library/Developer/CommandLineTools. If you’re curious about what tools exactly are we getting, run:
zsh-syntax-highlighting plugin, which enables highlighting of commands whilst they are typed at a zsh prompt into an interactive terminal. This is super helpful for catching typos that would result in syntax errors. Read how to install it in oh my zshhere
zsh-autocomplete plugin, which provides real-time type-ahead autocompletion to your command line. This one doesn’t include instructions about how to install in oh my zsh. Basically we just have to clone it in the plugins folder:
Note how above we’re using the ZSH_CUSTOMenvironment variable, which is used in Oh My Zsh to specify a custom directory for your plugins, themes, and custom configurations.
Once we’ve installed the plugins we want, we have to add them to the list of plugins in a our .zshrc file; this is what my list looks like:
Terminal window
plugins=(
git
zsh-autosuggestions
zsh-syntax-highlighting
zsh-autocomplete
)
To uninstall any of the plugins, we just have to remove it from the list of plugins above, and remove its folder; for example, to remove the zsh-syntax-highlighting folder:
Fzf is a command-line fuzzy finder which I find super useful. Let’s install it with brew:
Terminal window
brewinstallfzf
Here I had some problems integrating this tool with zsh, but searching through the internets I found out that we have to run an installation script to generate the necessary configuration files:
Terminal window
$(brew--prefix)/opt/fzf/install
NOTE
The $(brew --prefix) part is a command substitution that gives us the folder where Homebrew installs all the stuff; so if you run brew --prefix the output in my case, at the time of writing this, was /opt/homebrew (back in the day it was some other folder).
Running the command above appends the following lines to the bottom of our .zshrc:
Terminal window
exportNVM_DIR="$HOME/.nvm"
[ -s"$NVM_DIR/nvm.sh" ] && \."$NVM_DIR/nvm.sh"# This loads nvm
[ -s"$NVM_DIR/bash_completion" ] && \."$NVM_DIR/bash_completion"# This loads nvm bash_completion
To verify the installation we have to run:
Terminal window
command-vnvm
which should output nvm if the installation was successful. Please note that which nvm will not work, since nvm is a sourced shell function, not an executable binary.
NOTE
To download, compile, and install the latest release of Node.js, do this:
Terminal window
nvminstallnode# "node" is an alias for the latest version
In mathematics and computer science currying is the technique of translating a function that takes multiple arguments into a sequence of families of functions, each taking a single argument.
One constraint of FP is that functions should only receive one input, which are technically known as unary functions. We can think of a function as a pipe that takes in one input and produces one output. This is similar to how a water pipe takes in water and produces a flow of water out the other end.
But what if we need to write a function that takes in multiple input values? We could pass all of the inputs as properties of a single object and call it a day. For example:
greet({name:'John', greeting:'Good morning'}); // => Good morning, John!
This is a perfectly valid approach, but it does introduce some potential issues that can make functions less predictable and harder to optimize in a purely functional paradigm:
We introduce potential mutation risks if the object is modified elsewhere in the program.
We’re requiring the caller to know the exact property names. If the function expects { name: string, greeting: string } but receives { name: 'Alice' }, it could break at runtime.
The FP approach to deal with this is to create an outer function that receives a single input, wrapping an inner function (also unary). For example, let’s rewrite the previous example in a curried way:
functiongreet(greeting:string) {
returnfunction(name:string) {
return`${greeting}, ${name}!`
}
}
constresult=greet('Hello')('Bob')
console.log(result) // => Hello, Bob!
Note that greet is a function that returns another function. The first function takes in a single input, greeting, and returns another function that takes in a single input, name. The inner function then produces the final output. For example:
console.log(greet('Hello'))
We’re calling the only the outer functiongreet, this is the output:
Using arrow functions this resembles a series of pipes that take in one of the inputs and pass the output to the next pipe, creating a sort of pipeline.
This is similar to how a water pipe can be connected to multiple other pipes, each taking in one input and producing one output. Let’s create another simple example:
typeSum= (a:number) => (b:number) =>number;
constsum:Sum= (a) => (b) => a + b;
constresult=sum(1)(2);
console.log(result); // => 3
You may be wondering, how this is useful? Well, imagine we want to write a function that increments a number by a certain value. We could write a function reusing our sum function:
Now imagine that we have a function that takes in multiple inputs, and we want to curry it so we can use it as a curried function:
// Just an usual binary function
constnormalSum= (a:number, b:number) => a + b
typeCurry2= (f: (a:number, b:number) =>number)
=> (a:number)
=> (b:number)
=>number
constcurry2:Curry2=f=>a=>b=>f(a, b)
constcurriedSum=curry2(normalSum)
constpartialResult=curriedSum(1)
console.log(partialResult) // b => f(a, b)
constfinalResult=partialResult(41)
console.log(finalResult) // 42
In the code above:
normalSum is a normal function that takes in two inputs.
curry2 is a function that takes in a function f and returns a curried version of it.
curriedSum is the curried version of normalSum.
partialResult is the result of calling curriedSum with the first input, which returns a function that takes in the second input.
finalResult is the result of calling partialResult with the second input, which produces the final output.
So what’s the point of currying?
It allows us to create partial functions that can be reused in different contexts ().
It allows us to write multi-argument functions, which is easier to reason, then turn them into their curried versions.
We can use the partial functions to create other versions of the same function, like increment and decrement in the previous example.
Some libraries that include multi-argument functions can be curried, and the resulting functions can be used in a more functional way.
Developers often write regular functions first and curry them later if needed. Many libraries, like Lodash, provide a _.curry function so you don’t have to manually curry everything.
Let’s rewrite curry2 as a generic curry function that can curry any function, regardless of the type of their arguments. Here’s an example:
typeCurry2= <T, U, V>(f: (a:A, b:B) =>C)
=> (a:A)
=> (b:B)
=>C
This is a generic function that takes in a function f that takes in two arguments of type A and B, and returns a value of type C. The curried version of the function takes in the first argument of type A, and returns a function that takes in the second argument of type B, and returns a value of type C. Let’s see how this works in practice:
constnormalSum= (a:number, b:number) => a + b
typeCurry2= <TA, B, C>(f: (a:A, b:B) =>C)
=> (a:A)
=> (b:B)
=>C
constcurry2:Curry2=f=>a=>b=>f(a, b)
constcurriedSum=curry2(normalSum)
constpartialResult=curriedSum(1)
console.log(partialResult) // b => f(a, b)
constfinalResult=partialResult(41)
console.log(finalResult) // 42
This works exactly the same as before, but now we can use it with any function that takes in two arguments of any type. For example: