Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ jobs:
if: '!cancelled()'
run: npm run test:coverage

# Vercel serves a file before any rewrite, so a dist/index.html would take / from the landing.
- name: Build
if: '!cancelled()'
run: npm run build
run: |
npm run build
test -f dist/landing.html && test -f dist/app.html && test ! -e dist/index.html

# The examples are Flow's own diagrams: they must parse, and the SVGs next
# to them must be what the renderer draws today.
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,10 @@ production build in another.

## Deployment

The build is a static single page app, so any static host works as long as unknown paths fall back
to `index.html`. `docker/nginx.conf` does that for the container and `vercel.json` for Vercel.
The build is static, so any static host works as long as `/` serves `landing.html` and the
editor's routes (`/new`, `/flow` and below them) fall back to `app.html`. There is deliberately no
`index.html`: Vercel serves a file before any rewrite, so one would take `/` from the landing.
`docker/nginx.conf` does this for the container and `vercel.json` for Vercel.

## Known limits

Expand Down
File renamed without changes.
30 changes: 30 additions & 0 deletions backlog/tasks/fl-97 - Landing-page-at-the-site-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
id: FL-97
title: Landing page at the site root
status: Done
assignee: []
created_date: '2026-10-07 09:35'
updated_date: '2026-10-07 09:38'
labels:
- bug
dependencies: []
priority: high
ordinal: 27000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->

isketch.online/ serves the editor, not the landing page: Vercel serves dist/index.html for / before the rewrite to /landing.html applies. Build the editor as app.html so / falls through to the landing, and /new and /flow rewrite to the app.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria

<!-- AC:BEGIN -->

- [x] #1 isketch.online/ shows the landing page
- [x] #2 /new, /new/node/:id and /flow still open the editor, in Vercel, nginx, vite dev and preview
- [x] #3 The service worker still opens the app offline

<!-- AC:END -->
4 changes: 2 additions & 2 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ server {

# Client-side routes have no file on disk; without this a refresh returns 404.
location / {
try_files $uri $uri/ /index.html;
try_files $uri $uri/ /app.html;
}

location ~* \.(js|css|svg|png|jpg|jpeg|gif|ico|woff2)$ {
Expand All @@ -30,7 +30,7 @@ server {
}

# Not cached, or a deploy stays invisible until browsers drop their copy.
location = /index.html {
location = /app.html {
add_header Cache-Control "no-cache";
}

Expand Down
8 changes: 4 additions & 4 deletions src/pwa/__tests__/serviceWorker.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { PUBLIC_SHELL, serviceWorkerSource, shellFiles } from '../serviceWorker.
describe('shellFiles', () => {
it('lists the build and the public shell once each, without maps or itself', () => {
const files = shellFiles([
'index.html',
'app.html',
'assets/index-abc.js',
'assets/index-abc.js.map',
'sw.js',
])
expect(files).toContain('/index.html')
expect(files).toContain('/app.html')
expect(files).toContain('/assets/index-abc.js')
expect(files).not.toContain('/assets/index-abc.js.map')
expect(files).not.toContain('/sw.js')
Expand All @@ -20,11 +20,11 @@ describe('shellFiles', () => {
})

describe('serviceWorkerSource', () => {
const source = serviceWorkerSource('v1', ['/index.html', '/assets/a.js'])
const source = serviceWorkerSource('v1', ['/app.html', '/assets/a.js'])

it('names its cache after the build, and caches the shell on install', () => {
expect(source).toContain('const CACHE = "isketch-v1"')
expect(source).toContain('const SHELL = ["/index.html","/assets/a.js"]')
expect(source).toContain('const SHELL = ["/app.html","/assets/a.js"]')
expect(source).toContain('cache.addAll(SHELL)')
})

Expand Down
2 changes: 1 addition & 1 deletion src/pwa/serviceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function serviceWorkerSource(version, files) {
return `// isketch service worker, build ${version}. Written by the build; do not edit.
const CACHE = ${JSON.stringify(`isketch-${version}`)}
const SHELL = ${JSON.stringify(files)}
const APP = '/index.html'
const APP = '/app.html'
// A module import sends Origin and the install did not; the names are hashed, so Vary is moot.
const MATCH = { ignoreVary: true }

Expand Down
8 changes: 4 additions & 4 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{ "source": "/", "destination": "/landing.html" },
{ "source": "/new", "destination": "/index.html" },
{ "source": "/new/:path*", "destination": "/index.html" },
{ "source": "/flow", "destination": "/index.html" },
{ "source": "/flow/:path*", "destination": "/index.html" }
{ "source": "/new", "destination": "/app.html" },
{ "source": "/new/:path*", "destination": "/app.html" },
{ "source": "/flow", "destination": "/app.html" },
{ "source": "/flow/:path*", "destination": "/app.html" }
]
}
11 changes: 9 additions & 2 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import tailwindcss from '@tailwindcss/vite'
import { analyticsScript } from './src/api/analytics.js'
import { serviceWorkerSource, shellFiles } from './src/pwa/serviceWorker.js'

/** Where links point when the head is built; %SITE_URL% in index.html. */
/** Where links point when the head is built; %SITE_URL% in app.html. */
const SITE_URL = (process.env.VITE_SITE_URL ?? 'https://isketch.online').replace(/\/+$/, '')

/** The editor's routes, served app.html as vercel.json and nginx do. */
const APP_ROUTE = /^\/(new|flow)(\/|$)/

/** Serve the landing at / and content pages at their clean URL, as production does. */
function serveLanding(server) {
server.middlewares.use((req, res, next) => {
Expand All @@ -20,6 +23,10 @@ function serveLanding(server) {
req.url = '/landing.html' + url.search
return next()
}
if (APP_ROUTE.test(url.pathname)) {
req.url = '/app.html' + url.search
return next()
}
// A directory with an index, such as /docs/format: its page, not the SPA.
const hasIndex =
!url.pathname.endsWith('/') &&
Expand Down Expand Up @@ -103,7 +110,7 @@ export default defineConfig({
build: {
rollupOptions: {
input: {
index: fileURLToPath(new URL('./index.html', import.meta.url)),
app: fileURLToPath(new URL('./app.html', import.meta.url)),
convert: fileURLToPath(new URL('./convert.html', import.meta.url)),
},
},
Expand Down
Loading