@nextsparkjs/theme-blog 0.1.0-beta.189 → 0.1.0-beta.190

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.
@@ -4,6 +4,7 @@ import { useState } from 'react'
4
4
  import { Button } from '@nextsparkjs/core/components/ui/button'
5
5
  import { Download, Loader2 } from 'lucide-react'
6
6
  import { useToast } from '@nextsparkjs/core/hooks/useToast'
7
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
7
8
 
8
9
  interface ExportPostsButtonProps {
9
10
  className?: string
@@ -17,7 +18,7 @@ export function ExportPostsButton({ className }: ExportPostsButtonProps) {
17
18
  setIsExporting(true)
18
19
 
19
20
  try {
20
- const response = await fetch('/api/v1/posts?limit=1000', {
21
+ const response = await fetch(withBasePath('/api/v1/posts?limit=1000'), {
21
22
  method: 'GET',
22
23
  headers: {
23
24
  'Content-Type': 'application/json',
@@ -16,6 +16,7 @@ import { Badge } from '@nextsparkjs/core/components/ui/badge'
16
16
  import { ScrollArea } from '@nextsparkjs/core/components/ui/scroll-area'
17
17
  import { Upload, FileJson, AlertCircle, CheckCircle2, Loader2 } from 'lucide-react'
18
18
  import { useToast } from '@nextsparkjs/core/hooks/useToast'
19
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
19
20
 
20
21
  interface PostImport {
21
22
  title: string
@@ -108,7 +109,7 @@ export function ImportPostsDialog({ className, onImportComplete }: ImportPostsDi
108
109
  const post = posts[i]
109
110
 
110
111
  try {
111
- const response = await fetch('/api/v1/posts', {
112
+ const response = await fetch(withBasePath('/api/v1/posts'), {
112
113
  method: 'POST',
113
114
  headers: {
114
115
  'Content-Type': 'application/json',
@@ -12,6 +12,7 @@ import NextImage from 'next/image'
12
12
  import { X, Upload, Image as ImageIcon } from 'lucide-react'
13
13
  import { Button } from '@nextsparkjs/core/components/ui/button'
14
14
  import { cn } from '@nextsparkjs/core/lib/utils'
15
+ import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
15
16
 
16
17
  interface FeaturedImageUploadProps {
17
18
  value: string // URL string
@@ -111,7 +112,7 @@ export function FeaturedImageUpload({
111
112
  // Image Preview
112
113
  <div className="relative aspect-video rounded-lg overflow-hidden border border-border bg-muted" data-cy="featured-image-preview">
113
114
  <NextImage
114
- src={value}
115
+ src={withBasePathIfInApp(value)}
115
116
  alt="Featured image preview"
116
117
  fill
117
118
  className="object-cover"
@@ -9,6 +9,7 @@
9
9
  import Image from 'next/image'
10
10
  import Link from 'next/link'
11
11
  import { Twitter, Github, Linkedin, Globe, User } from 'lucide-react'
12
+ import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
12
13
 
13
14
  interface SocialLink {
14
15
  type: 'twitter' | 'github' | 'linkedin' | 'website'
@@ -43,7 +44,7 @@ export function AuthorBio({
43
44
  <div data-cy="author-bio-avatar" className="flex-shrink-0">
44
45
  {avatar ? (
45
46
  <Image
46
- src={avatar}
47
+ src={withBasePathIfInApp(avatar)}
47
48
  alt={name}
48
49
  width={64}
49
50
  height={64}
@@ -84,7 +85,7 @@ export function AuthorBio({
84
85
  return (
85
86
  <a
86
87
  key={link.type}
87
- href={link.url}
88
+ href={withBasePathIfInApp(link.url)}
88
89
  data-cy={`author-bio-social-${link.type}`}
89
90
  target="_blank"
90
91
  rel="noopener noreferrer"
@@ -11,6 +11,7 @@ import Link from 'next/link'
11
11
  import Image from 'next/image'
12
12
  import { User } from 'lucide-react'
13
13
  import { cn } from '@nextsparkjs/core/lib/utils'
14
+ import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
14
15
 
15
16
  interface AuthorCardProps {
16
17
  username: string
@@ -48,7 +49,7 @@ export function AuthorCard({
48
49
  <div data-cy={`author-card-avatar-${username}`} className="flex-shrink-0">
49
50
  {avatar ? (
50
51
  <Image
51
- src={avatar}
52
+ src={withBasePathIfInApp(avatar)}
52
53
  alt={name}
53
54
  width={48}
54
55
  height={48}
@@ -89,7 +90,7 @@ export function AuthorCard({
89
90
  <div data-cy={`author-card-avatar-${username}`} className="flex justify-center mb-4">
90
91
  {avatar ? (
91
92
  <Image
92
- src={avatar}
93
+ src={withBasePathIfInApp(avatar)}
93
94
  alt={name}
94
95
  width={96}
95
96
  height={96}
@@ -8,6 +8,7 @@
8
8
 
9
9
  import Link from 'next/link'
10
10
  import { Github, Twitter, Linkedin, Mail, Heart } from 'lucide-react'
11
+ import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
11
12
 
12
13
  interface SocialLink {
13
14
  name: string
@@ -117,7 +118,7 @@ export function BlogFooter({
117
118
  return (
118
119
  <a
119
120
  key={link.name}
120
- href={link.href}
121
+ href={withBasePathIfInApp(link.href)}
121
122
  data-cy={`blog-footer-social-${link.icon}`}
122
123
  target="_blank"
123
124
  rel="noopener noreferrer"
@@ -11,6 +11,7 @@ import Link from 'next/link'
11
11
  import Image from 'next/image'
12
12
  import { Calendar, Clock, User } from 'lucide-react'
13
13
  import { cn } from '@nextsparkjs/core/lib/utils'
14
+ import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
14
15
 
15
16
  interface PostCardProps {
16
17
  id: string
@@ -73,7 +74,7 @@ export function PostCard({
73
74
  <div data-cy={`post-card-image-${id}`} className="aspect-[21/9] relative overflow-hidden">
74
75
  {featuredImage ? (
75
76
  <Image
76
- src={featuredImage}
77
+ src={withBasePathIfInApp(featuredImage)}
77
78
  alt={title}
78
79
  fill
79
80
  className="object-cover transition-transform duration-500 group-hover:scale-105"
@@ -115,7 +116,7 @@ export function PostCard({
115
116
  <Link href={authorUrl} data-cy={`post-card-author-${id}`} className="flex items-center gap-2 hover:text-foreground transition-colors">
116
117
  {authorAvatar ? (
117
118
  <Image
118
- src={authorAvatar}
119
+ src={withBasePathIfInApp(authorAvatar)}
119
120
  alt={authorName}
120
121
  width={24}
121
122
  height={24}
@@ -132,7 +133,7 @@ export function PostCard({
132
133
  <div data-cy={`post-card-author-${id}`} className="flex items-center gap-2">
133
134
  {authorAvatar ? (
134
135
  <Image
135
- src={authorAvatar}
136
+ src={withBasePathIfInApp(authorAvatar)}
136
137
  alt={authorName}
137
138
  width={24}
138
139
  height={24}
@@ -174,7 +175,7 @@ export function PostCard({
174
175
  <div data-cy={`post-card-image-${id}`} className="flex-shrink-0 w-24 h-24 relative overflow-hidden rounded-md">
175
176
  {featuredImage ? (
176
177
  <Image
177
- src={featuredImage}
178
+ src={withBasePathIfInApp(featuredImage)}
178
179
  alt={title}
179
180
  fill
180
181
  className="object-cover"
@@ -216,7 +217,7 @@ export function PostCard({
216
217
  <div data-cy={`post-card-image-${id}`} className="aspect-[16/10] relative overflow-hidden">
217
218
  {featuredImage ? (
218
219
  <Image
219
- src={featuredImage}
220
+ src={withBasePathIfInApp(featuredImage)}
220
221
  alt={title}
221
222
  fill
222
223
  className="object-cover transition-transform duration-500 group-hover:scale-105"
@@ -256,7 +257,7 @@ export function PostCard({
256
257
  <Link href={authorUrl} data-cy={`post-card-author-${id}`} className="flex items-center gap-2 hover:text-foreground transition-colors">
257
258
  {authorAvatar ? (
258
259
  <Image
259
- src={authorAvatar}
260
+ src={withBasePathIfInApp(authorAvatar)}
260
261
  alt={authorName}
261
262
  width={20}
262
263
  height={20}
@@ -273,7 +274,7 @@ export function PostCard({
273
274
  <div data-cy={`post-card-author-${id}`} className="flex items-center gap-2">
274
275
  {authorAvatar ? (
275
276
  <Image
276
- src={authorAvatar}
277
+ src={withBasePathIfInApp(authorAvatar)}
277
278
  alt={authorName}
278
279
  width={20}
279
280
  height={20}
@@ -9,6 +9,7 @@
9
9
  import Link from 'next/link'
10
10
  import Image from 'next/image'
11
11
  import { Clock } from 'lucide-react'
12
+ import { withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
12
13
 
13
14
  interface RelatedPost {
14
15
  id: string
@@ -45,7 +46,7 @@ export function RelatedPosts({
45
46
  <div data-cy={`related-post-image-${post.id}`} className="aspect-[16/9] relative overflow-hidden">
46
47
  {post.featuredImage ? (
47
48
  <Image
48
- src={post.featuredImage}
49
+ src={withBasePathIfInApp(post.featuredImage)}
49
50
  alt={post.title}
50
51
  fill
51
52
  className="object-cover transition-transform duration-300 group-hover:scale-105"
@@ -100,25 +100,19 @@ export const APP_CONFIG_OVERRIDES = {
100
100
  // =============================================================================
101
101
  docs: {
102
102
  enabled: true,
103
- public: true,
103
+ publicAccess: true,
104
104
  searchEnabled: true,
105
105
  breadcrumbs: true,
106
- theme: {
106
+ public: {
107
107
  enabled: true,
108
108
  open: true,
109
109
  label: "Blog Theme",
110
110
  },
111
- plugins: {
112
- enabled: false,
113
- open: false,
114
- label: "Plugins",
115
- },
116
- core: {
111
+ superadmin: {
117
112
  enabled: true,
118
113
  open: false,
119
- label: "Core",
114
+ label: "Admin Docs",
120
115
  },
121
- showPluginsDocsInProd: false,
122
116
  },
123
117
 
124
118
  // =============================================================================
@@ -0,0 +1,99 @@
1
+ -- Migration: 090_billing_sample_data.sql
2
+ -- Description: Sample billing plans for the blog theme (free, pro, enterprise)
3
+ -- Theme: blog
4
+ -- Phase: Theme sample data - runs before 999_theme_sample_data.sql, whose
5
+ -- subscriptions reference these plans by slug.
6
+ --
7
+ -- Mirrors themes/blog/config/billing.config.ts (slugs, prices, features,
8
+ -- limits) so the seeded plans match what the app's billing config declares.
9
+
10
+ -- ============================================
11
+ -- SAMPLE PLANS
12
+ -- ============================================
13
+
14
+ -- Plan: Free
15
+ INSERT INTO public."plans" (
16
+ id,
17
+ slug,
18
+ name,
19
+ description,
20
+ type,
21
+ visibility,
22
+ "priceMonthly",
23
+ "priceYearly",
24
+ "trialDays",
25
+ features,
26
+ limits,
27
+ "sortOrder"
28
+ ) VALUES (
29
+ 'plan_blog_free',
30
+ 'free',
31
+ 'Free',
32
+ 'Free plan to get started',
33
+ 'free',
34
+ 'public',
35
+ 0,
36
+ 0,
37
+ 0,
38
+ '["basic_analytics"]'::jsonb,
39
+ '{"posts": 10, "monthly_views": 10000, "storage_gb": 1, "authors": 1}'::jsonb,
40
+ 1
41
+ ) ON CONFLICT (slug) DO NOTHING;
42
+
43
+ -- Plan: Pro
44
+ INSERT INTO public."plans" (
45
+ id,
46
+ slug,
47
+ name,
48
+ description,
49
+ type,
50
+ visibility,
51
+ "priceMonthly",
52
+ "priceYearly",
53
+ "trialDays",
54
+ features,
55
+ limits,
56
+ "sortOrder"
57
+ ) VALUES (
58
+ 'plan_blog_pro',
59
+ 'pro',
60
+ 'Pro',
61
+ 'Professional plan for bloggers',
62
+ 'paid',
63
+ 'public',
64
+ 2900,
65
+ 29000,
66
+ 14,
67
+ '["basic_analytics", "custom_domain", "advanced_seo"]'::jsonb,
68
+ '{"posts": -1, "monthly_views": 100000, "storage_gb": 10, "authors": 5}'::jsonb,
69
+ 2
70
+ ) ON CONFLICT (slug) DO NOTHING;
71
+
72
+ -- Plan: Enterprise
73
+ INSERT INTO public."plans" (
74
+ id,
75
+ slug,
76
+ name,
77
+ description,
78
+ type,
79
+ visibility,
80
+ "priceMonthly",
81
+ "priceYearly",
82
+ "trialDays",
83
+ features,
84
+ limits,
85
+ "sortOrder"
86
+ ) VALUES (
87
+ 'plan_blog_enterprise',
88
+ 'enterprise',
89
+ 'Enterprise',
90
+ 'Custom enterprise plan with unlimited features',
91
+ 'enterprise',
92
+ 'hidden',
93
+ NULL,
94
+ NULL,
95
+ 30,
96
+ '["*"]'::jsonb,
97
+ '{"posts": -1, "monthly_views": -1, "storage_gb": -1, "authors": -1}'::jsonb,
98
+ 3
99
+ ) ON CONFLICT (slug) DO NOTHING;
@@ -662,15 +662,15 @@ INSERT INTO public."subscriptions" (
662
662
  "externalSubscriptionId", "externalCustomerId", "createdAt"
663
663
  ) VALUES
664
664
  -- Marcos Tech → Pro Plan $29/mo (active, monthly)
665
- ('sub-blog-marcos', 'team-blog-marcos', 'usr-blog-marcos', 'plan_pro', 'active',
665
+ ('sub-blog-marcos', 'team-blog-marcos', 'usr-blog-marcos', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
666
666
  NOW() - INTERVAL '30 days', NOW() + INTERVAL '30 days', 'monthly', 'stripe',
667
667
  'sub_stripe_marcos', 'cus_marcos', NOW() - INTERVAL '6 months'),
668
668
  -- Lucia Lifestyle → Pro Plan $29/mo (active, monthly)
669
- ('sub-blog-lucia', 'team-blog-lucia', 'usr-blog-lucia', 'plan_pro', 'active',
669
+ ('sub-blog-lucia', 'team-blog-lucia', 'usr-blog-lucia', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
670
670
  NOW() - INTERVAL '15 days', NOW() + INTERVAL '15 days', 'monthly', 'stripe',
671
671
  'sub_stripe_lucia', 'cus_lucia', NOW() - INTERVAL '5 months'),
672
672
  -- Carlos Finance → Pro Plan (trialing, monthly)
673
- ('sub-blog-carlos', 'team-blog-carlos', 'usr-blog-carlos', 'plan_pro', 'trialing',
673
+ ('sub-blog-carlos', 'team-blog-carlos', 'usr-blog-carlos', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'trialing',
674
674
  NOW(), NOW() + INTERVAL '14 days', 'monthly', NULL, NULL, NULL, NOW() - INTERVAL '4 months')
675
675
  ON CONFLICT (id) DO NOTHING;
676
676
 
@@ -843,64 +843,64 @@ INSERT INTO public."subscriptions" (
843
843
  "createdAt", "canceledAt", "cancelAtPeriodEnd", metadata
844
844
  ) VALUES
845
845
  -- 1. Diana Design - Pro Monthly ACTIVE (8 months customer)
846
- ('sub-blog-diana', 'team-blog-diana', 'usr-blog-diana', 'plan_pro', 'active',
846
+ ('sub-blog-diana', 'team-blog-diana', 'usr-blog-diana', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
847
847
  NOW() - INTERVAL '25 days', NOW() + INTERVAL '5 days', 'monthly',
848
848
  'stripe', 'sub_stripe_diana', 'cus_diana',
849
849
  NOW() - INTERVAL '8 months', NULL, false, '{}'::jsonb),
850
850
 
851
851
  -- 2. Elena Eco - Pro Monthly ACTIVE (7 months customer)
852
- ('sub-blog-elena', 'team-blog-elena', 'usr-blog-elena', 'plan_pro', 'active',
852
+ ('sub-blog-elena', 'team-blog-elena', 'usr-blog-elena', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
853
853
  NOW() - INTERVAL '20 days', NOW() + INTERVAL '10 days', 'monthly',
854
854
  'stripe', 'sub_stripe_elena', 'cus_elena',
855
855
  NOW() - INTERVAL '7 months', NULL, false, '{}'::jsonb),
856
856
 
857
857
  -- 3. Felix Fitness - Pro Yearly ACTIVE (14 months = 2nd year)
858
- ('sub-blog-felix', 'team-blog-felix', 'usr-blog-felix', 'plan_pro', 'active',
858
+ ('sub-blog-felix', 'team-blog-felix', 'usr-blog-felix', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
859
859
  NOW() - INTERVAL '60 days', NOW() + INTERVAL '305 days', 'yearly',
860
860
  'stripe', 'sub_stripe_felix', 'cus_felix',
861
861
  NOW() - INTERVAL '14 months', NULL, false, '{}'::jsonb),
862
862
 
863
863
  -- 4. Gloria Gourmet - Pro Yearly ACTIVE (12 months)
864
- ('sub-blog-gloria', 'team-blog-gloria', 'usr-blog-gloria', 'plan_pro', 'active',
864
+ ('sub-blog-gloria', 'team-blog-gloria', 'usr-blog-gloria', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
865
865
  NOW() - INTERVAL '30 days', NOW() + INTERVAL '335 days', 'yearly',
866
866
  'stripe', 'sub_stripe_gloria', 'cus_gloria',
867
867
  NOW() - INTERVAL '12 months', NULL, false, '{}'::jsonb),
868
868
 
869
869
  -- 5. Hugo Health - Pro Monthly ACTIVE (6 months customer)
870
- ('sub-blog-hugo', 'team-blog-hugo', 'usr-blog-hugo', 'plan_pro', 'active',
870
+ ('sub-blog-hugo', 'team-blog-hugo', 'usr-blog-hugo', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'active',
871
871
  NOW() - INTERVAL '18 days', NOW() + INTERVAL '12 days', 'monthly',
872
872
  'stripe', 'sub_stripe_hugo', 'cus_hugo',
873
873
  NOW() - INTERVAL '6 months', NULL, false, '{}'::jsonb),
874
874
 
875
875
  -- 6. Iris Inspire - Pro Monthly PAST_DUE (failed payment)
876
- ('sub-blog-iris', 'team-blog-iris', 'usr-blog-iris', 'plan_pro', 'past_due',
876
+ ('sub-blog-iris', 'team-blog-iris', 'usr-blog-iris', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'past_due',
877
877
  NOW() - INTERVAL '35 days', NOW() - INTERVAL '5 days', 'monthly',
878
878
  'stripe', 'sub_stripe_iris', 'cus_iris',
879
879
  NOW() - INTERVAL '5 months', NULL, false,
880
880
  '{"lastPaymentAttempt": "failed", "retryCount": 2}'::jsonb),
881
881
 
882
882
  -- 7. Jorge Journey - Pro Monthly CANCELED (voluntary - user decided to cancel)
883
- ('sub-blog-jorge', 'team-blog-jorge', 'usr-blog-jorge', 'plan_pro', 'canceled',
883
+ ('sub-blog-jorge', 'team-blog-jorge', 'usr-blog-jorge', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'canceled',
884
884
  NOW() - INTERVAL '45 days', NOW() - INTERVAL '15 days', 'monthly',
885
885
  'stripe', 'sub_stripe_jorge', 'cus_jorge',
886
886
  NOW() - INTERVAL '9 months', NOW() - INTERVAL '15 days', false,
887
887
  '{"cancelReason": "voluntary", "canceledBy": "user", "feedback": "taking a break"}'::jsonb),
888
888
 
889
889
  -- 8. Karen Kitchen - Pro Yearly CANCELED (payment failed 4 times)
890
- ('sub-blog-karen', 'team-blog-karen', 'usr-blog-karen', 'plan_pro', 'canceled',
890
+ ('sub-blog-karen', 'team-blog-karen', 'usr-blog-karen', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'canceled',
891
891
  NOW() - INTERVAL '400 days', NOW() - INTERVAL '35 days', 'yearly',
892
892
  'stripe', 'sub_stripe_karen', 'cus_karen',
893
893
  NOW() - INTERVAL '15 months', NOW() - INTERVAL '35 days', false,
894
894
  '{"cancelReason": "payment_failed", "failedAttempts": 4, "lastCardEnding": "4242"}'::jsonb),
895
895
 
896
896
  -- 9. Leo Learn - Free Plan ACTIVE (no billing)
897
- ('sub-blog-leo', 'team-blog-leo', 'usr-blog-leo', 'plan_free', 'active',
897
+ ('sub-blog-leo', 'team-blog-leo', 'usr-blog-leo', (SELECT id FROM public."plans" WHERE slug = 'free'), 'active',
898
898
  NOW() - INTERVAL '2 months', NOW() + INTERVAL '100 years', 'monthly',
899
899
  NULL, NULL, NULL,
900
900
  NOW() - INTERVAL '2 months', NULL, false, '{}'::jsonb),
901
901
 
902
902
  -- 10. Maria Music - Pro Monthly TRIALING (5 days in)
903
- ('sub-blog-maria', 'team-blog-maria', 'usr-blog-maria', 'plan_pro', 'trialing',
903
+ ('sub-blog-maria', 'team-blog-maria', 'usr-blog-maria', (SELECT id FROM public."plans" WHERE slug = 'pro'), 'trialing',
904
904
  NOW() - INTERVAL '5 days', NOW() + INTERVAL '25 days', 'monthly',
905
905
  NULL, NULL, NULL,
906
906
  NOW() - INTERVAL '5 days', NULL, false, '{}'::jsonb)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextsparkjs/theme-blog",
3
- "version": "0.1.0-beta.189",
3
+ "version": "0.1.0-beta.190",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./config/theme.config.ts",
@@ -14,7 +14,7 @@
14
14
  "@nextsparkjs/core": ">=0.1.0-0",
15
15
  "@nextsparkjs/testing": ">=0.1.0-0",
16
16
  "lucide-react": "^0.539.0",
17
- "next": "^15.0.0",
17
+ "next": "^15.0.0 || ^16.0.0",
18
18
  "next-intl": "^4.0.0",
19
19
  "react": "^19.0.0",
20
20
  "react-dom": "^19.0.0",
@@ -13,6 +13,7 @@ import Image from 'next/image'
13
13
  import { Twitter, Linkedin, Globe, User } from 'lucide-react'
14
14
  import { PostCard } from '@/themes/blog/components/public/PostCard'
15
15
  import { cn } from '@nextsparkjs/core/lib/utils'
16
+ import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
16
17
 
17
18
  interface Author {
18
19
  id: string
@@ -60,7 +61,7 @@ export default function AuthorPage({ params }: PageProps) {
60
61
  setLoading(true)
61
62
  setError(null)
62
63
 
63
- const response = await fetch(`/api/v1/theme/blog/authors/${username}`, {
64
+ const response = await fetch(withBasePath(`/api/v1/theme/blog/authors/${username}`), {
64
65
  method: 'GET',
65
66
  headers: {
66
67
  'Content-Type': 'application/json',
@@ -137,7 +138,7 @@ export default function AuthorPage({ params }: PageProps) {
137
138
  <div className="flex-shrink-0">
138
139
  {author.image ? (
139
140
  <Image
140
- src={author.image}
141
+ src={withBasePathIfInApp(author.image)}
141
142
  alt={author.name}
142
143
  width={128}
143
144
  height={128}
@@ -165,7 +166,7 @@ export default function AuthorPage({ params }: PageProps) {
165
166
  <div className="flex items-center gap-4">
166
167
  {author.socialTwitter && (
167
168
  <a
168
- href={author.socialTwitter}
169
+ href={withBasePathIfInApp(author.socialTwitter)}
169
170
  target="_blank"
170
171
  rel="noopener noreferrer"
171
172
  className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
@@ -176,7 +177,7 @@ export default function AuthorPage({ params }: PageProps) {
176
177
  )}
177
178
  {author.socialLinkedin && (
178
179
  <a
179
- href={author.socialLinkedin}
180
+ href={withBasePathIfInApp(author.socialLinkedin)}
180
181
  target="_blank"
181
182
  rel="noopener noreferrer"
182
183
  className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
@@ -187,7 +188,7 @@ export default function AuthorPage({ params }: PageProps) {
187
188
  )}
188
189
  {author.socialWebsite && (
189
190
  <a
190
- href={author.socialWebsite}
191
+ href={withBasePathIfInApp(author.socialWebsite)}
191
192
  target="_blank"
192
193
  rel="noopener noreferrer"
193
194
  className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
@@ -10,6 +10,7 @@ import { useState, useEffect } from 'react'
10
10
  import Link from 'next/link'
11
11
  import Image from 'next/image'
12
12
  import { User } from 'lucide-react'
13
+ import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
13
14
 
14
15
  interface Author {
15
16
  id: string
@@ -31,7 +32,7 @@ export default function AuthorsPage() {
31
32
  setLoading(true)
32
33
  setError(null)
33
34
 
34
- const response = await fetch('/api/v1/theme/blog/authors', {
35
+ const response = await fetch(withBasePath('/api/v1/theme/blog/authors'), {
35
36
  method: 'GET',
36
37
  headers: {
37
38
  'Content-Type': 'application/json',
@@ -115,7 +116,7 @@ export default function AuthorsPage() {
115
116
  <div className="flex justify-center mb-4">
116
117
  {author.image ? (
117
118
  <Image
118
- src={author.image}
119
+ src={withBasePathIfInApp(author.image)}
119
120
  alt={author.name}
120
121
  width={80}
121
122
  height={80}
@@ -14,6 +14,7 @@ import { useTranslations } from 'next-intl'
14
14
  import { PostCard } from '@/themes/blog/components/public/PostCard'
15
15
  import { cn } from '@nextsparkjs/core/lib/utils'
16
16
  import { Loader2 } from 'lucide-react'
17
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
17
18
 
18
19
  interface Post {
19
20
  id: string
@@ -65,7 +66,7 @@ export default function BlogHomePage() {
65
66
 
66
67
  // Fetch published posts from ALL authors via public endpoint
67
68
  // This endpoint does not require authentication and aggregates posts cross-team
68
- const response = await fetch('/api/v1/theme/blog/posts/public?limit=20', {
69
+ const response = await fetch(withBasePath('/api/v1/theme/blog/posts/public?limit=20'), {
69
70
  method: 'GET',
70
71
  headers: {
71
72
  'Content-Type': 'application/json',
@@ -17,6 +17,7 @@ import { ReadingProgress } from '@/themes/blog/components/public/ReadingProgress
17
17
  import { AuthorBio } from '@/themes/blog/components/public/AuthorBio'
18
18
  import { RelatedPosts } from '@/themes/blog/components/public/RelatedPosts'
19
19
  import { Button } from '@nextsparkjs/core/components/ui/button'
20
+ import { withBasePath, withBasePathInHtml, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
20
21
 
21
22
  interface Post {
22
23
  id: string
@@ -58,7 +59,7 @@ async function fetchPost(identifier: string): Promise<Post | null> {
58
59
  try {
59
60
  const headers = buildHeaders()
60
61
 
61
- const byIdResponse = await fetch(`/api/v1/posts?ids=${encodeURIComponent(identifier)}&status=published&limit=1`, {
62
+ const byIdResponse = await fetch(withBasePath(`/api/v1/posts?ids=${encodeURIComponent(identifier)}&status=published&limit=1`), {
62
63
  credentials: 'include',
63
64
  headers,
64
65
  })
@@ -70,7 +71,7 @@ async function fetchPost(identifier: string): Promise<Post | null> {
70
71
  }
71
72
  }
72
73
 
73
- const bySlugResponse = await fetch(`/api/v1/posts?slug=${encodeURIComponent(identifier)}&status=published&limit=1`, {
74
+ const bySlugResponse = await fetch(withBasePath(`/api/v1/posts?slug=${encodeURIComponent(identifier)}&status=published&limit=1`), {
74
75
  credentials: 'include',
75
76
  headers,
76
77
  })
@@ -146,7 +147,7 @@ async function fetchRelatedPosts(currentPostId: string): Promise<RelatedPost[]>
146
147
  const headers = buildHeaders()
147
148
 
148
149
  // Fetch latest published posts
149
- const response = await fetch('/api/v1/posts?status=published&limit=4&sortBy=publishedAt&sortOrder=desc', {
150
+ const response = await fetch(withBasePath('/api/v1/posts?status=published&limit=4&sortBy=publishedAt&sortOrder=desc'), {
150
151
  credentials: 'include',
151
152
  headers,
152
153
  })
@@ -282,7 +283,7 @@ export default function PostPage({ params }: PageProps) {
282
283
  <figure className="mb-12">
283
284
  <div className="aspect-[21/9] relative overflow-hidden rounded-xl">
284
285
  <Image
285
- src={post.featuredImage}
286
+ src={withBasePathIfInApp(post.featuredImage)}
286
287
  alt={post.title}
287
288
  fill
288
289
  className="object-cover"
@@ -295,7 +296,7 @@ export default function PostPage({ params }: PageProps) {
295
296
 
296
297
  {/* Article Content */}
297
298
  <div className="article-content">
298
- <div dangerouslySetInnerHTML={{ __html: sanitizePostHtml(renderContent(post.content)) }} />
299
+ <div dangerouslySetInnerHTML={{ __html: withBasePathInHtml(sanitizePostHtml(renderContent(post.content))) }} />
299
300
  </div>
300
301
 
301
302
  {/* Divider */}
@@ -29,6 +29,7 @@ import {
29
29
  Eye,
30
30
  MoreHorizontal
31
31
  } from 'lucide-react'
32
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
32
33
 
33
34
  interface PostStats {
34
35
  total: number
@@ -65,7 +66,7 @@ async function fetchPosts(): Promise<{ stats: PostStats; recent: RecentPost[] }>
65
66
  const headers = buildHeaders()
66
67
 
67
68
  // Fetch all posts to get stats
68
- const response = await fetch('/api/v1/posts?limit=100&sortBy=updatedAt&sortOrder=desc', {
69
+ const response = await fetch(withBasePath('/api/v1/posts?limit=100&sortBy=updatedAt&sortOrder=desc'), {
69
70
  credentials: 'include',
70
71
  headers,
71
72
  })
@@ -39,6 +39,7 @@ import {
39
39
  import { Switch } from '@nextsparkjs/core/components/ui/switch'
40
40
  import { WysiwygEditor } from '@/themes/blog/components/editor/WysiwygEditor'
41
41
  import { FeaturedImageUpload } from '@/themes/blog/components/editor/FeaturedImageUpload'
42
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
42
43
 
43
44
  interface PostData {
44
45
  id: string
@@ -102,7 +103,7 @@ export default function EditPostPage({ params }: PageProps) {
102
103
  async function fetchPost() {
103
104
  try {
104
105
  const headers = buildHeaders()
105
- const response = await fetch(`/api/v1/posts/${id}`, {
106
+ const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
106
107
  credentials: 'include',
107
108
  headers,
108
109
  })
@@ -178,7 +179,7 @@ export default function EditPostPage({ params }: PageProps) {
178
179
 
179
180
  try {
180
181
  const headers = buildHeaders()
181
- const response = await fetch(`/api/v1/posts/${id}`, {
182
+ const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
182
183
  method: 'PATCH',
183
184
  headers,
184
185
  credentials: 'include',
@@ -232,7 +233,7 @@ export default function EditPostPage({ params }: PageProps) {
232
233
 
233
234
  try {
234
235
  const headers = buildHeaders()
235
- const response = await fetch(`/api/v1/posts/${id}`, {
236
+ const response = await fetch(withBasePath(`/api/v1/posts/${id}`), {
236
237
  method: 'DELETE',
237
238
  headers,
238
239
  credentials: 'include',
@@ -29,6 +29,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@nextsparkjs/core/comp
29
29
  import { Switch } from '@nextsparkjs/core/components/ui/switch'
30
30
  import { WysiwygEditor } from '@/themes/blog/components/editor/WysiwygEditor'
31
31
  import { FeaturedImageUpload } from '@/themes/blog/components/editor/FeaturedImageUpload'
32
+ import { withBasePath } from '@nextsparkjs/core/lib/base-path'
32
33
 
33
34
  interface PostData {
34
35
  title: string
@@ -122,7 +123,7 @@ export default function CreatePostPage() {
122
123
 
123
124
  try {
124
125
  const headers = buildHeaders()
125
- const response = await fetch('/api/v1/posts', {
126
+ const response = await fetch(withBasePath('/api/v1/posts'), {
126
127
  method: 'POST',
127
128
  headers,
128
129
  credentials: 'include',
@@ -63,6 +63,7 @@ import {
63
63
  Send,
64
64
  ImageIcon,
65
65
  } from 'lucide-react'
66
+ import { withBasePath, withBasePathIfInApp } from '@nextsparkjs/core/lib/base-path'
66
67
 
67
68
  interface Post {
68
69
  id: string
@@ -174,7 +175,7 @@ export default function PostsListPage() {
174
175
  sortOrder,
175
176
  })
176
177
 
177
- const response = await fetch(`/api/v1/posts?${params}`, {
178
+ const response = await fetch(withBasePath(`/api/v1/posts?${params}`), {
178
179
  credentials: 'include',
179
180
  headers,
180
181
  })
@@ -246,7 +247,7 @@ export default function PostsListPage() {
246
247
  setActionLoading(post.id)
247
248
  try {
248
249
  const headers = buildHeaders()
249
- const response = await fetch(`/api/v1/posts/${post.id}`, {
250
+ const response = await fetch(withBasePath(`/api/v1/posts/${post.id}`), {
250
251
  method: 'PATCH',
251
252
  headers,
252
253
  credentials: 'include',
@@ -269,7 +270,7 @@ export default function PostsListPage() {
269
270
  setActionLoading(post.id)
270
271
  try {
271
272
  const headers = buildHeaders()
272
- const response = await fetch(`/api/v1/posts/${post.id}`, {
273
+ const response = await fetch(withBasePath(`/api/v1/posts/${post.id}`), {
273
274
  method: 'PATCH',
274
275
  headers,
275
276
  credentials: 'include',
@@ -291,7 +292,7 @@ export default function PostsListPage() {
291
292
  setActionLoading(postToDelete.id)
292
293
  try {
293
294
  const headers = buildHeaders()
294
- const response = await fetch(`/api/v1/posts/${postToDelete.id}`, {
295
+ const response = await fetch(withBasePath(`/api/v1/posts/${postToDelete.id}`), {
295
296
  method: 'DELETE',
296
297
  headers,
297
298
  credentials: 'include',
@@ -555,7 +556,7 @@ export default function PostsListPage() {
555
556
  <div className="hidden sm:block flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-muted">
556
557
  {post.featuredImage ? (
557
558
  <Image
558
- src={post.featuredImage}
559
+ src={withBasePathIfInApp(post.featuredImage)}
559
560
  alt=""
560
561
  width={64}
561
562
  height={48}
@@ -690,7 +691,7 @@ export default function PostsListPage() {
690
691
  <div className="aspect-video relative bg-muted">
691
692
  {post.featuredImage ? (
692
693
  <Image
693
- src={post.featuredImage}
694
+ src={withBasePathIfInApp(post.featuredImage)}
694
695
  alt=""
695
696
  fill
696
697
  className="object-cover"