create-next-mdx-blog-app 2.1.3 → 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/package.json +1 -1
- package/scripts/sql/DDL/alterTable.sql +0 -2
- package/scripts/sql/DDL/createIncrementViewCountFunction.sql +19 -0
- package/scripts/sql/DDL/createViewCountsTable.sql +26 -0
- package/src/app/api/views/[slug]/route.ts +19 -0
- package/src/app/sample-blog-post-page/page.tsx +5 -1
- package/src/components/ArticleHeader.tsx +8 -1
- package/src/components/DynamicArticle.tsx +7 -4
- package/src/components/ViewCounter.tsx +32 -0
- package/src/utils/functions/index.ts +1 -0
- package/src/utils/functions/rpc/incrementViewCount.ts +19 -0
- package/src/utils/functions/rpc/index.ts +1 -0
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ npx create-next-mdx-blog-app .
|
|
|
59
59
|
### Package Information
|
|
60
60
|
|
|
61
61
|
- **Package Name**: `create-next-mdx-blog-app`
|
|
62
|
-
- **Version**: `2.1.
|
|
62
|
+
- **Version**: `2.1.4`
|
|
63
63
|
- **License**: MIT
|
|
64
64
|
- **Homepage**: [https://www.npmjs.com/package/create-next-mdx-blog-app](https://www.npmjs.com/package/create-next-mdx-blog-app/)
|
|
65
65
|
|
|
@@ -170,6 +170,9 @@ A thin green bar (`src/components/ReadingProgressBar.tsx`) is fixed to the top o
|
|
|
170
170
|
### Back to Top Button
|
|
171
171
|
A floating circular button (`src/components/BackToTopButton.tsx`) appears in the bottom-right corner of the screen once the reader has scrolled more than 400px down the page. Clicking it smoothly scrolls back to the top. The button is hidden when not needed and matches the green colour scheme of the rest of the UI.
|
|
172
172
|
|
|
173
|
+
### View Counter
|
|
174
|
+
Each article page displays a live view count stored in a dedicated `view_counts` Supabase table, incremented atomically via the `increment_view_count` RPC function. Dynamic pages increment server-side at render time; static pages use a `ViewCounter` client component that calls the `/api/views/[slug]` Route Handler after hydration.
|
|
175
|
+
|
|
173
176
|
## 🖥️ Code Sandbox
|
|
174
177
|
The project includes an interactive in-browser code execution environment powered by **Sandpack** (<b>route</b>: `/code-sandbox`).
|
|
175
178
|
|
|
@@ -266,7 +269,7 @@ docker run -e SUPABASE_URL=your_supabase_url \ -e SUPABASE_ANON_KEY=your_supabas
|
|
|
266
269
|
``
|
|
267
270
|
|
|
268
271
|
## 🔄 CRUD Operations and Supabase Actions
|
|
269
|
-
Implementation of the CRUD operation functions is stored in the `/src/utils/functions/crud` directory. This includes functions for creating, reading, updating, and deleting articles in the Supabase database.
|
|
272
|
+
Implementation of the CRUD operation functions is stored in the `/src/utils/functions/crud` directory. This includes functions for creating, reading, updating, and deleting articles in the Supabase database. Supabase RPC functions (such as `incrementViewCount`) are kept separately in `/src/utils/functions/rpc`.
|
|
270
273
|
|
|
271
274
|
The `article-manager.ts` file utilizes these CRUD functions to successfully perform the desired actions.
|
|
272
275
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Creates the increment_view_count RPC function used to atomically upsert
|
|
2
|
+
-- and increment the count for a given slug in the view_counts table.
|
|
3
|
+
-- Works for both static and dynamic article pages.
|
|
4
|
+
-- Run once in the Supabase SQL editor before using the view counter feature.
|
|
5
|
+
CREATE OR REPLACE FUNCTION increment_view_count(article_slug TEXT)
|
|
6
|
+
RETURNS INTEGER
|
|
7
|
+
LANGUAGE plpgsql
|
|
8
|
+
AS $$
|
|
9
|
+
DECLARE
|
|
10
|
+
new_count INTEGER;
|
|
11
|
+
BEGIN
|
|
12
|
+
INSERT INTO view_counts (slug, count)
|
|
13
|
+
VALUES (article_slug, 1)
|
|
14
|
+
ON CONFLICT (slug) DO UPDATE
|
|
15
|
+
SET count = view_counts.count + 1
|
|
16
|
+
RETURNING count INTO new_count;
|
|
17
|
+
RETURN new_count;
|
|
18
|
+
END;
|
|
19
|
+
$$;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Creates a dedicated view_counts table for tracking article view counts.
|
|
2
|
+
-- Decoupled from the Article table so both static and dynamic articles
|
|
3
|
+
-- can have their views tracked regardless of whether a full Article record exists.
|
|
4
|
+
CREATE TABLE view_counts (
|
|
5
|
+
slug VARCHAR PRIMARY KEY,
|
|
6
|
+
count INTEGER DEFAULT 0
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
-- Enable Row-Level Security
|
|
10
|
+
ALTER TABLE view_counts ENABLE ROW LEVEL SECURITY;
|
|
11
|
+
|
|
12
|
+
-- Allow select, insert and update for view count tracking
|
|
13
|
+
CREATE POLICY "Allow select on view_counts"
|
|
14
|
+
ON view_counts
|
|
15
|
+
FOR SELECT
|
|
16
|
+
USING (true);
|
|
17
|
+
|
|
18
|
+
CREATE POLICY "Allow insert on view_counts"
|
|
19
|
+
ON view_counts
|
|
20
|
+
FOR INSERT
|
|
21
|
+
WITH CHECK (true);
|
|
22
|
+
|
|
23
|
+
CREATE POLICY "Allow update on view_counts"
|
|
24
|
+
ON view_counts
|
|
25
|
+
FOR UPDATE
|
|
26
|
+
USING (true);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { incrementViewCount } from "@/utils/functions";
|
|
3
|
+
|
|
4
|
+
// Route Handler — increments the view count for a given article slug and returns the new count.
|
|
5
|
+
// Called client-side by the ViewCounter component on SSG pages.
|
|
6
|
+
export async function POST(
|
|
7
|
+
_req: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ slug: string }> }
|
|
9
|
+
): Promise<NextResponse> {
|
|
10
|
+
const { slug } = await params;
|
|
11
|
+
|
|
12
|
+
if (!slug || typeof slug !== "string" || slug.trim() === "") {
|
|
13
|
+
return NextResponse.json({ error: "Invalid slug" }, { status: 400 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const count = await incrementViewCount(slug.trim());
|
|
17
|
+
|
|
18
|
+
return NextResponse.json({ count });
|
|
19
|
+
}
|
|
@@ -5,6 +5,7 @@ import { ArticleAuthorInfoList, ArticleHeaderInfoList } from "@/utils/constants"
|
|
|
5
5
|
import StaticArticle from "@/components/StaticArticle";
|
|
6
6
|
import ReadingProgressBar from "@/components/ReadingProgressBar";
|
|
7
7
|
import BackToTopButton from "@/components/BackToTopButton";
|
|
8
|
+
import ViewCounter from "@/components/ViewCounter";
|
|
8
9
|
import type { Metadata } from 'next';
|
|
9
10
|
|
|
10
11
|
// Generate metadata for the Sample Blog Post page
|
|
@@ -28,7 +29,10 @@ const SampleBlogPostPage = () => {
|
|
|
28
29
|
<BackToTopButton />
|
|
29
30
|
<main className="flex-grow px-4 py-8">
|
|
30
31
|
<div className="max-w-4xl mx-auto">
|
|
31
|
-
<ArticleHeader
|
|
32
|
+
<ArticleHeader
|
|
33
|
+
articleHeaderInformation={ArticleHeaderInfoList}
|
|
34
|
+
viewCounterSlot={<ViewCounter slug="sample-blog-post-page" />}
|
|
35
|
+
/>
|
|
32
36
|
<StaticArticle />
|
|
33
37
|
<ArticleAuthorBio authorInformation={ArticleAuthorInfoList} />
|
|
34
38
|
</div>
|
|
@@ -5,9 +5,10 @@ import CopyLinkButton from "./CopyLinkButton";
|
|
|
5
5
|
import SocialShareButtons from "./SocialShareButtons";
|
|
6
6
|
import type { ArticleHeaderInfoType } from "@/utils/types";
|
|
7
7
|
import Image from "next/image";
|
|
8
|
+
import { Eye } from "lucide-react";
|
|
8
9
|
|
|
9
10
|
// Article Header custom component
|
|
10
|
-
export default function ArticleHeader({ articleHeaderInformation } : { articleHeaderInformation: ArticleHeaderInfoType }): React.JSX.Element {
|
|
11
|
+
export default function ArticleHeader({ articleHeaderInformation, viewCount, viewCounterSlot } : { articleHeaderInformation: ArticleHeaderInfoType; viewCount?: number; viewCounterSlot?: React.ReactNode }): React.JSX.Element {
|
|
11
12
|
return (
|
|
12
13
|
<div className="mb-8">
|
|
13
14
|
<Badge className="mb-3 bg-green-900/60 text-green-100 border border-green-500/50">
|
|
@@ -33,6 +34,12 @@ export default function ArticleHeader({ articleHeaderInformation } : { articleHe
|
|
|
33
34
|
month: 'long',
|
|
34
35
|
day: 'numeric'
|
|
35
36
|
})} • { articleHeaderInformation.articleReadingTime }
|
|
37
|
+
{ viewCount !== undefined && (
|
|
38
|
+
<span className="inline-flex items-center gap-1 ml-2">
|
|
39
|
+
• <Eye className="inline h-3 w-3" /> { viewCount.toLocaleString() } views
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
{ viewCounterSlot }
|
|
36
43
|
</p>
|
|
37
44
|
</div>
|
|
38
45
|
</div>
|
|
@@ -5,13 +5,16 @@ import { ArticleAuthorInfoList, ArticleHeaderInfoList } from "@/utils/constants"
|
|
|
5
5
|
import MDXRemoteArticle from "./MDXRemoteArticle";
|
|
6
6
|
import ReadingProgressBar from "@/components/ReadingProgressBar";
|
|
7
7
|
import BackToTopButton from "@/components/BackToTopButton";
|
|
8
|
-
import { fetchArticle } from "@/utils/functions";
|
|
8
|
+
import { fetchArticle, incrementViewCount } from "@/utils/functions";
|
|
9
9
|
|
|
10
10
|
// Custom Dynamic Article component encompasses loading article content stored in a Supabase database
|
|
11
11
|
export default async function DynamicArticle({ slug } : { slug: string }): Promise<React.JSX.Element> {
|
|
12
12
|
// Make a call to the back-end and fetch article information
|
|
13
|
-
const articleData = await
|
|
14
|
-
|
|
13
|
+
const [articleData, viewCount] = await Promise.all([
|
|
14
|
+
fetchArticle(slug),
|
|
15
|
+
incrementViewCount(slug)
|
|
16
|
+
]);
|
|
17
|
+
|
|
15
18
|
if (!articleData || !articleData.content) {
|
|
16
19
|
throw new Error("Invalid article");
|
|
17
20
|
}
|
|
@@ -22,7 +25,7 @@ export default async function DynamicArticle({ slug } : { slug: string }): Promi
|
|
|
22
25
|
<BackToTopButton />
|
|
23
26
|
<main className="flex-grow px-4 py-8">
|
|
24
27
|
<div className="max-w-4xl mx-auto">
|
|
25
|
-
<ArticleHeader articleHeaderInformation={ArticleHeaderInfoList} />
|
|
28
|
+
<ArticleHeader articleHeaderInformation={ArticleHeaderInfoList} viewCount={viewCount} />
|
|
26
29
|
<div className="glass-card p-8 mb-8">
|
|
27
30
|
<MDXRemoteArticle content={articleData.content} />
|
|
28
31
|
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { Eye } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
// ViewCounter — Client Component for SSG pages.
|
|
7
|
+
// Fires a POST to the /api/views/[slug] Route Handler on mount,
|
|
8
|
+
// which increments the count in Supabase and returns the new value.
|
|
9
|
+
export default function ViewCounter({ slug }: { slug: string }): React.JSX.Element | null {
|
|
10
|
+
const [count, setCount] = useState<number | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch(`/api/views/${encodeURIComponent(slug)}`, { method: "POST" })
|
|
14
|
+
.then((res) => res.json())
|
|
15
|
+
.then((data: { count?: number }) => {
|
|
16
|
+
if (typeof data.count === "number") {
|
|
17
|
+
setCount(data.count);
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
.catch(() => {
|
|
21
|
+
// Silently fail — view count is non-critical
|
|
22
|
+
});
|
|
23
|
+
}, [slug]);
|
|
24
|
+
|
|
25
|
+
if (count === null) return null;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<span className="inline-flex items-center gap-1 ml-2">
|
|
29
|
+
• <Eye className="inline h-3 w-3" /> {count.toLocaleString()} views
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import getSupabaseClient from "../supabase_client/SupabaseClient";
|
|
2
|
+
|
|
3
|
+
// Atomically increments the view count for a given article slug and returns the new count.
|
|
4
|
+
// Targets the dedicated view_counts table via an upsert — works for both static and dynamic pages.
|
|
5
|
+
//
|
|
6
|
+
// Required Supabase migration — run scripts/sql/DDL/createViewCountsTable.sql and
|
|
7
|
+
// scripts/sql/DDL/createIncrementViewCountFunction.sql once in the Supabase SQL editor.
|
|
8
|
+
|
|
9
|
+
export default async function incrementViewCount(slug: string): Promise<number> {
|
|
10
|
+
const { data, error } = await getSupabaseClient()
|
|
11
|
+
.rpc('increment_view_count', { article_slug: slug.trim() });
|
|
12
|
+
|
|
13
|
+
if (error) {
|
|
14
|
+
console.error('Could not increment view count:', error.message);
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (data as number) ?? 0;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as incrementViewCount } from "./incrementViewCount";
|