Skip to content

Commit d376205

Browse files
authored
Merge pull request #129 from ycode/develop
develop
2 parents ce75389 + 68b85e6 commit d376205

File tree

7 files changed

+123
-74
lines changed

7 files changed

+123
-74
lines changed

app/(builder)/ycode/components/TypographyControls.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ const TypographyControls = memo(function TypographyControls({ layer, onLayerUpda
7171
// Detect if underline is active
7272
const hasUnderline = textDecoration === 'underline';
7373

74+
// Detect if text transform is active
75+
const hasTransform = textTransform !== 'none' && textTransform !== '';
76+
7477
// Custom extractor for letter spacing (strips 'em' as default unit, like fontSize strips 'px')
7578
const extractLetterSpacingValue = (value: string): string => {
7679
if (!value) return '';
@@ -207,6 +210,18 @@ const TypographyControls = memo(function TypographyControls({ layer, onLayerUpda
207210
]);
208211
};
209212

213+
const handleAddTransform = () => {
214+
updateDesignProperty('typography', 'textTransform', 'uppercase');
215+
};
216+
217+
const handleRemoveTransform = () => {
218+
updateDesignProperty('typography', 'textTransform', null);
219+
};
220+
221+
const handleTransformChange = (value: string) => {
222+
updateDesignProperty('typography', 'textTransform', value);
223+
};
224+
210225
// Debounced handler for keyboard-typed hex values
211226
const handleDecorationColorChange = (value: string) => {
212227
const sanitized = removeSpaces(value);
@@ -301,6 +316,12 @@ const TypographyControls = memo(function TypographyControls({ layer, onLayerUpda
301316
>
302317
Underline
303318
</DropdownMenuItem>
319+
<DropdownMenuItem
320+
onClick={handleAddTransform}
321+
disabled={hasTransform}
322+
>
323+
Transform
324+
</DropdownMenuItem>
304325
</DropdownMenuContent>
305326
</DropdownMenu>
306327
)}
@@ -586,6 +607,38 @@ const TypographyControls = memo(function TypographyControls({ layer, onLayerUpda
586607
</div>
587608
</div>
588609
)}
610+
611+
{!isIcon && hasTransform && (
612+
<div className="grid grid-cols-3 items-start">
613+
<Label variant="muted" className="h-8">Transform</Label>
614+
<div className="col-span-2 flex items-center gap-2">
615+
<Select
616+
value={textTransform}
617+
onValueChange={handleTransformChange}
618+
>
619+
<SelectTrigger className="flex-1">
620+
<SelectValue />
621+
</SelectTrigger>
622+
<SelectContent>
623+
<SelectGroup>
624+
<SelectItem value="uppercase">Uppercase</SelectItem>
625+
<SelectItem value="lowercase">Lowercase</SelectItem>
626+
<SelectItem value="capitalize">Capitalize</SelectItem>
627+
<SelectItem value="normal-case">Normal</SelectItem>
628+
</SelectGroup>
629+
</SelectContent>
630+
</Select>
631+
<span
632+
role="button"
633+
tabIndex={0}
634+
className="p-0.5 rounded-sm opacity-70 hover:opacity-100 transition-opacity cursor-pointer"
635+
onClick={handleRemoveTransform}
636+
>
637+
<Icon name="x" className="size-2.5" />
638+
</span>
639+
</div>
640+
</div>
641+
)}
589642
</div>
590643
</div>
591644
);

app/(site)/[...slug]/page.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import PageRenderer from '@/components/PageRenderer';
99
import PasswordForm from '@/components/PasswordForm';
1010
import { getSettingByKey } from '@/lib/repositories/settingsRepository';
1111
import { parseAuthCookie, getPasswordProtection, fetchFoldersForAuth } from '@/lib/page-auth';
12+
import { getSiteBaseUrl } from '@/lib/url-utils';
1213
import type { Page, PageFolder, Translation, Redirect as RedirectType } from '@/types';
1314

1415
// Static by default for performance, dynamic only when pagination is requested
@@ -394,14 +395,23 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
394395
}
395396
}
396397

397-
return unstable_cache(
398-
async () => generatePageMetadata(data.page, {
399-
fallbackTitle: slugPath.charAt(0).toUpperCase() + slugPath.slice(1),
400-
collectionItem: data.collectionItem,
401-
pagePath: '/' + slugPath,
402-
globalSeoSettings: globalSettings,
398+
const { meta, baseUrl } = await unstable_cache(
399+
async () => ({
400+
meta: await generatePageMetadata(data.page, {
401+
fallbackTitle: slugPath.charAt(0).toUpperCase() + slugPath.slice(1),
402+
collectionItem: data.collectionItem,
403+
pagePath: '/' + slugPath,
404+
globalSeoSettings: globalSettings,
405+
}),
406+
baseUrl: getSiteBaseUrl({ globalCanonicalUrl: globalSettings.globalCanonicalUrl }),
403407
}),
404408
[`data-for-route-/${slugPath}-meta`],
405409
{ tags: ['all-pages', `route-/${slugPath}`], revalidate: false }
406410
)();
411+
412+
if (baseUrl) {
413+
try { meta.metadataBase = new URL(baseUrl); } catch { /* invalid URL */ }
414+
}
415+
416+
return meta;
407417
}

app/(site)/page.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import PageRenderer from '@/components/PageRenderer';
55
import PasswordForm from '@/components/PasswordForm';
66
import { generatePageMetadata, fetchGlobalPageSettings } from '@/lib/generate-page-metadata';
77
import { parseAuthCookie, getPasswordProtection, fetchFoldersForAuth } from '@/lib/page-auth';
8+
import { getSiteBaseUrl } from '@/lib/url-utils';
89
import type { Metadata } from 'next';
910

1011
// Static by default for performance, dynamic only when pagination is requested
@@ -213,13 +214,22 @@ export async function generateMetadata(): Promise<Metadata> {
213214
}
214215
}
215216

216-
return unstable_cache(
217-
async () => generatePageMetadata(data.page, {
218-
fallbackTitle: 'Home',
219-
pagePath: '/',
220-
globalSeoSettings: globalSettings,
217+
const { meta, baseUrl } = await unstable_cache(
218+
async () => ({
219+
meta: await generatePageMetadata(data.page, {
220+
fallbackTitle: 'Home',
221+
pagePath: '/',
222+
globalSeoSettings: globalSettings,
223+
}),
224+
baseUrl: getSiteBaseUrl({ globalCanonicalUrl: globalSettings.globalCanonicalUrl }),
221225
}),
222226
['data-for-route-/-meta'],
223227
{ tags: ['all-pages', 'route-/'], revalidate: false }
224228
)();
229+
230+
if (baseUrl) {
231+
try { meta.metadataBase = new URL(baseUrl); } catch { /* invalid URL */ }
232+
}
233+
234+
return meta;
225235
}

app/(site)/robots.txt/route.ts

Lines changed: 7 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,16 @@
55
*/
66

77
import { NextResponse } from 'next/server';
8-
import { getSettingByKey } from '@/lib/repositories/settingsRepository';
8+
import { getSettingsByKeys } from '@/lib/repositories/settingsRepository';
99
import { credentials } from '@/lib/credentials';
10+
import { getSiteBaseUrl } from '@/lib/url-utils';
1011
import type { SitemapSettings } from '@/types';
1112

12-
/**
13-
* Get the base URL for robots.txt generation
14-
*/
15-
function getBaseUrl(): string {
16-
// Use environment variable if set
17-
if (process.env.NEXT_PUBLIC_SITE_URL) {
18-
return process.env.NEXT_PUBLIC_SITE_URL.replace(/\/$/, '');
19-
}
20-
21-
// Fallback to Vercel URL
22-
if (process.env.VERCEL_URL) {
23-
return `https://${process.env.VERCEL_URL}`;
24-
}
25-
26-
return '';
27-
}
28-
2913
export async function GET() {
3014
try {
3115
const hasSupabaseCredentials = await credentials.exists();
3216
if (!hasSupabaseCredentials) {
33-
const baseUrl = getBaseUrl();
17+
const baseUrl = getSiteBaseUrl() || '';
3418
const fallback = `# Default robots.txt
3519
User-agent: *
3620
Allow: /
@@ -47,36 +31,28 @@ Sitemap: ${baseUrl}/sitemap.xml`;
4731
});
4832
}
4933

50-
// Get custom robots.txt content if set
51-
const customRobots = await getSettingByKey('robots_txt');
52-
53-
// Get sitemap settings to determine if we should include sitemap reference
54-
const sitemapSettings = await getSettingByKey('sitemap') as SitemapSettings | null;
34+
const allSettings = await getSettingsByKeys(['robots_txt', 'sitemap', 'global_canonical_url']);
35+
const sitemapSettings = allSettings.sitemap as SitemapSettings | null;
5536
const sitemapEnabled = sitemapSettings?.mode && sitemapSettings.mode !== 'none';
37+
const baseUrl = getSiteBaseUrl({ globalCanonicalUrl: allSettings.global_canonical_url }) || '';
5638

5739
let content: string;
5840

41+
const customRobots = allSettings.robots_txt;
5942
if (customRobots && typeof customRobots === 'string' && customRobots.trim()) {
60-
// Use custom robots.txt content
6143
content = customRobots.trim();
6244

63-
// If sitemap is enabled and not already referenced, append it
6445
if (sitemapEnabled && !content.toLowerCase().includes('sitemap:')) {
65-
const baseUrl = getBaseUrl();
6646
content += `\n\nSitemap: ${baseUrl}/sitemap.xml`;
6747
}
6848
} else {
69-
// Generate default robots.txt
70-
const baseUrl = getBaseUrl();
71-
7249
content = `# Default robots.txt
7350
User-agent: *
7451
Allow: /
7552
7653
# Disallow admin/editor paths
7754
Disallow: /ycode/`;
7855

79-
// Add sitemap reference if enabled
8056
if (sitemapEnabled) {
8157
content += `\n\n# Sitemap\nSitemap: ${baseUrl}/sitemap.xml`;
8258
}

app/(site)/sitemap.xml/route.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,23 @@ import { getAllPages } from '@/lib/repositories/pageRepository';
1111
import { getAllPublishedPageFolders } from '@/lib/repositories/pageFolderRepository';
1212
import { getAllLocales } from '@/lib/repositories/localeRepository';
1313
import { getTranslationsByLocale } from '@/lib/repositories/translationRepository';
14-
import { getSettingByKey } from '@/lib/repositories/settingsRepository';
14+
import { getSettingsByKeys } from '@/lib/repositories/settingsRepository';
1515
import { getItemsByCollectionId } from '@/lib/repositories/collectionItemRepository';
1616
import { getValuesByItemIds } from '@/lib/repositories/collectionItemValueRepository';
1717
import {
1818
generateSitemapUrls,
1919
generateSitemapXml,
2020
getDefaultSitemapSettings,
2121
} from '@/lib/sitemap-utils';
22+
import { getSiteBaseUrl } from '@/lib/url-utils';
2223
import type { SitemapSettings, Translation, CollectionItem } from '@/types';
2324

24-
/**
25-
* Get the base URL for sitemap generation
26-
*/
27-
function getBaseUrl(): string {
28-
// Use environment variable if set
29-
if (process.env.NEXT_PUBLIC_SITE_URL) {
30-
return process.env.NEXT_PUBLIC_SITE_URL.replace(/\/$/, '');
31-
}
32-
33-
// Fallback to Vercel URL
34-
if (process.env.VERCEL_URL) {
35-
return `https://${process.env.VERCEL_URL}`;
36-
}
37-
38-
return '';
39-
}
40-
4125
export async function GET() {
4226
try {
4327
const hasSupabaseCredentials = await credentials.exists();
4428
if (!hasSupabaseCredentials) {
4529
const xml = generateSitemapXml([]);
30+
4631
return new NextResponse(xml, {
4732
headers: {
4833
'Content-Type': 'application/xml',
@@ -51,9 +36,9 @@ export async function GET() {
5136
});
5237
}
5338

54-
// Get sitemap settings
55-
const storedSettings = await getSettingByKey('sitemap');
56-
const settings: SitemapSettings = storedSettings || getDefaultSitemapSettings();
39+
const allSettings = await getSettingsByKeys(['sitemap', 'global_canonical_url']);
40+
const settings: SitemapSettings = allSettings.sitemap || getDefaultSitemapSettings();
41+
const globalCanonicalUrl: string | null = allSettings.global_canonical_url || null;
5742

5843
// If sitemap is disabled, return 404
5944
if (settings.mode === 'none') {
@@ -71,7 +56,7 @@ export async function GET() {
7156
}
7257

7358
// Auto-generate sitemap
74-
const baseUrl = getBaseUrl();
59+
const baseUrl = getSiteBaseUrl({ globalCanonicalUrl }) || '';
7560

7661
// Fetch published pages and folders
7762
const [pages, folders, locales] = await Promise.all([

lib/generate-page-metadata.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getSettingsByKeys } from '@/lib/repositories/settingsRepository';
1515
import { getAssetById } from '@/lib/repositories/assetRepository';
1616
import { getAssetProxyUrl } from '@/lib/asset-utils';
1717
import { generateColorVariablesCss } from '@/lib/repositories/colorVariableRepository';
18+
import { getSiteBaseUrl } from '@/lib/url-utils';
1819

1920
/**
2021
* Global page render settings fetched once per page render
@@ -168,16 +169,10 @@ export async function generatePageMetadata(
168169
if (!isPreview) {
169170
const seoSettings = options.globalSeoSettings || await fetchGlobalSeoSettings();
170171

171-
siteBaseUrl = (
172-
seoSettings.globalCanonicalUrl
173-
|| primaryDomainUrl
174-
|| process.env.NEXT_PUBLIC_SITE_URL
175-
|| (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null)
176-
|| null
177-
);
178-
if (siteBaseUrl) {
179-
siteBaseUrl = siteBaseUrl.replace(/\/$/, '');
180-
}
172+
siteBaseUrl = getSiteBaseUrl({
173+
globalCanonicalUrl: seoSettings.globalCanonicalUrl,
174+
primaryDomainUrl,
175+
});
181176

182177
// Add Google Site Verification meta tag
183178
if (seoSettings.googleSiteVerification) {

lib/url-utils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Resolve the site's base URL from settings and environment.
3+
*
4+
* Priority: globalCanonicalUrl > primaryDomainUrl > NEXT_PUBLIC_SITE_URL
5+
* > VERCEL_PROJECT_PRODUCTION_URL > VERCEL_URL
6+
*/
7+
export function getSiteBaseUrl(options?: {
8+
globalCanonicalUrl?: string | null;
9+
primaryDomainUrl?: string | null;
10+
}): string | null {
11+
const raw =
12+
options?.globalCanonicalUrl
13+
|| options?.primaryDomainUrl
14+
|| process.env.NEXT_PUBLIC_SITE_URL
15+
|| (process.env.VERCEL_PROJECT_PRODUCTION_URL ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}` : null)
16+
|| (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null)
17+
|| null;
18+
19+
return raw ? raw.replace(/\/$/, '') : null;
20+
}

0 commit comments

Comments
 (0)