Compare commits

...

20 Commits

Author SHA1 Message Date
SauravDhakal
ab36028ccf fix: syntax
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-03 22:26:33 +05:45
SauravDhakal
c50baca85a test: test ci/cd 2026-04-03 22:07:10 +05:45
SauravDhakal
23a033acc3 feat: Added favicon and CV 2026-04-03 21:29:42 +05:45
SauravDhakal
570afd621d fix: gtag
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-02 09:04:20 +05:45
SauravDhakal
af7e549fc7 fix: hugo specific g-tag
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-02 08:55:15 +05:45
SauravDhakal
12f56ff11f fix: google analytics
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-02 08:36:48 +05:45
SauravDhakal
caca000c7f feat: new post, clean up old placeholders
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-01 21:55:48 +05:45
SauravDhakal
2007e1f7b5 fix: woodpecker event
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2026-04-01 18:31:19 +05:45
SauravDhakal
663e8c8d62 fix: test
Some checks are pending
ci/woodpecker/manual/woodpecker Pipeline is pending
2026-04-01 13:28:35 +05:45
SauravDhakal
2dd65b7b48 chore: add woodpecker 2026-04-01 13:10:14 +05:45
SauravDhakal
3a1d508c65 fix: Search
Some checks failed
Deploy Hugo Site / deploy (push) Has been cancelled
2026-04-01 12:11:17 +05:45
SauravDhakal
0c4e6715de fix: Layout and theme 2026-04-01 10:33:28 +05:45
SauravDhakal
38baeb86e8 fix: theme a bit 2026-04-01 09:14:17 +05:45
SauravDhakal
df6f488ccd feat: Init with clean layout 2026-04-01 08:06:08 +05:45
sauravdhakal12
1206387246 fix: link 2026-01-19 21:51:58 +05:45
sauravdhakal12
9995edb14d feat: New post 2026-01-19 21:37:20 +05:45
sauravdhakal12
8a5439d2aa fix: gsite 2025-12-29 21:50:44 +05:45
sauravdhakal12
e3224e321e fix: gtag 2025-12-29 21:48:57 +05:45
sauravdhakal12
1c47537b54 fix: build + build command change 2025-12-29 21:36:37 +05:45
sauravdhakal12
c940743fd8 fix: baseUrl set 2025-12-29 21:33:18 +05:45
285 changed files with 1564 additions and 2031 deletions

View File

@@ -1,34 +0,0 @@
name: Deploy Hugo Site
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: "latest"
extended: true
- name: Build site
run: hugo --minify
- name: Deploy via rsync
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
source: "public/*"
target: "/var/www/portfolio"
strip_components: 1

21
.gitignore vendored Normal file
View File

@@ -0,0 +1,21 @@
# Hugo build output
public/
resources/_gen/
# Hugo lock file
.hugo_build.lock
# OS files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
.idea/
.vscode/
# Environment files
.env
.env.local

12
.woodpecker.yml Normal file
View File

@@ -0,0 +1,12 @@
# Test
when:
event: push
branch: master
steps:
build:
image: hugomods/hugo:latest
commands:
- hugo --minify --destination /site/public
volumes:
- /home/saurav/site:/site # writes directly to where Caddy serves from

541
CUSTOMIZATION.md Normal file
View File

@@ -0,0 +1,541 @@
# Hugo + PaperMod Customization Guide
This guide covers how to customize your Hugo portfolio site using the PaperMod theme.
---
## Table of Contents
1. [Project Structure](#project-structure)
2. [Development Workflow](#development-workflow)
3. [Configuration (hugo.yaml)](#configuration-hugoyaml)
4. [Adding Content](#adding-content)
5. [Creating New Sections](#creating-new-sections)
6. [Customizing Styles (CSS)](#customizing-styles-css)
7. [Overriding Layouts](#overriding-layouts)
8. [Common Customizations](#common-customizations)
9. [Deployment](#deployment)
---
## Project Structure
```
MyPortfolio/
├── archetypes/ # Templates for new content
├── assets/
│ └── css/
│ └── extended/
│ └── custom.css # YOUR custom CSS (this gets merged)
├── content/
│ ├── posts/ # Long-form articles
│ │ └── _index.md # Section config
│ ├── notes/ # Short notes/TILs
│ │ └── _index.md # Section config
│ └── search.md # Search page
├── layouts/
│ ├── index.html # Custom home page (no recent posts)
│ ├── partials/ # Override theme partials
│ │ ├── footer.html # Custom footer
│ │ └── templates/
│ │ └── schema_json.html # Fixed JSON-LD schema
│ ├── posts/
│ │ └── list.html # Custom posts list (compact style)
│ └── notes/
│ └── list.html # Custom notes list (compact style)
├── static/
│ └── files/ # Static files (CV.pdf, images, etc.)
├── themes/
│ └── PaperMod/ # Theme (git submodule - DON'T edit)
├── hugo.yaml # Main configuration
└── .gitignore # Ignores public/, etc.
```
### Key Principle: Never Edit the Theme Directly
The `themes/PaperMod/` folder is a git submodule. To customize:
- **CSS**: Add to `assets/css/extended/custom.css`
- **Layouts**: Copy the file from `themes/PaperMod/layouts/` to `layouts/` and modify
- **Config**: Everything in `hugo.yaml`
---
## Development Workflow
### Local Development
```bash
# Start dev server (watches for changes)
hugo serve
# With drafts visible
hugo serve -D
# Build for production (outputs to public/)
hugo build --minify
```
### Why `public/` is Gitignored
- `public/` is generated output - it's rebuilt fresh every time
- Your CI/CD (GitHub Actions) builds it during deployment
- Never commit build artifacts to git
### Git Workflow
```bash
# After making changes
git add .
git commit -m "Add new post about X"
git push origin master # Triggers deployment
```
---
## Configuration (hugo.yaml)
### Key Sections
#### Site Basics
```yaml
baseURL: "/" # Use "/" for relative URLs (works on localhost + prod)
relativeURLs: true # Makes all URLs relative
title: Saurav Dhakal
languageCode: en-us
theme: ["PaperMod"]
```
#### Menu (Header Navigation)
```yaml
menu:
main:
- identifier: posts # Internal ID
name: Posts # Display name
url: /posts/ # URL path
weight: 10 # Order (lower = first)
- identifier: notes
name: Notes
url: /notes/
weight: 20
- identifier: tags
name: Tags
url: /tags/
weight: 30
- identifier: search
name: Search
url: /search/
weight: 40
```
#### Home Page Info
```yaml
params:
homeInfoParams:
Title: "Hi there 👋, I'm Saurav!"
Content: >
I'm a software engineer...
```
#### Social Icons
```yaml
params:
socialIcons:
- name: github
url: "https://github.com/yourusername"
- name: linkedin
url: "https://linkedin.com/in/yourusername"
- name: x
url: "https://x.com/yourusername"
```
Available icons: github, linkedin, x, twitter, email, rss, youtube, instagram, facebook, stackoverflow, etc.
---
## Adding Content
### New Post
```bash
hugo new posts/my-new-post.md
```
Or manually create `content/posts/my-new-post.md`:
```markdown
---
title: "My New Post"
date: 2025-08-20
summary: "A brief description for the list view"
tags: ["tag1", "tag2"]
categories: ["Category"]
draft: false
ShowToc: true
TocOpen: false
---
Your content here...
```
### New Note
Create `content/notes/my-note.md`:
```markdown
---
title: "Quick TIL"
date: 2025-08-20
summary: "Today I learned about X"
tags: ["til"]
---
Short content here...
```
### Front Matter Options
| Field | Description |
|-------|-------------|
| `title` | Post title |
| `date` | Publication date (YYYY-MM-DD) |
| `summary` | Short description for list views |
| `tags` | Array of tags |
| `categories` | Array of categories |
| `draft` | If `true`, won't be published |
| `ShowToc` | Show table of contents |
| `TocOpen` | TOC expanded by default |
| `ShowReadingTime` | Override global setting |
| `ShowWordCount` | Override global setting |
| `cover.image` | Cover image path |
---
## Creating New Sections
### Example: Adding a "Projects" Section
1. **Add to menu** in `hugo.yaml`:
```yaml
menu:
main:
# ... existing items
- identifier: projects
name: Projects
url: /projects/
weight: 25
```
2. **Create content directory**:
```bash
mkdir -p content/projects
```
3. **Create section index** `content/projects/_index.md`:
```markdown
---
title: "Projects"
description: "Things I've built"
---
```
4. **Add project pages** `content/projects/my-project.md`:
```markdown
---
title: "My Cool Project"
date: 2025-01-15
summary: "A brief description"
tags: ["golang", "cli"]
---
Project description...
```
5. **(Optional) Custom layout** - If you want projects to look different, create `layouts/projects/list.html`
---
## Customizing Styles (CSS)
### Where to Add CSS
**Only edit**: `assets/css/extended/custom.css`
PaperMod automatically includes this file. You don't need to import it anywhere.
### CSS Variables (Theme Colors)
PaperMod uses CSS variables. Override them in your custom.css:
```css
:root {
/* Light mode */
--primary: #282828; /* Main text */
--secondary: #3c3836; /* Secondary text */
--tertiary: rgb(214, 214, 214); /* Borders, etc */
--theme: rgb(255, 255, 255); /* Background */
--entry: rgb(255, 255, 255); /* Card background */
}
:root[data-theme="dark"] {
/* Dark mode */
--primary: #fbf1c7;
--secondary: #ebdbb2;
--theme: #181818;
--entry: rgb(46, 46, 51);
}
```
### Common CSS Customizations
#### Change fonts
```css
@import url("https://fonts.googleapis.com/css2?family=Your+Font&display=swap");
body {
font-family: "Your Font", sans-serif;
}
```
#### Style links
```css
main a {
text-decoration: underline;
text-decoration-color: var(--green);
}
main a:hover {
background-color: var(--green);
color: white;
}
```
#### Customize post cards
```css
main .post-entry {
border: 2px solid #383838;
background-color: var(--entry);
border-radius: 8px;
}
```
---
## Overriding Layouts
### How Layout Override Works
Hugo looks for templates in this order:
1. `layouts/` (your overrides)
2. `themes/PaperMod/layouts/` (theme defaults)
### To Override a Template
1. Find the file in `themes/PaperMod/layouts/`
2. Copy it to the same path in `layouts/`
3. Modify your copy
### Common Files to Override
| File | Purpose |
|------|---------|
| `layouts/partials/header.html` | Site header/nav |
| `layouts/partials/footer.html` | Site footer |
| `layouts/partials/post_meta.html` | Post metadata (date, read time) |
| `layouts/_default/list.html` | List pages (posts, tags) |
| `layouts/_default/single.html` | Individual post/page |
### Example: Simpler Footer
Your `layouts/partials/footer.html` already overrides the theme's footer.
### Section-Specific Layouts
Create `layouts/SECTION_NAME/list.html` for a custom list layout for that section.
**Current custom layouts:**
- `layouts/index.html` - Home page with just intro + social icons (no recent posts)
- `layouts/posts/list.html` - Compact list: `# Title` + date + 2-line description
- `layouts/notes/list.html` - Compact list: `# Title` + date (no description)
---
## Common Customizations
### Disable Reading Time for a Section
In the section's `_index.md`:
```markdown
---
title: "Notes"
ShowReadingTime: false
---
```
Or per-post in front matter.
### Add a Static Page (About, Contact)
Create `content/about.md`:
```markdown
---
title: "About"
layout: "single"
url: "/about/"
---
About me content...
```
Add to menu:
```yaml
menu:
main:
- identifier: about
name: About
url: /about/
weight: 50
```
### Add Favicon
1. Place favicon files in `static/`:
- `static/favicon.ico`
- `static/favicon-16x16.png`
- `static/favicon-32x32.png`
- `static/apple-touch-icon.png`
2. Update `hugo.yaml`:
```yaml
params:
assets:
favicon: "/favicon.ico"
favicon16x16: "/favicon-16x16.png"
favicon32x32: "/favicon-32x32.png"
apple_touch_icon: "/apple-touch-icon.png"
```
### Enable Comments (Disqus)
```yaml
params:
comments: true
disqusShortname: "your-disqus-shortname"
```
### Add Google Analytics
Already configured in your `hugo.yaml`:
```yaml
googleAnalytics: "G-XXXXXXXXXX"
```
---
## Deployment
### Current Setup (GitHub Actions → SSH/SCP)
Your `.github/workflows/deploy.yml`:
1. Triggers on push to `master`
2. Builds site with `hugo build --minify`
3. Deploys `public/` via SCP to your server
### Required GitHub Secrets
Set these in your repo's Settings → Secrets:
- `SSH_HOST` - Your server hostname/IP
- `SSH_USER` - SSH username
- `SSH_KEY` - Private SSH key
- `SSH_PORT` - SSH port (usually 22)
### Alternative: GitHub Pages
If you want to use GitHub Pages instead:
1. Change workflow to use `peaceiris/actions-gh-pages`
2. Set `baseURL` to `https://yourusername.github.io/repo-name/`
### Alternative: Netlify/Vercel
These platforms auto-detect Hugo and build for you:
1. Connect your GitHub repo
2. Set build command: `hugo --minify`
3. Set publish directory: `public`
---
## Quick Reference
### Useful Commands
```bash
# Local dev
hugo serve -D # Serve with drafts
# Create content
hugo new posts/title.md # New post
hugo new notes/title.md # New note
# Build
hugo build --minify # Production build
# Debug
hugo config # Show full config
hugo list all # List all content
```
### File Locations
| What | Where |
|------|-------|
| Config | `hugo.yaml` |
| Custom CSS | `assets/css/extended/custom.css` |
| Posts | `content/posts/` |
| Notes | `content/notes/` |
| Static files | `static/` |
| Layout overrides | `layouts/` |
---
## Troubleshooting
### Links Go to Production URL in Dev
Fixed by setting:
```yaml
baseURL: "/"
relativeURLs: true
```
### Changes Not Showing
1. Hard refresh browser (Ctrl+Shift+R)
2. Clear `public/` folder: `rm -rf public/`
3. Restart hugo serve
### Theme Not Loading
```bash
git submodule update --init --recursive
```
### Search Not Working
Ensure `hugo.yaml` has:
```yaml
outputs:
home:
- HTML
- RSS
- JSON # Required for search
```
---
## Resources
- [Hugo Documentation](https://gohugo.io/documentation/)
- [PaperMod Wiki](https://github.com/adityatelange/hugo-PaperMod/wiki)
- [PaperMod Demo](https://adityatelange.github.io/hugo-PaperMod/)

View File

@@ -50,63 +50,124 @@
} }
:root[data-theme="dark"] { :root[data-theme="dark"] {
--theme: #181818; --theme: #1d2021;
--entry: rgb(46, 46, 51); --entry: #282828;
--primary: #fbf1c7; --primary: #ebdbb2;
--secondary: #ebdbb2; --secondary: #a89984;
--tertiary: rgb(65, 66, 68); --tertiary: #3c3836;
--content: rgb(196, 196, 197); --content: #ebdbb2;
--code-block-bg: rgb(46, 46, 51); --code-block-bg: #282828;
--code-bg: rgb(55, 56, 62); --code-bg: #3c3836;
--border: rgb(151, 51, 51); --border: #3c3836;
color-scheme: dark; color-scheme: dark;
--post-entry-bg: #181818; /* bg1 */ --post-entry-bg: #282828;
--post-entry-fg: #ebdbb2; --post-entry-fg: #ebdbb2;
--green: #b8bb26;
--greenl: #8ec07c;
} }
body { body {
font-family: "Space Grotesk", sans-serif; font-family: "Space Grotesk", monospace;
}
nav #menu span {
padding: 0px 2px;
text-decoration: underline;
text-decoration-thickness: 2px;
text-decoration-color: var(--green);
} }
/* * /* *
* NAV SECTION * UNIVERSAL HOVER ANIMATION (Fill-up Effect)
* Target: Nav spans, main links (except entries), social links, and share icons.
* */ * */
nav #menu span:hover {
nav #menu a span {
position: relative;
text-decoration: none !important;
color: inherit;
transition: color 0.3s ease;
z-index: 1;
padding: 8px; /* Reduced vertical padding */
}
.entry-content {
color: var(--primary)
}
main a:not(.entry-link, .anchor),
.social-icons a,
.share-buttons a,
.note-item a {
position: relative;
text-decoration: none !important;
color: inherit;
transition: color 0.3s ease;
z-index: 1;
display: inline-block;
}
/* Note items are full-width flex containers */
.note-item a {
display: flex;
width: 100%;
padding: 8px 12px;
margin: 0 -12px; /* Offset padding for full-row effect */
}
/* The fill background */
nav #menu a span::before,
main a:not(.entry-link, .anchor)::before,
.social-icons a::before,
.share-buttons a::before,
.note-item a::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px; /* Static underline state */
background-color: var(--green); background-color: var(--green);
color: white; z-index: -1;
transition: height 0.3s cubic-bezier(0.19, 1, 0.22, 1);
} }
nav #menu .active { /* Notes list starts with 0 height to stay clean */
border: 0px; .note-item a::before,
color: var(--green); .share-buttons a::before {
height: 0;
text-decoration: underline;
text-decoration-thickness: 2px;
text-decoration-color: var(--green);
} }
/* * /* Hover state: Fill up the background */
* MAIN SECTION nav #menu a:hover span::before,
* */ main a:hover:not(.entry-link, .anchor)::before,
main a { .social-icons a:hover::before,
text-decoration: underline; .share-buttons a:hover::before,
text-decoration-thickness: 2px; .note-item a:hover::before {
text-decoration-color: var(--green); height: 100%;
} }
main a:hover:not(.entry-link) { /* CONTRAST FIX: Switch text/icon color on hover */
background-color: var(--green); nav #menu a:hover span,
color: white; main a:hover:not(.entry-link, .anchor),
.social-icons a:hover,
.share-buttons a:hover,
#searchResults a:hover,
.note-item a:hover .note-title,
.note-item a:hover .note-date {
color: var(--theme) !important;
}
/* Icon specific color switch */
.social-icons a:hover svg,
.share-buttons a:hover svg {
fill: var(--theme) !important;
stroke: var(--theme) !important;
}
/* Active Nav State - remain thick underline */
nav #menu .active span::before {
height: 4px;
}
nav #menu .active a:hover span::before {
height: 100%;
} }
main h1 { main h1 {
@@ -117,12 +178,64 @@ main h1 {
.toc, .toc,
.post-tags a { .post-tags a {
border: 0px; border: 0px;
background: transparent;
}
.post-tags a {
background: var(--tertiary);
color: var(--primary);
padding: 0px 8px;
border-radius: 0px;
transition: all 0.3s ease;
}
.post-tags a:hover {
background: var(--green);
color: var(--theme);
transform: translateY(-2px);
}
.share-buttons {
margin-top: 40px;
padding: 20px 0;
border-top: 1px solid var(--tertiary);
display: flex;
justify-content: center;
gap: 12px;
}
.share-buttons a {
padding: 10px;
border-radius: var(--radius);
} }
.anchor { .anchor {
text-decoration: none; text-decoration: none;
} }
.breadcrumbs {
margin-bottom: 2rem;
font-size: 0.9rem;
color: var(--secondary);
}
.post-header .post-title {
font-size: 3rem;
margin-bottom: 0.5rem;
color: var(--green);
font-weight: 700;
line-height: 1.2;
}
.post-description {
font-style: italic;
font-size: 1.2rem;
margin-bottom: 1.5rem;
color: var(--secondary);
line-height: 1.6;
}
/* main h1::before { */ /* main h1::before { */
/* content: "#"; */ /* content: "#"; */
/* color: var(--red); */ /* color: var(--red); */
@@ -134,8 +247,248 @@ main h1 {
/* } */ /* } */
main .post-entry { main .post-entry {
border: 0px; border: 1px solid var(--tertiary);
background-color: var(--post-entry-bg); background-color: var(--post-entry-bg);
color: var(--post-entry-fg); color: var(--post-entry-fg);
border: 2px solid #383838; transition: transform 0.2s ease, border-color 0.2s ease;
}
main .post-entry:hover {
transform: translateY(-2px);
border-color: var(--green);
}
/* *
* NOTES SECTION - Compact list style
* */
.notes-list {
list-style: none;
padding: 0;
margin: 0;
}
.note-item {
padding: 12px 0;
border-bottom: 1px solid var(--tertiary);
}
.note-item:last-child {
border-bottom: none;
}
.note-item a:hover .note-title,
.note-item a:hover .note-date {
color: var(--theme) !important;
}
.note-title {
font-size: 1.1rem;
font-weight: 500;
color: var(--primary);
}
.note-date {
font-size: 0.85rem;
color: var(--secondary);
white-space: nowrap;
}
.note-summary {
margin: 4px 0 0 0;
font-size: 0.96rem;
color: var(--secondary);
line-height: 1.5;
}
.delimiter {
margin: 0 10px;
font-weight: 800;
opacity: 0.5;
}
/* *
* POSTS SECTION - Same compact style as notes but with description
* */
.posts-list {
list-style: none;
padding: 0;
margin: 0;
}
.post-item {
padding: 12px 0;
border-bottom: 1px solid var(--tertiary);
}
.post-item:last-child {
border-bottom: none;
}
.post-item a {
position: relative;
display: flex;
justify-content: space-between;
align-items: baseline;
text-decoration: none !important;
gap: 16px;
padding: 8px 12px;
margin: 0 -12px;
color: inherit;
transition: color 0.3s ease;
z-index: 1;
width: 100%;
}
.post-item a::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0;
background-color: var(--green);
z-index: -1;
transition: height 0.3s cubic-bezier(0.19, 1, 0.22, 1);
}
.post-item a:hover::before {
height: 100%;
}
.post-item a:hover .post-title,
.post-item a:hover .post-date {
color: var(--theme) !important;
}
.post-title {
font-size: 1.1rem;
font-weight: 500;
color: var(--primary);
}
.post-date {
font-size: 0.85rem;
color: var(--secondary);
white-space: nowrap;
}
.post-summary {
margin: 4px 0 0 0;
font-size: 0.96rem;
color: var(--secondary);
line-height: 1.5;
}
/* Common list styles */
.note-item a,
.post-item a {
display: flex !important;
justify-content: flex-start;
align-items: center;
}
/* *
* POST CONTENT SPACINGS
* */
.post-content {
font-size: 1.1rem;
line-height: 1.7;
color: var(--primary);
}
.post-content p,
.post-content ol,
.post-content ul,
.post-content dl {
margin-bottom: 1.6em;
}
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4 {
margin-top: 1.8em;
margin-bottom: 0.8em;
color: var(--green);
}
.post-content blockquote {
margin: 2em 0;
padding: 0.5em 1.5em;
border-left: 4px solid var(--green);
background-color: var(--entry);
font-style: italic;
color: var(--secondary);
}
.post-content code {
font-size: 16px;
background-color: var(--tertiary);
color: var(--primary);
border-radius: 4px;
}
.highlight {
padding: 0.6em;
}
.post-content pre code {
background-color: var(--entry) !important;
}
.post-content blockquote p {
margin: 0;
padding: 0.5em 0em;
}
/* *
* SEARCH PAGE FIXES
* */
#searchResults .post-entry {
border-radius: 0; /* Square borders like notes */
padding: 0;
border: 1px solid var(--tertiary);
margin-bottom: 8px;
}
#searchResults a {
position: relative;
display: flex !important;
justify-content: flex-start;
align-items: center;
padding: 12px 15px;
text-decoration: none !important;
color: var(--primary);
z-index: 1;
}
#searchResults a .search-meta {
margin-left: auto; /* Push date to the right */
font-size: 0.85rem;
color: var(--secondary);
}
#searchResults a:hover .search-title,
#searchResults a:hover .search-meta {
color: var(--theme) !important;
}
#searchResults a::before {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 0;
background-color: var(--green);
z-index: -1;
transition: height 0.3s cubic-bezier(0.19, 1, 0.22, 1);
}
#searchResults a:hover::before {
height: 100%;
}
#searchResults a:hover {
color: var(--theme) !important;
} }

BIN
assets/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

153
assets/js/fastsearch.js Normal file
View File

@@ -0,0 +1,153 @@
import * as params from '@params';
let fuse; // holds our search engine
let resList = document.getElementById('searchResults');
let sInput = document.getElementById('searchInput');
let first, last, current_elem = null
let resultsAvailable = false;
// load our search index
window.onload = function () {
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let data = JSON.parse(xhr.responseText);
if (data) {
// fuse.js options; check fuse.js website for details
let options = {
distance: 100,
threshold: 0.4,
ignoreLocation: true,
keys: [
'title',
'permalink',
'summary',
'content'
]
};
if (params.fuseOpts) {
options = {
isCaseSensitive: params.fuseOpts.iscasesensitive ?? false,
includeScore: params.fuseOpts.includescore ?? false,
includeMatches: params.fuseOpts.includematches ?? false,
minMatchCharLength: params.fuseOpts.minmatchcharlength ?? 1,
shouldSort: params.fuseOpts.shouldsort ?? true,
findAllMatches: params.fuseOpts.findallmatches ?? false,
keys: params.fuseOpts.keys ?? ['title', 'permalink', 'summary', 'content'],
location: params.fuseOpts.location ?? 0,
threshold: params.fuseOpts.threshold ?? 0.4,
distance: params.fuseOpts.distance ?? 100,
ignoreLocation: params.fuseOpts.ignorelocation ?? true
}
}
fuse = new Fuse(data, options); // build the index from the json file
}
} else {
console.log(xhr.responseText);
}
}
};
xhr.open('GET', "../index.json");
xhr.send();
}
function activeToggle(ae) {
document.querySelectorAll('.focus').forEach(function (element) {
// rm focus class
element.classList.remove("focus")
});
if (ae) {
ae.focus()
document.activeElement = current_elem = ae;
ae.parentElement.classList.add("focus")
} else {
document.activeElement.parentElement.classList.add("focus")
}
}
function reset() {
resultsAvailable = false;
resList.innerHTML = sInput.value = ''; // clear inputbox and searchResults
sInput.focus(); // shift focus to input box
}
// execute search as each character is typed
sInput.onkeyup = function (e) {
// run a search query (for "term") every time a letter is typed
// in the search box
if (fuse) {
let results;
if (params.fuseOpts) {
results = fuse.search(this.value.trim(), {limit: params.fuseOpts.limit}); // the actual query being run using fuse.js along with options
} else {
results = fuse.search(this.value.trim()); // the actual query being run using fuse.js
}
if (results.length !== 0) {
// build our html if result exists
let resultSet = ''; // our results bucket
for (let item in results) {
resultSet += `<li class="post-entry"><a href="${results[item].item.permalink}" aria-label="${results[item].item.title}">` +
`<div class="search-title"># ${results[item].item.title}</div>` +
`<div class="search-meta"><span class="delimiter">&bull;</span> ${results[item].item.date}</div></a></li>`
}
resList.innerHTML = resultSet;
resultsAvailable = true;
first = resList.firstChild;
last = resList.lastChild;
} else {
resultsAvailable = false;
resList.innerHTML = '';
}
}
}
sInput.addEventListener('search', function (e) {
// clicked on x
if (!this.value) reset()
})
// kb bindings
document.onkeydown = function (e) {
let key = e.key;
let ae = document.activeElement;
let inbox = document.getElementById("searchbox").contains(ae)
if (ae === sInput) {
let elements = document.getElementsByClassName('focus');
while (elements.length > 0) {
elements[0].classList.remove('focus');
}
} else if (current_elem) ae = current_elem;
if (key === "Escape") {
reset()
} else if (!resultsAvailable || !inbox) {
return
} else if (key === "ArrowDown") {
e.preventDefault();
if (ae == sInput) {
// if the currently focused element is the search input, focus the <a> of first <li>
activeToggle(resList.firstChild.lastChild);
} else if (ae.parentElement != last) {
// if the currently focused element's parent is last, do nothing
// otherwise select the next search result
activeToggle(ae.parentElement.nextSibling.lastChild);
}
} else if (key === "ArrowUp") {
e.preventDefault();
if (ae.parentElement == first) {
// if the currently focused element is first item, go to input box
activeToggle(sInput);
} else if (ae != sInput) {
// if the currently focused element is input box, do nothing
// otherwise select the previous search result
activeToggle(ae.parentElement.previousSibling.lastChild);
}
} else if (key === "ArrowRight") {
ae.click(); // click on active link
}
}

4
content/notes/_index.md Normal file
View File

@@ -0,0 +1,4 @@
---
title: "Notes"
description: "Quick notes, TILs, and short thoughts"
---

4
content/posts/_index.md Normal file
View File

@@ -0,0 +1,4 @@
---
title: "Posts"
description: "Long-form articles on software development, architecture, and engineering"
---

View File

@@ -1,19 +0,0 @@
---
author: ["Saurav Dhakal"]
title: "Understanding Separation of Concerns (SoC) in NestJS"
date: "2025-08-19"
summary: "A guide to understanding Separation of Concerns in NestJS using modules, services, and controllers."
tags: ["nestjs", "typescript", "architecture"]
categories: ["Backend Development", "NestJS"]
series: ["NestJS"]
ShowToc: true
TocOpen: false
---
When building applications, one of the most important design principles to keep in mind is **Separation of Concerns (SoC)**. NestJS, with its modular architecture, makes applying SoC almost effortless — but understanding _why_ it matters and _how_ to use it properly will help you write cleaner, testable, and future-proof code.
## What is Separation of Concerns and Why it Matters?
The basic idea is:
> A program should be divided into distinct sections, where each section addresses a single responsibility.

View File

@@ -0,0 +1,105 @@
---
title: "The Likes Table Problem: Why We Went Polymorphic."
date: 2026-04-01T21:50:00+05:45
draft: false
tags: ["architecture", "backend", "PSQL"]
description: "Concept of polymorphic table in an SQL database"
---
A few days ago, I was working on adding a **Community** section to an application. The idea was simple, users should be able to:
- Create posts
- Leave comments
- Like posts
- Like comments
We also had a separate News section. The new requirement was users should be able to like news articles as well.
Building model for `posts` and `comments` was pretty straight forward. The real challenge was to model the`likes`table.
## The Problem: How Do We Store Likes?
Were using PostgreSQL, so enforcing relationships with foreign keys is easy and clean.
If only **one** thing could be liked (say, News), the schema would be simple, we would have a `news_like` table which could look something like this:
| user_id | news_id | timestamp |
| --- | --- | --- |
| (foreign key) | (foreign key) | 1775059642 |
But we didnt have one entity. We had three. `posts`, `comments` and `news`.
We had to decide:
> Do we create three separate like tables? Or do we design one flexible solution?
## Option 1: Three Separate Tables
We could create three tables: `post_likes` , `comment_likes` and `news_likes` . Each table would have proper foreign key relationships. This approach would be a clean relational way of doing things. It:
- Keeps strong relational integrity
- Makes joins easy
- Keeps structure explicit
This is the most “pure relational” approach. But it felt repetitive. The schema grows horizontally.
And if tomorrow we add something else that can be liked, wed need yet another table.
It works, but it doesnt scale elegantly.
## Option 2: A Polymorphic Table (What We Chose)
Instead of multiple tables, we created a **single polymorphic likes table**.
### What is a Polymorphic Table?
A polymorphic table can reference multiple types of resources using a shared structure.
We designed our `likes` table to look something like this:
| user_id | resource_id | resource_type | timestamp |
| --- | --- | --- | --- |
| (foreign key) | (uuid) | (POST / COMMENT / NEWS) | |
Heres how it works:
- `user_id`: who liked, foreign key to `users` table.
- `resource_id` : the UUID of the item, just the uuid, no foreign key relation.
- `resource_type` : what type of item it is
- `timestamp` : timestamp
Instead of a strict foreign key to one table, we store:
- The ID
- The type of resource
Together, they uniquely identify what was liked.
With this approach, we had one clean and centralized table to store all kinds of likes. Its much easier to expand and flexible. Since “like” is a feature common to many parts of the system, this design keeps it generic and reusable.
But it does has a major downside. We lose direct foreign key enforcement on `resource_id`. Because PostgreSQL cant enforce a foreign key that dynamically points to multiple tables, referential integrity must be handled at the application level. We cannot write a simple join query to join from `comments` table or `posts` table.
For example, to fetch likes for a resource:
```tsx
SELECT COUNT(*)
FROM likes
WHERE resource_id = 'some-uuid'
AND resource_type = 'POST';
```
Now, if we need resource details plus likes, we may need separate queries or application-side logic.
For our use case though, that trade-off was acceptable. We dont perform heavy cross-entity joins on likes, so the downside was minimal.
## Why This Design Felt Right
The key insight was this:
> “Like” is not tightly coupled to Posts, Comments, or News. Its a behavior shared across resources.
>
By modeling it polymorphically, we treated “like” as a reusable system capability rather than a feature embedded in each entity.
And as our application grows, this decision will likely save us refactoring time.

View File

@@ -1,8 +1,7 @@
--- ---
title: "Search" # in any language you want title: "Search"
layout: "search" # necessary for search layout: "search"
# url: "/archive" url: "/search/"
# description: "Description for Search" summary: "Search posts and notes"
summary: "search" placeholder: "Search..."
placeholder: "placeholder text in search input box"
--- ---

View File

@@ -1,5 +1,5 @@
# DEmo baseURL: "/"
baseURL: https://example.org/ relativeURLs: true
languageCode: en-us languageCode: en-us
title: Saurav Dhakal title: Saurav Dhakal
theme: ["PaperMod"] theme: ["PaperMod"]
@@ -32,6 +32,7 @@ params:
DateFormat: "January 2, 2006" DateFormat: "January 2, 2006"
defaultTheme: dark # dark, light defaultTheme: dark # dark, light
disableThemeToggle: true disableThemeToggle: true
googleAnalytics: "G-V0CXG8ZEG2"
ShowReadingTime: true ShowReadingTime: true
ShowShareButtons: true ShowShareButtons: true
@@ -51,11 +52,11 @@ params:
assets: assets:
# disableHLJS: true # to disable highlight.js # disableHLJS: true # to disable highlight.js
# disableFingerprinting: true # disableFingerprinting: true
favicon: "<link / abs url>" favicon: ./favicon.png
favicon16x16: "<link / abs url>" favicon16x16: ./favicon.png
favicon32x32: "<link / abs url>" favicon32x32: ./favicon.png
apple_touch_icon: "<link / abs url>" apple_touch_icon: ./favicon.png
safari_pinned_tab: "<link / abs url>" safari_pinned_tab: ./favicon.png
label: label:
text: "SauravDhakal" text: "SauravDhakal"
@@ -77,27 +78,31 @@ params:
# home-info mode # home-info mode
homeInfoParams: homeInfoParams:
Title: "Hi there \U0001F44B, I'm Saurav!" Title: "Hi there \U0001F44B, Im Saurav!"
Content: > Content: >
Im a software engineer who enjoys building thoughtful systems and learning how things really work. Im a software engineer who enjoys building thoughtful systems and learning how things really work.
<br /> <br /><br />
This is my digital garden - notes, projects, and lessons along the way. Here I write about things I learn, find interesting, or want to remember. I'm a self-hosting and cloud enthusiast, actively experimenting with both.
<br /><br />
In my free time, I'm either reading or tinkering with something.
<br /><br />
Checkout my [CV](CV.pdf) for my works and projects.
- <br /> #demo
Checkout my [CV](files/CV.pdf) for my works and projects.
socialIcons: socialIcons:
- name: github - name: github
url: "https://github.com/sauravdhakal12" url: "https://github.com/sauravdhakal12"
- name: gitea
url: "https://gitea.sauravdhakal.com.np/explore"
- name: linkedin - name: linkedin
url: "https://www.linkedin.com/in/saurav-dhakal-9a8b27220/" url: "https://www.linkedin.com/in/saurav-dhakal-9a8b27220/"
- name: x - name: dev
url: "https://x.com/s0x1495" url: "https://dev.to/sauravdhakal12/"
analytics: analytics:
google: google:
SiteVerificationTag: "G-V0CXG8ZEG2" SiteVerificationTag: ""
cover: cover:
hidden: true # hide everywhere but not in structured data hidden: true # hide everywhere but not in structured data
@@ -123,16 +128,27 @@ params:
menu: menu:
main: main:
- identifier: posts - identifier: posts
name: posts name: Posts
url: /posts/ url: /posts/
weight: 10 weight: 10
- identifier: tags - identifier: notes
name: tags name: Notes
url: /tags/ url: /notes/
weight: 20 weight: 20
- identifier: tags
name: Tags
url: /tags/
weight: 30
- identifier: search
name: Search
url: /search/
weight: 40
# Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma # Read: https://github.com/adityatelange/hugo-PaperMod/wiki/FAQs#using-hugos-syntax-highlighter-chroma
pygmentsUseClasses: true pygmentsUseClasses: true
markup: markup:
goldmark:
renderer:
unsafe: true
highlight: highlight:
noClasses: false noClasses: false
# anchorLineNos: true # anchorLineNos: true
@@ -140,3 +156,8 @@ markup:
# guessSyntax: true # guessSyntax: true
# lineNos: true # lineNos: true
# style: monokai # style: monokai
googleAnalytics: "G-V0CXG8ZEG2"
services:
googleAnalytics:
id: "G-V0CXG8ZEG2"

View File

@@ -0,0 +1,7 @@
{{- $.Scratch.Add "index" slice -}}
{{- range site.RegularPages -}}
{{- if and (not .Params.searchHidden) (ne .Layout `archives`) (ne .Layout `search`) }}
{{- $.Scratch.Add "index" (dict "title" .Title "content" .Plain "permalink" .Permalink "summary" .Summary "date" (.Date.Format "Jan 2, 2006")) -}}
{{- end }}
{{- end -}}
{{- $.Scratch.Get "index" | jsonify -}}

19
layouts/index.html Normal file
View File

@@ -0,0 +1,19 @@
{{- define "main" }}
{{- if .Site.Params.homeInfoParams }}
<article class="first-entry home-info">
<header class="entry-header">
<h1>{{ .Site.Params.homeInfoParams.Title | markdownify }}</h1>
</header>
<div class="entry-content">
{{ .Site.Params.homeInfoParams.Content | markdownify }}
</div>
<footer class="entry-footer">
{{- partial "social_icons.html" (dict "align" site.Params.homeInfoParams.AlignSocialIconsTo) }}
</footer>
</article>
{{- end }}
{{- /* No recent posts/notes - clean home page */ -}}
{{- end }}{{/* end main */}}

22
layouts/notes/list.html Normal file
View File

@@ -0,0 +1,22 @@
{{- define "main" }}
<header class="page-header">
<h1>{{ .Title }}</h1>
{{- if .Description }}
<div class="post-description">{{ .Description }}</div>
{{- end }}
</header>
<ul class="notes-list">
{{- range .Pages }}
<li class="note-item">
<a href="{{ .Permalink }}">
<span class="note-title"># {{ .Title }}</span>
<span class="delimiter">&bull;</span>
<span class="note-date">{{ .Date.Format "Jan 2, 2006" }}</span>
</a>
</li>
{{- end }}
</ul>
{{- end }}{{/* end main */}}

View File

@@ -12,7 +12,7 @@
{{- end }} {{- end }}
{{- if (not site.Params.disableScrollToTop) }} {{- if (not site.Params.disableScrollToTop) }}
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g"> <a href="#top" aria-label="go to top" title="Go to Top" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" /> <path d="M12 6H0l6-6z" />
</svg> </svg>

View File

@@ -0,0 +1,113 @@
{{ if .IsHome }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "{{- ( site.Params.schema.publisherType | default "Organization") | title -}}",
"name": {{ site.Title | jsonify }},
"url": {{ site.Home.Permalink | jsonify }},
"description": {{ site.Params.description | plainify | truncate 180 | jsonify }},
{{- if (eq site.Params.schema.publisherType "Person") }}
"image": {{ site.Params.assets.favicon | default "favicon.ico" | absURL | jsonify }},
{{- else }}
"logo": {{ site.Params.assets.favicon | default "favicon.ico" | absURL | jsonify }},
{{- end }}
"sameAs": [
{{- if site.Params.schema.sameAs }}
{{ range $i, $e := site.Params.schema.sameAs }}{{ if $i }}, {{ end }}{{ trim $e " " | jsonify }}{{ end }}
{{- else}}
{{ range $i, $e := site.Params.SocialIcons }}{{ if $i }}, {{ end }}{{ trim $e.url " " | jsonify }}{{ end }}
{{- end}}
]
}
</script>
{{- else if (or .IsPage .IsSection) }}
{{/* BreadcrumbList */}}
{{- $url := replace .Parent.Permalink ( printf "%s" site.Home.Permalink) "" }}
{{- $lang_url := strings.TrimPrefix ( printf "%s/" .Lang) $url }}
{{- $bc_list := (split $lang_url "/")}}
{{- $scratch := newScratch }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{{- range $index, $element := $bc_list }}
{{- $scratch.Add "path" (printf "%s/" $element ) | safeJS }}
{{- $bc_pg := site.GetPage ($scratch.Get "path") -}}
{{- if (and ($bc_pg) (gt (len . ) 0))}}
{{- if $index }}, {{end }}
{
"@type": "ListItem",
"position": {{ add 1 $index }},
"name": {{ $bc_pg.Name | jsonify }},
"item": {{ $bc_pg.Permalink | jsonify }}
}
{{- end }}
{{- end }}
{{- /* self-page addition */ -}}
, {
"@type": "ListItem",
"position": {{ add (len $bc_list) 1 }},
"name": {{ .Name | jsonify }},
"item": {{ .Permalink | jsonify }}
}
]
}
</script>
{{- if .IsPage }}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"headline": {{ .Title | plainify | jsonify }},
"name": {{ .Title | plainify | jsonify }},
"description": {{ with .Description | plainify }}{{ . | jsonify }}{{ else }}{{ .Summary | plainify | truncate 150 | jsonify }}{{ end }},
"keywords": [
{{- if .Params.keywords }}
{{ range $i, $e := .Params.keywords }}{{ if $i }}, {{ end }}{{ $e | jsonify }}{{ end }}
{{- else }}
{{ range $i, $e := .Params.tags }}{{ if $i }}, {{ end }}{{ $e | jsonify }}{{ end }}
{{- end }}
],
"wordCount": {{ .WordCount }},
"inLanguage": {{ .Language.Lang | default "en-us" | jsonify }},
"datePublished": {{ .PublishDate.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
"dateModified": {{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" | jsonify }},
{{- with (.Params.author | default site.Params.author) }}
"author":
{{- if (or (eq (printf "%T" .) "[]string") (eq (printf "%T" .) "[]interface {}")) -}}
[{{- range $i, $v := . -}}
{{- if $i }}, {{end -}}
{
"@type": "Person",
"name": {{ $v | jsonify }}
}
{{- end }}],
{{- else -}}
{
"@type": "Person",
"name": {{ . | jsonify }}
},
{{- end -}}
{{- end }}
"mainEntityOfPage": {
"@type": "WebPage",
"@id": {{ .Permalink | jsonify }}
},
"publisher": {
"@type": "{{- ( site.Params.schema.publisherType | default "Organization") | title -}}",
"name": {{ site.Title | jsonify }},
"logo": {
"@type": "ImageObject",
"url": {{ site.Params.assets.favicon | default "favicon.ico" | absURL | jsonify }}
}
}
}
</script>
{{- end }}{{/* .IsPage end */}}
{{- end -}}

97
layouts/partials/toc.html Normal file
View File

@@ -0,0 +1,97 @@
{{- $headers := findRE "<h[1-6].*?>(.|\n])+?</h[1-6]>" .Content -}}
{{- $has_headers := ge (len $headers) 1 -}}
{{- if $has_headers -}}
<div class="toc">
<details {{if (.Param "TocOpen") }} open{{ end }}>
<summary accesskey="c" title="">
<span class="details">{{- i18n "toc" | default "Table of Contents" }}</span>
</summary>
<div class="inner">
{{- if (.Param "UseHugoToc") }}
{{- .TableOfContents -}}
{{- else }}
{{- $largest := 6 -}}
{{- range $headers -}}
{{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
{{- $headerLevel := len (seq $headerLevel) -}}
{{- if lt $headerLevel $largest -}}
{{- $largest = $headerLevel -}}
{{- end -}}
{{- end -}}
{{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}}
{{- $.Scratch.Set "bareul" slice -}}
<ul>
{{- range seq (sub $firstHeaderLevel $largest) -}}
<ul>
{{- $.Scratch.Add "bareul" (sub (add $largest .) 1) -}}
{{- end -}}
{{- range $i, $header := $headers -}}
{{- $headerLevel := index (findRE "[1-6]" . 1) 0 -}}
{{- $headerLevel := len (seq $headerLevel) -}}
{{/* get id="xyz" */}}
{{- $id := index (findRE "(id=\"(.*?)\")" $header 9) 0 }}
{{- /* strip id="" to leave xyz, no way to get regex capturing groups in hugo */ -}}
{{- $cleanedID := replace (replace $id "id=\"" "") "\"" "" }}
{{- $header := replaceRE "<h[1-6].*?>((.|\n])+?)</h[1-6]>" "$1" $header -}}
{{- if ne $i 0 -}}
{{- $prevHeaderLevel := index (findRE "[1-6]" (index $headers (sub $i 1)) 1) 0 -}}
{{- $prevHeaderLevel := len (seq $prevHeaderLevel) -}}
{{- if gt $headerLevel $prevHeaderLevel -}}
{{- range seq $prevHeaderLevel (sub $headerLevel 1) -}}
<ul>
{{/* the first should not be recorded */}}
{{- if ne $prevHeaderLevel . -}}
{{- $.Scratch.Add "bareul" . -}}
{{- end -}}
{{- end -}}
{{- else -}}
</li>
{{- if lt $headerLevel $prevHeaderLevel -}}
{{- range seq (sub $prevHeaderLevel 1) -1 $headerLevel -}}
{{- if in ($.Scratch.Get "bareul") . -}}
</ul>
{{/* manually do pop item */}}
{{- $tmp := $.Scratch.Get "bareul" -}}
{{- $.Scratch.Delete "bareul" -}}
{{- $.Scratch.Set "bareul" slice}}
{{- range seq (sub (len $tmp) 1) -}}
{{- $.Scratch.Add "bareul" (index $tmp (sub . 1)) -}}
{{- end -}}
{{- else -}}
</ul>
</li>
{{- end -}}
{{- end -}}
{{- end -}}
{{- end }}
<li>
<a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify | safeHTML -}}">{{- $header | plainify | safeHTML -}}</a>
{{- else }}
<li>
<a href="#{{- $cleanedID -}}" aria-label="{{- $header | plainify | safeHTML -}}">{{- $header | plainify | safeHTML -}}</a>
{{- end -}}
{{- end -}}
<!-- {{- $firstHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers 0) 1) 0)) -}} -->
{{- $firstHeaderLevel := $largest }}
{{- $lastHeaderLevel := len (seq (index (findRE "[1-6]" (index $headers (sub (len $headers) 1)) 1) 0)) }}
</li>
{{- range seq (sub $lastHeaderLevel $firstHeaderLevel) -}}
{{- if in ($.Scratch.Get "bareul") (add . $firstHeaderLevel) }}
</ul>
{{- else }}
</ul>
</li>
{{- end -}}
{{- end }}
</ul>
{{- end }}
</div>
</details>
</div>
{{- end }}

25
layouts/posts/list.html Normal file
View File

@@ -0,0 +1,25 @@
{{- define "main" }}
<header class="page-header">
<h1>{{ .Title }}</h1>
{{- if .Description }}
<div class="post-description">{{ .Description }}</div>
{{- end }}
</header>
<ul class="posts-list">
{{- range .Pages }}
<li class="post-item">
<a href="{{ .Permalink }}">
<span class="post-title"># {{ .Title }}</span>
<span class="delimiter">&bull;</span>
<span class="post-date">{{ .Date.Format "Jan 2, 2006" }}</span>
</a>
{{- if .Summary }}
<p class="post-summary">{{ .Summary | plainify | truncate 150 }}</p>
{{- end }}
</li>
{{- end }}
</ul>
{{- end }}{{/* end main */}}

View File

@@ -1,2 +0,0 @@
<!doctype html><html lang=en dir=auto data-theme=dark><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name=robots content="index, follow"><title>404 Page not found | Saurav Dhakal</title><meta name=keywords content><meta name=description content="A personal blog"><meta name=author content="saurav"><link rel=canonical href=http://localhost:1313/404.html><meta name=google-site-verification content="G-V0CXG8ZEG2"><link crossorigin=anonymous href=/assets/css/stylesheet.1819d1b52fd9f2af4d88316fde3f9b918c48d08dac9a2e1ef0a7ca49d1f18ddb.css integrity="sha256-GBnRtS/Z8q9NiDFv3j+bkYxI0I2smi4e8KfKSdHxjds=" rel="preload stylesheet" as=style><link rel=icon href=http://localhost:1313/%3Clink%20/%20abs%20url%3E><link rel=icon type=image/png sizes=16x16 href=http://localhost:1313/%3Clink%20/%20abs%20url%3E><link rel=icon type=image/png sizes=32x32 href=http://localhost:1313/%3Clink%20/%20abs%20url%3E><link rel=apple-touch-icon href=http://localhost:1313/%3Clink%20/%20abs%20url%3E><link rel=mask-icon href=http://localhost:1313/%3Clink%20/%20abs%20url%3E><meta name=theme-color content="#2e2e33"><meta name=msapplication-TileColor content="#2e2e33"><link rel=alternate hreflang=en href=http://localhost:1313/404.html><noscript><style>#theme-toggle,.top-link{display:none}</style></noscript><meta property="og:url" content="http://localhost:1313/404.html"><meta property="og:site_name" content="Saurav Dhakal"><meta property="og:title" content="404 Page not found"><meta property="og:description" content="A personal blog"><meta property="og:locale" content="en-us"><meta property="og:type" content="website"><meta name=twitter:card content="summary"><meta name=twitter:title content="404 Page not found"><meta name=twitter:description content="A personal blog"></head><body class=list id=top><header class=header><nav class=nav><div class=logo><a href=http://localhost:1313/ accesskey=h title="SauravDhakal (Alt + H)">SauravDhakal</a><div class=logo-switches></div></div><ul id=menu><li><a href=http://localhost:1313/posts/ title=posts><span>posts</span></a></li><li><a href=http://localhost:1313/tags/ title=tags><span>tags</span></a></li></ul></nav></header><main class=main><div class=not-found>404</div></main><footer class=footer><span>Copyright &copy; 2025 SauravDhakal</span></footer><a href=#top aria-label="go to top" title="Go to Top (Alt + G)" class=top-link id=top-link accesskey=g><svg viewBox="0 0 12 6" fill="currentColor"><path d="M12 6H0l6-6z"/></svg>
</a><script>let menu=document.getElementById("menu");if(menu){const e=localStorage.getItem("menu-scroll-position");e&&(menu.scrollLeft=parseInt(e,10)),menu.onscroll=function(){localStorage.setItem("menu-scroll-position",menu.scrollLeft)}}document.querySelectorAll('a[href^="#"]').forEach(e=>{e.addEventListener("click",function(e){e.preventDefault();var t=this.getAttribute("href").substr(1);window.matchMedia("(prefers-reduced-motion: reduce)").matches?document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView():document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView({behavior:"smooth"}),t==="top"?history.replaceState(null,null," "):history.pushState(null,null,`#${t}`)})})</script><script>var mybutton=document.getElementById("top-link");window.onscroll=function(){document.body.scrollTop>800||document.documentElement.scrollTop>800?(mybutton.style.visibility="visible",mybutton.style.opacity="1"):(mybutton.style.visibility="hidden",mybutton.style.opacity="0")}</script></body></html>

Some files were not shown because too many files have changed in this diff Show More