mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-08-31 15:43:25 +00:00
rework page metadata and links
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
# create-svelte
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
@@ -5,15 +5,6 @@
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
|
||||
<meta property="og:image" content="https://gpx.studio/og_logo.png" />
|
||||
<meta property="og:url" content="https://gpx.studio/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="gpx.studio" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content="https://gpx.studio/og_logo.png" />
|
||||
<meta name="twitter:url" content="https://gpx.studio/" />
|
||||
<meta name="twitter:site" content="@gpxstudio" />
|
||||
<meta name="twitter:creator" content="@gpxstudio" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
|
@@ -3,7 +3,8 @@
|
||||
import LanguageSelect from '$lib/components/LanguageSelect.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import { AtSign, BookOpenText, Heart, Home, Map } from 'lucide-svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { getURLForLanguage } from '$lib/languages';
|
||||
</script>
|
||||
|
||||
<footer class="w-full">
|
||||
@@ -24,15 +25,27 @@
|
||||
<div class="grow max-w-2xl flex flex-row flex-wrap justify-between gap-x-10 gap-y-6">
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span class="font-semibold">{$_('homepage.website')}</span>
|
||||
<Button variant="link" class="h-6 px-0 text-muted-foreground" href="./about">
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 text-muted-foreground"
|
||||
href={getURLForLanguage('/[...language]', $locale)}
|
||||
>
|
||||
<Home size="16" class="mr-1" />
|
||||
{$_('homepage.home')}
|
||||
</Button>
|
||||
<Button variant="link" class="h-6 px-0 text-muted-foreground" href="./">
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 text-muted-foreground"
|
||||
href={getURLForLanguage('/[...language]/app', $locale)}
|
||||
>
|
||||
<Map size="16" class="mr-1" />
|
||||
{$_('homepage.app')}
|
||||
</Button>
|
||||
<Button variant="link" class="h-6 px-0 text-muted-foreground" href="./documentation">
|
||||
<Button
|
||||
variant="link"
|
||||
class="h-6 px-0 text-muted-foreground"
|
||||
href={getURLForLanguage('/[...language]/documentation', $locale)}
|
||||
>
|
||||
<BookOpenText size="16" class="mr-1" />
|
||||
{$_('homepage.documentation')}
|
||||
</Button>
|
||||
|
60
website/src/lib/components/Head.svelte
Normal file
60
website/src/lib/components/Head.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { page } from '$app/stores';
|
||||
import { languages } from '$lib/languages';
|
||||
import { _, isLoading } from 'svelte-i18n';
|
||||
|
||||
$: location = $page.route.id?.split('/').slice(1).at(1) ?? 'home';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if $isLoading}
|
||||
<title>gpx.studio — the online GPX file editor</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
||||
/>
|
||||
<meta property="og:title" content="gpx.studio — the online GPX file editor" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
||||
/>
|
||||
<meta name="twitter:title" content="gpx.studio — the online GPX file editor" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
||||
/>
|
||||
{:else}
|
||||
<title>gpx.studio — {$_(`metadata.${location}_title`)}</title>
|
||||
<meta name="description" content={$_('metadata.description')} />
|
||||
<meta property="og:title" content="gpx.studio — {$_(`metadata.${location}_title`)}" />
|
||||
<meta property="og:description" content={$_('metadata.description')} />
|
||||
<meta name="twitter:title" content="gpx.studio — {$_(`metadata.${location}_title`)}" />
|
||||
<meta name="twitter:description" content={$_('metadata.description')} />
|
||||
{/if}
|
||||
|
||||
<meta property="og:image" content="https://gpx.studio/og_logo.png" />
|
||||
<meta property="og:url" content="https://gpx.studio/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="gpx.studio" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content="https://gpx.studio/og_logo.png" />
|
||||
<meta name="twitter:url" content="https://gpx.studio/" />
|
||||
<meta name="twitter:site" content="@gpxstudio" />
|
||||
<meta name="twitter:creator" content="@gpxstudio" />
|
||||
|
||||
<link
|
||||
rel="alternate"
|
||||
hreflang="x-default"
|
||||
href="https://gpx.studio{base}/{location === 'home' ? '' : location}"
|
||||
/>
|
||||
{#each Object.keys(languages) as lang}
|
||||
<link
|
||||
rel="alternate"
|
||||
hreflang={lang}
|
||||
href="https://gpx.studio{base}/{lang === 'en' ? '' : lang + '/'}{location === 'home'
|
||||
? ''
|
||||
: location}"
|
||||
/>
|
||||
{/each}
|
||||
</svelte:head>
|
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { getURLForLanguage, languages } from '$lib/languages';
|
||||
import { Languages } from 'lucide-svelte';
|
||||
@@ -18,14 +18,25 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Select.Root bind:selected onSelectedChange={(s) => goto(getURLForLanguage(s?.value))}>
|
||||
<Select.Root bind:selected>
|
||||
<Select.Trigger class="w-[180px] {$$props.class ?? ''}">
|
||||
<Languages size="16" />
|
||||
<Select.Value class="ml-2 mr-auto" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each Object.entries(languages) as [key, value]}
|
||||
<Select.Item value={key}>{value}</Select.Item>
|
||||
{#each Object.entries(languages) as [lang, label]}
|
||||
<a href={getURLForLanguage($page.route.id, lang)}>
|
||||
<Select.Item value={lang}>{label}</Select.Item>
|
||||
</a>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
|
||||
<!-- hidden links for svelte crawling -->
|
||||
<div class="hidden">
|
||||
{#each Object.entries(languages) as [lang, label]}
|
||||
<a href={getURLForLanguage($page.route.id, lang)}>
|
||||
{label}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
@@ -73,6 +73,7 @@
|
||||
import { mode, setMode, systemPrefersMode } from 'mode-watcher';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { getURLForLanguage, languages } from '$lib/languages';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const {
|
||||
distanceUnits,
|
||||
@@ -123,7 +124,7 @@
|
||||
<div
|
||||
class="w-fit flex flex-row items-center justify-center p-1 bg-background rounded-b-md md:rounded-md pointer-events-auto shadow-md"
|
||||
>
|
||||
<a href="./" target="_blank">
|
||||
<a href={getURLForLanguage('/[...language]', $locale)} target="_blank">
|
||||
<Logo class="h-5 mt-0.5 mx-2 md:hidden" iconOnly={true} />
|
||||
<Logo class="h-5 mt-0.5 mx-2 hidden md:block" />
|
||||
</a>
|
||||
@@ -363,9 +364,9 @@
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$locale}>
|
||||
{#each Object.entries(languages) as [code, name]}
|
||||
<a href={getURLForLanguage(code)}>
|
||||
<Menubar.RadioItem value={code}>{name}</Menubar.RadioItem>
|
||||
{#each Object.entries(languages) as [lang, label]}
|
||||
<a href={getURLForLanguage($page.route.id, lang)}>
|
||||
<Menubar.RadioItem value={lang}>{label}</Menubar.RadioItem>
|
||||
</a>
|
||||
{/each}
|
||||
</Menubar.RadioGroup>
|
||||
@@ -415,7 +416,7 @@
|
||||
<div class="h-fit flex flex-row items-center ml-1 gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
href="./documentation"
|
||||
href={getURLForLanguage('/[...language]/documentation', $locale)}
|
||||
target="_blank"
|
||||
class="cursor-default h-fit rounded-sm px-3 py-0.5"
|
||||
>
|
||||
|
@@ -3,24 +3,37 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import ModeSwitch from '$lib/components/ModeSwitch.svelte';
|
||||
import { BookOpenText, Home, Map } from 'lucide-svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { getURLForLanguage } from '$lib/languages';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<nav class="w-full sticky top-0 bg-background z-10">
|
||||
<div class="mx-6 py-2 flex flex-row items-center border-b gap-4 md:gap-8">
|
||||
<a href="./" class="shrink-0">
|
||||
<a href={getURLForLanguage('/[...language]', $locale)} class="shrink-0">
|
||||
<Logo class="h-8 sm:hidden" iconOnly={true} />
|
||||
<Logo class="h-7 hidden sm:block" />
|
||||
</a>
|
||||
<Button variant="link" class="text-base px-0" href="./">
|
||||
<Button
|
||||
variant="link"
|
||||
class="text-base px-0"
|
||||
href={getURLForLanguage('/[...language]', $locale)}
|
||||
>
|
||||
<Home size="18" class="mr-1.5" />
|
||||
{$_('homepage.home')}
|
||||
</Button>
|
||||
<Button variant="link" class="text-base px-0" href="./app">
|
||||
<Button
|
||||
variant="link"
|
||||
class="text-base px-0"
|
||||
href={getURLForLanguage('/[...language]/app', $locale)}
|
||||
>
|
||||
<Map size="18" class="mr-1.5" />
|
||||
{$_('homepage.app')}
|
||||
</Button>
|
||||
<Button variant="link" class="text-base px-0" href="./documentation">
|
||||
<Button
|
||||
variant="link"
|
||||
class="text-base px-0"
|
||||
href={getURLForLanguage('/[...language]/documentation', $locale)}
|
||||
>
|
||||
<BookOpenText size="18" class="mr-1.5" />
|
||||
{$_('homepage.documentation')}
|
||||
</Button>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { dbUtils, getFile } from "$lib/db";
|
||||
import { castDraft, freeze } from "immer";
|
||||
import { freeze } from "immer";
|
||||
import { GPXFile, Track, TrackSegment, Waypoint } from "gpx";
|
||||
import { selection } from "./Selection";
|
||||
import { newGPXFile } from "$lib/stores";
|
||||
|
@@ -1,18 +1,18 @@
|
||||
export const languages: Record<string, string> = {
|
||||
'en': 'English',
|
||||
'fr': 'Français',
|
||||
};
|
||||
|
||||
export function getURLForLanguage(lang?: string): string {
|
||||
let currentPath = window.location.pathname;
|
||||
let currentPathArray = currentPath.split('/');
|
||||
|
||||
if (currentPathArray.length > 1 && languages.hasOwnProperty(currentPathArray[1])) {
|
||||
currentPathArray.splice(1, 1);
|
||||
export function getURLForLanguage(route: string | null, lang: string | null | undefined): string {
|
||||
if (route === null) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (lang !== undefined && lang !== 'en') {
|
||||
currentPathArray.splice(1, 0, lang);
|
||||
}
|
||||
let url = route.replace('[...language]', (lang === null || lang === undefined) ? 'en' : lang).replace('/en', '');
|
||||
|
||||
return currentPathArray.join('/');
|
||||
if (url === '') {
|
||||
return '/';
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
@@ -1,31 +1,9 @@
|
||||
import fs from 'fs';
|
||||
import { languages } from "./languages";
|
||||
|
||||
function getURL(lang: string, path: string = '/') {
|
||||
return 'https://gpx.studio' + (lang === 'en' ? '' : ('/' + lang)) + path;
|
||||
}
|
||||
|
||||
function generateSitemap() {
|
||||
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">` +
|
||||
console.log('Generating sitemap...');
|
||||
|
||||
Object.keys(languages).map((lang) => `
|
||||
<url>
|
||||
<loc>${getURL(lang)}</loc>${Object.keys(languages).map((lang2) => `
|
||||
<xhtml:link rel="alternate" hreflang="${lang2}" href="${getURL(lang2)}"/>`).join('')}
|
||||
</url>`).join('') +
|
||||
|
||||
Object.keys(languages).map((lang) => `
|
||||
<url>
|
||||
<loc>${getURL(lang, '/about')}</loc>${Object.keys(languages).map((lang2) => `
|
||||
<xhtml:link rel="alternate" hreflang="${lang2}" href="${getURL(lang2, '/about')}"/>`).join('')}
|
||||
</url>`).join('') +
|
||||
|
||||
`
|
||||
</urlset>
|
||||
`;
|
||||
|
||||
return sitemap;
|
||||
return '';
|
||||
}
|
||||
|
||||
fs.writeFileSync('build/sitemap.xml', generateSitemap());
|
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"metadata": {
|
||||
"app_title": "the online GPX file editor",
|
||||
"home_title": "home",
|
||||
"app_title": "the online GPX file editor",
|
||||
"documentation_title": "documentation",
|
||||
"description": "View, edit and create GPX files online with advanced route planning capabilities and file processing tools, beautiful maps and detailed data visualizations."
|
||||
},
|
||||
"menu": {
|
||||
|
@@ -2,18 +2,20 @@
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
import { isLoading, locale, _ } from 'svelte-i18n';
|
||||
import { page } from '$app/stores';
|
||||
import Head from '$lib/components/Head.svelte';
|
||||
import Nav from '$lib/components/Nav.svelte';
|
||||
import Footer from '$lib/components/Footer.svelte';
|
||||
|
||||
$: if ($page.params.language === '' && $locale !== 'en') {
|
||||
locale.set('en');
|
||||
} else if ($page.params.language && $locale !== $page.params.language) {
|
||||
locale.set($page.params.language);
|
||||
locale.set($page.params.language.replace('/', ''));
|
||||
}
|
||||
|
||||
const appRoute = '/[...language]/app';
|
||||
</script>
|
||||
|
||||
<Head />
|
||||
<ModeWatcher />
|
||||
|
||||
{#if !$isLoading}
|
||||
|
@@ -1,15 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { base } from '$app/paths';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import DocsLoader from '$lib/components/docs/DocsLoader.svelte';
|
||||
import Logo from '$lib/components/Logo.svelte';
|
||||
import ElevationProfile from '$lib/components/ElevationProfile.svelte';
|
||||
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
|
||||
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
|
||||
import { languages } from '$lib/languages';
|
||||
import { settings } from '$lib/db';
|
||||
import { BookOpenText, Heart, Map } from 'lucide-svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { exampleGPXFile } from '$lib/assets/example';
|
||||
import { writable } from 'svelte/store';
|
||||
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
||||
@@ -22,6 +20,7 @@
|
||||
import cyclosmMap from '$lib/assets/img/cyclosm.png?enhanced';
|
||||
import waymarkedMap from '$lib/assets/img/waymarked.png?enhanced';
|
||||
import mapScreenshot from '$lib/assets/img/map.png?enhanced';
|
||||
import { getURLForLanguage } from '$lib/languages';
|
||||
|
||||
let gpxStatistics = writable(exampleGPXFile.getStatistics());
|
||||
let slicedGPXStatistics = writable(undefined);
|
||||
@@ -39,24 +38,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>gpx.studio — {$_('metadata.home_title')}</title>
|
||||
<meta name="description" content={$_('metadata.description')} />
|
||||
<meta property="og:title" content="gpx.studio — {$_('metadata.home_title')}" />
|
||||
<meta property="og:description" content={$_('metadata.description')} />
|
||||
<meta name="twitter:title" content="gpx.studio — {$_('metadata.home_title')}" />
|
||||
<meta name="twitter:description" content={$_('metadata.description')} />
|
||||
|
||||
<link rel="alternate" hreflang="x-default" href="{base}/" />
|
||||
{#each Object.keys(languages) as lang}
|
||||
{#if lang === 'en'}
|
||||
<link rel="alternate" hreflang="en" href="{base}/" />
|
||||
{:else}
|
||||
<link rel="alternate" hreflang={lang} href="{base}/{lang}/" />
|
||||
{/if}
|
||||
{/each}
|
||||
</svelte:head>
|
||||
|
||||
<div class="space-y-24 my-24">
|
||||
<div class="px-12 w-full flex flex-col items-center">
|
||||
<div class="flex flex-col gap-6 items-center max-w-3xl">
|
||||
@@ -65,11 +46,15 @@
|
||||
{$_('metadata.description')}
|
||||
</div>
|
||||
<div class="w-full flex flex-row justify-center gap-3">
|
||||
<Button href="./app" class="w-1/3 min-w-fit">
|
||||
<Button href={getURLForLanguage('/[...language]/app', $locale)} class="w-1/3 min-w-fit">
|
||||
<Map size="18" class="mr-1.5" />
|
||||
{$_('homepage.app')}
|
||||
</Button>
|
||||
<Button variant="secondary" href="./documentation" class="w-1/3 min-w-fit">
|
||||
<Button
|
||||
variant="secondary"
|
||||
href={getURLForLanguage('/[...language]/documentation', $locale)}
|
||||
class="w-1/3 min-w-fit"
|
||||
>
|
||||
<BookOpenText size="18" class="mr-1.5" />
|
||||
<span>{$_('homepage.documentation')}</span>
|
||||
</Button>
|
||||
@@ -190,7 +175,7 @@
|
||||
</div>
|
||||
<div class="px-12 flex flex-col items-center">
|
||||
<div class="max-w-5xl flex flex-col items-center gap-6">
|
||||
<DocsLoader path="about/funding.md" />
|
||||
<DocsLoader path="home/funding.md" />
|
||||
<Button
|
||||
href="https://ko-fi.com/gpxstudio"
|
||||
target="_blank"
|
||||
@@ -203,7 +188,7 @@
|
||||
</div>
|
||||
<div class="px-12 flex flex-col items-center">
|
||||
<div class="max-w-5xl">
|
||||
<DocsLoader path="about/translation.md" />
|
||||
<DocsLoader path="home/translation.md" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 md:px-24 flex flex-col items-center">
|
||||
@@ -218,7 +203,7 @@
|
||||
<Logo company="mapbox" class="w-60" />
|
||||
</a>
|
||||
</div>
|
||||
<DocsLoader path="about/mapbox.md" />
|
||||
<DocsLoader path="home/mapbox.md" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,26 +1,5 @@
|
||||
<script lang="ts">
|
||||
import App from '$lib/components/App.svelte';
|
||||
import { base } from '$app/paths';
|
||||
import { languages } from '$lib/languages';
|
||||
import { _ } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>gpx.studio — {$_('metadata.app_title')}</title>
|
||||
<meta name="description" content={$_('metadata.description')} />
|
||||
<meta property="og:title" content="gpx.studio — {$_('metadata.app_title')}" />
|
||||
<meta property="og:description" content={$_('metadata.description')} />
|
||||
<meta name="twitter:title" content="gpx.studio — {$_('metadata.app_title')}" />
|
||||
<meta name="twitter:description" content={$_('metadata.description')} />
|
||||
|
||||
<link rel="alternate" hreflang="x-default" href="{base}/" />
|
||||
{#each Object.keys(languages) as lang}
|
||||
{#if lang === 'en'}
|
||||
<link rel="alternate" hreflang="en" href="{base}/" />
|
||||
{:else}
|
||||
<link rel="alternate" hreflang={lang} href="{base}/{lang}/" />
|
||||
{/if}
|
||||
{/each}
|
||||
</svelte:head>
|
||||
|
||||
<App />
|
||||
|
Reference in New Issue
Block a user