├── .cursor └── rules │ └── directory-website-creator.mdc ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── LICENSE ├── README.md ├── astro.config.mjs ├── modules.d.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.svg └── meditation.jpg ├── src ├── components │ ├── analytics │ │ └── Posthog.astro │ ├── app │ │ ├── AppShell.astro │ │ ├── Footer.astro │ │ ├── Logo.astro │ │ ├── Navbar.astro │ │ ├── Prose.astro │ │ ├── SidebarNavbar.astro │ │ ├── SidebarShell.astro │ │ ├── SidebarTags.astro │ │ └── header │ │ │ ├── Banner.astro │ │ │ ├── ColorSelector.astro │ │ │ └── Dot.astro │ ├── blog │ │ └── Grid.astro │ ├── directory │ │ ├── FeaturedTag.astro │ │ ├── Grid.astro │ │ ├── PureGrid.astro │ │ ├── Search.astro │ │ ├── cards │ │ │ ├── BulletCard.astro │ │ │ ├── RectangleCard.astro │ │ │ ├── SmallHorizontalCard.astro │ │ │ └── index.astro │ │ └── index.ts │ ├── hero │ │ ├── SimpleLeftHero.astro │ │ ├── ThemeHero.astro │ │ └── TwoColumnHero.astro │ ├── listings │ │ └── TitleHeader.astro │ └── ui │ │ ├── ListCard.astro │ │ ├── ListItem.astro │ │ └── tags │ │ ├── Grid.vue │ │ ├── Select.vue │ │ └── Tag.astro ├── config │ ├── settings.toml │ └── themes │ │ ├── brookmint.toml │ │ ├── hemingway.toml │ │ ├── peppermint.toml │ │ └── spearmint.toml ├── content.config.ts ├── data │ ├── blog │ │ └── test.md │ ├── directory │ │ ├── directory.csv │ │ ├── directory.json │ │ ├── starter.md │ │ ├── starter2.md │ │ └── starter3.md │ ├── images │ │ ├── 007.png │ │ └── calm.png │ └── pages │ │ ├── blog.mdx │ │ └── index.mdx ├── env.d.ts ├── layouts │ ├── Article.astro │ ├── BaseLayout.astro │ ├── Card.astro │ ├── Landing.astro │ ├── Listing.astro │ ├── Sidebar.astro │ ├── Thin.astro │ └── Wide.astro ├── lib │ ├── getListings.ts │ ├── getRootPages.ts │ └── loaders │ │ ├── index.ts │ │ └── sheets.ts ├── pages │ ├── [...slug].astro │ ├── blog │ │ └── [slug].astro │ ├── og │ │ ├── [...slug].png.ts │ │ └── blog │ │ │ └── [...slug].png.ts │ └── tags │ │ └── [slug].astro ├── store.js ├── styles │ └── global.css ├── types │ ├── Tag.ts │ ├── ThemeConfig.ts │ ├── content.ts │ └── global.d.ts ├── util │ ├── formatString.ts │ ├── getOGImage.ts │ └── themeConfig.ts └── validation │ ├── data_source.ts │ ├── directory.ts │ └── settings.ts ├── tailwind.config.mjs └── tsconfig.json /.cursor/rules/directory-website-creator.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | 7 | # Directory Website Creation. 8 | 9 | - You are an expert directory website creator. 10 | 11 | ## Project Description 12 | - The project is written with Astro and typescript 13 | - Each directory listing can have the properties specified here [directory.ts](mdc:src/validation/directory.ts) 14 | - Directory listings can be added in json format in [directory.json](mdc:src/data/directory/directory.json) 15 | - The root of the json should be an array. where all of the listings are added to. 16 | - Remember that `id` is required for each listing when adding them with json. 17 | - If the user needs the directory listings to have their own page with content: 18 | - you need to set in [settings.toml](mdc:src/config/settings.toml) `directory.data.source` to `default` 19 | - you need to set `directory.data.links` to outbound to directly redirect the user to the website when each listing is clicked. 20 | - then add markdown files in `src/data/directory/`. 21 | - use frontmatter to add the properties that the listings need. 22 | 23 | ## Steps To Follow 24 | 1. Find potential listings online with the search tool. Only search for listings in the niche that the user requested. 25 | 2. Read the file [settings.toml](mdc:src/config/settings.toml). This file contains the settings for the directory. 26 | - The options are contained in [settings.ts](mdc:src/validation/settings.ts) 27 | - customize the [settings.toml](mdc:src/config/settings.toml) file to the directory of the user if it does not fit the needs of the directory. 28 | - do not include images since this will be easier for the user to do themselves, keep it empty. 29 | 3. Finally update the [index.mdx](mdc:src/data/pages/index.mdx) file which is the frontpage of the directory only by replacing the text inside of SimpleLeftHero. 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | # jetbrains setting folder 24 | .idea/ 25 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Mark Bruderer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Minted Directory Astro

3 |

Markdown driven directory template. Built with Astro and Tailwindcss. Optimized for SEO. Beautiful Customizable Style

4 |
5 | 6 |
7 | 8 |
9 | Minted Directory Screenshot 10 |
11 | 12 |
13 | 14 | ## Features: 15 | + 🖌️ Add listings from is possible from different formats: `markdown`, `csv`, `json`, `google sheets`, `notion`, `airtable`. 16 | + 🔋 SEO optimized and programmatic SEO out of the box 17 | + 💻 Pre-built components for directories. 18 | + 💅 Customizable styles. 19 | + 🌙 Dark/Light mode 20 | + 💸 Sponsored Content 21 | + 👀 Tags + Search 22 | 23 | ## Getting Started 24 | 25 | ### Local Development 26 | 27 | Duplicate the template then clone the repository. 28 | 29 | ```sh 30 | git clone git@github.com:youraccount/projectname.git my-directory 31 | ``` 32 | 33 | Or use the github cli to create a repository based on the template and clone in one command: 34 | 35 | ```sh 36 | gh repo create my-directory --template masterkram/minted-directory-astro --private --clone 37 | ``` 38 | 39 | Go to the cloned folder: 40 | ```sh 41 | cd my-directory 42 | ``` 43 | 44 | Install dependencies 45 | 46 | ```sh 47 | pnpm install 48 | ``` 49 | 50 | Run the website: 51 | 52 | ```sh 53 | pnpm dev 54 | ``` 55 | 56 | Congrats :tada: 57 | 58 | You can start customizing and building your directory. 59 | 60 | ## Adding Content 61 | 62 | Adding content to the directory can be done using one of the following formats: 63 | + markdown 64 | + json 65 | + csv 66 | + notion 67 | + google sheets 68 | + airtable 69 | 70 | Remember that listings will not be shown on the live website until the site is re-built and deployed. This is done to ensure the fastest possible performance by serving static html, css and js. 71 | 72 | ### Using markdown listings: 73 | 1. add markdown files to the `src/data/directory` folder. All markdown files will be automatically loaded as listings. 74 | 2. You must specify required properties of a listing such as title and description in the [frontmatter]() of the file 75 | 76 | ### Using json listings: 77 | 1. add a single json file: `src/data/directory/directory.json` 78 | 2. in this json file, the root element is an array. This array can contain objects which are the listings of the directory. These require the properties of `id`, `name` and `description`. 79 | 80 | ### Using csv listings: 81 | 1. add a single csv file: `src/data/directory/directory.csv` 82 | 2. add rows to this csv file, these require the properties of `id`, `name` and `description`. 83 | 84 | ### Using google sheets listings: 85 | 1. Create a [google sheets](https://docs.google.com/spreadsheets/u/0/) document. 86 | 2. You need to select your table and click `Format > Convert to table` 87 | 3. Make it publicly shareable. When you share the link. copy the id 88 | 4. Go to `settings.toml`, here you need to set the `source=sheets` and `key = ` the copied id of the file 89 | 90 | Use this spreadsheet as a starting point: [directory google sheet data](https://docs.google.com/spreadsheets/d/1BKVVFysQT8ZuPY8hUp--jwTrN-U20TrtML0idECIWmc/edit?usp=sharing) 91 | 92 | ### Using notion listings: 93 | 94 | ### Using airtable listings: 95 | 96 | 97 | ## Customization 98 | 99 | To customize the directory style: 100 | + Change the `--color-primary-x00` variables, `--color-gray-x00` variables in the `src/styles/global.css` 101 | + Change the font: 102 | + install from [fontsource]() 103 | + import font in `BaseLayout.astro` 104 | + change the `--font-sans` variable in `global.css` 105 | + Customize the `src/data/config/settings.toml` to your preferences. 106 | 107 | ### Pre-made styles: 108 | - spearmint 109 | - peppermint 110 | 111 | ### Adding Content 112 | 113 | Add listings by adding markdown files to `/src/content/directory` 114 | 115 | ## Deployment 116 | 117 | Deploy as a static site for best SEO performance: 118 | 119 | ```bash 120 | pnpm run build 121 | ``` 122 | 123 | ## Community 124 | 125 | [Join the discord](https://discord.gg/5UbrTNzX7y) 126 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, envField } from 'astro/config'; 3 | import vue from '@astrojs/vue'; 4 | import mdx from '@astrojs/mdx'; 5 | import icon from 'astro-icon'; 6 | import sitemap from '@astrojs/sitemap'; 7 | import { ViteToml } from 'vite-plugin-toml'; 8 | import tailwindcss from '@tailwindcss/vite'; 9 | 10 | // https://astro.build/config 11 | export default defineConfig({ 12 | site: "https://bestmeditationapps.com", 13 | integrations: [ 14 | vue(), 15 | mdx(), 16 | icon(), 17 | sitemap() 18 | ], 19 | vite: { 20 | plugins: [tailwindcss(), ViteToml()] 21 | }, 22 | env: { 23 | schema: { 24 | POSTHOG_API_KEY: envField.string({ context: "client", access: "public", optional: true }), 25 | POSTHOG_API_HOST: envField.string({ context: "client", access: "public", optional: true }), 26 | NOTION_TOKEN: envField.string({ context: "server", access: "secret", optional: true }) 27 | } 28 | } 29 | }); -------------------------------------------------------------------------------- /modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.toml" { 2 | const value: any; 3 | export default value; 4 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mintedastro", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@ascorbic/airtable-loader": "^1.0.5", 14 | "@ascorbic/csv-loader": "^2.0.3", 15 | "@ascorbic/mock-loader": "^2.0.2", 16 | "@astrojs/check": "^0.9.4", 17 | "@astrojs/mdx": "^4.2.0", 18 | "@astrojs/sitemap": "^3.2.1", 19 | "@astrojs/vue": "^5.0.7", 20 | "@fontsource-variable/dm-sans": "^5.2.5", 21 | "@fontsource-variable/eb-garamond": "^5.2.5", 22 | "@fontsource-variable/gabarito": "^5.2.5", 23 | "@fontsource-variable/inter": "^5.2.5", 24 | "@fontsource/gabarito": "^5.1.0", 25 | "@headlessui/vue": "^1.7.23", 26 | "@iarna/toml": "^2.2.5", 27 | "@nanostores/vue": "^0.11.0", 28 | "@tailwindcss/vite": "^4.0.14", 29 | "@types/node": "^22.7.4", 30 | "@vercel/og": "^0.6.5", 31 | "astro": "^5.5.2", 32 | "astro-color-scheme": "^1.1.5", 33 | "astro-sheet-loader": "^1.0.5", 34 | "dotenv": "^16.4.7", 35 | "google-auth-library": "^9.15.1", 36 | "google-spreadsheet": "^4.1.4", 37 | "googleapis": "^146.0.0", 38 | "nanostores": "^0.11.3", 39 | "notion-astro-loader": "^0.4.0", 40 | "sharp": "^0.33.5", 41 | "tailwindcss": "^4.0.14", 42 | "typescript": "^5.6.2", 43 | "vue": "^3.5.11", 44 | "zod": "^3.24.2" 45 | }, 46 | "devDependencies": { 47 | "@iconify-json/tabler": "^1.2.5", 48 | "@tailwindcss/typography": "^0.5.15", 49 | "astro-icon": "^1.1.1", 50 | "vite-plugin-toml": "^0.7.0" 51 | } 52 | } -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /public/meditation.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/masterkram/minted-directory-astro/f71c8ae3fcff741285107415701fb6d1390f55b6/public/meditation.jpg -------------------------------------------------------------------------------- /src/components/analytics/Posthog.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 | 62 | -------------------------------------------------------------------------------- /src/components/app/AppShell.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Posthog from "../analytics/Posthog.astro"; 3 | import Footer from "./Footer.astro"; 4 | import Navbar from "./Navbar.astro"; 5 | import Banner from "./header/Banner.astro"; 6 | --- 7 | 8 |
9 | 10 | 11 | 12 |
14 | -------------------------------------------------------------------------------- /src/components/app/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AppLogo from "./Logo.astro"; 3 | import { Icon } from "astro-icon/components"; 4 | import config from "@util/themeConfig"; 5 | 6 | const socials = Object.values(config.footer.socials); 7 | const socialNames = Object.keys(config.footer.socials); 8 | 9 | const navigation = [ 10 | { 11 | title: "Directory", 12 | links: [ 13 | { title: "Submit", link: "/submit" }, 14 | { title: "Advertise", link: "/advertise" }, 15 | ], 16 | }, 17 | { 18 | title: "Categories", 19 | links: config.directoryData.tags 20 | ?.filter((e) => e && e.name) 21 | .map((e) => ({ 22 | title: e.name, 23 | link: `/tags/${e.key}`, 24 | })) 25 | .slice(0, 4), 26 | }, 27 | { 28 | title: "Blog", 29 | links: [{ title: "Articles", link: "/blog" }], 30 | }, 31 | { 32 | title: "Legal", 33 | links: [ 34 | { title: "Privacy Policy", link: "/legal/terms-of-service" }, 35 | { title: "Terms of Service", link: "/legal/privacy-policy" }, 36 | ], 37 | }, 38 | ]; 39 | --- 40 | 41 | 123 | -------------------------------------------------------------------------------- /src/components/app/Logo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | import config from "@util/themeConfig"; 4 | --- 5 | 6 | 7 | { 8 | config.general.logo ? ( 9 | {config?.general.title 15 | ) : ( 16 | 20 | ) 21 | } 22 | {config.general.title} 26 | 27 | -------------------------------------------------------------------------------- /src/components/app/Navbar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | import ColorSelector from "./header/ColorSelector.astro"; 4 | import Logo from "./Logo.astro"; 5 | import config from "@util/themeConfig"; 6 | 7 | // Helper function to check if a route is active 8 | const isActive = (path: string, currentPath: string | undefined) => { 9 | if (!currentPath) return false; 10 | return ( 11 | currentPath.split("/")[1] === path.split("/")[1] || currentPath === path 12 | ); 13 | }; 14 | 15 | // Get the current path safely 16 | const currentPath = new URL(Astro.request.url).pathname; 17 | --- 18 | 19 | 132 | 133 | 134 | 149 | -------------------------------------------------------------------------------- /src/components/app/Prose.astro: -------------------------------------------------------------------------------- 1 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /src/components/app/SidebarNavbar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from "astro-icon/components"; 3 | import ColorSelector from "./header/ColorSelector.astro"; 4 | import Logo from "./Logo.astro"; 5 | import config from "@util/themeConfig"; 6 | 7 | // Helper function to check if a route is active 8 | const isActive = (path: string, currentPath: string | undefined) => { 9 | if (!currentPath) return false; 10 | return currentPath.split('/')[1] === path.split('/')[1] || currentPath === path; 11 | }; 12 | 13 | // Get the current path safely 14 | const currentPath = new URL(Astro.request.url).pathname; 15 | --- 16 | 17 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/components/app/SidebarShell.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Footer from "./Footer.astro"; 3 | import SidebarNavbar from "./SidebarNavbar.astro"; 4 | import SidebarTags from "./SidebarTags.astro"; 5 | import Banner from "./header/Banner.astro"; 6 | 7 | const { showSidebar = true } = Astro.props; 8 | --- 9 | 10 |
11 | 12 |
13 | {showSidebar && } 14 |
15 | 16 | 17 |
18 |
19 |
20 |