add blog subdomains

This commit is contained in:
Jordi Enric
2024-04-03 01:01:17 +02:00
parent 4e6990683e
commit 437718504c
8 changed files with 255 additions and 2 deletions
@@ -0,0 +1,48 @@
/* eslint-disable @next/next/no-img-element */
import { ContentRenderer } from "@/cms/ContentRenderer";
import { createClient } from "app/supa";
import React from "react";
type Props = {};
const Post = async ({
params: { slug, subdomain },
}: {
params: { slug: string; subdomain: string };
}) => {
const supa = createClient();
const { data: post } = await supa
.from("public_posts_v1")
.select("title, content, cover_image")
.eq("slug", slug)
.eq("blog_slug", subdomain)
.single();
if (!post) {
return <div>Post not found</div>;
}
return (
<div className="mx-auto max-w-xl px-2 py-8">
<div className="p-4">
<h1 className="text-2xl font-medium">{post.title}</h1>
</div>
<div className="p-2">
{post.cover_image && (
<img
src={post.cover_image}
height="400"
width={(16 / 9) * 400}
loading="lazy"
alt={post.title}
/>
)}
</div>
<div className="overflow-auto p-4">
<ContentRenderer content={post.content} />
</div>
</div>
);
};
export default Post;
+45
View File
@@ -0,0 +1,45 @@
import { createClient } from "../../supa";
import Link from "next/link";
import React from "react";
async function HostedBlog({
params: { subdomain },
}: {
params: { subdomain: string };
}) {
const supa = createClient();
const posts = await supa
.from("public_posts_v1")
.select("title, slug, published_at")
.eq("blog_slug", subdomain);
const formatDate = (date: string) => {
return new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
return (
<div className="mx-auto max-w-xl px-2 py-8">
<h2 className="mb-4 p-2">Blog</h2>
{posts.data?.map((post) => (
<Link
className="group grid grid-cols-6 border-b border-transparent p-2 transition-all hover:border-slate-300"
key={post.slug}
href={`/blog/${post.slug}`}
>
<span className="col-span-2 text-slate-500">
{formatDate(post.published_at)}
</span>
<span className="col-span-4 font-medium group-hover:text-orange-500">
{post.title}
</span>
</Link>
))}
</div>
);
}
export default HostedBlog;
+13
View File
@@ -0,0 +1,13 @@
import React from "react";
type Props = {};
const Pub = (props: Props) => {
return (
<div>
<pre>{JSON.stringify(props, null, 2)}</pre>
</div>
);
};
export default Pub;
+36
View File
@@ -0,0 +1,36 @@
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
export function createClient() {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: "", ...options });
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
);
}
+2 -1
View File
@@ -2,12 +2,13 @@
import { EditorContent, useEditor } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Image from "@tiptap/extension-image";
import Link from "@tiptap/extension-link";
type Props = {
content: any;
};
const extensions = [StarterKit, Image];
const extensions = [StarterKit, Image, Link];
export const ContentRenderer = ({ content }: Props) => {
const editor = useEditor({
extensions,
+32
View File
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
// inspired by https://github.com/vercel/platforms/blob/main/middleware.ts
export const config = {
matcher: [
/*
* Match all paths except for:
* 1. /api routes
* 2. /_next (Next.js internals)
* 3. /_static (inside /public)
* 4. all root files inside /public (e.g. /favicon.ico)
*/
"/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)",
],
};
const invalidSubdomains = ["www", "localhost:3000", "zenblog", "127"];
export default async function middleware(req: NextRequest) {
const subdomain = req.headers.get("host")?.split(".")[0];
const path = req.nextUrl.pathname;
if (!subdomain || invalidSubdomains.includes(subdomain)) {
return;
}
const newPath = `/pub/${subdomain}${path}`;
const url = new URL(newPath, req.url);
return NextResponse.rewrite(url);
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"headers": [
{
"source": "/api/(.*)",
"source": "/api/public/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Credentials", "value": "true" },
{ "key": "Access-Control-Allow-Origin", "value": "*" },
@@ -0,0 +1,78 @@
alter table "public"."blogs" drop column "public_id";
alter table "public"."blogs" add column "slug" text;
CREATE UNIQUE INDEX blogs_slug_key ON public.blogs USING btree (slug);
alter table "public"."blogs" add constraint "blogs_slug_key" UNIQUE using index "blogs_slug_key";
create or replace view "public"."public_posts_v1" as SELECT p.created_at,
p.blog_id,
p.title,
p.published,
p.published_at,
p.content,
p.updated_at,
p.slug,
p.id AS post_id,
p.cover_image,
p.metadata,
p.deleted,
COALESCE(array_agg(bt.name) FILTER (WHERE (bt.id IS NOT NULL)), '{}'::text[]) AS tags,
s.status AS subscription_status,
b.slug AS blog_slug
FROM ((((posts p
LEFT JOIN blogs b ON ((p.blog_id = b.id)))
LEFT JOIN subscriptions s ON ((b.user_id = s.user_id)))
LEFT JOIN post_tags pt ON ((p.id = pt.post_id)))
LEFT JOIN blog_tags bt ON ((pt.tag_id = bt.id)))
GROUP BY p.created_at, p.blog_id, p.title, p.published, p.published_at, p.content, p.updated_at, p.slug, p.id, p.cover_image, p.metadata, p.deleted, p.user_id, s.status, b.slug
ORDER BY p.created_at DESC;
create or replace view "public"."posts_with_blog_and_subscription_status" as SELECT p.created_at,
p.blog_id,
p.title,
p.published,
p.content,
p.updated_at,
p.slug,
p.id AS post_id,
p.cover_image,
p.metadata,
p.deleted,
COALESCE(array_agg(bt.name) FILTER (WHERE (bt.id IS NOT NULL)), '{}'::text[]) AS tags,
s.status AS subscription_status
FROM ((((posts p
LEFT JOIN blogs b ON ((p.blog_id = b.id)))
LEFT JOIN subscriptions s ON ((b.user_id = s.user_id)))
LEFT JOIN post_tags pt ON ((p.id = pt.post_id)))
LEFT JOIN blog_tags bt ON ((pt.tag_id = bt.id)))
GROUP BY p.created_at, p.blog_id, p.title, p.published, p.content, p.updated_at, p.slug, p.id, p.cover_image, p.metadata, p.deleted, p.user_id, s.status
ORDER BY p.created_at DESC;
create or replace view "public"."posts_with_blog_and_subscription_status_v2" as SELECT p.created_at,
p.blog_id,
p.title,
p.published,
p.published_at,
p.content,
p.updated_at,
p.slug,
p.id AS post_id,
p.cover_image,
p.metadata,
p.deleted,
COALESCE(array_agg(bt.name) FILTER (WHERE (bt.id IS NOT NULL)), '{}'::text[]) AS tags,
s.status AS subscription_status
FROM ((((posts p
LEFT JOIN blogs b ON ((p.blog_id = b.id)))
LEFT JOIN subscriptions s ON ((b.user_id = s.user_id)))
LEFT JOIN post_tags pt ON ((p.id = pt.post_id)))
LEFT JOIN blog_tags bt ON ((pt.tag_id = bt.id)))
GROUP BY p.created_at, p.blog_id, p.title, p.published, p.published_at, p.content, p.updated_at, p.slug, p.id, p.cover_image, p.metadata, p.deleted, p.user_id, s.status
ORDER BY p.created_at DESC;