<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 周大良</title>
    <description>The latest articles on DEV Community by 周大良 (@_14afa706ec20e3c3de3a3).</description>
    <link>https://dev.to/_14afa706ec20e3c3de3a3</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4102396%2F3fb3eedf-5b76-4a07-bac2-9daa2af82f82.png</url>
      <title>DEV Community: 周大良</title>
      <link>https://dev.to/_14afa706ec20e3c3de3a3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_14afa706ec20e3c3de3a3"/>
    <language>en</language>
    <item>
      <title>How I Built a 400+ Game Static Site for Schools (No Framework, No Build Step)</title>
      <dc:creator>周大良</dc:creator>
      <pubDate>Mon, 31 Aug 2026 08:36:15 +0000</pubDate>
      <link>https://dev.to/_14afa706ec20e3c3de3a3/how-i-built-a-400-game-static-site-for-schools-no-framework-no-build-step-apg</link>
      <guid>https://dev.to/_14afa706ec20e3c3de3a3/how-i-built-a-400-game-static-site-for-schools-no-framework-no-build-step-apg</guid>
      <description>&lt;p&gt;I wanted a free, instant game site that a student could open on a school Chromebook and play in seconds — no download, no signup, nothing for IT to install. The simplest way to get there was to build it as a plain static site with zero frameworks and zero build step. Here's how it works and why I made those choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core constraint
&lt;/h2&gt;

&lt;p&gt;The main constraint was &lt;em&gt;school computers&lt;/em&gt;. That means Chromebooks, locked-down networks, and often slow connections. So the site had to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lightweight (fast on modest hardware)&lt;/li&gt;
&lt;li&gt;fully browser-based (nothing to install)&lt;/li&gt;
&lt;li&gt;responsive (phone, tablet, laptop)&lt;/li&gt;
&lt;li&gt;easy to host and cheap to run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That pushed me toward a plain static site: HTML, CSS, and vanilla JavaScript. No React, no Next.js, no bundler, no server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The big idea: embed games, don't rebuild them
&lt;/h2&gt;

&lt;p&gt;A big part of keeping the site lightweight is that I don't host or reimplement the games. Each game is loaded in an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; pointing to its own hosted URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"game-frame"&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://example-host/game"&lt;/span&gt;
  &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"2048 Classic"&lt;/span&gt;
  &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100%"&lt;/span&gt;
  &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"540"&lt;/span&gt;
  &lt;span class="na"&gt;frameborder=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;
  &lt;span class="na"&gt;allow=&lt;/span&gt;&lt;span class="s"&gt;"pointer-lock; fullscreen; autoplay; encrypted-media; clipboard-write"&lt;/span&gt;
  &lt;span class="na"&gt;allowfullscreen&lt;/span&gt;
  &lt;span class="na"&gt;loading=&lt;/span&gt;&lt;span class="s"&gt;"lazy"&lt;/span&gt;
  &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means the game code lives elsewhere, and my page just provides a clean, consistent frame around it. The &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; gets lazy-loaded, and the surrounding layout stays small. For a real production launch, you'd want to verify each source's embed terms or use an official embed from a licensed game platform — but architecturally, iframes keep the site fast and cheap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating hundreds of pages from data
&lt;/h2&gt;

&lt;p&gt;The tricky part was scale. I didn't want to hand-write hundreds of near-identical pages. So I keep the game catalog as data and generate the pages with one script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tools/generate_game_pages.mjs&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GAME_PAGES&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./game-pages-data.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CATALOG_GAME_PAGES&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./catalog-game-pages-data.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;REMAINING_GAME_PAGES&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./remaining-games-data.mjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ASSET_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;20260828p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each game is just an object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2048-classic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2048 Classic&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;embedUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example-host/game&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script orders the games by a priority list, then renders a page for each one and writes it to the &lt;code&gt;games/&lt;/code&gt; directory. Re-run the script and the whole site regenerates.&lt;/p&gt;

&lt;p&gt;One nice detail: the asset version constant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stylesheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../assets/css/style.css?v=20260828p&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every CSS/JS reference is suffixed with &lt;code&gt;?v=20260828p&lt;/code&gt;, so when I update assets I bump the constant and visitors get the new files instead of a stale cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  The layout
&lt;/h2&gt;

&lt;p&gt;The site uses a two-column layout: a left sidebar listing the games, and a larger main area on the right where the game loads. It's the kind of clean, predictable layout that works for a "pick a game and play" flow.&lt;/p&gt;

&lt;p&gt;On mobile the sidebar collapses so it doesn't cover the game. Keeping that simple and predictable was more important than anything fancy.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO as a first-class concern
&lt;/h2&gt;

&lt;p&gt;A game site lives on search traffic, so SEO wasn't an afterthought. Every generated page gets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a unique &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;meta name="description"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a self-referencing &lt;code&gt;&amp;lt;link rel="canonical"&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open Graph tags (&lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:type&lt;/code&gt;, &lt;code&gt;og:url&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;robots&lt;/code&gt; meta set to &lt;code&gt;index, follow&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;structured data via JSON-LD (&lt;code&gt;WebSite&lt;/code&gt;, plus &lt;code&gt;VideoGame&lt;/code&gt; for the game area and &lt;code&gt;FAQPage&lt;/code&gt; for the FAQ)&lt;/li&gt;
&lt;li&gt;one &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; per page, with a clean &lt;code&gt;H2&lt;/code&gt;/&lt;code&gt;H3&lt;/code&gt; hierarchy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site also ships a &lt;code&gt;robots.txt&lt;/code&gt; that allows all crawlers and declares &lt;code&gt;sitemap.xml&lt;/code&gt;, and a &lt;code&gt;sitemap.xml&lt;/code&gt; that lists every indexable URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User-agent: *
Allow: /

Sitemap: https://classroom-game.com/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance and analytics
&lt;/h2&gt;

&lt;p&gt;Since everything is static, performance is easy. No heavy scripts, no external font library, no big images. Google Analytics is loaded with &lt;code&gt;async&lt;/code&gt; so it doesn't block rendering, and it sits right after the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; in the head. Because the analytics and the game frame are independent, the analytics script never delays the game.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;async&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="nx"&gt;dataLayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;);}&lt;/span&gt;
  &lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;G-XXXXXXXXXX&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;p&gt;The whole thing is a folder of static files, so deploying is trivial. I serve it from a static host and put Cloudflare in front for DNS and caching. No server to maintain, no build pipeline, no database.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The catalog grows over time, so the main workflow is "add a game to the data + re-run the generator." Beyond that, I'd like to improve internal linking between games, make the game pages richer, and eventually add more focused content around the games to help teachers find what they need.&lt;/p&gt;

&lt;p&gt;Building it this way kept the site fast, cheap, and easy to maintain — and let me focus on the thing that actually matters: giving a student a game they can play in the next ten seconds.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>html</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
