├── .env.example
├── .github
└── workflows
│ └── studio.yml
├── .gitignore
├── LICENSE
├── README.md
├── app.config.ts
├── app.vue
├── components
├── OgImage
│ ├── CoverImage.vue
│ ├── Custom.vue
│ ├── Emoji.vue
│ └── Simple.vue
├── app
│ ├── Footer.vue
│ ├── Layout.vue
│ ├── Logo.vue
│ ├── Navbar.vue
│ ├── Renderer.vue
│ └── header
│ │ ├── Banner.vue
│ │ ├── ColorSelector.vue
│ │ └── Dot.vue
├── content
│ ├── PreRenderTags.vue
│ ├── advertisements
│ │ ├── FeatureExample.vue
│ │ └── SponsorExample.vue
│ ├── blog
│ │ └── BlogGrid.vue
│ ├── directory
│ │ ├── DirectoryGrid.vue
│ │ └── Search.vue
│ ├── general
│ │ ├── LinkButton.vue
│ │ ├── NewsletterForm.vue
│ │ ├── NewsletterWithCTA.vue
│ │ └── Tweet.vue
│ ├── hero
│ │ ├── CenterHero.vue
│ │ ├── ImageHero.vue
│ │ ├── SimpleLeftHero.vue
│ │ └── TwoColumnHero.vue
│ ├── prose
│ │ └── ProseA.vue
│ └── submit
│ │ └── TallyForm.vue
├── directory
│ ├── EmptyQueryIndicator.vue
│ ├── Featured
│ │ ├── Recommendation.vue
│ │ └── Tag.vue
│ ├── Item.vue
│ ├── PureGrid.vue
│ └── SubmitBox.vue
├── document
│ ├── Empty.vue
│ ├── NotFound.vue
│ └── Prose.vue
└── ui
│ ├── Button.vue
│ ├── Card.vue
│ ├── ShadowCard.vue
│ ├── Tag.vue
│ └── tag
│ ├── Grid.vue
│ └── Select.vue
├── composables
├── useCurrentPage.ts
├── useDirectory.ts
├── useFeatured.ts
├── useKeyFocus.ts
└── useTags.ts
├── content.config.ts
├── content
├── advertise.md
├── blog.md
├── blog
│ └── blog-post-1.md
├── dir
│ └── starter.md
├── index.md
├── legal
│ ├── privacy-policy.md
│ └── terms-of-service.md
└── submit.md
├── eslint.config.mjs
├── layouts
├── card.vue
├── default.vue
├── thin.vue
└── wide.vue
├── middleware
└── setLayout.ts
├── nuxt.config.ts
├── nuxt.schema.ts
├── package.json
├── pages
├── [...slug].vue
└── tags
│ └── [slug].vue
├── pnpm-lock.yaml
├── public
├── favicon.ico
├── logo.png
└── test.jpg
├── tailwind.config.ts
├── tsconfig.json
├── types
└── Tag.ts
└── utils
├── formatString.ts
└── setSeo.ts
/.env.example:
--------------------------------------------------------------------------------
1 | POSTHOG_PUBLIC_KEY=
2 | POSTHOG_HOST=
--------------------------------------------------------------------------------
/.github/workflows/studio.yml:
--------------------------------------------------------------------------------
1 |
2 | name: studio-nuxt-build
3 | run-name: studio nuxt build
4 |
5 | on:
6 | # Runs on pushes targeting the default branch
7 | push:
8 | branches:
9 | - 'main'
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Add write workflow permissions
15 | permissions:
16 | contents: write
17 |
18 | # Allow one concurrent deployment
19 | concurrency:
20 | group: "pages"
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | # Build job
25 | build-and-deploy:
26 | runs-on: ${{ matrix.os }}
27 | defaults:
28 | run:
29 | working-directory: .
30 |
31 | strategy:
32 | matrix:
33 | os: [ubuntu-latest]
34 | node: [20]
35 |
36 | steps:
37 | - name: Checkout
38 | uses: actions/checkout@v4
39 |
40 | - name: Identify package manager
41 | id: pkgman
42 | run: |
43 | cache=`[ -f "./pnpm-lock.yaml" ] && echo "pnpm" || ([ -f "./package-lock.json" ] && echo "npm" || ([ -f "./yarn.lock" ] && echo "yarn" || echo ""))`
44 | package_manager=`[ ! -z "$cache" ] && echo "$cache" || echo "pnpm"`
45 | echo "cache=$cache" >> $GITHUB_OUTPUT
46 | echo "package_manager=$package_manager" >> $GITHUB_OUTPUT
47 |
48 | - uses: pnpm/action-setup@v4
49 | if: ${{ steps.pkgman.outputs.package_manager == 'pnpm' }}
50 | name: Install pnpm
51 | id: pnpm-install
52 |
53 | - uses: actions/setup-node@v4
54 | with:
55 | version: ${{ matrix.node }}
56 | cache: ${{ steps.pkgman.outputs.cache }}
57 |
58 | - name: Install dependencies
59 | run: ${{ steps.pkgman.outputs.package_manager }} install
60 |
61 | - name: Generate
62 | run: npx nuxi build --preset github_pages
63 | env:
64 | NUXT_CONTENT_PREVIEW_API: https://api.nuxt.studio
65 |
66 |
67 | # Deployment job
68 | - name: Deploy 🚀
69 | uses: JamesIves/github-pages-deploy-action@v4
70 | with:
71 | folder: ./.output/public
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Nuxt dev/build outputs
2 | .output
3 | .data
4 | .nuxt
5 | .nitro
6 | .cache
7 | dist
8 |
9 | # Node dependencies
10 | node_modules
11 |
12 | # Logs
13 | logs
14 | *.log
15 |
16 | # Misc
17 | .DS_Store
18 | .fleet
19 | .idea
20 |
21 | # Local env files
22 | .env
23 | .env.*
24 | !.env.example
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 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 Template
3 |
Markdown driven directory template. Built with Nuxt, Nuxt Content and Tailwindcss. Optimized for SEO. Beautiful Customizable Style
4 |
5 |
6 |
7 |
8 |
9 |

10 |
11 |
12 |
13 |
14 | Learn more at [minteddirectory.com](https://minteddirectory.com)
15 |
16 | Read the [docs](https://minteddirectory.com/docs)
17 |
18 | ### Features:
19 | + 🖌️ Add Listings with markdown.
20 | + 🔋 Batteries included for SEO (nuxt seo module).
21 | + 💻 Pre-built components for directories.
22 | + 💅 Customizable style.
23 | + 🌙 Dark/Light mode
24 | + 💸 Sponsored Content
25 |
26 | ## Getting Started
27 |
28 | ### Local Development
29 |
30 | Duplicate the template then clone the repository.
31 |
32 | ```sh
33 | git clone git@github.com:youraccount/projectname.git my-directory
34 | ```
35 |
36 | Or use the github cli to create a repository based on the template and clone in one command:
37 |
38 | ```sh
39 | gh repo create my-directory --template masterkram/minted-directory --private --clone
40 | ```
41 |
42 | Go to the cloned folder:
43 | ```sh
44 | cd my-directory
45 | ```
46 |
47 | Install dependencies
48 |
49 | ```sh
50 | pnpm install
51 | ```
52 |
53 | Run the website:
54 |
55 | ```sh
56 | pnpm dev
57 | ```
58 |
59 | Congrats :tada:
60 |
61 | You can start customizing and building your directory.
62 |
63 | ### Customization
64 |
65 | To customize the directory style:
66 | + Change the `primary`, `secondary` color and `fontFamily` in `tailwind.config.ts`
67 | + Customize the `app.config.ts`
68 |
69 | Read about the possible changes to the app config [here](https://minteddirectory/docs/settings).
70 |
71 | ### Adding Content
72 |
73 | Add listings by adding markdown files to `/content/dir`
74 |
75 | Add blog articles by adding markdown files to `/content/blog`
76 |
77 | ### Deployment
78 |
79 | Deploy as a pre-rendered, static site for best SEO performance:
80 |
81 | ```bash
82 | pnpm run generate
83 | ```
84 |
85 | Check out the [deployment documentation](https://minteddirectory.com/docs/deployment) for more information.
86 |
87 | ## Community
88 |
89 | [Join the discord](https://discord.gg/5UbrTNzX7y)
90 |
--------------------------------------------------------------------------------
/app.config.ts:
--------------------------------------------------------------------------------
1 | export default defineAppConfig({
2 | general: {
3 | title: 'Minted Directory',
4 | logo: '',
5 | iconLogo: 'fluent-emoji-flat:leaf-fluttering-in-wind',
6 | language: 'en',
7 | },
8 | site: {
9 | // override the general settings for seo tags.
10 | // leave empty for general priority.
11 | // url is necessary for correct function of seo module.
12 | name: 'Minted Directory',
13 | description: 'Example Description',
14 | url: 'https://example.com',
15 | favicon: {
16 | image: '',
17 | emoji: '🍃',
18 | },
19 | },
20 | directory: {
21 | listingPageLayout: 'card',
22 | search: {
23 | placeholder: 'Search among {0} tools',
24 | icon: 'tabler:bow',
25 | tags: {
26 | // options: none,select,show-all,
27 | display: 'select',
28 | intersection: false,
29 | },
30 | },
31 | grid: {
32 | list: false,
33 | emptyState: {
34 | text: 'Seems that this entry is missing from the archives.',
35 | // options: button, simple, link
36 | type: 'button',
37 | icon: 'tabler:exclamation-mark',
38 | },
39 | card: {
40 | image: true,
41 | // options: dashed, shadow, outline, bullet
42 | type: 'shadow',
43 | },
44 | submit: {
45 | show: true,
46 | first: false,
47 | title: 'Submit a template',
48 | description:
49 | 'Submit a template to show off a good project to other people.',
50 | hideable: true,
51 | },
52 | },
53 | featured: {
54 | showOnAllPages: true,
55 | showOnSide: true,
56 | icon: 'tabler:star',
57 | labelForCard: 'Featured ✨',
58 | },
59 | tags: [
60 | { name: 'SAAS', color: 'blue' },
61 | { name: 'dashboard', color: 'green' },
62 | { name: 'landing-page' },
63 | { name: 'toolbox' },
64 | { name: 'agency' },
65 | { name: 'markdown-based' },
66 | { name: 'basics', color: 'indigo' },
67 | ],
68 | tagPages: {
69 | title: 'Available {0} products:',
70 | description:
71 | 'View all available tools and templates in the {0} category...',
72 | },
73 | },
74 | header: {
75 | banner: {
76 | show: true,
77 | text: 'Create your own directory website in minutes.',
78 | link: 'https://minteddirectory.com',
79 | brandText: 'MintedDirectory',
80 | },
81 | navbar: {
82 | colorModeSelector: true,
83 | links: [
84 | { name: 'Directory', to: '/' },
85 | { name: 'Blog', to: '/blog' },
86 | { name: 'Advertise', to: '/advertise' },
87 | {
88 | name: 'Analytics',
89 | to: 'https://us.posthog.com/shared/7dgSk4cvgNYnJwBu6R47kZXHBUBJWQ',
90 | target: '_blank',
91 | },
92 | ],
93 | },
94 | actionButton: {
95 | text: 'Submit a listing',
96 | href: '/submit',
97 | },
98 | },
99 | footer: {
100 | description: "Best directory for my niche.",
101 | navigation: [
102 | {
103 | title: "Directory", links: [{ title: "Submit", link: "/submit" }, { title: "Advertise", link: "/advertise" }],
104 | },
105 | {
106 | title: "Categories", links: [
107 | { title: "SAAS", link: "/tags/saas" },
108 | { title: "Dashboard", link: "/tags/dashboard" },
109 | { title: "Landing Page", link: "/tags/landing-page" },
110 | { title: "Toolbox", link: "/tags/toolbox" },
111 | ],
112 | },
113 | {
114 | title: "Blog", links: [{ title: "Articles", link: "/blog" }],
115 | },
116 | {
117 | title: "Legal", links: [{ title: "Privacy Policy", link: "/legal/terms-of-service" }, { title: "Terms of Service", link: "/legal/privacy-policy" }],
118 | },
119 | ],
120 | socials: {
121 | github: {
122 | link: '',
123 | icon: 'tabler:brand-github',
124 | },
125 | facebook: {
126 | link: '',
127 | icon: 'tabler:brand-facebook',
128 | },
129 | instagram: {
130 | link: '',
131 | icon: 'tabler:brand-instagram',
132 | },
133 | x: {
134 | link: 'https://x.com/mark_bruderer',
135 | icon: 'tabler:brand-twitter',
136 | },
137 | youtube: {
138 | link: 'https://www.youtube.com/@mark_hacks',
139 | icon: 'tabler:brand-youtube',
140 | },
141 | },
142 | },
143 | ui: {
144 | icons: {
145 | dark: 'tabler:moon',
146 | light: 'tabler:sun',
147 | },
148 | },
149 | });
150 |
--------------------------------------------------------------------------------
/app.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
63 |
--------------------------------------------------------------------------------
/components/OgImage/CoverImage.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
14 |
15 |
![]()
16 |
22 | {{ title }}
23 |
24 |
25 | {{ description }}
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/components/OgImage/Custom.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
16 |
17 |
![]()
25 |
33 | {{ title }}
34 |
35 |
39 | {{ description }}
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/components/OgImage/Emoji.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | {{ emoji ?? '😎' }}
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/components/OgImage/Simple.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/components/app/Footer.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
93 |
94 |
--------------------------------------------------------------------------------
/components/app/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/components/app/Logo.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
17 |
22 | {{ config.general.title
23 | }}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/components/app/Navbar.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 |
27 |
28 |
29 |
30 |
31 |
34 |
35 | Open main menu
36 |
42 |
48 |
49 |
50 |
79 |
80 |
81 |
85 |
89 | {{ config?.actionButton?.text }}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
105 | {{ item.name }}
106 |
110 |
111 |
112 |
113 |
114 |
115 |
119 | {{ config?.actionButton?.text }}
120 |
121 |
125 |
126 |
127 |
128 |
129 |
130 |
--------------------------------------------------------------------------------
/components/app/Renderer.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/components/app/header/Banner.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
62 |
63 |
--------------------------------------------------------------------------------
/components/app/header/ColorSelector.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
32 |
33 |
--------------------------------------------------------------------------------
/components/app/header/Dot.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/components/content/PreRenderTags.vue:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/components/content/advertisements/FeatureExample.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/components/content/advertisements/SponsorExample.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 | Sponsor
8 |
9 | {{ title }}
10 |
11 | {{ description }}
12 |
13 |
--------------------------------------------------------------------------------
/components/content/blog/BlogGrid.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/components/content/directory/DirectoryGrid.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
37 |
38 |
--------------------------------------------------------------------------------
/components/content/directory/Search.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 |
24 |
28 |
33 |
34 |
41 |
42 | ⌘K
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/components/content/general/LinkButton.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/components/content/general/NewsletterForm.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/content/general/NewsletterWithCTA.vue:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/content/general/Tweet.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/components/content/hero/CenterHero.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
25 |
26 |
--------------------------------------------------------------------------------
/components/content/hero/ImageHero.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
![]()
15 |
16 |
17 |
30 |
31 |
32 |
33 |
34 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/components/content/hero/SimpleLeftHero.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/components/content/hero/TwoColumnHero.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
31 |
32 |
33 |
42 |
--------------------------------------------------------------------------------
/components/content/prose/ProseA.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/components/content/submit/TallyForm.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 | Please wait we are loading the form
28 |
29 |
30 |
32 |
33 |
--------------------------------------------------------------------------------
/components/directory/EmptyQueryIndicator.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | {{ config.emptyState.text }}
9 |
10 |
{{ config.emptyState.text }}
14 |
18 |
22 |
23 | {{ config.emptyState.text }}
24 |
25 |
29 |
30 | Be the first to add it
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/components/directory/Featured/Recommendation.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
13 |
14 |
15 |
{{ featured?.title }}
16 |
{{ featured?.description }}
17 |
18 |
19 | Learn More
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/components/directory/Featured/Tag.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
8 | {{ config?.directory?.featured?.labelForCard }}
9 |
10 |
--------------------------------------------------------------------------------
/components/directory/Item.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
8 |
9 |
10 | {{ item.title }}
11 |
12 |
13 | {{ item.description }}
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/components/directory/PureGrid.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/components/directory/SubmitBox.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
14 |
18 |
19 | {{ config?.submit?.title }}
23 |
24 |
25 | {{ config?.submit?.description }}
26 |
27 |
28 |
33 |
37 | {{ config?.submit?.description }}
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/components/document/Empty.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
This page has no content yet.
4 |
We are working on the content on this page.
5 |
6 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/components/document/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
404
4 |
Page not found
5 |
Sorry, we couldn’t find the page you’re looking
6 | for.
7 |
8 |
9 | Back to home
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/components/document/Prose.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/components/ui/Button.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
11 |
12 |
--------------------------------------------------------------------------------
/components/ui/Card.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
21 |
25 |
32 |
36 | {{ item.title }}
37 |
38 |
39 |
40 | {{ item.title }}
41 |
42 |
43 | {{ item.description }}
44 |
45 |
46 |
51 |
52 |
53 |
54 |
58 |
59 |
![]()
63 |
{{ item.title }} -
64 |
{{ item.description }}
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/components/ui/ShadowCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/components/ui/Tag.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 | {{ tag }}
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/components/ui/tag/Grid.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
16 | {{ tag.name }}
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/components/ui/tag/Select.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
10 | removeTag(myTag)"
11 | class="absolute text-gray-500 opacity-0 transition-all group-hover:opacity-100 hover:bg-gray-100 flex items-center justify-center -top-4 left-0 bg-white rounded-full h-6 w-6 border dark:bg-gray-700 dark:border-gray-600 dark:hover:bg-gray-800">
12 |
13 |
14 | {{ myTag }}
15 |
16 |
23 |
24 |
--------------------------------------------------------------------------------
/composables/useCurrentPage.ts:
--------------------------------------------------------------------------------
1 | import type { DirectoryCollectionItem, PagesCollectionItem } from '@nuxt/content';
2 | import { useAsyncData, type AsyncData } from 'nuxt/app';
3 | import type { RouteLocationNormalizedLoaded } from 'vue-router';
4 |
5 | export function useCurrentPage(route: RouteLocationNormalizedLoaded): AsyncData {
6 | const splitUrl = route.path.split('/');
7 | const lastSlug = splitUrl[splitUrl.length - 1];
8 | const key = `current-page-${lastSlug || 'root'}`;
9 |
10 | return useAsyncData(key, async () => {
11 | if (route.path.startsWith('/blog/')) {
12 | const blogPage = await queryCollection('blog').path(`/blog/${lastSlug}`).first();
13 |
14 | if (blogPage) {
15 | return blogPage;
16 | }
17 | }
18 |
19 | const extraPage = await queryCollection('pages').path(`/${lastSlug}`).first();
20 |
21 | if (extraPage) {
22 | return extraPage;
23 | }
24 |
25 | return await queryCollection('directory').path(`/dir/${lastSlug}`).first();
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/composables/useDirectory.ts:
--------------------------------------------------------------------------------
1 | import type { DirectoryCollectionItem } from '@nuxt/content';
2 | import { useAsyncData } from 'nuxt/app';
3 |
4 | export function useDirectory() {
5 | const directoryData = useAsyncData('board', () => {
6 | const query = queryCollection('directory');
7 |
8 | query.select('featured', 'card_image', 'description', 'title', 'path', 'tags');
9 |
10 | return query.order('featured', 'DESC').all() as Promise;
11 | });
12 |
13 | return directoryData;
14 | }
15 |
--------------------------------------------------------------------------------
/composables/useFeatured.ts:
--------------------------------------------------------------------------------
1 | import type { DirectoryCollectionItem } from '@nuxt/content';
2 | import { useAsyncData, type AsyncData } from 'nuxt/app';
3 |
4 | export function useFeatured(): AsyncData {
5 | return useAsyncData('featured-listing', () =>
6 | queryCollection('directory').where('featured', '=', 'true').first(),
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/composables/useKeyFocus.ts:
--------------------------------------------------------------------------------
1 | export default function () {
2 | const searchInput = ref();
3 |
4 | const keyListener = function (e: KeyboardEvent) {
5 | if (e.key === 'k' && (e.ctrlKey || e.metaKey)) {
6 | e.preventDefault();
7 | searchInput.value.focus();
8 | }
9 | };
10 |
11 | onMounted(() => {
12 | document.body.addEventListener('keydown', keyListener);
13 | });
14 |
15 | onBeforeUnmount(() => {
16 | document.body.removeEventListener('keydown', keyListener);
17 | });
18 |
19 | return searchInput;
20 | }
21 |
--------------------------------------------------------------------------------
/composables/useTags.ts:
--------------------------------------------------------------------------------
1 | import { computed } from 'vue';
2 | import type Tag from '~~/types/Tag';
3 | import { useState } from '#app';
4 | import { useAppConfig } from '#imports';
5 |
6 | export function useTags() {
7 | const selectedTags = useState('tags', () => []);
8 | const tags = useAppConfig().directory.tags as Tag[] | undefined;
9 |
10 | const availableTags = computed((): Tag[] => {
11 | if (!tags) {
12 | return [];
13 | }
14 | return tags.filter(
15 | (e: Tag) => e && !selectedTags.value.includes((e as Tag).name),
16 | );
17 | });
18 |
19 | function removeTag(myTag: string) {
20 | const index = selectedTags.value.indexOf(myTag);
21 | if (index > -1) {
22 | selectedTags.value.splice(index, 1);
23 | }
24 | }
25 |
26 | function addTagWithEvent(event: Event) {
27 | const select = event.target as HTMLSelectElement;
28 | const selectedValue = select.value;
29 |
30 | addTagByName(selectedValue);
31 | select.value = '';
32 | }
33 |
34 | function addTagByName(name: string) {
35 | if (selectedTags.value && !selectedTags.value.includes(name)) {
36 | selectedTags.value.push(name);
37 | }
38 | }
39 |
40 | function toggleTagByName(name: string) {
41 | const index = selectedTags.value.indexOf(name);
42 | if (index != -1) {
43 | return selectedTags.value.splice(index, 1);
44 | }
45 |
46 | selectedTags.value.push(name);
47 | }
48 |
49 | return {
50 | selectedTags,
51 | availableTags,
52 | addTagWithEvent,
53 | toggleTagByName,
54 | removeTag,
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/content.config.ts:
--------------------------------------------------------------------------------
1 | import { defineContentConfig, defineCollection, z } from '@nuxt/content';
2 |
3 | export default defineContentConfig({
4 | collections: {
5 | pages: defineCollection({
6 | type: 'page',
7 | source: '*.md',
8 | schema: z.object({
9 | layout: z.string().optional(),
10 | og_image: z.object({
11 | style: z.enum(['Custom', 'Emoji', 'Simple', 'CoverImage']),
12 | title: z.string().optional(),
13 | description: z.string().optional(),
14 | emoji: z.string().emoji().optional(),
15 | }).optional(),
16 | }),
17 | }),
18 | blog: defineCollection({
19 | type: 'page',
20 | source: 'blog/**/*.md',
21 | schema: z.object({
22 | og_image: z.object({
23 | style: z.enum(['Custom', 'Emoji', 'Simple', 'CoverImage']),
24 | title: z.string().optional(),
25 | description: z.string().optional(),
26 | emoji: z.string().emoji().optional(),
27 | }).optional(),
28 | }),
29 | }),
30 | directory: defineCollection({
31 | type: 'page',
32 | source: 'dir/**/*.md',
33 | schema: z.object({
34 | card_image: z.string().optional(),
35 | cover: z.string().optional(),
36 | featured: z.boolean().optional(),
37 | tags: z.array(z.string()).optional(),
38 | layout: z.enum(['wide', 'default', 'card', 'thin']).optional(),
39 | og_image: z.object({
40 | style: z.enum(['Custom', 'Emoji', 'Simple', 'CoverImage']),
41 | title: z.string().optional(),
42 | description: z.string().optional(),
43 | emoji: z.string().emoji().optional(),
44 | }).optional(),
45 | }),
46 | }),
47 | },
48 | });
49 |
--------------------------------------------------------------------------------
/content/advertise.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ogImageStyle: Emoji
4 | ogImage:
5 | emoji: 💶
6 | ---
7 |
8 | ::center-hero{icon="tabler:ad-2"}
9 | # Advertise your new app!
10 |
11 | #description
12 | Reach out to thousands of boilerplate fans, indiehackers, developers, and creators who visit and explore MintedDirectory every month. By advertising your service or product here, you’ll be able to connect directly with this engaged audience. :rocket:
13 | ::
14 | ---
15 |
16 | ## Paid Listings
17 | Paid listings to secure a spot in our directory by paying a fee. These listings gain more exposure than free listings, ensuring that businesses are prominently displayed to potential customers. By upgrading to a paid listing, you increase the chances of being noticed in a competitive space.
18 |
19 | ## Featured Listings
20 | Even in free directories, we offer featured or boosted listings. Businesses can pay for a premium position at the top of the directory, ensuring maximum visibility. Featured listings are highlighted to stand out, making it easier for potential customers to find and engage with businesses.
21 |
22 | Here’s an example of how featured listings will look to Minted Directory visitors:
23 |
24 | :FeatureExample
25 |
26 | ## Sponsoring
27 | Sponsors can place their brand on the homepage gaining exposure to our growing audience. This is an excellent option for businesses looking to build brand awareness and connect with our community.
28 |
29 | :SponsorExample{title="Mike's Roofing" description="Get a free price estimation for your roof. 🏠"}
30 |
31 | ## Contact
32 |
33 | If you are interested in advertising on :SiteName contact us at [contact@minteddirectory.com](mailto:contact@minteddirectory.com)
34 |
--------------------------------------------------------------------------------
/content/blog.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: wide
3 | description: Read the best articles related to the directory.
4 | ---
5 |
6 | ::center-hero{icon="tabler:zeppelin"}
7 | # Welcome to the Blog
8 |
9 | #description
10 | Read the best articles related to the directory.
11 | And earn SEO points by writing more articles.
12 | ::
13 |
14 | :blogGrid
--------------------------------------------------------------------------------
/content/blog/blog-post-1.md:
--------------------------------------------------------------------------------
1 | # Example Blog Post
2 |
3 | content of the first blog post. 🌺
--------------------------------------------------------------------------------
/content/dir/starter.md:
--------------------------------------------------------------------------------
1 | ---
2 | cover: /logo.png
3 | featured: true
4 | tags:
5 | - SAAS
6 | ---
7 |
8 | # First Listing
9 |
10 | This is an example listing to get started.
--------------------------------------------------------------------------------
/content/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: wide
3 | og_image:
4 | title: My Directory
5 | description: A brand new directory 🎉
6 | ---
7 |
8 | ::pre-render-tags
9 | ::
10 |
11 | # This is your brand new directory ! 👋
12 |
13 | You can customize this page in markdown.
14 |
15 | See the [📚 **documentation**](https://minteddirectory.com/docs) to see how to customize your landing page.
16 |
17 | Get inspired by other directories:
18 | + 📗 [Nuxtjs Starters](https://nuxtstarters.com)
19 | + 🏠 [FortyTwoTools](https://fortytwotools.com)
20 |
21 | ---
22 |
23 | Here is your starting listings grid 👇
24 |
25 | ::search
26 | ::
27 |
28 | ::directory-grid
29 | ::
--------------------------------------------------------------------------------
/content/legal/privacy-policy.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory/2a52894f0cba4d483fea6939bbe1e318aeecb704/content/legal/privacy-policy.md
--------------------------------------------------------------------------------
/content/legal/terms-of-service.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory/2a52894f0cba4d483fea6939bbe1e318aeecb704/content/legal/terms-of-service.md
--------------------------------------------------------------------------------
/content/submit.md:
--------------------------------------------------------------------------------
1 | # Grow your business by submitting it to directory x.
2 |
3 | Submit your startup to the Minted directory to showcase your business to a growing community of founders, developers, and entrepreneurs.
4 | - directory x gets **+100** visitors each month.
5 | - directory x gets **10** unique visitors each month.
6 |
7 | ---
8 |
9 | ::tally-form
10 | ---
11 | embed: https://tally.so/embed/mOYErk?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1
12 | ---
13 | ::
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import withNuxt from './.nuxt/eslint.config.mjs'
3 |
4 | export default withNuxt(
5 | // Your custom configs here
6 | )
7 |
--------------------------------------------------------------------------------
/layouts/card.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/layouts/thin.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/layouts/wide.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/middleware/setLayout.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutKey } from '../.nuxt/types/layouts';
2 |
3 | export default defineNuxtRouteMiddleware(async (to, _) => {
4 | const { data: page } = await useCurrentPage(to);
5 | setPageLayout(page?.value?.layout as LayoutKey || 'default');
6 | });
7 |
--------------------------------------------------------------------------------
/nuxt.config.ts:
--------------------------------------------------------------------------------
1 | // https://nuxt.com/docs/api/configuration/nuxt-config
2 | export default defineNuxtConfig({
3 |
4 | modules: [
5 | '@nuxtjs/seo',
6 | '@nuxtjs/tailwindcss',
7 | '@nuxt/fonts',
8 | '@nuxt/icon',
9 | '@nuxt/content',
10 | '@nuxtjs/color-mode',
11 | '@nuxt/eslint',
12 | '@nuxt/image',
13 | ],
14 | devtools: { enabled: true },
15 | site: {
16 | url: 'https://example.com',
17 | },
18 | colorMode: {
19 | classSuffix: '',
20 | },
21 | content: {
22 | preview: {
23 | api: 'https://api.nuxt.studio',
24 | },
25 | },
26 | runtimeConfig: {
27 | public: {
28 | posthogPublicKey: process.env.POSTHOG_PUBLIC_KEY,
29 | posthogHost: process.env.POSTHOG_HOST,
30 | mdc: {
31 | headings: {
32 | anchorLinks: {
33 | h1: false,
34 | h2: false,
35 | h3: false,
36 | h4: false,
37 | h5: false,
38 | h6: false,
39 | },
40 | },
41 | },
42 | },
43 | },
44 | compatibilityDate: '2025-01-15',
45 | nitro: {
46 | prerender: {
47 | failOnError: false,
48 | crawlLinks: true,
49 | routes: ['/', '/sitemap.xml'],
50 | },
51 | },
52 | eslint: {
53 | config: {
54 | stylistic: {
55 | semi: true,
56 | quotes: 'single',
57 | },
58 | },
59 | },
60 | });
61 |
--------------------------------------------------------------------------------
/nuxt.schema.ts:
--------------------------------------------------------------------------------
1 | import { field, group } from '@nuxt/content/preview';
2 |
3 | export default defineNuxtSchema({
4 | appConfig: {
5 | general: group({
6 | title: 'General',
7 | description: 'General configuration for the app.',
8 | fields: {
9 | title: field({
10 | type: 'string',
11 | title: 'Title',
12 | description: 'Title of the application.',
13 | default: 'New Directory',
14 | }),
15 | language: field({
16 | type: 'string',
17 | title: 'Language',
18 | description: 'Language Code For The directory.',
19 | default: 'en',
20 | }),
21 | logo: field({
22 | type: 'media',
23 | title: 'Logo',
24 | description: 'Logo of the application.',
25 | default: '/logo.png',
26 | }),
27 | },
28 | }),
29 | site: group({
30 | title: 'Site Settings',
31 | description: 'SEO and Site Metadata Settings',
32 | fields: {
33 | name: field({
34 | type: 'string',
35 | title: 'Title',
36 | description:
37 | 'Title of the application. If empty general title will be used.',
38 | }),
39 | description: field({
40 | type: 'string',
41 | title: 'SEO Site Description',
42 | description:
43 | 'Used for the description meta tag. If empty general description will be used.',
44 | }),
45 | url: field({
46 | type: 'string',
47 | title: 'URL',
48 | description:
49 | 'Base url of your website, important for the correct function of the seo module.',
50 | default: 'https://example.com',
51 | }),
52 | favicon: group({
53 | title: '',
54 | fields: {
55 | emoji: field({
56 | type: 'string',
57 | title: 'emoji favicon',
58 | default: '🍃',
59 | }),
60 | image: field({
61 | type: 'file',
62 | tile: 'image favicon',
63 | }),
64 | },
65 | }),
66 | },
67 | }),
68 | directory: group({
69 | title: 'Directory',
70 | description: 'Directory configuration.',
71 | fields: {
72 | featured: group({
73 | fields: {
74 | showOnAllPages: field({
75 | type: 'boolean',
76 | title: 'Show Featured On All Pages',
77 | description: 'Show featured items on all pages.',
78 | default: true,
79 | }),
80 | showOnSide: field({
81 | type: 'boolean',
82 | title: 'Featured listing location',
83 | description:
84 | 'Show recommended listing on side of the screen or bottom of the.',
85 | default: true,
86 | }),
87 | labelForCard: field({
88 | type: 'string',
89 | title: 'Featured Text',
90 | description: 'Text to display for featured items.',
91 | default: 'Featured ✨',
92 | }),
93 | icon: field({
94 | type: 'icon',
95 | title: 'Featured Icon',
96 | description: 'Icon for the banner of featured listings',
97 | }),
98 | },
99 | }),
100 | search: group({
101 | title: 'Search',
102 | description: 'Search settings for the directory.',
103 | fields: {
104 | placeholder: field({
105 | type: 'string',
106 | title: 'Placeholder',
107 | description:
108 | 'Placeholder text for the search input. Use {0} to mark where to fill in the number of listings.',
109 | default: 'Search among {0} tools',
110 | }),
111 | icon: field({
112 | type: 'icon',
113 | title: 'Icon',
114 | description: 'Icon for the search input.',
115 | default: 'tabler:bow',
116 | }),
117 | tags: group({
118 | title: 'Tags',
119 | description: 'Tag settings for the search.',
120 | fields: {
121 | display: field({
122 | type: 'string',
123 | title: 'Display',
124 | description: 'Display option for tags.',
125 | default: 'show-all',
126 | required: ['none', 'select', 'show-all'],
127 | }),
128 | intersection: field({
129 | type: 'boolean',
130 | title: 'Intersection',
131 | description: 'Use intersection for tags.',
132 | default: false,
133 | }),
134 | },
135 | }),
136 | },
137 | }),
138 | grid: group({
139 | title: 'Grid',
140 | description: 'Grid settings for the directory.',
141 | fields: {
142 | list: field({
143 | type: 'boolean',
144 | title: 'List',
145 | description: 'Display as list.',
146 | default: false,
147 | }),
148 | emptyState: group({
149 | title: 'Empty State',
150 | description: 'Settings for the empty state.',
151 | fields: {
152 | text: field({
153 | type: 'string',
154 | title: 'Text',
155 | description: 'Text to display when no matches are found.',
156 | default: 'No matches for this query.',
157 | }),
158 | type: field({
159 | type: 'string',
160 | title: 'Type',
161 | description: 'Type of empty state.',
162 | default: 'button',
163 | required: ['button', 'simple', 'link'],
164 | }),
165 | icon: field({
166 | type: 'icon',
167 | title: 'Icon',
168 | description: 'Icon for the empty state.',
169 | default: 'tabler:ship',
170 | }),
171 | },
172 | }),
173 | card: group({
174 | title: 'Listing Card',
175 | description: 'Settings on how to display individual cards.',
176 | fields: {
177 | image: field({
178 | type: 'boolean',
179 | title: 'Image',
180 | description: 'Images on cards',
181 | default: false,
182 | }),
183 | type: field({
184 | type: 'string',
185 | title: 'Type',
186 | description: 'Type of empty state.',
187 | default: 'dashed',
188 | required: ['dashed', 'shadow', 'outline', 'bullet'],
189 | }),
190 | },
191 | }),
192 | submit: group({
193 | title: 'Submit',
194 | description: 'Settings for the submit button.',
195 | fields: {
196 | show: field({
197 | type: 'boolean',
198 | title: 'Show',
199 | description: 'Show the submit button.',
200 | default: true,
201 | }),
202 | first: field({
203 | type: 'boolean',
204 | title: 'First',
205 | description: 'Show first submit button.',
206 | default: false,
207 | }),
208 | title: field({
209 | type: 'string',
210 | title: 'Title',
211 | description: 'Title for the submit button.',
212 | default: 'Submit a template',
213 | }),
214 | description: field({
215 | type: 'string',
216 | title: 'Description',
217 | description: 'Description for the submit button.',
218 | default:
219 | 'Submit a template to show off a good project to other people.',
220 | }),
221 | hideable: field({
222 | type: 'boolean',
223 | title: 'Hideable',
224 | description: 'Allow hiding the submit button.',
225 | default: true,
226 | }),
227 | icon: field({
228 | type: 'icon',
229 | title: 'Icon',
230 | description:
231 | 'Icon shown in the submit suggestion in the grid.',
232 | default: 'tabler:send',
233 | }),
234 | },
235 | }),
236 | },
237 | }),
238 | tags: field({
239 | type: 'array',
240 | title: 'Tags',
241 | description: 'Tags for the directory.',
242 | default: [
243 | { name: 'SAAS', color: 'blue' },
244 | { name: 'dashboard', color: 'green' },
245 | { name: 'landing-page' },
246 | { name: 'toolbox' },
247 | { name: 'agency' },
248 | { name: 'markdown-based' },
249 | { name: 'basics', color: 'indigo' },
250 | ],
251 | }),
252 | tagPages: group({
253 | fields: {
254 | title: field({
255 | type: 'string',
256 | default: 'All {0} products',
257 | }),
258 | description: field({
259 | type: 'string',
260 | default: 'All {0} products',
261 | }),
262 | },
263 | }),
264 | },
265 | }),
266 | header: group({
267 | title: 'Header',
268 | description: 'Header configuration.',
269 | fields: {
270 | banner: group({
271 | title: 'Banner',
272 | description: 'Banner settings for the header.',
273 | fields: {
274 | show: field({
275 | type: 'boolean',
276 | title: 'Show',
277 | description: 'Show the banner.',
278 | default: true,
279 | }),
280 | text: field({
281 | type: 'string',
282 | title: 'Text',
283 | description: 'Text to display in the banner.',
284 | default: 'Create your own directory website in minutes.',
285 | }),
286 | link: field({
287 | type: 'string',
288 | title: 'Link',
289 | description: 'Link for the banner.',
290 | default: 'https://github.com/masterkram/nuxt-directory',
291 | }),
292 | brandText: field({
293 | type: 'string',
294 | title: 'Brand Text',
295 | description: 'Brand text for the banner.',
296 | default: 'MintedDirectory',
297 | }),
298 | },
299 | }),
300 | navbar: group({
301 | title: 'Navbar',
302 | description: 'Navbar settings for the header.',
303 | fields: {
304 | colorModeSelector: field({
305 | type: 'boolean',
306 | title: 'Color Mode Selector',
307 | description: 'Show the color mode selector in the navbar.',
308 | default: true,
309 | }),
310 | links: field({
311 | type: 'array',
312 | title: 'Link Array',
313 | description: 'Links to show on the navbar.',
314 | default: [],
315 | }),
316 | },
317 | }),
318 | actionButton: group({
319 | title: 'Action Button',
320 | description: 'Action button settings for the header.',
321 | fields: {
322 | text: field({
323 | type: 'string',
324 | title: 'Text',
325 | description: 'Text for the action button.',
326 | default: 'Submit a listing',
327 | }),
328 | href: field({
329 | type: 'string',
330 | title: 'Href',
331 | description: 'Link for the action button.',
332 | default: '/submit',
333 | }),
334 | },
335 | }),
336 | },
337 | }),
338 | footer: group({
339 | title: 'Footer',
340 | description: 'Footer configuration.',
341 | fields: {
342 | description: field({
343 | type: 'string',
344 | title: 'Description',
345 | description: 'Description to display in the footer.',
346 | default: 'Find the the best Nuxt templates.',
347 | }),
348 | navigation: field({
349 | type: 'array',
350 | title: 'Links',
351 | description: 'Array of navigation object displayed in footer.',
352 | default: []
353 | }),
354 | socials: group({
355 | title: 'Socials',
356 | description: 'Social links for the footer.',
357 | fields: {
358 | github: group({
359 | fields: {
360 | link: field({
361 | type: 'string',
362 | title: 'Github Link',
363 | description: 'Link to github profile',
364 | }),
365 | icon: field({
366 | type: 'icon',
367 | title: 'Github Icon',
368 | }),
369 | },
370 | }),
371 | x: group({
372 | fields: {
373 | link: field({
374 | type: 'string',
375 | title: 'X Link',
376 | description: 'Link to X profile',
377 | }),
378 | icon: field({
379 | type: 'icon',
380 | title: 'X icon',
381 | }),
382 | },
383 | }),
384 | instagram: group({
385 | fields: {
386 | link: field({
387 | type: 'string',
388 | title: 'Instagram Link',
389 | description: 'Link to Instagram Profile',
390 | }),
391 | icon: field({
392 | type: 'icon',
393 | title: 'Instagram Icon',
394 | }),
395 | },
396 | }),
397 | youtube: group({
398 | fields: {
399 | link: field({
400 | type: 'string',
401 | title: 'Youtube Link',
402 | description: 'Link to Youtube Account',
403 | }),
404 | icon: field({
405 | type: 'icon',
406 | title: 'Youtube Icon',
407 | }),
408 | },
409 | }),
410 | },
411 | }),
412 | },
413 | }),
414 | ui: group({
415 | title: 'UI',
416 | description: 'UI Customization.',
417 | fields: {
418 | icons: group({
419 | title: 'Icons',
420 | description: 'Manage icons used in UI.',
421 | fields: {
422 | dark: field({
423 | type: 'icon',
424 | title: 'Dark mode',
425 | description: 'Icon of color mode button for dark mode.',
426 | default: 'tabler:moon',
427 | }),
428 | light: field({
429 | type: 'icon',
430 | title: 'Light mode',
431 | description: 'Icon of color mode button for light mode.',
432 | default: 'tabler:sun',
433 | }),
434 | },
435 | }),
436 | },
437 | }),
438 | },
439 | });
440 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minted-directory",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "nuxt build",
7 | "dev": "nuxt dev",
8 | "generate": "nuxt generate",
9 | "preview": "nuxt preview",
10 | "postinstall": "nuxt prepare"
11 | },
12 | "dependencies": {
13 | "@headlessui/vue": "^1.7.23",
14 | "@nuxt/content": "^3.2.2",
15 | "@nuxt/eslint": "^0.7.6",
16 | "@nuxt/fonts": "^0.9.2",
17 | "@nuxt/image": "^1.9.0",
18 | "@nuxt/scripts": "^0.9.5",
19 | "@nuxtjs/color-mode": "^3.5.2",
20 | "@nuxtjs/seo": "^2.2.0",
21 | "@nuxtjs/tailwindcss": "^6.13.1",
22 | "@tailwindcss/forms": "^0.5.10",
23 | "@tailwindcss/typography": "^0.5.16",
24 | "nuxt": "^3.15.4",
25 | "posthog-js": "^1.225.1",
26 | "vue": "^3.5.13",
27 | "vue-router": "^4.5.0"
28 | },
29 | "devDependencies": {
30 | "@iconify-json/tabler": "^1.2.16",
31 | "@nuxt/icon": "^1.10.3",
32 | "typescript": "^5.8.2"
33 | },
34 | "packageManager": "pnpm@9.12.2"
35 | }
36 |
--------------------------------------------------------------------------------
/pages/[...slug].vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/pages/tags/[slug].vue:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
{{ title }}
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory/2a52894f0cba4d483fea6939bbe1e318aeecb704/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory/2a52894f0cba4d483fea6939bbe1e318aeecb704/public/logo.png
--------------------------------------------------------------------------------
/public/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/masterkram/minted-directory/2a52894f0cba4d483fea6939bbe1e318aeecb704/public/test.jpg
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import colors from "tailwindcss/colors";
2 | import typography from "@tailwindcss/typography";
3 | import forms from "@tailwindcss/forms";
4 |
5 | export default {
6 | darkMode: "class",
7 | plugins: [typography(), forms()],
8 | theme: {
9 | extend: {
10 | fontFamily: {
11 | display: ["DM Sans"],
12 | },
13 | colors: {
14 | primary: colors.sky,
15 | secondary: colors.indigo,
16 | gray: colors.zinc,
17 | },
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // https://nuxt.com/docs/guide/concepts/typescript
3 | "extends": "./.nuxt/tsconfig.json"
4 | }
5 |
--------------------------------------------------------------------------------
/types/Tag.ts:
--------------------------------------------------------------------------------
1 | export default interface Tag {
2 | name: string;
3 | color?: string;
4 | }
5 |
--------------------------------------------------------------------------------
/utils/formatString.ts:
--------------------------------------------------------------------------------
1 | const formatString = (template: string, ...args: any) => {
2 | return template.replace(
3 | /{([0-9]+)}/g,
4 | function (match: string, index: number) {
5 | return typeof args[index] === 'undefined' ? match : args[index];
6 | },
7 | );
8 | };
9 |
10 | export default formatString;
11 |
--------------------------------------------------------------------------------
/utils/setSeo.ts:
--------------------------------------------------------------------------------
1 | import type { BlogCollectionItem, DirectoryCollectionItem, PagesCollectionItem } from '@nuxt/content';
2 |
3 | export default function setSeo(page: DirectoryCollectionItem | BlogCollectionItem | PagesCollectionItem) {
4 | const config = useAppConfig();
5 |
6 | useSeoMeta({
7 | title: page.title || config.general.title || 'Empty Title',
8 | description: page.description || config.general.title || 'Empty Description',
9 | });
10 |
11 | defineOgImage({
12 | component: page.og_image?.style || 'Custom',
13 | title: page.og_image?.title || page.title || config.general.title || 'Empty Title',
14 | description: page.og_image?.description || page.description || config.general.title || 'Empty Description',
15 | emoji: page.og_image?.emoji,
16 | });
17 | }
18 |
--------------------------------------------------------------------------------