├── public ├── robots.txt └── favicon.ico ├── server ├── tsconfig.json ├── api │ ├── posts.get.ts │ └── posts │ │ └── new.post.ts └── routes │ └── auth │ └── github.get.ts ├── tsconfig.json ├── eslint.config.mjs ├── app ├── middleware │ └── logged-in.ts ├── pages │ ├── index.vue │ └── new.vue ├── app.vue └── components │ └── TiptapEditor.client.vue ├── .gitignore ├── shared └── post.ts ├── package.json ├── nuxt.config.ts └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielroe/nuxtpressable/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // https://nuxt.com/docs/guide/concepts/typescript 3 | "extends": "./.nuxt/tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/middleware/logged-in.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware(() => { 2 | const { loggedIn } = useUserSession() 3 | 4 | if (!loggedIn.value) { 5 | return navigateTo('/') 6 | } 7 | }) 8 | -------------------------------------------------------------------------------- /.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 | 26 | .posts 27 | -------------------------------------------------------------------------------- /shared/post.ts: -------------------------------------------------------------------------------- 1 | import * as v from 'valibot' 2 | 3 | const postSchema = v.object({ 4 | slug: v.string(), 5 | title: v.string(), 6 | body: v.string(), 7 | }) 8 | 9 | export const validatePost = (data: unknown) => v.parse(postSchema, data) 10 | 11 | export type Post = v.InferInput & { 12 | author: string 13 | createdAt: string 14 | } 15 | -------------------------------------------------------------------------------- /server/api/posts.get.ts: -------------------------------------------------------------------------------- 1 | import type { Post } from '#shared/post' 2 | 3 | const storage = useStorage() 4 | 5 | export default defineCachedEventHandler(async () => { 6 | const postKeys = await storage.getKeys('posts') 7 | const posts = await storage.getItems(postKeys) 8 | return posts.map(p => p.value as Post).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) 9 | }, { swr: true }) 10 | -------------------------------------------------------------------------------- /app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | -------------------------------------------------------------------------------- /server/api/posts/new.post.ts: -------------------------------------------------------------------------------- 1 | import type { Post } from '#shared/post' 2 | import { validatePost } from '#shared/post' 3 | 4 | const storage = useStorage('posts') 5 | 6 | export default defineEventHandler(async (event) => { 7 | const session = await getUserSession(event) 8 | if (!session.user) { 9 | throw createError({ 10 | message: 'Not logged in', 11 | status: 401, 12 | }) 13 | } 14 | const { body, slug, title } = await readValidatedBody(event, validatePost) 15 | 16 | await storage.setItem(slug, { 17 | title, 18 | body, 19 | slug, 20 | author: session.user.name, 21 | createdAt: new Date().toISOString(), 22 | } satisfies Post) 23 | 24 | return null 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 | "lint": "eslint .", 9 | "generate": "nuxt generate", 10 | "preview": "nuxt preview", 11 | "postinstall": "nuxt prepare" 12 | }, 13 | "dependencies": { 14 | "@nuxt/eslint": "^0.7.1", 15 | "@picocss/pico": "^2.0.6", 16 | "nuxt": "^3.14.159", 17 | "nuxt-auth-utils": "^0.5.5", 18 | "nuxt-tiptap-editor": "^2.0.0", 19 | "valibot": "1.0.0-beta.7", 20 | "vue": "latest", 21 | "vue-router": "latest" 22 | }, 23 | "devDependencies": { 24 | "typescript": "^5.6.3" 25 | }, 26 | "packageManager": "pnpm@9.13.2" 27 | } 28 | -------------------------------------------------------------------------------- /server/routes/auth/github.get.ts: -------------------------------------------------------------------------------- 1 | export default defineOAuthGitHubEventHandler({ 2 | config: { 3 | scope: ['user:email'], 4 | emailRequired: true, 5 | }, 6 | async onSuccess(event, { user }) { 7 | await setUserSession(event, { 8 | user: { 9 | name: user.name, 10 | email: user.email, 11 | githubId: user.id, 12 | }, 13 | }) 14 | return sendRedirect(event, '/') 15 | }, 16 | // Optional, will return a json error and 401 status code by default 17 | onError(event, error) { 18 | console.error('GitHub OAuth error:', error) 19 | return sendRedirect(event, '/') 20 | }, 21 | }) 22 | 23 | declare module '#auth-utils' { 24 | interface User { 25 | name: string 26 | email: string 27 | githubId: string 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | modules: [ 4 | 'nuxt-tiptap-editor', 5 | 'nuxt-auth-utils', 6 | '@nuxt/eslint', 7 | ], 8 | $production: { 9 | nitro: { 10 | storage: { 11 | posts: { 12 | driver: 'cloudflareKVHTTP', 13 | accountId: process.env.CF_ACCOUNT_ID, 14 | namespaceId: process.env.CF_NAMESPACE_ID, 15 | apiToken: process.env.CF_API_TOKEN, 16 | }, 17 | }, 18 | }, 19 | }, 20 | $development: { 21 | nitro: { 22 | storage: { 23 | posts: { 24 | driver: 'fsLite', 25 | base: '.posts', 26 | }, 27 | }, 28 | }, 29 | }, 30 | devtools: { enabled: true }, 31 | css: ['@picocss/pico'], 32 | runtimeConfig: { 33 | oauth: { 34 | github: { 35 | clientId: '', 36 | clientSecret: '', 37 | }, 38 | }, 39 | }, 40 | future: { 41 | compatibilityVersion: 4, 42 | }, 43 | compatibilityDate: '2024-04-03', 44 | eslint: { 45 | config: { 46 | stylistic: true, 47 | }, 48 | }, 49 | }) 50 | -------------------------------------------------------------------------------- /app/pages/new.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt + WordPress (nuxtpressable) 2 | 3 | This is a tiny, incomplete clone of WordPress built in [Nuxt](https://nuxt.com/) for a live-coding session at [Vue Toronto](https://vuetoronto.com/). 4 | 5 |

6 | 7 | Screenshot of the nuxtpressable home page, displaying a single blog post 8 | 9 |

10 | 11 | - [✨  Live Demo](https://nuxtpressable.netlify.app/) 12 | 13 | 14 | ## Features 15 | 16 | - Built with [Nuxt](https://nuxt.com/) 17 | - Authentication via [nuxt-auth-utils](https://github.com/atinux/nuxt-auth-utils) and GitHub OAuth 18 | - Data stored in [Cloudflare KV](https://developers.cloudflare.com/kv/) with [unstorage](https://unstorage.unjs.io/) 19 | - Tiptap editor via [nuxt-tiptap-editor](https://github.com/modbender/nuxt-tiptap-editor) 20 | - Light & dark mode using [pico.css](https://picocss.com/docs/) 21 | - Hosted on Netlify 22 | 23 | ## Try it out 24 | 25 | ### Setup 26 | 27 | ```bash 28 | # install dependencies 29 | pnpm install 30 | 31 | # serve in dev mode, with hot reload at localhost:3000 32 | pnpm dev 33 | 34 | # build for production + preview locally 35 | pnpm build && pnpm preview 36 | ``` 37 | 38 | ## License 39 | 40 | MIT 41 | -------------------------------------------------------------------------------- /app/components/TiptapEditor.client.vue: -------------------------------------------------------------------------------- 1 | 128 | 129 | 150 | --------------------------------------------------------------------------------