├── .gitignore ├── static ├── favicon.png └── robots.txt ├── src ├── lib │ └── images │ │ ├── svelte-welcome.png │ │ ├── svelte-welcome.webp │ │ ├── github.svg │ │ └── svelte-logo.svg ├── routes │ ├── +page.js │ ├── about │ │ ├── +page.js │ │ └── +page.svelte │ ├── sverdle │ │ ├── how-to-play │ │ │ ├── +page.js │ │ │ └── +page.svelte │ │ ├── reduced-motion.js │ │ ├── +page.server.js │ │ ├── game.js │ │ └── +page.svelte │ ├── +layout.svelte │ ├── +page.svelte │ ├── styles.css │ ├── Counter.svelte │ └── Header.svelte ├── app.d.ts └── app.html ├── vite.config.js ├── render.yaml ├── svelte.config.js ├── jsconfig.json ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/render-examples/sveltekit/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/lib/images/svelte-welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/render-examples/sveltekit/HEAD/src/lib/images/svelte-welcome.png -------------------------------------------------------------------------------- /src/lib/images/svelte-welcome.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/render-examples/sveltekit/HEAD/src/lib/images/svelte-welcome.webp -------------------------------------------------------------------------------- /src/routes/+page.js: -------------------------------------------------------------------------------- 1 | // since there's no dynamic data here, we can prerender 2 | // it so that it gets served as a static asset in production 3 | export const prerender = true; 4 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import { defineConfig } from "vite"; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | }); 7 | -------------------------------------------------------------------------------- /render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: sveltekit 4 | runtime: node 5 | buildCommand: npm install && npm run build 6 | startCommand: node build/index.js 7 | autoDeploy: false 8 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-node"; 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | kit: { 6 | adapter: adapter(), 7 | }, 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/routes/about/+page.js: -------------------------------------------------------------------------------- 1 | import { dev } from "$app/environment"; 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement 5 | export const csr = dev; 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in production 9 | export const prerender = true; 10 | -------------------------------------------------------------------------------- /src/routes/sverdle/how-to-play/+page.js: -------------------------------------------------------------------------------- 1 | import { dev } from "$app/environment"; 2 | 3 | // we don't need any JS on this page, though we'll load 4 | // it in dev so that we get hot module replacement 5 | export const csr = dev; 6 | 7 | // since there's no dynamic data here, we can prerender 8 | // it so that it gets served as a static asset in production 9 | export const prerender = true; 10 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | }, 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit", 3 | "scripts": { 4 | "dev": "vite dev", 5 | "build": "vite build", 6 | "preview": "vite preview", 7 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 8 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" 9 | }, 10 | "devDependencies": { 11 | "@fontsource/fira-mono": "^4.5.10", 12 | "@neoconfetti/svelte": "^1.0.0", 13 | "@sveltejs/adapter-node": "^5.3.2", 14 | "@sveltejs/kit": "^2.39.1", 15 | "@sveltejs/vite-plugin-svelte": "^4.0.4", 16 | "svelte": "^5.38.10", 17 | "svelte-check": "^4.3.1", 18 | "typescript": "^5.9.2", 19 | "vite": "^5.4.20" 20 | }, 21 | "type": "module" 22 | } 23 | -------------------------------------------------------------------------------- /src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | About 3 | 4 | 5 | 6 |
7 |

About this app

8 | 9 |

10 | This is a SvelteKit app. You can make your 11 | own by typing the following into your command line and following the prompts: 12 |

13 | 14 |
npm create svelte@latest
15 | 16 |

17 | The page you're looking at is purely static HTML, with no client-side 18 | interactivity needed. Because of that, we don't need to load any JavaScript. 19 | Try viewing the page's source, or opening the devtools network panel and 20 | reloading. 21 |

22 | 23 |

24 | The Sverdle page illustrates SvelteKit's data loading 25 | and form handling. Try using it with JavaScript disabled! 26 |

27 |
28 | -------------------------------------------------------------------------------- /src/routes/sverdle/reduced-motion.js: -------------------------------------------------------------------------------- 1 | import { readable } from "svelte/store"; 2 | import { browser } from "$app/environment"; 3 | 4 | const reduced_motion_query = "(prefers-reduced-motion: reduce)"; 5 | 6 | const get_initial_motion_preference = () => { 7 | if (!browser) return false; 8 | return window.matchMedia(reduced_motion_query).matches; 9 | }; 10 | 11 | export const reduced_motion = readable( 12 | get_initial_motion_preference(), 13 | (set) => { 14 | if (browser) { 15 | /** 16 | * @param {MediaQueryListEvent} event 17 | */ 18 | const set_reduced_motion = (event) => { 19 | set(event.matches); 20 | }; 21 | const media_query_list = window.matchMedia(reduced_motion_query); 22 | media_query_list.addEventListener("change", set_reduced_motion); 23 | 24 | return () => { 25 | media_query_list.removeEventListener("change", set_reduced_motion); 26 | }; 27 | } 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 | 13 | 18 |
19 | 20 | 56 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | Home 9 | 10 | 11 | 12 |
13 |

14 | 15 | 16 | 17 | Welcome 18 | 19 | 20 | 21 | to your new
SvelteKit app 22 |

23 | 24 |

25 | try editing src/routes/+page.svelte 26 |

27 | 28 | 29 |
30 | 31 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sveltekit 2 | 3 | This repo contains code for a SvelteKit application generated using the [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte) package. 4 | 5 | To create your own SvelteKit project, you can either 6 | 7 | - [Create your own repo from this template](https://github.com/render-examples/sveltekit/generate) and modify it for your needs 8 | - Create a new SvelteKit project by following the [SvelteKit Getting Started Guide](https://kit.svelte.dev/docs) and then making a few small modifications as shown in [this commit](https://github.com/render-examples/sveltekit/commit/3ea50803f118da041745fd8cb51094972ac87f3c) to deploy it to Render as a Node.js service. 9 | 10 | ## Developing 11 | 12 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 13 | 14 | ```bash 15 | npm run dev 16 | 17 | # or start the server and open the app in a new browser tab 18 | npm run dev -- --open 19 | ``` 20 | 21 | ## Building 22 | 23 | ```bash 24 | npm run build 25 | ``` 26 | 27 | > You can preview the built app with `npm run preview`. This should _not_ be used to serve your app in production. 28 | 29 | ## Deploying to Render 30 | 31 | Follow the deploy instructions at https://render.com/docs/deploy-sveltekit or click the button below: 32 | 33 | 34 | Deploy to Render 35 | 36 | -------------------------------------------------------------------------------- /src/lib/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 16 | -------------------------------------------------------------------------------- /src/lib/images/svelte-logo.svg: -------------------------------------------------------------------------------- 1 | svelte-logo -------------------------------------------------------------------------------- /src/routes/sverdle/+page.server.js: -------------------------------------------------------------------------------- 1 | import { fail } from "@sveltejs/kit"; 2 | import { Game } from "./game"; 3 | 4 | /** @satisfies {import('./$types').PageServerLoad} */ 5 | export const load = ({ cookies }) => { 6 | const game = new Game(cookies.get("sverdle")); 7 | 8 | return { 9 | /** 10 | * The player's guessed words so far 11 | */ 12 | guesses: game.guesses, 13 | 14 | /** 15 | * An array of strings like '__x_c' corresponding to the guesses, where 'x' means 16 | * an exact match, and 'c' means a close match (right letter, wrong place) 17 | */ 18 | answers: game.answers, 19 | 20 | /** 21 | * The correct answer, revealed if the game is over 22 | */ 23 | answer: game.answers.length >= 6 ? game.answer : null, 24 | }; 25 | }; 26 | 27 | /** @satisfies {import('./$types').Actions} */ 28 | export const actions = { 29 | /** 30 | * Modify game state in reaction to a keypress. If client-side JavaScript 31 | * is available, this will happen in the browser instead of here 32 | */ 33 | update: async ({ request, cookies }) => { 34 | const game = new Game(cookies.get("sverdle")); 35 | 36 | const data = await request.formData(); 37 | const key = data.get("key"); 38 | 39 | const i = game.answers.length; 40 | 41 | if (key === "backspace") { 42 | game.guesses[i] = game.guesses[i].slice(0, -1); 43 | } else { 44 | game.guesses[i] += key; 45 | } 46 | 47 | cookies.set("sverdle", game.toString(), { path: "/" }); 48 | }, 49 | 50 | /** 51 | * Modify game state in reaction to a guessed word. This logic always runs on 52 | * the server, so that people can't cheat by peeking at the JavaScript 53 | */ 54 | enter: async ({ request, cookies }) => { 55 | const game = new Game(cookies.get("sverdle")); 56 | 57 | const data = await request.formData(); 58 | const guess = /** @type {string[]} */ (data.getAll("guess")); 59 | 60 | if (!game.enter(guess)) { 61 | return fail(400, { badGuess: true }); 62 | } 63 | 64 | cookies.set("sverdle", game.toString(), { path: "/" }); 65 | }, 66 | 67 | restart: async ({ cookies }) => { 68 | cookies.delete("sverdle", { path: "/" }); 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/routes/sverdle/game.js: -------------------------------------------------------------------------------- 1 | import { words, allowed } from "./words.server"; 2 | 3 | export class Game { 4 | /** 5 | * Create a game object from the player's cookie, or initialise a new game 6 | * @param {string | undefined} serialized 7 | */ 8 | constructor(serialized = undefined) { 9 | if (serialized) { 10 | const [index, guesses, answers] = serialized.split("-"); 11 | 12 | this.index = +index; 13 | this.guesses = guesses ? guesses.split(" ") : []; 14 | this.answers = answers ? answers.split(" ") : []; 15 | } else { 16 | this.index = Math.floor(Math.random() * words.length); 17 | this.guesses = ["", "", "", "", "", ""]; 18 | this.answers = /** @type {string[]} */ ([]); 19 | } 20 | 21 | this.answer = words[this.index]; 22 | } 23 | 24 | /** 25 | * Update game state based on a guess of a five-letter word. Returns 26 | * true if the guess was valid, false otherwise 27 | * @param {string[]} letters 28 | */ 29 | enter(letters) { 30 | const word = letters.join(""); 31 | const valid = allowed.has(word); 32 | 33 | if (!valid) return false; 34 | 35 | this.guesses[this.answers.length] = word; 36 | 37 | const available = Array.from(this.answer); 38 | const answer = Array(5).fill("_"); 39 | 40 | // first, find exact matches 41 | for (let i = 0; i < 5; i += 1) { 42 | if (letters[i] === available[i]) { 43 | answer[i] = "x"; 44 | available[i] = " "; 45 | } 46 | } 47 | 48 | // then find close matches (this has to happen 49 | // in a second step, otherwise an early close 50 | // match can prevent a later exact match) 51 | for (let i = 0; i < 5; i += 1) { 52 | if (answer[i] === "_") { 53 | const index = available.indexOf(letters[i]); 54 | if (index !== -1) { 55 | answer[i] = "c"; 56 | available[index] = " "; 57 | } 58 | } 59 | } 60 | 61 | this.answers.push(answer.join("")); 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * Serialize game state so it can be set as a cookie 68 | */ 69 | toString() { 70 | return `${this.index}-${this.guesses.join(" ")}-${this.answers.join(" ")}`; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/routes/styles.css: -------------------------------------------------------------------------------- 1 | @import "@fontsource/fira-mono"; 2 | 3 | :root { 4 | --font-body: Arial, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 5 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 6 | --font-mono: "Fira Mono", monospace; 7 | --color-bg-0: rgb(202, 216, 228); 8 | --color-bg-1: hsl(209, 36%, 86%); 9 | --color-bg-2: hsl(224, 44%, 95%); 10 | --color-theme-1: #ff3e00; 11 | --color-theme-2: #4075a6; 12 | --color-text: rgba(0, 0, 0, 0.7); 13 | --column-width: 42rem; 14 | --column-margin-top: 4rem; 15 | font-family: var(--font-body); 16 | color: var(--color-text); 17 | } 18 | 19 | body { 20 | min-height: 100vh; 21 | margin: 0; 22 | background-attachment: fixed; 23 | background-color: var(--color-bg-1); 24 | background-size: 100vw 100vh; 25 | background-image: radial-gradient( 26 | 50% 50% at 50% 50%, 27 | rgba(255, 255, 255, 0.75) 0%, 28 | rgba(255, 255, 255, 0) 100% 29 | ), 30 | linear-gradient( 31 | 180deg, 32 | var(--color-bg-0) 0%, 33 | var(--color-bg-1) 15%, 34 | var(--color-bg-2) 50% 35 | ); 36 | } 37 | 38 | h1, 39 | h2, 40 | p { 41 | font-weight: 400; 42 | } 43 | 44 | p { 45 | line-height: 1.5; 46 | } 47 | 48 | a { 49 | color: var(--color-theme-1); 50 | text-decoration: none; 51 | } 52 | 53 | a:hover { 54 | text-decoration: underline; 55 | } 56 | 57 | h1 { 58 | font-size: 2rem; 59 | text-align: center; 60 | } 61 | 62 | h2 { 63 | font-size: 1rem; 64 | } 65 | 66 | pre { 67 | font-size: 16px; 68 | font-family: var(--font-mono); 69 | background-color: rgba(255, 255, 255, 0.45); 70 | border-radius: 3px; 71 | box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); 72 | padding: 0.5em; 73 | overflow-x: auto; 74 | color: var(--color-text); 75 | } 76 | 77 | .text-column { 78 | display: flex; 79 | max-width: 48rem; 80 | flex: 0.6; 81 | flex-direction: column; 82 | justify-content: center; 83 | margin: 0 auto; 84 | } 85 | 86 | input, 87 | button { 88 | font-size: inherit; 89 | font-family: inherit; 90 | } 91 | 92 | button:focus:not(:focus-visible) { 93 | outline: none; 94 | } 95 | 96 | @media (min-width: 720px) { 97 | h1 { 98 | font-size: 2.4rem; 99 | } 100 | } 101 | 102 | .visually-hidden { 103 | border: 0; 104 | clip: rect(0 0 0 0); 105 | height: auto; 106 | margin: 0; 107 | overflow: hidden; 108 | padding: 0; 109 | position: absolute; 110 | width: 1px; 111 | white-space: nowrap; 112 | } 113 | -------------------------------------------------------------------------------- /src/routes/sverdle/how-to-play/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | How to play Sverdle 3 | 4 | 5 | 6 |
7 |

How to play Sverdle

8 | 9 |

10 | Sverdle is a clone of Wordle, the word guessing game. To play, enter a five-letter English word. For 13 | example: 14 |

15 | 16 |
17 | r 18 | i 19 | t 20 | z 21 | y 22 |
23 | 24 |

25 | The y is in the right place. 26 | r 27 | and 28 | t 29 | are the right letters, but in the wrong place. The other letters are wrong, and 30 | can be discarded. Let's make another guess: 31 |

32 | 33 |
34 | p 35 | a 36 | r 37 | t 38 | y 39 |
40 | 41 |

42 | This time we guessed right! You have six guesses to get the 43 | word. 44 |

45 | 46 |

47 | Unlike the original Wordle, Sverdle runs on the server instead of in the 48 | browser, making it impossible to cheat. It uses <form> and 49 | cookies to submit data, meaning you can even play with JavaScript disabled! 50 |

51 |
52 | 53 | 103 | -------------------------------------------------------------------------------- /src/routes/Counter.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 33 | 34 |
35 |
39 | 42 | {Math.floor($displayed_count)} 43 |
44 |
45 | 46 | 54 |
55 | 56 | 122 | -------------------------------------------------------------------------------- /src/routes/Header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | 10 | SvelteKit 11 | 12 |
13 | 14 | 37 | 38 |
39 | 40 | GitHub 41 | 42 |
43 |
44 | 45 | 134 | -------------------------------------------------------------------------------- /src/routes/sverdle/+page.svelte: -------------------------------------------------------------------------------- 1 | 91 | 92 | 93 | 94 | 95 | Sverdle 96 | 97 | 98 | 99 |

Sverdle

100 | 101 |
{ 105 | // prevent default callback from resetting the form 106 | return ({ update }) => { 107 | update({ reset: false }); 108 | }; 109 | }} 110 | > 111 | How to play 112 | 113 |
114 | {#each Array.from(Array(6).keys()) as row (row)} 115 | {@const current = row === i} 116 |

Row {row + 1}

117 |
118 | {#each Array.from(Array(5).keys()) as column (column)} 119 | {@const guess = current ? currentGuess : data.guesses[row]} 120 | {@const answer = data.answers[row]?.[column]} 121 | {@const value = guess?.[column] ?? ""} 122 | {@const selected = current && column === guess.length} 123 | {@const exact = answer === "x"} 124 | {@const close = answer === "c"} 125 | {@const missing = answer === "_"} 126 |
133 | {value} 134 | 135 | {#if exact} 136 | (correct) 137 | {:else if close} 138 | (present) 139 | {:else if missing} 140 | (absent) 141 | {:else} 142 | empty 143 | {/if} 144 | 145 | 146 |
147 | {/each} 148 |
149 | {/each} 150 |
151 | 152 |
153 | {#if won || data.answers.length >= 6} 154 | {#if !won && data.answer} 155 |

the answer was "{data.answer}"

156 | {/if} 157 | 160 | {:else} 161 |
162 | 167 | 168 | 177 | 178 | {#each ["qwertyuiop", "asdfghjkl", "zxcvbnm"] as row} 179 |
180 | {#each row as letter} 181 | 193 | {/each} 194 |
195 | {/each} 196 |
197 | {/if} 198 |
199 |
200 | 201 | {#if won} 202 |
212 | {/if} 213 | 214 | 426 | --------------------------------------------------------------------------------