Support non-root base paths

Allows materialious to be hosted on a subdirectory, e.g. https://domain/materialious/

Add base configuration comments to svelte.config.js and vite.config.ts
This commit is contained in:
Precific
2025-11-01 17:21:26 +01:00
parent 2627c076a6
commit 06bad1753c
24 changed files with 80 additions and 47 deletions
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
import { cleanNumber } from '$lib/numbers';
import { onMount } from 'svelte';
@@ -28,7 +29,7 @@
});
</script>
<a href={`/channel/${channel.authorId}`} class="wave" style="min-width: 100%;min-height: 100%;">
<a href={`${base}/channel/${channel.authorId}`} class="wave" style="min-width: 100%;min-height: 100%;">
<div class="padding">
{#if !$interfaceLowBandwidthMode}
<div class="center-align">
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { getComments } from '$lib/api';
import { type Comment, type Comments } from '$lib/api/model';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
@@ -34,7 +35,7 @@
function commentTimestamps(html: string): string {
const regex =
/<a href="([^"]+)" data-onclick="jump_to_time" data-jump-time="(\d+)">([^<]+)<\/a>\s*(.+)/g;
const replacement = `<a href="/watch/${videoId}?time=$2" data-sveltekit-preload-data="off" class="link">$3 $4</a>`;
const replacement = `<a href="${base}/watch/${videoId}?time=$2" data-sveltekit-preload-data="off" class="link">$3 $4</a>`;
const processedHtml = html.replace(regex, replacement);
@@ -63,7 +64,7 @@
<progress class="circle"></progress>
{/if}
<div class="comment-info">
<a href={`/channel/${comment.authorId}`} class="author">
<a href={`${base}/channel/${comment.authorId}`} class="author">
<span class="bold" class:channel-owner={comment.authorIsChannelOwner}>
{comment.author}
</span>
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { cleanNumber } from '$lib/numbers';
import { _ } from '$lib/i18n';
import type { HashTag } from '../api/model';
@@ -11,7 +12,7 @@
let { hashtag }: Props = $props();
</script>
<a href={hashtag.url} class="wave" style="min-width: 100%;min-height: 100%;">
<a href={base+hashtag.url} class="wave" style="min-width: 100%;min-height: 100%;">
<div class="padding">
<h5 class="center-align">{truncate(hashtag.title, 14)}</h5>
<h6 style="margin-top: 0;" class="center-align grey-text medium-text">
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { page } from '$app/stores';
import { getBestThumbnail } from '$lib/images';
import { padTime, videoLength } from '$lib/numbers';
@@ -355,7 +356,7 @@
if (data.video.captions) {
for (const caption of data.video.captions) {
await player.addTextTrackAsync(
caption.url.startsWith('http') ? caption.url : `${get(instanceStore)}${caption.url}`,
caption.url.startsWith('http') ? caption.url : `${new URL(get(instanceStore)).origin}${caption.url}`,
caption.language_code,
'captions',
undefined,
@@ -693,7 +694,7 @@
if (!data.playlistId) {
if ($playerAutoplayNextByDefaultStore) {
goto(
`/${$isAndroidTvStore ? 'tv' : 'watch'}/${data.video.recommendedVideos[0].videoId}`,
`${base}/${$isAndroidTvStore ? 'tv' : 'watch'}/${data.video.recommendedVideos[0].videoId}`,
{ replaceState: $isAndroidTvStore }
);
}
@@ -739,7 +740,7 @@
}
goto(
`/${$isAndroidTvStore ? 'tv' : 'watch'}/${goToVideo.videoId}?playlist=${data.playlistId}`,
`${base}/${$isAndroidTvStore ? 'tv' : 'watch'}/${goToVideo.videoId}?playlist=${data.playlistId}`,
{ replaceState: $isAndroidTvStore }
);
}
@@ -1,5 +1,5 @@
<script lang="ts">
import { getBestThumbnail } from '$lib/images';
import { base } from '$app/paths';
import { letterCase } from '$lib/letterCasing';
import { onMount } from 'svelte';
import { _ } from '$lib/i18n';
@@ -48,7 +48,7 @@
</script>
<a
href={playlistLink}
href={base+playlistLink}
class:link-disabled={disabled}
style="width: 100%; overflow: hidden;min-height:100px;"
class="wave"
@@ -77,11 +77,11 @@
<div class="small-padding">
<nav class="no-margin">
<div class="max">
<a class:link-disabled={disabled} href={playlistLink}
<a class:link-disabled={disabled} href={base+playlistLink}
><div class="bold">{letterCase(truncate(playlist.title))}</div></a
>
<div>
<a class:link-disabled={playlist.authorId === null} href={`/channel/${playlist.authorId}`}
<a class:link-disabled={playlist.authorId === null} href={`${base}/channel/${playlist.authorId}`}
>{playlist.author}</a
>
</div>
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import Mousetrap from 'mousetrap';
import { createEventDispatcher, onMount, tick } from 'svelte';
@@ -42,7 +43,7 @@
if (search.trim() === '') return;
selectedSuggestionIndex = -1;
goto(`/search/${encodeURIComponent(search)}`);
goto(`${base}/search/${encodeURIComponent(search)}`);
suggestionsForSearch = [];
showSearchBox = false;
@@ -154,7 +155,7 @@
handleSubmit();
}}
class:selected={index === selectedSuggestionIndex}
href={`/search/${encodeURIComponent(suggestion)}`}
href={`${base}/search/${encodeURIComponent(suggestion)}`}
>
<div>{suggestion}</div>
</a>
@@ -168,7 +169,7 @@
onclick={() => {
search = history;
}}
href={`/search/${encodeURIComponent(history)}`}
href={`${base}/search/${encodeURIComponent(history)}`}
>
<div>{history}</div>
</a>
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { bookmarkletSaveToUrl } from '$lib/externalSettings';
import { letterCase, titleCases } from '$lib/letterCasing';
import { setAmoledTheme } from '$lib/theme';
@@ -89,7 +90,7 @@
instanceStore.set(instance);
authStore.set(null);
goto('/', { replaceState: true });
goto(base+'/', { replaceState: true });
ui('#dialog-settings');
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { instanceStore } from '$lib/store';
import { Clipboard } from '@capacitor/clipboard';
import { Capacitor } from '@capacitor/core';
@@ -33,7 +34,7 @@
if (Capacitor.isNativePlatform()) {
url = `${get(instanceStore)}/watch/${video.videoId}`;
} else {
url = `${location.origin}/watch/${video.videoId}`;
url = `${location.origin}${base}/watch/${video.videoId}`;
}
if (includeTimestamp && currentTime !== undefined) {
url += `?time=${Math.floor(currentTime)}`;
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import { peerJs, removeWindowQueryFlag, setWindowQueryFlag } from '$lib/misc';
import type { PlayerEvents } from '$lib/player';
@@ -20,7 +21,7 @@
events.events.forEach((event) => {
if (event.type === 'change-video' && event.videoId) {
currentUrl.pathname = `/watch/${event.videoId}`;
currentUrl.pathname = `${base}/watch/${event.videoId}`;
goto(currentUrl);
} else if (event.type === 'goto' && event.path && event.path !== $page.url.pathname) {
if (blockedPages.includes(event.path.replace('/', ''))) {
@@ -72,7 +73,7 @@
}
page.subscribe((newPage) => {
if (!newPage.url.pathname.startsWith('/watch') && $syncPartyPeerStore) {
if (!newPage.url.pathname.startsWith(base+'/watch') && $syncPartyPeerStore) {
$syncPartyConnectionsStore?.forEach((conn) => {
conn.send({
events: [
@@ -124,14 +125,14 @@
<input
name="sync-share"
readonly
value={`${location.origin}?sync=${$syncPartyPeerStore.id}`}
value={`${location.origin}${base}/?sync=${$syncPartyPeerStore.id}`}
type="text"
/>
<label class="active" for="sync-share">Share URL</label>
</div>
<button
onclick={async () => {
await Clipboard.write({ string: `${location.origin}?sync=${$syncPartyPeerStore?.id}` });
await Clipboard.write({ string: `${location.origin}${base}/?sync=${$syncPartyPeerStore?.id}` });
}}
class="square round"
>
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { getBestThumbnail } from '$lib/images';
import { letterCase } from '$lib/letterCasing';
import { cleanNumber, videoLength } from '$lib/numbers';
@@ -214,7 +215,7 @@
</a>
<div>
<a tabindex="-1" class:author={!sideways} href={`/channel/${video.authorId}`}
<a tabindex="-1" class:author={!sideways} href={`${base}/channel/${video.authorId}`}
>{video.author}
</a>
@@ -48,7 +48,7 @@
isLoading = true;
transcript = null;
const resp = await fetch(`${!video.fallbackPatch ? get(instanceStore) : ''}${url}`);
const resp = await fetch(`${!video.fallbackPatch ? new URL(get(instanceStore)).origin : ''}${url}`);
transcript = await parseText(await resp.text(), { strict: false });
// Group cues by Math.ceil(startTime)
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { deleteUnsubscribe, postSubscribe } from '$lib/api';
import type { VideoPlay } from '$lib/api/model';
import { getBestThumbnail, proxyGoogleImage } from '$lib/images';
@@ -21,7 +22,7 @@
</script>
<nav>
<a href={`/channel/${video.authorId}`}>
<a href={`${base}/channel/${video.authorId}`}>
<nav style="gap: 0.5em;">
{#if !$interfaceLowBandwidthMode}
<img
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { numberWithCommas } from '$lib/numbers';
import { _ } from '$lib/i18n';
import type { VideoPlay } from '$lib/api/model';
@@ -34,7 +35,7 @@
<article class="border">
<nav class="scroll">
{#each video.keywords as keyword}
<a href={`/search/${encodeURIComponent(keyword)}`} class="chip">{keyword}</a>
<a href={`${base}/search/${encodeURIComponent(keyword)}`} class="chip">{keyword}</a>
{/each}
</nav>
</article>
+11 -2
View File
@@ -1,4 +1,5 @@
import { pushState } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import he from 'he';
import type Peer from 'peerjs';
@@ -25,7 +26,15 @@ export function decodeHtmlCharCodes(str: string): string {
export function proxyVideoUrl(source: string): string {
const rawSrc = new URL(source);
rawSrc.host = get(instanceStore).replace('http://', '').replace('https://', '');
const instURLStr = get(instanceStore);
const instURL = new URL(instURLStr);
rawSrc.host = instURL.host;
rawSrc.protocol = instURL.protocol;
let pathPrefix = instURL.pathname;
if (pathPrefix.endsWith("/")) {
pathPrefix = pathPrefix.substring(0, pathPrefix.length - 1);
}
rawSrc.pathname = pathPrefix + rawSrc.pathname;
return rawSrc.toString();
}
@@ -131,7 +140,7 @@ export function expandSummery(id: string) {
export function createVideoUrl(videoId: string, playlistId: string): URL {
const watchUrl = new URL(
`${location.origin}/${get(isAndroidTvStore) ? 'tv' : 'watch'}/${videoId}`
`${location.origin}${base}/${get(isAndroidTvStore) ? 'tv' : 'watch'}/${videoId}`
);
if (playlistId !== '') {
+8 -7
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { goto } from '$app/navigation';
import { navigating, page } from '$app/stores';
@@ -91,7 +92,7 @@
return;
}
goto(`/watch/${videoId}`);
goto(`${base}/watch/${videoId}`);
} else {
// Auth response handling for Mobile
const username = url.searchParams.get('username');
@@ -117,7 +118,7 @@
path.search = searchParams.toString();
await Browser.open({ url: path.toString() });
} else {
searchParams.set('callback_url', `${location.origin}/auth`);
searchParams.set('callback_url', `${location.origin}${base}/auth`);
path.search = searchParams.toString();
document.location.href = path.toString();
}
@@ -162,7 +163,7 @@
console.log(sid);
authStore.set({ username: rawUsername, token: sid });
await ui('#tv-login');
goto('/', { replaceState: true });
goto(base+'/', { replaceState: true });
return;
}
}
@@ -176,7 +177,7 @@
feedCacheStore.set({});
searchCacheStore.set({});
playlistCacheStore.set({});
goto('/');
goto(base+'/');
}
async function loadNotifications() {
@@ -266,7 +267,7 @@
<nav class="left m l surface-container" class:tv-nav={$isAndroidTvStore}>
<header
role="presentation"
onclick={() => goto($interfaceDefaultPage)}
onclick={() => goto(base+$interfaceDefaultPage)}
style="cursor: pointer;"
tabindex="-1"
class="small-padding"
@@ -275,7 +276,7 @@
</header>
{#each getPages() as navPage}
{#if !navPage.requiresAuth || isLoggedIn}
<a href={navPage.href} class:active={$page.url.href.endsWith(navPage.href)}
<a href={base+navPage.href} class:active={$page.url.href.endsWith(navPage.href)}
><i>{navPage.icon}</i>
<div>{navPage.name}</div>
</a>
@@ -354,7 +355,7 @@
{#if !navPage.requiresAuth || isLoggedIn}
<a
class="round"
href={navPage.href}
href={base+navPage.href}
class:active={$page.url.href.endsWith(navPage.href)}
data-sveltekit-preload-data="off"
><i>{navPage.icon}</i>
@@ -1,5 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/stores';
import PageLoading from '$lib/components/PageLoading.svelte';
import { authStore } from '$lib/store';
@@ -17,7 +18,7 @@
});
}
goto('/');
goto(base+'/');
});
</script>
@@ -1,10 +1,11 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { error } from '@sveltejs/kit';
export async function load({ url }) {
const playlistId = url.searchParams.get('list');
if (playlistId) {
goto(`/playlist/${playlistId}`);
goto(`${base}/playlist/${playlistId}`);
} else {
error(404);
}
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { unsafeRandomItem } from '$lib/misc';
import { cleanNumber } from '$lib/numbers';
import { isAndroidTvStore, playlistSettingsStore } from '$lib/store';
@@ -15,8 +16,8 @@
<nav>
<a
href={!$isAndroidTvStore
? `/watch/${data.playlist.videos[0].videoId}?playlist=${data.playlist.info.playlistId}`
: `/tv/${data.playlist.videos[0].videoId}?playlist=${data.playlist.info.playlistId}`}
? `${base}/watch/${data.playlist.videos[0].videoId}?playlist=${data.playlist.info.playlistId}`
: `${base}/tv/${data.playlist.videos[0].videoId}?playlist=${data.playlist.info.playlistId}`}
class="button circle extra no-margin"
>
<i>play_arrow</i>
@@ -27,8 +28,8 @@
<a
href={!$isAndroidTvStore
? `/watch/${unsafeRandomItem(data.playlist.videos).videoId}?playlist=${data.playlist.info.playlistId}`
: `/tv/${unsafeRandomItem(data.playlist.videos).videoId}?playlist=${data.playlist.info.playlistId}`}
? `${base}/watch/${unsafeRandomItem(data.playlist.videos).videoId}?playlist=${data.playlist.info.playlistId}`
: `${base}/tv/${unsafeRandomItem(data.playlist.videos).videoId}?playlist=${data.playlist.info.playlistId}`}
onclick={() =>
playlistSettingsStore.set({
[data.playlist.info.playlistId]: { shuffle: true, loop: false }
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import { deleteUnsubscribe } from '$lib/api';
import Fuse from 'fuse.js';
import { _ } from '$lib/i18n';
@@ -45,7 +46,7 @@
{#each subscriptions as sub}
<article>
<nav>
<a href={`/channel/${sub.authorId}`}
<a href={`${base}/channel/${sub.authorId}`}
><h6>
{sub.author}
</h6></a
+2 -1
View File
@@ -1,4 +1,5 @@
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { error } from '@sveltejs/kit';
export async function load({ url }) {
@@ -7,7 +8,7 @@ export async function load({ url }) {
const timestamp = url.searchParams.get('t');
if (videoId) {
let goToUrl = new URL(`${location.origin}/watch/${videoId}`);
let goToUrl = new URL(`${location.origin}${base}/watch/${videoId}`);
if (playlistId) {
goToUrl.searchParams.set('playlist', playlistId);
@@ -1,4 +1,5 @@
<script lang="ts">
import { base } from '$app/paths';
import {
addPlaylistVideo,
getComments,
@@ -501,7 +502,7 @@
{$_('videos')}
</p>
<p>
<a href={`/channel/${$playlistCacheStore[data.playlistId].info.authorId}`}
<a href={`${base}/channel/${$playlistCacheStore[data.playlistId].info.authorId}`}
>{$playlistCacheStore[data.playlistId].info.author}</a
>
</p>
+6 -5
View File
@@ -1,5 +1,6 @@
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import androidTv from '$lib/android/plugins/androidTv';
import { getResolveUrl } from '$lib/api';
import '$lib/i18n'; // Import to initialize. Important :)
@@ -17,13 +18,13 @@ export async function load({ url }) {
isAndroidTvStore.set((await androidTv.isAndroidTv()).value);
if (url.pathname.startsWith('/@')) {
const username = url.pathname.split('/')[1];
if (url.pathname.startsWith(base+'/@')) {
const username = url.pathname.substring(base.length).split('/')[1];
try {
const resolvedUrl = await getResolveUrl(`www.youtube.com/${username}`);
if (resolvedUrl.pageType === 'WEB_PAGE_TYPE_CHANNEL') {
goto(`/channel/${resolvedUrl.ucid}`);
goto(`${base}/channel/${resolvedUrl.ucid}`);
}
} catch {}
}
@@ -34,12 +35,12 @@ export async function load({ url }) {
defaultPage &&
defaultPage !== '/' &&
defaultPage.startsWith('/') &&
url.pathname === '/' &&
url.pathname === base+'/' &&
window.history.length < 3
) {
getPages().forEach((page) => {
if (page.href === defaultPage && (!page.requiresAuth || get(authStore))) {
goto(defaultPage);
goto(base+defaultPage);
}
});
}
+3
View File
@@ -7,6 +7,9 @@ const config = {
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// Optional: Set base path for Materialious here and in vite.config.ts
//paths: {base: '/materialious'},
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
+2
View File
@@ -4,6 +4,8 @@ import { defineConfig } from 'vite';
import pkg from './package.json';
export default defineConfig({
// Optional: Set base path for Materialious here and in svelte.config.js
//base: '/materialious',
plugins: [
SvelteKitPWA({
injectRegister: 'inline',