mirror of
https://github.com/jordienr/zenblog.git
synced 2026-09-28 01:56:07 -04:00
add blog subdomains
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user