A modern, content-first personal website built with Astro, designed for technology articles, wikis, projects, and indie web principles. Write in Obsidian, publish with ease.
- 📝 Multiple Content Collections: Blog, Wiki, Projects, and standalone pages (About, Now, Uses, Blogroll, Bookmarks)
- 🌳 Hierarchical Wiki: Nested folder structure with automatic tree navigation
- 🎨 Tailwind CSS v4: Centralized styling system, minimal class repetition
- 🔍 Full-text Search: Powered by Pagefind
- 🌓 Theme Switching: Built-in dark/light mode toggle
- 📱 Fully Responsive: Mobile-first design
- 🚀 100% Static: Lightning-fast performance with zero JavaScript overhead (except for interactive features)
- 🔗 Indie Web Ready: Blogroll, bookmarks (Raindrop-backed), and RSS feed support
- ✍️ Obsidian Integration: Write content in Obsidian, sync via GitHub Actions (see TODO section)
- 🏷️ Smart Tagging: Automatic tag aggregation and filtering
- 📄 SEO Optimized: Sitemap, RSS feed, and semantic HTML
The site is built around 8 content types, all defined in src/content.config.ts:
- blog: Technology articles and posts
- wiki: Hierarchical knowledge base
- projects: Portfolio and project showcases
- about: Personal information
- uses: Tools and setup
- now: Current activities (inspired by nownownow.com)
- blogroll: Curated list of blogs you follow
- bookmarks: Saved links and resources (backed by Raindrop.io collections — see 'Raindrop.io Integration' below)
Note: The
readingpage is also powered by a Raindrop collection named "reading" rather than being a local content collection.
All site behavior is controlled through three main files:
Global site metadata (title, description, author, social links, etc.)
Content collection schemas using Zod. Defines frontmatter structure for each collection.
Collection metadata, pagination settings, URLs, and site structure.
src/content/
├── blog/ # Blog posts
├── wiki/ # Hierarchical wiki
│ ├── ai/
│ │ └── agentes.md
│ └── development/
│ └── web/
├── projects/ # Project showcases
├── about.md # About page
├── uses.md # Uses page
├── now.md # Now page
├── blogroll.md # Blogroll
└── bookmarks.md # Bookmarks (now backed by Raindrop collections)
- Node.js 18+
- npm or pnpm
# Clone the repository
git clone https://github.com/yourusername/your-repo.git
cd your-repo
# Install dependencies
npm install
# Start development server
npm run devVisit http://localhost:4321 to see your site.
npm run dev # Start development server
npm run build # Build for production
npm run preview # Preview production build
npm run astro # Run Astro CLI commandsQuick checklist:
- Ensure Node.js 18+ is installed
- Copy
.env.sampleto.envand fill required tokens (see 'Environment variables') - Install dependencies with
npm install - Run
npm run devwhile developing
Each content type has specific frontmatter requirements. Example for a blog post:
---
title: "My First Post"
description: "A great introduction to my blog"
date: 2025-10-11
mod: 2025-10-11
published: true
tags: [astro, web-development]
---
Your content here...- Set
published: truein frontmatter to make content visible - Use
published: falseto keep drafts hidden - The
modfield tracks last modification date - Tags are automatically slugified and aggregated
The wiki supports nested folders for hierarchical content:
wiki/
├── development/
│ ├── web/
│ │ └── frameworks.md
│ └── devtools.md
Navigation is automatically generated from folder structure.
The project uses Tailwind CSS v4 with a centralized styling approach:
- Global styles in
src/styles/global.css - Component-specific styles are minimal
- Theme tokens defined in CSS custom properties
- Dark mode support via class strategy
Layouts are modular and composable:
src/layout/
├── default.astro # Base layout
├── single.astro # Single pages (About, Uses, etc.)
├── archive.astro # Archive/listing pages
├── tag.astro # Tag pages
└── collection/
├── collection.astro # Collection wrapper
├── collection.default.astro # Standard collection layout
├── collection.entry.astro # Single entry layout
└── collection.wiki.astro # Wiki-specific layout
- @astrojs/sitemap: Automatic XML sitemap generation
- astro-pagefind: Full-text search indexing
Custom plugins in src/lib/rehype.ts:
- removeH1Plugin: Removes H1 tags (titles come from frontmatter)
- External link enhancement: Adds icons and
target="_blank"to external links
- astro-rehype-relative-markdown-links: Converts relative MD links to proper routes
- rehype-external-links: Enhanced external link handling
This project integrates with Raindrop.io to power the Bookmarks and Reading sections. These are no longer implemented as local content collections: instead, the site fetches your Raindrop collections and items at build/runtime.
Quick setup:
- Create a Raindrop API access token at: https://raindrop.io/settings/api
- Copy the provided
.env.sampleto.envand set your token:
RAINDROP_ACCESS_TOKEN=your_raindrop_access_token_here
- Ensure your environment (local development and deployment) provides the
RAINDROP_ACCESS_TOKENenv var. In code the value is read fromimport.meta.env.RAINDROP_ACCESS_TOKEN.
Notes and behavior:
- The
readingpage expects a Raindrop collection titledreading. Create or rename a collection in Raindrop to populate this list. - The bookmarks index lists your Raindrop collections (the app excludes the
readingcollection from the bookmarks list by default). - If the access token is missing or invalid, bookmark/reading fetches will fail and those sections will appear empty; errors are logged to the console.
- The Raindrop client in
src/lib/raindropimplements simple caching and pagination to avoid hitting API rate limits; see that folder for implementation details.
Collection naming and filters:
- Collections are filtered by the site name. The mapper strips the
site.name.prefix from collection titles. To make collections visible in the bookmarks list, name them following this convention (e.g.,ansango.readingoransango.work). Thereadingcollection should be namedreading(without site prefix) if you want it to be used by the Reading page. - The site excludes the collection titled
readingfrom the bookmarks collection list and uses it for the Reading page.
Deployment:
Set RAINDROP_ACCESS_TOKEN in your hosting provider (Vercel, Netlify, etc.) environment variables before building/previewing the site.
Environment variables
RAINDROP_ACCESS_TOKEN: required to fetch Raindrop collections and items. Add this to your.envduring development and as an environment variable in production.
Search & sitemap
- Search is powered by Pagefind (integration via
astro-pagefind). The index is generated at build time and served from/pagefind/. - Sitemap is generated with
@astrojs/sitemapand published at/sitemap-index.xml.
Troubleshooting
- If bookmarks or reading are empty, verify:
RAINDROP_ACCESS_TOKENis valid and set in the environment- Collections exist in Raindrop and are named correctly (see the 'Collection naming and filters' notes)
- Check server logs for fetch errors; the raindrop client will log HTTP status and errors
- If Pagefind doesn't show recent content, rebuild the site to regenerate the index (
npm run build).
/
├── public/ # Static assets
├── src/
│ ├── components/ # Reusable Astro components
│ │ ├── layout/ # Layout components
│ │ ├── ui/ # UI components
│ │ ├── searcher/ # Search functionality
│ │ └── theme/ # Theme switcher
│ ├── content/ # Content collections
│ ├── layout/ # Page layouts
│ ├── lib/ # Utilities and helpers
│ │ ├── collections.ts # Content fetching & pagination
│ │ ├── wikis.ts # Wiki tree generation
│ │ └── rehype.ts # Custom rehype plugins
│ ├── pages/ # Astro pages
│ ├── styles/ # Global styles
│ ├── constants.ts # Site configuration
│ ├── content.config.ts # Collection schemas
│ └── site.json # Site metadata
├── astro.config.ts # Astro configuration
└── package.json
- Define schema in
src/content.config.ts:
const newCollection = defineCollection({
schema: z.object({
title: z.string(),
description: z.string(),
published: z.boolean().default(false),
// ... more fields
}),
});- Add metadata in
src/constants.ts:
export const COLLECTION_METADATA = {
// ... existing collections
"new-collection": {
title: "New Collection",
description: "Description here",
url: "/new-collection",
entriesPerPage: 10,
},
};- Create content folder:
mkdir src/content/new-collectionEdit src/site.json:
{
"title": "Your Site Name",
"description": "Your site description",
"author": "Your Name",
"url": "https://yoursite.com",
"social": {
"github": "yourusername",
"twitter": "yourusername"
}
}Adjust entries per page in src/constants.ts:
export const COLLECTION_METADATA = {
blog: {
// ...
entriesPerPage: 10, // Change this value
},
};Search is powered by Pagefind and automatically indexes all published content during build:
- Searches titles, descriptions, and content
- Fuzzy matching support
- Zero-config setup
- Lightweight client (~10kb gzipped)
RSS feed is automatically generated at /rss.xml and includes:
- All published blog posts
- Full content for each entry
- Proper timestamps and metadata
Curate a list of blogs you follow in src/content/blogroll.md:
---
title: "Blogroll"
description: "Blogs I follow and recommend"
published: true
---
## Web Development
- [Blog Name](https://example.com) - DescriptionShare what you're currently working on in src/content/now.md (inspired by Derek Sivers' Now page movement).
Document your tools and setup in src/content/uses.md.
To sync content from an Obsidian vault stored in a separate GitHub repository, you'll need to create a GitHub Action workflow. Below is a template structure:
- Trigger: On push to your Obsidian repository or on schedule
- Action: Clone Obsidian repo, copy markdown files to this repo
- Commit: Auto-commit and push changes
- Create a new repo for your Obsidian vault
- Add a GitHub Actions workflow in this repo
.github/workflows/sync.yml:
TODO
- Create a Personal Access Token (PAT) with
reposcope - Add it as a repository secret in your site repository settings (choose a name for the secret) so your GitHub Actions workflow can authenticate to the vault repository
- Structure your Obsidian vault to match the content structure
- Customize the
rsynccommands based on your folder structure - Optional: Add content validation or frontmatter checks before committing
- Image sync: Add steps to copy images from Obsidian to
public/ - Link conversion: Process Obsidian-style
[[wikilinks]]to markdown links - Frontmatter validation: Add a step to validate frontmatter before pushing
- Trigger deployment: Add a step to trigger Vercel/Netlify deployment
You can implement this workflow after setting up your Obsidian vault structure to match your content collections.
TODO
The site is optimized for performance with:
- 100% static output
- Minimal JavaScript (only for interactive features)
- Efficient CSS with Tailwind's purge feature
- Fast load times and high Lighthouse scores
MIT License - feel free to use this template for your own personal site!
- Built with Astro
- Styled with Tailwind CSS
- Search powered by Pagefind
- Inspired by the IndieWeb movement
This is a personal website template, but suggestions and improvements are welcome! Feel free to open an issue or submit a pull request.
Made with ❤️ and Astro
