Add next/script customization

This commit is contained in:
Sam Becker 2025-09-21 14:30:29 -05:00
parent 8482a76dd6
commit 47fe1cf383
6 changed files with 62 additions and 2 deletions

View File

@ -194,6 +194,12 @@ Application behavior can be changed by configuring the following environment var
- `NEXT_PUBLIC_SITE_FEEDS = 1` enables feeds at `/feed.json` and `/rss.xml`
- `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top)
#### Scripts & Analytics
- `PAGE_SCRIPT_URLS`
- comma-separated list of URLs to be added to the bottom of the body tag via "next/script"
- urls must begin with 'https'
- ⚠️ this will invoke arbitrary script execution on every page—use with caution
## Alternate storage providers
Only one storage adapter—Vercel Blob, Cloudflare R2, AWS S3, or MinIO—can be used at a time. Ideally, this is configured before photos are uploaded (see [Issue #34](https://github.com/sambecker/exif-photo-blog/issues/34) for migration considerations). If you have multiple adapters, you can set one as preferred by storing `aws-s3`, `cloudflare-r2`, `minio`, or `vercel-blob` in `NEXT_PUBLIC_STORAGE_PREFERENCE`. See [FAQ](#will-there-be-support-for-image-storage-providers-beyond-vercel-aws-and-cloudflare) regarding unsupported providers.

View File

@ -10,6 +10,7 @@ import {
HTML_LANG,
SITE_FEEDS_ENABLED,
ADMIN_DEBUG_TOOLS_ENABLED,
PAGE_SCRIPT_URLS,
} from '@/app/config';
import AppStateProvider from '@/app/AppStateProvider';
import ToasterWithThemes from '@/toast/ToasterWithThemes';
@ -30,6 +31,7 @@ import SharedHoverProvider from '@/components/shared-hover/SharedHoverProvider';
import { PATH_FEED_JSON, PATH_RSS_XML } from '@/app/path';
import SelectPhotosProvider from '@/admin/select/SelectPhotosProvider';
import AdminBatchEditPanel from '@/admin/select/AdminBatchEditPanel';
import Script from 'next/script';
import '../tailwind.css';
@ -144,6 +146,7 @@ export default function RootLayout({
</SelectPhotosProvider>
</AppTextProvider>
</AppStateProvider>
{PAGE_SCRIPT_URLS.map(url => <Script key={url} src={url} />)}
</body>
</html>
);

View File

@ -34,6 +34,8 @@ import ColorDot from '@/photo/color/ColorDot';
import { Oklch } from '@/photo/color/client';
import { getOrderedKeyListStatus } from '@/utility/key';
import { DEFAULT_SOCIAL_KEYS, SOCIAL_KEYS } from '@/social';
import MaskedScroll from '@/components/MaskedScroll';
import { IoLink } from 'react-icons/io5';
export default function AdminAppConfigurationClient({
// Storage
@ -122,6 +124,9 @@ export default function AdminAppConfigurationClient({
socialKeys,
areSiteFeedsEnabled,
isOgTextBottomAligned,
// Scripts & Analytics
hasPageScriptUrls,
pageScriptUrls,
// Internal
areInternalToolsEnabled,
areAdminDebugToolsEnabled,
@ -885,6 +890,35 @@ export default function AdminAppConfigurationClient({
{renderEnvVars(['NEXT_PUBLIC_OG_TEXT_ALIGNMENT'])}
</ChecklistRow>
</>;
case 'Scripts & Analytics':
return <>
<ChecklistRow
title="Custom page scripts"
status={hasPageScriptUrls}
optional
>
{pageScriptUrls.length > 0 &&
<div className="mt-2 text-xs space-y-1.5">
{pageScriptUrls.map(url =>
<MaskedScroll
key={url}
className={clsx(
'inline-flex items-center gap-1',
'bg-dim rounded-md px-1.5 py-0.5',
)}
direction="horizontal"
>
<IoLink size={14} className="shrink-0 translate-y-[0.5px]"/>
<span className="font-medium text-nowrap">
{url}
</span>
</MaskedScroll>)}
</div>}
Set environment variable to comma-separated list of URLs
to be added to the bottom of the body tag via {'"next/script"'}:
{renderEnvVars(['PAGE_SCRIPT_URLS'])}
</ChecklistRow>
</>;
case 'Internal':
return <>
<ChecklistRow

View File

@ -6,6 +6,7 @@ import { HiOutlineCog, HiSparkles } from 'react-icons/hi';
import { IoMdGrid } from 'react-icons/io';
import { PiPaintBrushHousehold } from 'react-icons/pi';
import { RiSpeedMiniLine } from 'react-icons/ri';
import { TbBrandGoogleAnalytics } from 'react-icons/tb';
export interface AdminConfigSection {
title: string;
@ -59,6 +60,10 @@ const ADMIN_CONFIG_SECTIONS = [{
title: 'Settings',
required: false,
icon: <HiOutlineCog size={17} className="translate-y-[0.5px]" />,
}, {
title: 'Scripts & Analytics',
required: false,
icon: <TbBrandGoogleAnalytics size={18} className="translate-y-[1px]" />,
}, {
title: 'Internal',
required: false,

View File

@ -370,6 +370,15 @@ export const SITE_FEEDS_ENABLED =
export const OG_TEXT_BOTTOM_ALIGNMENT =
(process.env.NEXT_PUBLIC_OG_TEXT_ALIGNMENT ?? '').toUpperCase() === 'BOTTOM';
// SCRIPTS & ANALYTICS
export const PAGE_SCRIPT_URLS = process.env.PAGE_SCRIPT_URLS
? process.env.PAGE_SCRIPT_URLS
.split(',')
.map(url => url.trim().toLocaleLowerCase())
.filter(url => url.startsWith('https://'))
: [];
// INTERNAL
export const ADMIN_DEBUG_TOOLS_ENABLED = process.env.ADMIN_DEBUG_TOOLS === '1';
@ -489,6 +498,9 @@ export const APP_CONFIGURATION = {
socialKeys: SOCIAL_NETWORKS,
areSiteFeedsEnabled: SITE_FEEDS_ENABLED,
isOgTextBottomAligned: OG_TEXT_BOTTOM_ALIGNMENT,
// Scripts & Analytics
hasPageScriptUrls: PAGE_SCRIPT_URLS.length > 0,
pageScriptUrls: PAGE_SCRIPT_URLS,
// Internal
areInternalToolsEnabled: (
ADMIN_DEBUG_TOOLS_ENABLED ||

View File

@ -84,7 +84,7 @@ export default function useMaskedScroll({
useEffect(() => {
const ref = containerRef?.current;
const contentRect = ref?.children[0].getBoundingClientRect();
const contentRect = ref?.children[0]?.getBoundingClientRect();
if (scrollToEndOnMount && ref && contentRect) {
ref.scrollTo(isVertical
? { top: contentRect.height }