Extend self-hosting variables

This commit is contained in:
Lim Chee Aun 2023-12-25 19:25:48 +08:00
parent 563a7bf03b
commit f520e30858
13 changed files with 179 additions and 50 deletions

7
.env
View file

@ -1,3 +1,4 @@
VITE_CLIENT_NAME=Phanpy PHANPY_CLIENT_NAME=Phanpy
VITE_CLIENT_ID=social.phanpy PHANPY_WEBSITE=https://phanpy.social
VITE_WEBSITE=https://phanpy.social PHANPY_LINGVA_INSTANCES="lingva.phanpy.social lingva.lunar.icu lingva.garudalinux.org translate.plausibility.cloud"
PHANPY_PRIVACY_POLICY_URL="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"

View file

@ -126,10 +126,66 @@ This is a **pure static web app**. You can host it anywhere you want.
Two ways (choose one): Two ways (choose one):
1. (Recommended) Go to [Releases](https://github.com/cheeaun/phanpy/releases) and download the latest `phanpy-dist.zip`. It's pre-built so don't need to run any install/build commands. Extract it. Serve the folder of extracted files. ### Easy way
2. Download or `git clone` this repository. Build it by running `npm run build` (after `npm install`). Serve the `dist` folder.
Try search for "how to self-host static sites" as there are many ways to do it. Go to [Releases](https://github.com/cheeaun/phanpy/releases) and download the latest `phanpy-dist.zip` or `phanpy-dist.tar.gz`. It's pre-built so don't need to run any install/build commands. Extract it. Serve the folder of extracted files.
### Custom-build way
Download or `git clone` this repository. Build it by running `npm run build` (after `npm install`). Serve the `dist` folder.
Customization can be done by passing environment variables to the build command. Examples:
```bash
PHANPY_APP_TITLE="Phanpy Dev" \
PHANPY_WEBSITE="https://dev.phanpy.social" \
npm run build
```
```bash
PHANPY_DEFAULT_INSTANCE=hachyderm.io \
PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL=https://hachyderm.io/auth/sign_up \
PHANPY_PRIVACY_POLICY_URL=https://hachyderm.io/privacy-policy \
npm run build
```
It's also possible to set them in the `.env` file.
Available variables:
- `PHANPY_APP_TITLE` (optional, default: `Phanpy`) affects:
- Web page title, shown in the browser window or tab title
- App title, when installed as PWA, shown in the Home screen, macOS dock, Windows taskbar, etc
- OpenGraph card title, when shared on social networks
- Client name, when [registering the app for authentication](https://docs.joinmastodon.org/client/token/#app) and shown as client used on posts in some apps/clients
- `PHANPY_WEBSITE` (optional but recommended, default: `https://phanpy.social`) affects:
- Canonical URL of the website
- OpenGraph card URL, when shared on social networks
- Root path for the OpenGraph card image
- Client URL, when [registering the app for authentication](https://docs.joinmastodon.org/client/token/#app) and shown as client used on posts in some apps/clients
- `PHANPY_DEFAULT_INSTANCE` (optional, no defaults):
- e.g. 'mastodon.social', without `https://`
- Default instance for log-in
- When logging in, the user will be redirected instantly to the instance's authentication page instead of having to manually type the instance URL and submit
- `PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL` (optional, no defaults):
- URL of the instance registration page
- E.g. `https://mastodon.social/auth/sign_up`
- `PHANPY_PRIVACY_POLICY_URL` (optional, default to official instance's privacy policy):
- URL of the privacy policy page
- May specify the instance's own privacy policy
- `PHANPY_LINGVA_INSTANCES` (optional, space-separated list, default: `lingva.phanpy.social [...hard-coded list of fallback instances]`):
- Specify a space-separated list of instances. First will be used as default before falling back to the subsequent instances. If there's only 1 instance, means no fallback.
- May specify a self-hosted Lingva instance, powered by either [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api)
- List of fallback instances hard-coded in `/.env`
- [↗️ List of lingva-translate instances](https://github.com/thedaviddelta/lingva-translate?tab=readme-ov-file#instances)
### Static site hosting
Try online search for "how to self-host static sites" as there are many ways to do it.
#### Lingva-translate or lingva-api hosting
See documentation for [lingva-translate](https://github.com/thedaviddelta/lingva-translate) or [lingva-api](https://github.com/cheeaun/lingva-api).
## Community deployments ## Community deployments

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Compose / %VITE_CLIENT_NAME%</title> <title>Compose / %PHANPY_CLIENT_NAME%</title>
<meta name="color-scheme" content="dark light" /> <meta name="color-scheme" content="dark light" />
<meta name="google" content="notranslate" /> <meta name="google" content="notranslate" />
</head> </head>

View file

@ -6,7 +6,7 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover" content="width=device-width, initial-scale=1, viewport-fit=cover"
/> />
<title>%VITE_CLIENT_NAME%</title> <title>%PHANPY_CLIENT_NAME%</title>
<meta <meta
name="description" name="description"
content="Minimalistic opinionated Mastodon web client" content="Minimalistic opinionated Mastodon web client"
@ -14,10 +14,10 @@
<meta name="color-scheme" content="dark light" /> <meta name="color-scheme" content="dark light" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="%VITE_CLIENT_NAME%" /> <meta name="apple-mobile-web-app-title" content="%PHANPY_CLIENT_NAME%" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<link rel="canonical" href="%VITE_WEBSITE%" /> <link rel="canonical" href="%PHANPY_WEBSITE%" />
<meta <meta
name="" name=""
data-theme-setting="manual" data-theme-setting="manual"
@ -46,13 +46,13 @@
<!-- Metacrap https://broken-links.com/2015/12/01/little-less-metacrap/ --> <!-- Metacrap https://broken-links.com/2015/12/01/little-less-metacrap/ -->
<meta property="twitter:card" content="summary_large_image" /> <meta property="twitter:card" content="summary_large_image" />
<meta property="og:url" content="%VITE_WEBSITE%" /> <meta property="og:url" content="%PHANPY_WEBSITE%" />
<meta property="og:title" content="%VITE_CLIENT_NAME%" /> <meta property="og:title" content="%PHANPY_CLIENT_NAME%" />
<meta <meta
property="og:description" property="og:description"
content="Minimalistic opinionated Mastodon web client" content="Minimalistic opinionated Mastodon web client"
/> />
<meta property="og:image" content="%VITE_WEBSITE%/og-image-2.jpg" /> <meta property="og:image" content="%PHANPY_WEBSITE%/og-image-2.jpg" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View file

@ -1,7 +1,6 @@
// Fetch https://lingva.ml/api/v1/languages/{source|target}
import fs from 'fs'; import fs from 'fs';
fetch('https://lingva.ml/api/v1/languages/source') fetch('https://lingva.phanpy.social/api/v1/languages/source')
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
const file = './src/data/lingva-source-languages.json'; const file = './src/data/lingva-source-languages.json';
@ -9,7 +8,7 @@ fetch('https://lingva.ml/api/v1/languages/source')
fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8'); fs.writeFileSync(file, JSON.stringify(json.languages, null, '\t'), 'utf8');
}); });
fetch('https://lingva.ml/api/v1/languages/target') fetch('https://lingva.phanpy.social/api/v1/languages/target')
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
const file = './src/data/lingva-target-languages.json'; const file = './src/data/lingva-target-languages.json';

View file

@ -12,19 +12,16 @@ import pmem from '../utils/pmem';
import Icon from './icon'; import Icon from './icon';
import Loader from './loader'; import Loader from './loader';
const { PHANPY_LINGVA_INSTANCES } = import.meta.env;
const LINGVA_INSTANCES = PHANPY_LINGVA_INSTANCES
? PHANPY_LINGVA_INSTANCES.split(/\s+/)
: [];
const throttle = pThrottle({ const throttle = pThrottle({
limit: 1, limit: 1,
interval: 2000, interval: 2000,
}); });
// Using other API instances instead of lingva.ml because of this bug (slashes don't work):
// https://github.com/thedaviddelta/lingva-translate/issues/68
const LINGVA_INSTANCES = [
'lingva.phanpy.social',
'lingva.lunar.icu',
'lingva.garudalinux.org',
'translate.plausibility.cloud',
];
let currentLingvaInstance = 0; let currentLingvaInstance = 0;
function _lingvaTranslate(text, source, target) { function _lingvaTranslate(text, source, target) {
@ -243,4 +240,4 @@ function TranslationBlock({
); );
} }
export default TranslationBlock; export default LINGVA_INSTANCES?.length ? TranslationBlock : () => null;

View file

@ -12,6 +12,8 @@ import { getAuthorizationURL, registerApplication } from '../utils/auth';
import store from '../utils/store'; import store from '../utils/store';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const { PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE } = import.meta.env;
function Login() { function Login() {
useTitle('Log in'); useTitle('Log in');
const instanceURLRef = useRef(); const instanceURLRef = useRef();
@ -19,6 +21,7 @@ function Login() {
const [uiState, setUIState] = useState('default'); const [uiState, setUIState] = useState('default');
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const instance = searchParams.get('instance'); const instance = searchParams.get('instance');
const submit = searchParams.get('submit');
const [instanceText, setInstanceText] = useState( const [instanceText, setInstanceText] = useState(
instance || cachedInstanceURL?.toLowerCase() || '', instance || cachedInstanceURL?.toLowerCase() || '',
); );
@ -129,6 +132,12 @@ function Login() {
submitInstance(selectedInstanceText); submitInstance(selectedInstanceText);
}; };
if (submit) {
useEffect(() => {
submitInstance(instance || selectedInstanceText);
}, []);
}
return ( return (
<main id="login" style={{ textAlign: 'center' }}> <main id="login" style={{ textAlign: 'center' }}>
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
@ -200,11 +209,13 @@ function Login() {
</div> </div>
<Loader hidden={uiState !== 'loading'} /> <Loader hidden={uiState !== 'loading'} />
<hr /> <hr />
<p> {!DEFAULT_INSTANCE && (
<a href="https://joinmastodon.org/servers" target="_blank"> <p>
Don't have an account? Create one! <a href="https://joinmastodon.org/servers" target="_blank">
</a> Don't have an account? Create one!
</p> </a>
</p>
)}
<p> <p>
<Link to="/">Go home</Link> <Link to="/">Go home</Link>
</p> </p>

View file

@ -24,6 +24,10 @@ import store from '../utils/store';
const DEFAULT_TEXT_SIZE = 16; const DEFAULT_TEXT_SIZE = 16;
const TEXT_SIZES = [15, 16, 17, 18, 19, 20]; const TEXT_SIZES = [15, 16, 17, 18, 19, 20];
const {
PHANPY_WEBSITE: WEBSITE,
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
} = import.meta.env;
function Settings({ onClose }) { function Settings({ onClose }) {
const snapStates = useSnapshot(states); const snapStates = useSnapshot(states);
@ -535,7 +539,7 @@ function Settings({ onClose }) {
</a>{' '} </a>{' '}
&middot;{' '} &middot;{' '}
<a <a
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD" href={PRIVACY_POLICY_URL}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -544,7 +548,14 @@ function Settings({ onClose }) {
</p> </p>
{__BUILD_TIME__ && ( {__BUILD_TIME__ && (
<p> <p>
Version:{' '} {WEBSITE && (
<>
<span class="insignificant">Site:</span>{' '}
{WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '')}
<br />
</>
)}
<span class="insignificant">Version:</span>{' '}
<input <input
type="text" type="text"
class="version-string" class="version-string"

View file

@ -75,6 +75,18 @@
margin-top: 0; margin-top: 0;
} }
.app-site-version {
text-align: center;
opacity: 0.5;
color: var(--text-insignificant-color);
font-family: var(--monospace-font), monospace;
small {
font-size: 11px;
letter-spacing: -0.2px;
}
}
#why-container { #why-container {
padding: 0 16px; padding: 0 16px;
} }

View file

@ -12,6 +12,21 @@ import Link from '../components/link';
import states from '../utils/states'; import states from '../utils/states';
import useTitle from '../utils/useTitle'; import useTitle from '../utils/useTitle';
const {
PHANPY_DEFAULT_INSTANCE: DEFAULT_INSTANCE,
PHANPY_WEBSITE: WEBSITE,
PHANPY_PRIVACY_POLICY_URL: PRIVACY_POLICY_URL,
PHANPY_DEFAULT_INSTANCE_REGISTRATION_URL: DEFAULT_INSTANCE_REGISTRATION_URL,
} = import.meta.env;
const appSite = WEBSITE
? WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '')
: null;
const appVersion = __BUILD_TIME__
? `${__BUILD_TIME__.slice(0, 10).replace(/-/g, '.')}${
__COMMIT_HASH__ ? `.${__COMMIT_HASH__}` : ''
}`
: null;
function Welcome() { function Welcome() {
useTitle(null, ['/', '/welcome']); useTitle(null, ['/', '/welcome']);
return ( return (
@ -33,18 +48,41 @@ function Welcome() {
</h1> </h1>
<p class="desc">A minimalistic opinionated Mastodon web client.</p> <p class="desc">A minimalistic opinionated Mastodon web client.</p>
<p> <p>
<Link to="/login" class="button"> <Link
Log in with Mastodon to={
DEFAULT_INSTANCE
? `/login?instance=${DEFAULT_INSTANCE}&submit=1`
: '/login'
}
class="button"
>
{DEFAULT_INSTANCE ? 'Log in' : 'Log in with Mastodon'}
</Link> </Link>
</p> </p>
<p class="insignificant"> {DEFAULT_INSTANCE && DEFAULT_INSTANCE_REGISTRATION_URL && (
<p>
<a href={DEFAULT_INSTANCE_REGISTRATION_URL} class="button plain5">
Sign up
</a>
</p>
)}
{!DEFAULT_INSTANCE && (
<p class="insignificant">
<small>
Connect your existing Mastodon/Fediverse account.
<br />
Your credentials are not stored on this server.
</small>
</p>
)}
</div>
{(appSite || appVersion) && (
<p class="app-site-version">
<small> <small>
Connect your existing Mastodon/Fediverse account. {appSite} {appVersion}
<br />
Your credentials are not stored on this server.
</small> </small>
</p> </p>
</div> )}
<p> <p>
<a href="https://github.com/cheeaun/phanpy" target="_blank"> <a href="https://github.com/cheeaun/phanpy" target="_blank">
Built Built
@ -61,10 +99,7 @@ function Welcome() {
@cheeaun @cheeaun
</a> </a>
.{' '} .{' '}
<a <a href={PRIVACY_POLICY_URL} target="_blank">
href="https://github.com/cheeaun/phanpy/blob/main/PRIVACY.MD"
target="_blank"
>
Privacy Policy Privacy Policy
</a> </a>
. .

View file

@ -1,4 +1,4 @@
const { VITE_CLIENT_NAME: CLIENT_NAME, VITE_WEBSITE: WEBSITE } = import.meta const { PHANPY_CLIENT_NAME: CLIENT_NAME, PHANPY_WEBSITE: WEBSITE } = import.meta
.env; .env;
const SCOPES = 'read write follow push'; const SCOPES = 'read write follow push';

View file

@ -4,7 +4,7 @@ import { subscribeKey } from 'valtio/utils';
import states from './states'; import states from './states';
const { VITE_CLIENT_NAME: CLIENT_NAME } = import.meta.env; const { PHANPY_CLIENT_NAME: CLIENT_NAME } = import.meta.env;
export default function useTitle(title, path) { export default function useTitle(title, path) {
function setTitle() { function setTitle() {

View file

@ -2,6 +2,7 @@ import preact from '@preact/preset-vite';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
import { uid } from 'uid/single';
import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite'; import { defineConfig, loadEnv, splitVendorChunkPlugin } from 'vite';
import generateFile from 'vite-plugin-generate-file'; import generateFile from 'vite-plugin-generate-file';
import htmlPlugin from 'vite-plugin-html-config'; import htmlPlugin from 'vite-plugin-html-config';
@ -10,13 +11,19 @@ import removeConsole from 'vite-plugin-remove-console';
const { NODE_ENV } = process.env; const { NODE_ENV } = process.env;
const { const {
VITE_CLIENT_NAME: CLIENT_NAME, PHANPY_CLIENT_NAME: CLIENT_NAME,
VITE_CLIENT_ID: CLIENT_ID, PHANPY_APP_ERROR_LOGGING: ERROR_LOGGING,
VITE_APP_ERROR_LOGGING: ERROR_LOGGING,
} = loadEnv('production', process.cwd()); } = loadEnv('production', process.cwd());
const now = new Date(); const now = new Date();
const commitHash = execSync('git rev-parse --short HEAD').toString().trim(); let commitHash;
try {
commitHash = execSync('git rev-parse --short HEAD').toString().trim();
} catch (error) {
// If error, means git is not installed or not a git repo (could be downloaded instead of git cloned)
// Fallback to random hash which should be different on every build run 🤞
commitHash = uid();
}
const rollbarCode = fs.readFileSync( const rollbarCode = fs.readFileSync(
resolve(__dirname, './rollbar.js'), resolve(__dirname, './rollbar.js'),
@ -26,6 +33,7 @@ const rollbarCode = fs.readFileSync(
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
base: './', base: './',
envPrefix: ['VITE_', 'PHANPY_'],
mode: NODE_ENV, mode: NODE_ENV,
define: { define: {
__BUILD_TIME__: JSON.stringify(now), __BUILD_TIME__: JSON.stringify(now),
@ -55,7 +63,6 @@ export default defineConfig({
]), ]),
VitePWA({ VitePWA({
manifest: { manifest: {
id: CLIENT_ID,
name: CLIENT_NAME, name: CLIENT_NAME,
short_name: CLIENT_NAME, short_name: CLIENT_NAME,
description: 'Minimalistic opinionated Mastodon web client', description: 'Minimalistic opinionated Mastodon web client',