├── .gitignore ├── README.md ├── app.vue ├── assets └── logos │ ├── github-mark-white.svg │ ├── nuxt-hub-logo-white.svg │ ├── nuxt-logo-white.svg │ ├── resend-logo-white.svg │ ├── tailwindcss-logo-white.svg │ └── vuemail-logo-dark.png ├── components └── ui │ ├── buttons │ └── Button.vue │ ├── cards │ ├── Card.vue │ └── TestimonialCard.vue │ └── content │ └── ContentBlock.vue ├── drizzle.config.ts ├── nuxt.config.ts ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── robots.txt ├── server ├── api │ └── join-waitlist.post.ts ├── database │ ├── migrations │ │ ├── 0000_silent_hitman.sql │ │ ├── 0001_illegal_lenny_balinger.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ └── _journal.json │ └── schema.ts ├── plugins │ └── migrations.ts ├── tsconfig.json └── utils │ └── drizzle.ts └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NuxtHubLanding - Landing Page Boilerplate 2 | The Nuxt boilerplate for building yor landing page in minutes, while deploy it with NuxtHub. 3 | 4 | > Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 5 | 6 | ## Tech Stack 7 | [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=fff)](https://www.typescriptlang.org/) 8 | [![Nuxt.js](https://img.shields.io/badge/Nuxt.js-002E3B?logo=nuxtdotjs&logoColor=#00DC82)](https://www.nuxt.com/) 9 | [![TailwindCSS](https://img.shields.io/badge/tailwindcss-0F172A?&logo=tailwindcss)](https://tailwindcss.com/) 10 | 11 | - [Typescript](https://www.typescriptlang.org/) - Language 12 | - [Nuxt.js](https://www.nuxt.com/) - Framework 13 | - [Drizzle](https://orm.drizzle.team/) - ORM 14 | - [Tailwind](https://tailwindcss.com/) - CSS 15 | - [NuxtHub](https://hub.nuxt.com) - Hosting 16 | 17 | ## Prerequisites 18 | - [NuxtHub](https://hub.nuxt.com/) Account 19 | 20 | ## Setup 21 | 22 | ```bash 23 | # clone repository 24 | git clone https://github.com/lowbits/nuxt-hub-landing.git [YOUR_APP_NAME] 25 | cd [YOUR_APP_NAME] 26 | 27 | # npm 28 | npm install 29 | ``` 30 | 31 | ## Development Server 32 | 33 | Start the development server on `http://localhost:3000`: 34 | 35 | ```bash 36 | # npm 37 | npm run dev 38 | ``` 39 | 40 | ## Deploy 41 | 42 | Deploy the application to NuxtHub 43 | 44 | ```bash 45 | npx nuxthub deploy 46 | 47 | # Choose 48 | # Deploy to NuxtHub?: Yes 49 | # Select a project: Create a new project 50 | # Project name: [YOUR_APP_NAME] 51 | # Select a region for the storage: [YOUR_REGION] 52 | # Production branch: main 53 | ``` 54 | 55 | > Run the migrations 56 | 57 | ```bash 58 | npx nuxt dev --production 59 | ``` 60 | 61 | Check out the [deployment documentation](https://hub.nuxt.com/docs/getting-started/deploy) for more information. 62 | 63 | 64 | ## Roadmap 65 | - [X] Add rate limiting via [NuxtSecurity](https://nuxt-security.vercel.app/documentation/middleware/rate-limiter) 66 | - [ ] Add Double-Opt-in 67 | - [ ] Add more basic components 68 | - [ ] Faq-Area 69 | - [ ] Pricing-Area 70 | - [ ] Add ability of customisation via config 71 | - [ ] Add light mode 72 | 73 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 67 | 255 | -------------------------------------------------------------------------------- /assets/logos/github-mark-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/logos/nuxt-hub-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logos/nuxt-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/logos/resend-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/logos/tailwindcss-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/logos/vuemail-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowbits/nuxt-hub-landing/093aeb6d98fcc7146f119ffd19e8e76829fc0626/assets/logos/vuemail-logo-dark.png -------------------------------------------------------------------------------- /components/ui/buttons/Button.vue: -------------------------------------------------------------------------------- 1 | 6 | 28 | -------------------------------------------------------------------------------- /components/ui/cards/Card.vue: -------------------------------------------------------------------------------- 1 | 7 | 30 | -------------------------------------------------------------------------------- /components/ui/cards/TestimonialCard.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /components/ui/content/ContentBlock.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 29 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit' 2 | 3 | export default defineConfig({ 4 | dialect: 'sqlite', 5 | schema: './server/database/schema.ts', 6 | out: './server/database/migrations' 7 | }) 8 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | compatibilityDate: '2024-04-03', 4 | devtools: {enabled: true}, 5 | modules: [ 6 | '@nuxthub/core', 7 | '@nuxtjs/tailwindcss', 8 | 'nuxt-security' 9 | ], 10 | 11 | 12 | routeRules: { 13 | '/api/join-waitlist': { 14 | security: { 15 | rateLimiter: { 16 | tokensPerInterval: 1 17 | } 18 | } 19 | } 20 | }, 21 | 22 | hub: { 23 | database: true 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-app", 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 | "db:generate": "drizzle-kit generate" 12 | }, 13 | "dependencies": { 14 | "@nuxthub/core": "^0.7.12", 15 | "@nuxtjs/tailwindcss": "^6.12.1", 16 | "drizzle-orm": "^0.33.0", 17 | "h3-zod": "^0.5.3", 18 | "nuxt": "^3.13.0", 19 | "nuxt-security": "^2.0.0", 20 | "vue": "latest", 21 | "vue-router": "latest", 22 | "zod": "^3.23.8" 23 | }, 24 | "devDependencies": { 25 | "drizzle-kit": "^0.24.2", 26 | "wrangler": "^3.76.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lowbits/nuxt-hub-landing/093aeb6d98fcc7146f119ffd19e8e76829fc0626/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/api/join-waitlist.post.ts: -------------------------------------------------------------------------------- 1 | import {useDrizzle} from "~/server/utils/drizzle"; 2 | import {consola} from "consola"; 3 | import {useValidatedBody, z} from "h3-zod"; 4 | 5 | export default defineEventHandler(async event => { 6 | consola.info("User trying to signup for waitlist...") 7 | 8 | const {email} = await useValidatedBody(event, z.object( 9 | { 10 | email: z.string().email().refine(async (email) => { 11 | const alreadyExists = await useDrizzle().query.waitlist.findFirst({ 12 | where: (waitlist, {eq}) => (eq(waitlist.email, email)), 13 | }) 14 | 15 | return !alreadyExists 16 | }, 'Email already in use') 17 | } 18 | )) 19 | 20 | 21 | consola.info("User joining waitlist...") 22 | 23 | 24 | const entry = await useDrizzle().insert(tables.waitlist).values({ 25 | email, 26 | createdAt: new Date(), 27 | }).returning().get() 28 | 29 | 30 | consola.info(`User ${entry.email} is now on waitlist...`) 31 | 32 | return entry 33 | }) 34 | -------------------------------------------------------------------------------- /server/database/migrations/0000_silent_hitman.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `waitlist` ( 2 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | `email` text NOT NULL, 4 | `created_at` integer NOT NULL 5 | ); 6 | --> statement-breakpoint 7 | CREATE UNIQUE INDEX `waitlist_email_unique` ON `waitlist` (`email`); -------------------------------------------------------------------------------- /server/database/migrations/0001_illegal_lenny_balinger.sql: -------------------------------------------------------------------------------- 1 | CREATE UNIQUE INDEX `email_idx` ON `waitlist` (`email`); -------------------------------------------------------------------------------- /server/database/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "cf043203-7d1d-4ea0-ba20-ee1be749df9b", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "waitlist": { 8 | "name": "waitlist", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": true 16 | }, 17 | "email": { 18 | "name": "email", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "created_at": { 25 | "name": "created_at", 26 | "type": "integer", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | } 31 | }, 32 | "indexes": { 33 | "waitlist_email_unique": { 34 | "name": "waitlist_email_unique", 35 | "columns": [ 36 | "email" 37 | ], 38 | "isUnique": true 39 | } 40 | }, 41 | "foreignKeys": {}, 42 | "compositePrimaryKeys": {}, 43 | "uniqueConstraints": {} 44 | } 45 | }, 46 | "enums": {}, 47 | "_meta": { 48 | "schemas": {}, 49 | "tables": {}, 50 | "columns": {} 51 | }, 52 | "internal": { 53 | "indexes": {} 54 | } 55 | } -------------------------------------------------------------------------------- /server/database/migrations/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6", 3 | "dialect": "sqlite", 4 | "id": "cb616729-fbff-4573-ad26-2bad022df348", 5 | "prevId": "cf043203-7d1d-4ea0-ba20-ee1be749df9b", 6 | "tables": { 7 | "waitlist": { 8 | "name": "waitlist", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": true 16 | }, 17 | "email": { 18 | "name": "email", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true, 22 | "autoincrement": false 23 | }, 24 | "created_at": { 25 | "name": "created_at", 26 | "type": "integer", 27 | "primaryKey": false, 28 | "notNull": true, 29 | "autoincrement": false 30 | } 31 | }, 32 | "indexes": { 33 | "waitlist_email_unique": { 34 | "name": "waitlist_email_unique", 35 | "columns": [ 36 | "email" 37 | ], 38 | "isUnique": true 39 | }, 40 | "email_idx": { 41 | "name": "email_idx", 42 | "columns": [ 43 | "email" 44 | ], 45 | "isUnique": true 46 | } 47 | }, 48 | "foreignKeys": {}, 49 | "compositePrimaryKeys": {}, 50 | "uniqueConstraints": {} 51 | } 52 | }, 53 | "enums": {}, 54 | "_meta": { 55 | "schemas": {}, 56 | "tables": {}, 57 | "columns": {} 58 | }, 59 | "internal": { 60 | "indexes": {} 61 | } 62 | } -------------------------------------------------------------------------------- /server/database/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1726066158194, 9 | "tag": "0000_silent_hitman", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "6", 15 | "when": 1727187012751, 16 | "tag": "0001_illegal_lenny_balinger", 17 | "breakpoints": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /server/database/schema.ts: -------------------------------------------------------------------------------- 1 | import {sqliteTable, text, integer, uniqueIndex} from 'drizzle-orm/sqlite-core' 2 | 3 | export const waitlist = sqliteTable('waitlist', { 4 | id: integer('id').primaryKey({autoIncrement: true}), 5 | email: text('email').notNull().unique(), 6 | createdAt: integer('created_at', {mode: 'timestamp'}).notNull(), 7 | }, (table) => ({ 8 | emailIdx: uniqueIndex('email_idx').on(table.email) 9 | })) 10 | -------------------------------------------------------------------------------- /server/plugins/migrations.ts: -------------------------------------------------------------------------------- 1 | import { consola } from 'consola' 2 | import { migrate } from 'drizzle-orm/d1/migrator' 3 | import {useDrizzle} from "~/server/utils/drizzle"; 4 | 5 | export default defineNitroPlugin(async () => { 6 | if (!import.meta.dev) return 7 | 8 | onHubReady(async () => { 9 | await migrate(useDrizzle(), { migrationsFolder: 'server/database/migrations' }) 10 | .then(() => { 11 | consola.success('Database migrations done') 12 | }) 13 | .catch((err) => { 14 | consola.error('Database migrations failed', err) 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /server/utils/drizzle.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/d1' 2 | export { sql, eq, and, or } from 'drizzle-orm' 3 | 4 | import * as schema from '../database/schema' 5 | 6 | export const tables = schema 7 | 8 | export function useDrizzle() { 9 | return drizzle(hubDatabase(), { schema }) 10 | } 11 | 12 | export type Waitlist = typeof schema.waitlist.$inferSelect 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json", 4 | } 5 | --------------------------------------------------------------------------------