website stack
How I built this website
There are two truths behind this website:
- I wanted to revive my old website, pgabriel.gitlab.io , over a single weekend.
- I love Obsidian but don't want to pay for Obsidian Publish ($8 per month).
Here's how I did it for free (except for the custom domain name).
Requirements
- Github for deploying; you need an account
- Download Obsidian for writing
- Obsidian Digital Garden for publishing
- Vercel for hosting; you can use your Github account
- Obsidian Digital Garden for publishing
- ImprovMX for custom domain email forwarding
Setup
To be honest, I just followed the steps in the getting started page of the Obsidian Digital Garden plugin. It was very straight-forward.
Security
This domain DOES NOT send out emails. If you get an email from this domain, it is not from me.
Wisdump.work provided a guide for adding email security. The Vercel setup already adds an SPF policy, but the additional recommendation for DMARC policy worked out-of-box.
Publishing
My routine is simple:
- Create a new note; optional - use template to populate properties
- Write
- Set
df-publish: true
- Use Digital Garden Publication Center to publish
Maps
Creating "Index" page
Since the digital swamp is completely flat, I wanted a view where anyone could see all the published pages in a single, sorted table.
So I used dataform to create Everything.
To get the in-line counts, just at "$=" to the beginning of these two lines:
new Set(dv.pages().where(p => p["dg-publish"] === true).flatMap(p => p.file?.tags || [])).size
dv.pages().where(p => p["dg-publish"] == true).length
To generate the tables, set these code blocks to dataview
TABLE length(rows) AS "File Count"
FROM ""
FLATTEN file.tags AS Tag
WHERE file.frontmatter.dg-publish
GROUP BY Tag
SORT length(rows) DESC
TABLE file.folder as "Path", file.tags as "Tags", file.mtime as "Last Modified", file.frontmatter.dg-pinned as "Pinned"
WHERE file.frontmatter.dg-publish
SORT file.name ASC
Map templates
A more focused "index" is called a "Map", sometimes called a map of content.
Tags map
These maps are usually just for specific tags. To build a map for a specific tag, you can just paste the following template into an empty markdown file:
---
dg-publish: false
dg-home-link: true
dg-show-backlinks: true
dg-show-local-graph: true
dg-show-inline-title: true
dg-show-file-tree: true
dg-enable-search: true
dg-show-toc: true
noteIcon: signpost
tags:
- dataview
---
[!success]
The auditor hands you a list of TAG...
# My recipes
__Total: __ , _Sort order: `file.name`, A->Z_
'''dataview
LIST
FROM ""
WHERE contains(file.tags, "#TAG")
AND file.frontmatter.dg-publish
'''
---
[!info] Generated using [Dataview plugin](https://blacksmithgu.github.io/obsidian-dataview/resources/examples/).
---
Customization
Obsidian vault setup
My vault is very flat, it looks like:
_static/ - for unchanging files like images
template/ - for obsidian note templates
Maps/ - contains maps of content to navigate swamp
Swamp/ - flat layout of pages
about.md
contact.md
exit.md
home.md
Make an Obsidian note template for new pages
The properties are kind of hard to find, here is the full set for convenience
---
dg-publish: false
dg-home: false
dg-home-link: false
dg-show-backlinks: false
dg-show-local-graph: false
dg-show-inline-title: false
dg-show-file-tree: false
dg-enable-search: false
dg-show-toc: false
dg-permalink: "mynote"
dg-path: "Advanced/Features.md"
dg-pinned: true
dg-hide: true
dg-hide-in-graph: true
dg-note-icon: 1
---
I tried using dg-path
but it messed with Transclusion
More information about default and advanced properties.
Custom domain from Vercel
Buying a domain from Vercel was very easy, you just have to start here. By keeping the public web parts all on Vercel, it was trivial to add the custom domain to the web app.
Custom avatar
Confession, I did not draw my avatar. Instead, I used ChatGPT Image Generation to turn my LinkedIn profile picture.
My prompt looked something like this:
Please generate a simple cartoon version of the attached image. Keep it to two tones.
After some adjustments, I then converted the output image to gray-scale.
If you also use AI to make art, please consider supporting a local artist.
Custom favicon
Digital Garden's favicon support is limited to SVG files. I was able to make a custom text favicon in two steps:
- Use favicon.io to create a custom text icon pack (I used my initials)
- This produces a zip file of several formats, including PNG
- Use an online file converter to turn one of the smaller PNG files into SVG
Custom footer
This one is a little tricky... the Digital Garden plug-in supports something called "slots". In this case, it means that you can define custom snippets of code that will be inserted into specific parts of the webpage HTML.
In this case, the slot in question is <footer></footer>
, and we want to add code inside this section.
To do this, you have to work with the github repository directly:
- Clone the github repo of your website to your local machine
- Create the following folder(s) that complete the path:
src/site/_includes/components/user/common/footer/
common
here means the code will inserted into all footers; you can replace withindex
ornotes
to limit scope
- Create a new file for each code block you want to inject
- Re-build the website
Custom footer - links
I wanted to center some convenient external links.
<div style="text-align: center;">
© 2013–2025
<a href="https://paologabriel.com">Paolo Gabriel</a> |
<a href="https://www.linkedin.com/in/paolo-gabriel/">LinkedIn</a> |
<a href="https://github.com/palol/">Github</a> |
<a href="https://scholar.google.com/citations?user=x_E1WPAAAAAJ&hl=en">Google Scholar</a> |
<a href="https://www.paologabriel.com/feed.xml">RSS feed</a>
</div>
Custom footer - analytics
<script defer src="/_vercel/insights/script.js"></script>
Then you need to enable Analytics in Vercel
Custom footer - theme switcher
The icon in the bottom right corner is another custom footer. See the discussion here, and reference from pkms-notes.
- First, create a new file
<aside id="floating-control">
<a id="emailme" href="mailto:bruvistrue93@gmail.com?subject=Regarding {{title}}&body=Discussing {{meta.siteBaseUrl}}{{permalink}}"><i icon-name="mail-plus" title="Discuss" aria-hidden="true"></i></a>
<span id="theme-switch">
<i class="svg-icon light" icon-name="sun" aria-hidden="true"></i>
<i class="svg-icon dark" icon-name="moon" aria-hidden="true"></i>
<i class="svg-icon auto" icon-name="sun-moon" aria-hidden="true"></i>
</span>
</aside>
<script>
function setThemeIcon(theme) {
let toAdd;
switch (theme) {
case 'dark':
toRemove = ['auto', 'light'];
break;
case 'light':
toAdd = 'fa-adjust';
toRemove = ['dark', 'auto'];
break;
default:
toRemove = ['light', 'dark'];
break;
}
document.getElementById('theme-switch').classList.add(theme);
document.getElementById('theme-switch').classList.remove(...toRemove);
}
function setTheme(theme, setIcon) {
if (setIcon) {
setThemeIcon(theme);
}
if (theme == 'dark') {
document.body.classList.remove('theme-light');
document.body.classList.add('theme-dark');
} else if (theme == "light") {
document.body.classList.add('theme-light');
document.body.classList.remove('theme-dark');
} else {
theme = (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
setTheme(theme, false);
}
}
let theme = window.localStorage.getItem('site-theme') || "light";
setTheme(theme, true);
window.theme = theme;
window.localStorage.setItem('site-theme', theme);
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(event) {
const settings = window.localStorage.getItem('site-theme');
if (!settings || settings == "auto") {
window.localStorage.setItem('site-theme', "auto");
setTheme("auto", true);
}
});
document.getElementById('theme-switch').addEventListener('click', function() {
let theme;
if (window.theme == 'auto') {
theme = "dark";
} else if (window.theme == 'dark') {
theme = 'light'
} else {
theme = 'auto';
}
setTheme(theme, true);
window.localStorage.setItem('site-theme', theme);
window.theme = theme;
})
</script>
- Update custom CSS
#floating-control {
position: fixed;
color: var(--link-color);
bottom: 1vmax;
right: 1vmax;
font-size: 24px;
z-index: 999999;
display: flex;
flex-direction: row;
justify-content: flex-end;
gap: 10px;
.svg-icon,
i {
cursor: pointer;
height: 24px;
width: auto;
}
#theme-switch {
.light {
display: none;
}
.dark {
display: none;
}
.auto {
display: none;
}
}
#theme-switch.light {
.light {
display: inline;
}
}
#theme-switch.dark {
.dark {
display: inline;
}
}
#theme-switch.auto {
.auto {
display: inline;
}
}
}
Custom footer - random navigation
Product of a vibe coding session, pretty proud of this one
Hermitage forest
See this discussion. You also need to copy over the image files from topobon
repo and tag your notes.
Overall, I had to:
- Copy over the svg files from
topobon
repo - Add the
forest.njk
to notes header slot - Enable all front-matter to be passed through from obsidian
- Add
dg-note-icon
parameter to your note
It helps to set defaults using the plug-in. For example, I want the "1" icon to be used by default for new notes. You can also add to a template, if that's your thing.