diff --git a/.github/workflows/cloudflare_pages.yml b/.github/workflows/cloudflare_pages.yml new file mode 100644 index 0000000000..8e26a9ff8a --- /dev/null +++ b/.github/workflows/cloudflare_pages.yml @@ -0,0 +1,54 @@ +name: Deploy to Cloudflare Pages + +on: + # Uncomment *ONLY* this to enable CD for every new release + # push: + # tags: + # - 'v*' + # Uncomment *ONLY* this to enable CD for every push to the `main` branch + # push: + # branch: release + workflow_dispatch: {} + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - run: corepack enable + + - name: Set node + uses: actions/setup-node@v3 + with: + node-version: 18 + cache: pnpm + + - name: Install build dependencies + run: npm install -g pnpm && pnpm i --frozen-lockfile + + - name: Build Elk + run: pnpm run build + env: + HOST: 0.0.0.0 + NODE_ENV: production + NUXT_DEPLOY_URL: ${{ vars.NUXT_DEPLOY_URL }} + NUXT_PUBLIC_DEFAULT_SERVER: ${{ vars.NUXT_PUBLIC_DEFAULT_SERVER }} + NUXT_STORAGE_DRIVER: kv-binding + VITE_DEV_PWA: true + NITRO_PRESET: ${{ vars.NITRO_PRESET }} + + - name: Publish + uses: cloudflare/pages-action@1 + with: + directory: .output/public + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.CLOUDFLARE_PAGES_PROJECT_NAME }} + apiToken: ${{ secrets.CLOUDFLARE_PAGES_API_TOKEN }} + gitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/composables/cache.ts b/composables/cache.ts index b03cb2aefc..cfcd1c253b 100644 --- a/composables/cache.ts +++ b/composables/cache.ts @@ -1,8 +1,16 @@ import { LRUCache } from 'lru-cache' import type { mastodon } from 'masto' +// expire in an hour const cache = new LRUCache({ max: 1000, + ttl: 3600000, + ttlAutopurge: true, + allowStaleOnFetchAbort: true, + allowStaleOnFetchRejection: true, + allowStale: true, + noUpdateTTL: true, + ttlResolution: 60000, }) if (process.dev && process.client) @@ -17,19 +25,138 @@ function removeCached(key: string) { cache.delete(key) } -export function fetchStatus(id: string, force = false): Promise { - const server = currentServer.value - const userId = currentUser.value?.account.id - const key = `${server}:${userId}:status:${id}` - const cached = cache.get(key) - if (cached && !force) - return cached - const promise = useMastoClient().v1.statuses.fetch(id) - .then((status) => { - cacheStatus(status) - return status +function generateStatusIdCacheKeyAccessibleToCurrentUser(statusId: string) { + return `${currentServer.value}:${currentUser.value?.account.id}:status:${statusId}` +} + +async function federateRemoteStatus(statusUri: string, force = false): Promise { + if (cache.has(`stop:${statusUri}`)) { + if (process.dev) + // eslint-disable-next-line no-console + console.debug(`Skipping further processing for invalid status URI: ${statusUri}`) + return Promise.resolve(null) + } + + if (statusUri.startsWith(`https://${currentServer.value}`)) { + if (process.dev) + // eslint-disable-next-line no-console + console.info(`Local domain is authoritative, so redirecting resolution request for status: ${statusUri}`) + + return fetchStatus(statusUri.split('/').pop() ?? statusUri.replace(`https://${currentServer.value}/`, '')) + } + + if (statusUri.search(/^\d+$/) !== -1) { + if (process.dev) + // eslint-disable-next-line no-console + console.info(`statusUri parameter was passed an ID, so redirecting resolution request: ${statusUri}`) + + return fetchStatus(statusUri, force) + } + + const localStatusIdCacheKey = generateStatusIdCacheKeyAccessibleToCurrentUser(statusUri) + + const cached: mastodon.v1.Status | Promise | undefined | null | number = cache.get(localStatusIdCacheKey, { allowStale: false, updateAgeOnGet: false }) + if (cached) { + if ( + !!cached + && !(typeof cached === 'number') + && !(cached instanceof Promise) + && (cached.uri === statusUri) + && !force + ) { + return cached + } + else if (cached instanceof Promise) { + return cached + } + else if (typeof cached === 'number') { + if ([401, 403, 418].includes(cached)) + console.error(`Current user is forbidden or lacks authorization to fetch status: ${statusUri}`) + if ([404].includes(cached)) + console.error(`The requested status URI cannot be found: ${statusUri}`) + if ([429].includes(cached)) + console.error('The request was rate-limited by the Mastodon server') + if ([500, 501, 503].includes(cached)) + console.error('The Mastodon server is unresponsive') + return Promise.resolve(null) + } + } + + const promise = useMastoClient().v2.search({ q: statusUri, type: 'statuses', resolve: (!!currentUser.value), limit: 1 }) + .then(async (results) => { + const post = results.statuses.pop() + if (!post) { + console.error(`Status could not be federated, perhaps it no longer exists: '${statusUri}'`) + cache.set(localStatusIdCacheKey, 404) + return Promise.resolve(null) + } + + const splitUri = post.account.url.replace('https://', '').split('/@') + const accountWebfinger = `${splitUri[1]}@${splitUri[0]}` + post.account.acct = accountWebfinger + + cache.set(localStatusIdCacheKey, post) + return post }) - cache.set(key, promise) + .catch((e) => { + console.error(`Encountered error while federating status using URI '${statusUri}' | ${(e as Error)}`) + cache.set(localStatusIdCacheKey, null) + return Promise.resolve(null) + }) + cache.set(localStatusIdCacheKey, promise) + return promise +} + +export async function fetchStatus(statusId: string, force = false): Promise { + if (cache.has(`stop:${statusId}`)) { + if (process.dev) + // eslint-disable-next-line no-console + console.debug(`Skipping further processing for invalid status Id: ${statusId}`) + return Promise.resolve(null) + } + + // Handle scenario where the value of statusId is actually an URI + if (statusId.startsWith('h')) { + if (process.dev) + // eslint-disable-next-line no-console + console.info(`statusId parameter was passed an URI, so redirecting resolution request: ${statusId}`) + return federateRemoteStatus(statusId, force) + } + + // handle invalid statusId + if ((statusId.search(/^\d+$/) === -1)) { + console.error(`Malformed or unrecognized Status ID: ${statusId}`) + cache.set(`stop:${statusId}`, 418) + return Promise.resolve(null) + } + + const localStatusIdCacheKey = generateStatusIdCacheKeyAccessibleToCurrentUser(statusId) + const cached: mastodon.v1.Status | Promise | undefined | null = cache.get(localStatusIdCacheKey, { allowStale: false, updateAgeOnGet: false }) + if (cached) { + // avoid race condition by returning the existing promise instead of restarting the chain of events all over again + if (cached instanceof Promise) + return cached + if (typeof cached === 'number') { + // wait for the cached value to expire before trying again + if ([401, 403, 404, 418, 429, 500, 501, 503].includes(cached)) + return null + } + else if (cached.id === statusId) { + // if we don't care about authoritative values then return cached value + if (!force) + return cached + } + } + + const promise = useMastoClient().v1.statuses.fetch(statusId) + .then(async (post) => { + const splitUri = post.account.url.replace('https://', '').split('/@') + const accountWebfinger = `${splitUri[1]}@${splitUri[0]}` + post.account.acct = accountWebfinger + cache.set(localStatusIdCacheKey, post) + return post + }) + cache.set(localStatusIdCacheKey, promise) return promise } diff --git a/docs/content/2.deployment/1.netlify.md b/docs/content/2.deployment/1.netlify.md deleted file mode 100644 index 22fe4caf52..0000000000 --- a/docs/content/2.deployment/1.netlify.md +++ /dev/null @@ -1,61 +0,0 @@ -# Netlify and Cloudflare - -Want to host Elk for your Mastodon instance? You came to the right place! - -For this guide we're going to use Netlify for hosting the app, and Cloudflare for key value storage. Both of which can be used on their free tiers if your instance is small. - -## Forking Elk - -In order to use Netlify with Elk, we'll need to fork the Elk repo. - -Fork the repository from [https://github.com/elk-zone/elk](https://github.com/elk-zone/elk). Make sure you deselect "Copy the main branch only" if you want to use the stable `release` branch. -![The settings to use for forking the Elk repository](/docs/images/selfhosting-guide/github-fork.png) - -## Importing the Elk repo into Netlify - -On the main page of your Netlify dashboard, press the "Import from GitHub" button. Point it to your Elk fork. - -On the third page with Site settings, change the "Branch to deploy" to `release` if you wish. Press "Deploy site". - -That's one step done! Keep the tab open while we hop over to CloudFlare for a bit. - -## Setting up CloudFlare Workers KV - -From your CloudFlare dashboard, open "Workers". If this is your first time opening this tab, CloudFlare will ask you to set up a free custom Cloudflare Workers subdomain. Follow the instructions. - -Go to "KV" and create a new namespace. - -Then go to "Overview" and click on API tokens. We want to create an API token that will let Elk modify our newly made Worker. Click on "Create token" and then in the Custom token section click "Get started". - -The only permission that we'll need is to edit the Workers KV Storage. -![The settings to use for the CloudFlare API token](/docs/images/selfhosting-guide/cf-api-token-settings.png) - -Save the newly made token in a safe spot. Keep the tab open while we'll configure the environment variables on Netlify. - -## Setting the environment variables on Netlify - -On your project page, go to "Site settings", and open the "Environment variables" section. - -There are 5 environment variables to add. - -| Environment variable | What it is | -|---|---| -| NUXT_CLOUDFLARE_ACCOUNT_ID | This is your CloudFlare Account ID. You can find it in "Workers > Overview". | -| NUXT_CLOUDFLARE_API_TOKEN | Put your CloudFlare API token here. | -| NUXT_CLOUDFLARE_NAMESPACE_ID | This is your CloudFlare KV Namespace ID. You can find it in "Workers > KV". | -| NUXT_STORAGE_DRIVER | Because we're using CloudFlare, we'll need to set this to `cloudflare`. | -| NUXT_PUBLIC_DEFAULT_SERVER | This is the address of the Mastodon instance that will show up when a user visits your Elk deployment and is not logged in. If you don't make that variable, it will point to `m.webtoo.ls` by default. | -| NUXT_PUBLIC_SINGLE_INSTANCE | If enabled it will disable signing in to servers other than the server specified in `NUXT_PUBLIC_DEFAULT_SERVER` | -| NUXT_PUBLIC_PRIVACY_POLICY_URL | This is the URL to a web page with information on your privacy policy. | - -That's it! All that's left to do is... - -## Deploy Elk! -On your project page open the Deploys tab, click on "Trigger deploy" and "Deploy site". In a few minutes Elk should be up and running! - -## Use a custom domain -If you want to use a custom domain, go to "Domain settings" on your Netlify project page, and press "Add custom domain". If your domain is not bought from Netlify, it will ask you to add a CNAME record. Do that. - -Once the custom domain is added, you'll need to add an SSL/TLS certificate. At the bottom of the page press "Verify DNS configuration" and if it succeeds, press "Provision certificate". If that fails, you may need to wait some time until your DNS propagetes. - -And that's it! Enjoy your instance's Elk! diff --git a/docs/content/2.deployment/10.getting_started.md b/docs/content/2.deployment/10.getting_started.md new file mode 100644 index 0000000000..b196edf6fe --- /dev/null +++ b/docs/content/2.deployment/10.getting_started.md @@ -0,0 +1,82 @@ +# Getting Started +## Introduction + +Want to host Elk for your Mastodon instance? You came to the right place! This section covers topics that are common to several kinds of deployments, including: +- [Preparing for deployment](#prerequisites) +- [Obtaining the Elk source code](#creating-a-fork) +- [Configuring persistent storage](#setting-up-cloudflare-workers-kv) + + +## Prerequisites +Before getting started, please ensure that you have access to the following: +- If you don't have a GitHub account, [create one](https://github.com/signup?ref_cta=Sign+up&ref_loc=header+logged+out&ref_page=%2F&source=header-home) before proceeding. The _Free_ tier includes everything you need to deploy Elk. +- If you plan to deploy Elk using serverless infrastructure (e.g. Netlify, Cloudflare Pages) _**or**_ intend to use continuous integration/continuous deployment tools (e.g. GitHub Actions), then you'll need a Cloudflare account in order to setup a [key-value store](https://www.cloudflare.com/products/workers-kv/) that will be used to save Elk application and user credentials. If you don't have a Cloudflare account, [create one](https://dash.cloudflare.com/sign-up/workers) before proceeding. The _Workers Free_ plan includes everything you need to deploy Elk and run a personal/low-traffic instance, **however**, you may incur charges if you [exceed plan limits](https://www.cloudflare.com/plans/developer-platform/) + +![Cloudflare Workers Free plan limits as of February 19, 2023.](/docs/images/selfhosting-guide/cf-workers-limits.png) + +- If you plan to deploy Elk to a containerized environment, you'll need to provision a file system-based persistent storage and mount it as a volume. Please refer to [Docker's official documentation](https://docs.docker.com/engine/reference/commandline/volume_create/) and/or your hosting provider's product guide for detailed instructions on how to do this. + +
[back to top](#getting-started) + +## Creating a fork + +[Click here to fork the Elk repository](https://github.com/elk-zone/elk/fork). Make sure to _deselect_ "Copy the main branch only" so that you have access to the [`release`](https://github.com/elk-zone/elk/tree/release) branch, which is more stable than the [`main`](https://github.com/elk-zone/elk) branch. + + +![The settings to use for forking the Elk repository](/docs/images/selfhosting-guide/github-fork.png) + +For a more detailed guide to forking a repository, please refer to [GitHub's official documentation](https://docs.github.com/en/get-started/quickstart/fork-a-repo). + +
[back to top](#getting-started) + +### Which branch(es) to deploy? + +> **_A note about alpha software:_** Elk is under active development. Many core features are ready for daily use, but many others remain a work-in-progress. Please be mindful of this fact as you consider which branch to deploy (and to which audiences). + +
+ +If you followed [these instructions](#creating-a-fork), you have access to 2 branches for your deployment: `release` and `main`. You can deploy one, or the other, or both. + +If you're looking to Elk as a replacement for daily use, we strongly recommend sticking to the `release` branch. If you are a developer that likes to live on the cutting edge, even if that means dealing with feature regressions and new bugs, then the `main` branch will be more to your liking. + +Unless you're an experienced developer that's actively contributing to the Elk project, deploying any branch *other than* `release` and `main` is **_strongly discouraged_**; these other branches contain features in various stages of testing and development- all of them have the potential to permanently corrupt or destroy data without warning. + +If you're a developer that's interested in contributing to the Elk project, please review the [Contributing](https://github.com/elk-zone/elk/tree/release#-contributing) section on the repository's main landing page. + +
[back to top](#getting-started) + +## Setting up Cloudflare Workers KV +### Create a KV Namespace +As [mentioned earlier](#prerequisites), you'll need to create a Cloudflare KV Namespace if you are deploying Elk to a serverless environment, such as [Netlify Functions](https://www.netlify.com/products/#netlify-functions) or [Cloudflare Pages](https://pages.cloudflare.com/). The following instructions will guide you through the process by using the Cloudflare Dashboard:[^1] + +1. Log into your CloudFlare account. +2. From the left navigation bar on the *main* account Dashboard (this is one navigation level _above_ the website-level dashboards): + - Click "Workers", + > _Note:_ if this is your first time opening this tab, CloudFlare will ask you to set up a free custom Cloudflare Workers subdomain. Follow the instructions to set up the domain. For the purposes of this guide, it doesn't matter what you choose as your subdomain (besides, you can change it later) + + - Then, click "KV" in the navigation bar: + + ![Where to find the Workers and KV Namespace links](/docs/images/selfhosting-guide/cf-nav-bar.png) + +3. Click the blue "Create namespace" button that will appear in the main body of the page. Follow the instructions to name the KV Namespace. For Elk's purposes, you can label the KV Namespace whatever you want, but Cloudflare will not allow you to change the label. Click "Add" then make a note of the newly-created KV Namespace **ID** + +![How to create a KV Namespace using the Cloudflare Dashboard](/docs/images/selfhosting-guide/cf-create-kv-namespace.png) + +
[back to top](#getting-started) + +### Find your account ID and create an API token +If your hosting provider is _**not**_ Cloudflare, then you'll need to create an API token that will allow Elk to read and write to the newly-created KV Namespace. + +1. Again, from the left-side navigation bar on the *main* account Dashboard, click on "Workers" +2. Halfway down the **right-side** navigation bar, you will see a gray box labeled, `Account ID`- make a note of that value and save it for later. +3. Beneath `Account ID` field you will see a link to "API Tokens", click on that link and then click on the blue button labeled "Create Token" (or "Get started" if this is the first Custom Token you create in your account): +4. Follow the directions to label and configure your API Token. For Elk's purposes, the only required permission is **Account | Workers KV Storage | Edit** : + +![The settings to use for the CloudFlare API token](/docs/images/selfhosting-guide/cf-api-token-settings.png) + +5. Save the newly made token in a safe spot and **DO NOT** add it to any file that you may (intentionally or unintentionally) commit to any GitHub repository. You may want to keep the tab open while you configure the rest of your deployment. + +
[back to top](#getting-started) + + +[^1]: If you prefer using the command line, then please refer to [Cloudflare's official documentation](https://developers.cloudflare.com/workers/wrangler/workers-kv/) diff --git a/docs/content/2.deployment/20.netlify.md b/docs/content/2.deployment/20.netlify.md new file mode 100644 index 0000000000..f794f37392 --- /dev/null +++ b/docs/content/2.deployment/20.netlify.md @@ -0,0 +1,41 @@ +# Netlify +> πŸ‘‹πŸΌ **Before you continue:** please read through the [Getting Started](10.getting_started.md) page as it contains important information that can help you avoid common gotchas. It also offers instructions to help you fork the Elk repository and to configure a Cloudflare KV namespace and an API token, which are prerequisites to the section below. +-------------- +## Introduction +This section covers the basics of deploying Elk to Netlify. This kind of deployment requires Cloudflare KV for persistent storage. Both companies offer a _Free Tier_ that should suffice for a small/personal instance. + +## Importing the Elk repo into Netlify + +On the main page of your Netlify dashboard, press the "Import from GitHub" button. Point it to your Elk fork. + +On the third page with Site settings, change the "Branch to deploy" to `release` if you wish. Press "Deploy site". + +That's one step done! Keep the tab open while we hop over to Cloudflare for a bit. + +## Setting the environment variables on Netlify + +On your project page, go to "Site settings", and open the "Environment variables" section. + +There are 5 environment variables to add. + +| Environment variable | What it is | +|---|---| +| NUXT_CLOUDFLARE_ACCOUNT_ID | This is your Cloudflare Account ID. You can find it in "Workers > Overview" | +| NUXT_CLOUDFLARE_API_TOKEN | Put your Cloudflare API token here. | +| NUXT_CLOUDFLARE_NAMESPACE_ID | This is your Cloudflare KV Namespace ID. Note: this is not the *label*/*name* of the KV Namespace, but the Cloudflare-generated ID assigned to it; you can find it under "Workers > KV" | +| NUXT_STORAGE_DRIVER | This must be set to `cloudflare` for this kind of deployment | +| NUXT_PUBLIC_DEFAULT_SERVER | This is the address of the Mastodon instance that will show up when a user visits your Elk deployment and is not logged in. If you don't make that variable, it will point to `m.webtoo.ls` by default. | +| NUXT_PUBLIC_SINGLE_INSTANCE | If enabled it will disable signing in to servers other than the server specified in `NUXT_PUBLIC_DEFAULT_SERVER` | +| NUXT_PUBLIC_PRIVACY_POLICY_URL | This is the URL to a web page with information on your privacy policy. | + +That's it! All that's left to do is... + +## Deploy Elk! +On your project page open the Deploys tab, click on "Trigger deploy" and "Deploy site". In a few minutes Elk should be up and running! + +## Use a custom domain +If you want to use a custom domain, go to "Domain settings" on your Netlify project page, and press "Add custom domain". If your domain is not bought from Netlify, it will ask you to add a CNAME record. Do that. + +Once the custom domain is added, you'll need to add an SSL/TLS certificate. At the bottom of the page press "Verify DNS configuration" and if it succeeds, press "Provision certificate". If that fails, you may need to wait some time until your DNS propagetes. + +And that's it! Enjoy your instance's Elk! diff --git a/docs/content/2.deployment/30.cloudflare_pages.md b/docs/content/2.deployment/30.cloudflare_pages.md new file mode 100644 index 0000000000..0e5cbd077a --- /dev/null +++ b/docs/content/2.deployment/30.cloudflare_pages.md @@ -0,0 +1,218 @@ +# Cloudflare Pages +> πŸ‘‹πŸΌ **Before you continue:** please read through the [Getting Started](10.getting_started.md) page as it contains important information that can help you avoid common gotchas. It also has instructions to help you fork the Elk repository and to configure a Cloudflare KV namespace, which are prerequisites to the section below. +-------------- +## Introduction +This section covers the basics of deploying Elk to Cloudflare Pages. Many of the steps in this guide can be completed using the Cloudflare Dashboard or Cloudflare's command-line interface, [`wrangler`](https://developers.cloudflare.com/workers/wrangler/), but some of the steps can only be completed using Wrangler while others can only be completed using the Dashboard. In order to minimize confusion, this guide focuses on the `wrangler` (CLI) approach as much as possible. + +If you prefer to use the Cloudflare Dashboard, please refer to [Cloudflare's official documentation](https://pages.cloudflare.com) to set up your Pages project and for guidance on using [Direct Upload](https://developers.cloudflare.com/pages/platform/direct-upload/) to publish your site. + + +## Initial (one-time) setup +
+Create a Cloudflare Pages Project +
+ +You'll need to create a Cloudflare Pages project before you can upload your assets. To do this, replace the placeholder values and run the following command: +```sh +wrangler pages project create YOUR_DESIRED_CLOUDFLARE_PROJECT_NAME --production-branch GITHUB_BRANCH_YOU_ARE_USING +``` +
+πŸ‘€ Known Gotcha + +> If you omit the --production-branch flag, Cloudflare Pages designates the first branch that you publish as your Production deployment. If, later on, you publish a different branch using the *same* project name, Cloudflare Pages will designate that as your Preview deployment. +
+ +
+ +Then, in your browser, navigate to the *account-level* [Cloudflare Dashboard](https://dash.cloudflare.com), then click on the "Pages" link in the left-side navigation bar. You should see your newly-created Pages project near the top of the list: +

+![Screenshot of the Pages section of the Cloudflare Dashboard](/docs/images/selfhosting-guide/cf-pages-project.png) +

+Click on the name of your Pages project, then navigate to Settings > Environment variables > Add variables: +

+![Screenshot of the Pages section of the Cloudflare Dashboard](/docs/images/selfhosting-guide/cf-pages-settings-1.png) +

+Add the following environment variables to your Production environment: + +| Variable name | value | +| :- | -: | +`HOST` | `0.0.0.0` +`NODE_ENV` | `production` +`NUXT_DEPLOY_URL` | `https://REPLACE_WITH_YOUR_ELK_DOMAIN` +`NUXT_PUBLIC_DEFAULT_SERVER` | `REPLACE_WITH_YOUR_MASTODON_DOMAIN` +`NUXT_STORAGE_DRIVER` | `kv-binding` +`PORT` | `443` + +Once you've added all the values, click save, then click the "Functions" link: +

+![Screenshot of the Pages section of the Cloudflare Dashboard](/docs/images/selfhosting-guide/cf-pages-settings-2.png) + +Scroll down the page until you reach the section labeled **KV namespace bindings**. Enter `STORAGE` (in all-caps) in the box labeled **Variable name**, then select the namespace that you [created earlier](10.getting_started.md) from the drop-down menu. Lastly, click **Save**: +

+![Screenshot of the Pages section of the Cloudflare Dashboard](/docs/images/selfhosting-guide/cf-pages-settings-3.png) + +
[back to top](#cloudflare-pages) + +
+ +
+Install (or Update) Cloudflare Wrangler +
+ +If you haven't done so already, install Cloudflare's command-line interface, Wrangler, by running: +```sh +npm -g add wrangler +``` + +
+ +If you already have `wrangler` installed, verify that you're running wrangler 2.10+ by calling `wrangler version`. To update, run: + +```sh +npm -g update wrangler +``` + +
+πŸ‘€ Known Gotcha + +> Installing wrangler to the local (Elk project) directory is strongly discouraged as doing so may inadvertently trigger a cascade of errors stemming from incompatible dependencies while simultaneously throwing the `pnpm-lock.yaml` and `package.json` files out of sync. This will, in turn, cause Elk build-time errors. +> +> If you don't want to install `wrangler` globally (or lack the permissions to do so), then we recommend using the `npx` command instead of installing `wrangler` to the project directory. To do this, simply prepend `npx` to any command in this guide that begins with `wrangler`. For example, `wrangler pages dev` would become `npx wrangler pages dev`. Note: this approach still requires that you have npm installed. +> +> If you run into problems during Wrangler installation, please refer to [Cloudflare's Wrangler documentation](https://developers.cloudflare.com/workers/wrangler/install-and-update/) for detailed instructions and troubleshooting steps +
+ +
[back to top](#cloudflare-pages) +
+ +## Build and publish + +> πŸ‘€ There's a bash script at the [end of this guide](#resources) that combines the following steps + +Open a terminal window, and change the working directory to the folder containing the forked repository: +```sh +cd REPLACE_WITH_THE_PATH_TO_THE_LOCAL_CLONE_OF_THE_ELK_REPO +``` + +Install project dependencies by running the following command: +```sh +npx pnpm --filter=\!./docs i +``` + +Build the app by replacing the placeholder values and running the following command: +```sh +NODE_ENV=production \ + HOST=0.0.0.0 \ + NUXT_PUBLIC_DEFAULT_SERVER=REPLACE_WITH_YOUR_MASTODON_DOMAIN \ + NUXT_DEPLOY_URL=https://REPLACE_WITH_YOUR_ELK_DOMAIN \ + NUXT_STORAGE_DRIVER=kv-binding \ + VITE_DEV_PWA=true \ + NITRO_PRESET=cloudflare_pages npx nuxi build +``` + +
+πŸ‘€ Known Gotcha + +> There's a fix for [unjs/nitro issue #196](https://github.com/unjs/nitro/issues/196), [unjs/nitro issue #497](https://github.com/unjs/nitro/issues/497), and [unjs/nitro issue #787](https://github.com/unjs/nitro/issues/787) working its way through the review process. If you find that your deployment is unable to handle `POST` requests, you may need to wait until that fix is officially released or patch your nitro dependency to include [unjs/nitro PR #968](https://github.com/unjs/nitro/pull/968) and (optionally) [unjs/nitro PR #965](https://github.com/unjs/nitro/pull/965). +
+ +Upon the completion of a successful build, you will find two (2) additional folders in your project directory: `.nuxt` and `.output`. For the purposes of Cloudflare Pages, we only care about the `.output/public` folder. + +Publish your site to Cloudflare Pages by running the following command + +```sh +wrangler pages publish .output/public --project-name=YOUR_CLOUDFLARE_PROJECT_NAME --branch=GITHUB_BRANCH_YOU_ARE_USING +``` + +## That's it! πŸ₯³ +You should be able to see your Elk deployment by visiting the link provided by `wrangler` upon completion + +
[back to top](#cloudflare-pages) + +## CI/CD using Github Actions +
+πŸ‘€ Known Gotcha + +> You have to set up your Cloudflare Pages project [as described above](#initial-one-time-setup) before you can use the Github Actions workflow. +
+ +A Github Actions workflow definition file that can be used to automate deployments to Cloudflare Pages is included in the Elk project repository (see `./.github/workflows/cloudflare_pages.yml`). The included workflow is configured to use a manual trigger by default, and includes examples that can be used to automatically trigger deployments every time a new release becomes available or whenever there's a push to the `release` branch. To use this workflow, you'll first need to: + +1. Add your [Cloudflare account ID](10.getting_started.md) to your fork of the Elk repository **as a secret** with the name/label `CLOUDFLARE_ACCOUNT_ID` +2. Add your Cloudflare Pages project name to your fork of the Elk repository **as a secret** with the name/label `CLOUDFLARE_PAGES_PROJECT_NAME` +3. Create a Cloudflare API token with `Cloudflare Pages:Edit` permissions, then add the token to your fork of the Elk repository **as a secret** with the name/label `CLOUDFLARE_PAGES_API_TOKEN` +4. Add the following to your fork of the Elk repository **as environment variables**: + - `NUXT_DEPLOY_URL` + - `NUXT_PUBLIC_DEFAULT_SERVER` + +
+πŸ‘€ Known Gotcha + +> You do *NOT* need to specify `GITHUB_TOKEN` as this value is passed to your Github Actions build runner automatically by Github. +
+ +
[back to top](#cloudflare-pages) + +## Deploying to Custom Domains +If you want to use a custom domain, go to "Custom domains" within your Cloudflare Pages project page, click on "Setup a custom domain" and follow the prompts. If you need further details or run into issues, please refer to [Cloudflare's official documentation](https://developers.cloudflare.com/pages/platform/custom-domains/) on this topic. + +
[back to top](#cloudflare-pages) + +## FAQ +
+ Is it possible to set up a CI/CD pipeline using something other than Github Actions? + + > Yes, it is possible to set up a CI/CD pipeline using CircleCI or Travis CI by following the [instructions published by Cloudflare](https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/). If you've implemented a CI/CD pipeline for Elk using another platform, we welcome your contributions to this guide. +
+ +
+ Did you try deploying Elk using the built-in Cloudflare Pages Github integration? + +> **Short answer:** Yes, but we would not recommend deploying Elk via that option. +> +> **Long answer:** Yes, but Cloudflare Pages uses outdated build images that [lack support for Node.js >= v17.x](https://developers.cloudflare.com/pages/platform/build-configuration/). After spending several hours on Cloudflare Community and Cloudflare Discord, it's clear that there are no workarounds to this issue. Cloudflare representatives on Discord point to a forthcoming update to their build image library, but offer no specific date of release. Separately, Cloudflare Pages lacks native support for `pnpm`; we [found a workaround](https://community.cloudflare.com/t/add-pnpm-to-pre-installed-cloudflare-pages-tools/288514/3) for this issue but that wasn't enough to overcome the build-time errors stemming from deprecated libraries and missing peer dependencies. +> +> If you've successfully deployed Elk using Cloudflare's Github integration, we welcome your contributions to this guide. +
+ +
+ Will this guide also work for deployment directly to Cloudflare Workers (i.e. as a Workers Site)? + +> No. While we were able to shoe-horn an Elk deployment to Workers Sites, we would not recommend it based on our testing. Moreover, [Cloudflare's official documentation](https://developers.cloudflare.com/workers/platform/sites/) suggests that the company is phasing out support for Workers Sites in favor of Cloudflare Pages. +
+ +
[back to top](#getting-started) + +## Resources + ```sh + #!/usr/bin/env bash + + trap 'exit 0' SIGTERM + set -e + + CLOUDFLARE_PROJECT_NAME="elk" # Replace this with the name of your Cloudflare Pages Project + GITHUB_BRANCH="release" # Replace this with the name of the Github branch you want to deploy + + echo "Setting up build environment ..." + # Uncomment the following line if you want to start from a clean slate every time time you trigger a build (recommended) + # rm -rf .nuxt && rm -rf .netlify && rm -rf dist && rm -rf .output && rm -rf node_modules + npx pnpm --filter=\!./docs i + + + echo "Building ..." + NODE_ENV=production \ + HOST=0.0.0.0 \ + NUXT_PUBLIC_DEFAULT_SERVER=REPLACE_WITH_YOUR_MASTODON_LOCAL_DOMAIN \ + NUXT_DEPLOY_URL=https://REPLACE_WITH_YOUR_ELK_DOMAIN \ + NUXT_STORAGE_DRIVER=kv-binding \ + VITE_DEV_PWA=true \ + NITRO_PRESET=cloudflare_pages npx nuxi build + + echo "Publishing ..." + # Uncomment the next line the *first time* you deploy Elk, and then delete or re-comment it out + # wrangler pages project create "${CLOUDFLARE_PROJECT_NAME}" + wrangler pages publish .output/public --project-name="${CLOUDFLARE_PROJECT_NAME}" --branch="${GITHUB_BRANCH}" + + ``` + +
[back to top](#cloudflare-pages) \ No newline at end of file diff --git a/docs/public/images/selfhosting-guide/cf-create-kv-namespace.png b/docs/public/images/selfhosting-guide/cf-create-kv-namespace.png new file mode 100644 index 0000000000..cb563c1114 Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-create-kv-namespace.png differ diff --git a/docs/public/images/selfhosting-guide/cf-nav-bar.png b/docs/public/images/selfhosting-guide/cf-nav-bar.png new file mode 100644 index 0000000000..79c0399178 Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-nav-bar.png differ diff --git a/docs/public/images/selfhosting-guide/cf-pages-project.png b/docs/public/images/selfhosting-guide/cf-pages-project.png new file mode 100644 index 0000000000..523c4732dd Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-pages-project.png differ diff --git a/docs/public/images/selfhosting-guide/cf-pages-settings-1.png b/docs/public/images/selfhosting-guide/cf-pages-settings-1.png new file mode 100644 index 0000000000..4d7586c0c5 Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-pages-settings-1.png differ diff --git a/docs/public/images/selfhosting-guide/cf-pages-settings-2.png b/docs/public/images/selfhosting-guide/cf-pages-settings-2.png new file mode 100644 index 0000000000..1fe8e52da2 Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-pages-settings-2.png differ diff --git a/docs/public/images/selfhosting-guide/cf-pages-settings-3.png b/docs/public/images/selfhosting-guide/cf-pages-settings-3.png new file mode 100644 index 0000000000..8fa4a1eebb Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-pages-settings-3.png differ diff --git a/docs/public/images/selfhosting-guide/cf-workers-limits.png b/docs/public/images/selfhosting-guide/cf-workers-limits.png new file mode 100644 index 0000000000..d237a622d1 Binary files /dev/null and b/docs/public/images/selfhosting-guide/cf-workers-limits.png differ diff --git a/middleware/1.permalink.global.ts b/middleware/1.permalink.global.ts index c6aa8901ec..db3f43127e 100644 --- a/middleware/1.permalink.global.ts +++ b/middleware/1.permalink.global.ts @@ -42,13 +42,15 @@ export default defineNuxtRouteMiddleware(async (to, from) => { return getAccountRoute(account) } - // If we're logged in, search for the local id the account or status corresponds to - const { accounts, statuses } = await masto.client.value.v2.search({ q: `https:/${to.fullPath}`, resolve: true, limit: 1 }) - if (statuses[0]) - return getStatusRoute(statuses[0]) - - if (accounts[0]) - return getAccountRoute(accounts[0]) + if (currentUser.value) { + // If we're logged in, search for the local id the account or status corresponds to + const { accounts, statuses } = await masto.client.value.v2.search({ q: `https:/${to.fullPath}`, resolve: true, limit: 1 }) + if (statuses[0]) + return getStatusRoute(statuses[0]) + + if (accounts[0]) + return getAccountRoute(accounts[0]) + } } catch (err) { console.error(err) diff --git a/pages/[[server]]/status/[status].vue b/pages/[[server]]/status/[status].vue index 78085345f8..a4a8996729 100644 --- a/pages/[[server]]/status/[status].vue +++ b/pages/[[server]]/status/[status].vue @@ -5,7 +5,13 @@ definePageMeta({ const params = to.params const id = params.status as string const status = await fetchStatus(id) - return getStatusRoute(status) + if (status) + return getStatusRoute(status) + + if (process.dev) + console.error(`Status not found: ${id}`) + + return undefined }, })