KeyPress

repository·main·Indexed 42 days ago

https://github.com/zernonia/keypress

An open-source, keyboard-first multi-tenant blogging platform built with Nuxt 3 and Supabase. It supports custom subdomains and domains, featuring a specific database schema for profiles and posts, and a keyboard-driven interface that allows users to trigger the 'Write' action using the '/' key.

Tokens
1.8K
Snippets
5
Records
6
Agent score
54%

What's inside KeyPress

  1. How multi-tenant subdomains work in KeyPress

    main

    KeyPress implements a multi-tenant architecture inspired by Vercel's Platform Starter Kit using Nuxt 3. The routing logic follows this flow:

    1. Middleware Detection: server/middleware/subdomain.ts checks the current incoming domain and sets the SSR (Server Side Rendering) context.
    2. Route Mapping: app/router.options.ts uses that SSR context to map the request to a specific route.
    3. Dynamic Routing: The application uses pages/user/[siteId] as the new router root to serve content based on the identified site/user.
  2. Configure the Supabase Database schema

    main

    KeyPress requires a specific Supabase database schema to handle multi-tenancy via subdomains, user profiles, and blog posts. You must create the domains, profiles, and posts tables, along with a tags_view and a trigger to automatically create a profile when a new user authenticates.

    create table domains (
      user_id uuid,
      url text not null primary key,
      active boolean,
      created_at timestamp default now()
    );
    
    create table profiles (
      id uuid default uuid_generate_v4() primary key,
      username text,
      avatar_url text,
      name text,
      created_at timestamp default now(),
      subdomain text references domains (url)
    );
    
    create table posts (
      id uuid default uuid_generate_v4() primary key,
      author_id uuid references profiles (id),
      created_at timestamp default now(),
      slug text not null,
      title text,
      body text,
      cover_img text,
      active boolean,
      tags ARRAY,
      featured boolean not null
    );
    
    create or replace view tags_view as
      select *, count(*)
      from
        (select unnest(tags) as name from posts where active is true) s
      group by name;
    
    create or replace function public.handle_new_user()
      returns trigger as $$
      begin
        insert into public.profiles (id, avatar_url, username, name)
        values (new.id, new.raw_user_meta_data->>'avatar_url', new.raw_user_meta_data->>'user_name', new.raw_user_meta_data->>'preferred_username');
        return new;
      end;
      $$ language plpgsql security definer;
    
    create trigger on_auth_user_created
      after insert on auth.users
      for each row execute procedure public.handle_new_user();
  3. Install and run KeyPress locally

    main

    To set up a local development environment for KeyPress, ensure you have yarn installed, then clone the repository, install the dependencies, and start the development server.

    # Install yarn globally if you haven't already
    npm install --global yarn
    
    # Clone the repository
    git clone https://github.com/zernonia/keypress.git
    
    # Install NPM packages
    cd keypress
    yarn install
    
    # Run local development instance
    yarn dev
  4. Trigger the 'Write' action using the '/' key

    main

    The application implements a keyboard shortcut where pressing the / (Slash) key automatically focuses and clicks the primary 'Write' link (which navigates to /write). This is achieved using the useMagicKeys composable to watch for the Slash key and interacting with the NuxtLink component via a template ref.

    const writeEl = ref();
    const { Slash } = useMagicKeys();
    
    watch(Slash, (n) => {
      if (n && writeEl.value) {
        writeEl.value.$el.focus();
        setTimeout(() => {
          writeEl.value.$el.click();
        }, 300);
      }
    });
  5. Fetch posts from Supabase

    main

    Posts are retrieved from the Supabase posts table using useAsyncData. The query selects post data along with associated profile information (avatar_url, name, username) and domain details. It filters for active posts and orders them by featured status and created_at date in descending order.

    const client = useSupabaseClient();
    const { data, pending } = useAsyncData(
      "posts",
      async () => {
        const { data } = await client
          .from<Posts>("posts")
          .select("*, profiles(avatar_url, name, username, domains (url, active) )")
          .eq("active", true)
          .order("featured", { ascending: false })
          .order("created_at", { ascending: false });
    
        return data;
      },
      { lazy: true }
    );
  6. Fetch active posts for a specific profile

    main

    To display a user's blog posts, use useAsyncData with a Supabase client query. The query targets the posts table, performs an inner join on the profiles table to retrieve the username, and filters for posts where active is true and the profile ID matches the current subdomain profile. Results are ordered by featured (descending) and then by created_at (descending).

    const { data, pending } = useAsyncData("posts", async () => {
      const { data, error } = await client
        .from<Posts>("posts")
        .select("*, profiles!inner (username)")
        .eq("active", true)
        .eq("profiles.id", profile.value.id)
        .order("featured", { ascending: false })
        .order("created_at", { ascending: false });
    
      return data;
    });