├── .gitignore ├── assets ├── css │ ├── theme.css │ ├── utils.css │ ├── base.css │ ├── layout.css │ ├── main.css │ ├── reset.css │ └── components.css └── js │ ├── main.js │ ├── app.js │ └── html-include.js ├── includes ├── github.html ├── footer.html └── header.html ├── package.json ├── README.md ├── index.html ├── todo.html └── _worker.js /.gitignore: -------------------------------------------------------------------------------- 1 | .wrangler 2 | node_modules -------------------------------------------------------------------------------- /assets/css/theme.css: -------------------------------------------------------------------------------- 1 | /* dark/light theme styles */ 2 | -------------------------------------------------------------------------------- /includes/github.html: -------------------------------------------------------------------------------- 1 | view source on Github -------------------------------------------------------------------------------- /assets/css/utils.css: -------------------------------------------------------------------------------- 1 | .u-bold { 2 | font-weight: var(--font-weight-7) !important; 3 | } 4 | -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | import './html-include.js' 2 | 3 | if (document.querySelector('#app')) { 4 | import('./app.js') 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "web-dev-server --open --watch" 4 | }, 5 | "devDependencies": { 6 | "@web/dev-server": "^0.4.6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /assets/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: var(--font-neo-grotesque); 3 | font-size: var(--font-size-1); 4 | font-weight: var(--font-weight-4); 5 | line-height: var(--font-lineheight-3); 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Buildless Web Development 2 | 3 | A quick experiment on how modern web development without a build step could look like. 4 | Read more: [https://mxb.dev/blog/buildless](https://mxb.dev/blog/buildless) 5 | -------------------------------------------------------------------------------- /includes/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/css/layout.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | flex-direction: column; 4 | min-height: 100vh; 5 | } 6 | 7 | .container { 8 | padding-inline: 1rem; 9 | max-inline-size: 1080px; 10 | margin-inline: auto; 11 | } 12 | 13 | main { 14 | width: 100%; 15 | padding-block: 2rem; 16 | flex: 1 0 auto; 17 | } 18 | -------------------------------------------------------------------------------- /includes/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | zerobuild 5 | 6 | 12 | 13 |
14 |
-------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | @layer default, layout, components, utils, theme; 2 | 3 | @import 'https://unpkg.com/open-props' layer(default); 4 | @import 'reset.css' layer(default); 5 | @import 'base.css' layer(default); 6 | @import 'layout.css' layer(layout); 7 | @import 'components.css' layer(components); 8 | @import 'utils.css' layer(utils); 9 | @import 'theme.css' layer(theme); 10 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import { render, Component, html } from 'preact/htm' 2 | 3 | class App extends Component { 4 | addTodo() { 5 | const { todos = [] } = this.state 6 | this.setState({ todos: todos.concat(`Item ${todos.length + 1}`) }) 7 | } 8 | render({ page }, { todos = [] }) { 9 | return html` 10 |
11 | 14 | 15 |
16 | ` 17 | } 18 | } 19 | 20 | render(html`<${App} />`, document.getElementById('app')) 21 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zero Build 7 | 8 | 15 | 16 | 17 | 18 | 19 |
20 |

Look Ma, No Build!

21 |
22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Zero Build 7 | 8 | 15 | 16 | 17 | 18 | 19 |
20 |

So much to do

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /assets/css/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | 1. Use a more-intuitive box-sizing model. 3 | */ 4 | *, 5 | *::before, 6 | *::after { 7 | box-sizing: border-box; 8 | } 9 | /* 10 | 2. Remove default spacing 11 | */ 12 | * { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | /* 17 | Typographic tweaks! 18 | 3. Add accessible line-height 19 | 4. Improve text rendering 20 | */ 21 | body { 22 | line-height: 1.5; 23 | -webkit-font-smoothing: antialiased; 24 | } 25 | /* 26 | 5. Improve media defaults 27 | */ 28 | img, 29 | picture, 30 | video, 31 | canvas, 32 | svg { 33 | display: block; 34 | max-width: 100%; 35 | } 36 | /* 37 | 6. Remove built-in form typography styles 38 | */ 39 | input, 40 | button, 41 | textarea, 42 | select { 43 | font: inherit; 44 | } 45 | /* 46 | 7. Avoid text overflows 47 | */ 48 | p, 49 | h1, 50 | h2, 51 | h3, 52 | h4, 53 | h5, 54 | h6 { 55 | overflow-wrap: break-word; 56 | } 57 | -------------------------------------------------------------------------------- /assets/css/components.css: -------------------------------------------------------------------------------- 1 | .header { 2 | color: white; 3 | background-color: var(--purple-8); 4 | box-shadow: var(--shadow-2); 5 | 6 | & a { 7 | color: inherit; 8 | text-decoration: none; 9 | } 10 | 11 | & .container { 12 | display: flex; 13 | align-items: center; 14 | justify-content: space-between; 15 | padding-block: 1rem; 16 | } 17 | } 18 | 19 | .nav { 20 | & ul { 21 | display: flex; 22 | justify-content: flex-end; 23 | list-style: none; 24 | } 25 | & a { 26 | display: block; 27 | padding: 0.5rem 1rem; 28 | } 29 | } 30 | 31 | .footer { 32 | color: var(--gray-2); 33 | background-color: var(--gray-8); 34 | 35 | & a { 36 | color: inherit; 37 | } 38 | 39 | & .container { 40 | display: flex; 41 | align-items: center; 42 | justify-content: space-between; 43 | padding-block: 2rem; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /assets/js/html-include.js: -------------------------------------------------------------------------------- 1 | export class HTMLIncludeElement extends HTMLElement { 2 | get src() { 3 | return this.getAttribute('src') 4 | } 5 | 6 | async connectedCallback() { 7 | if (this.src) { 8 | let text = '' 9 | try { 10 | const response = await fetch(this.src, { mode: 'cors' }) 11 | if (!response.ok) { 12 | throw new Error( 13 | `html-include fetch failed: ${response.statusText}` 14 | ) 15 | } 16 | text = await response.text() 17 | } catch (e) { 18 | console.error(e) 19 | } 20 | 21 | this.replaceContent(text) 22 | this.dispatchEvent(new Event('load')) 23 | } 24 | } 25 | 26 | replaceContent(text) { 27 | const template = document.createElement('template') 28 | template.innerHTML = text 29 | this.before(template.content) 30 | this.remove() 31 | } 32 | } 33 | 34 | if ('customElements' in window) { 35 | customElements.define('html-include', HTMLIncludeElement) 36 | } 37 | 38 | export default HTMLIncludeElement 39 | -------------------------------------------------------------------------------- /_worker.js: -------------------------------------------------------------------------------- 1 | class HTMLIncludeElementHandler { 2 | constructor(origin) { 3 | this.origin = origin 4 | } 5 | 6 | async element(element) { 7 | const src = element.getAttribute('src') 8 | if (src) { 9 | try { 10 | const content = await this.fetchContents(src) 11 | if (content) { 12 | element.before(content, { html: true }) 13 | element.remove() 14 | } 15 | } catch (err) { 16 | console.error('could not replace element', err) 17 | } 18 | } 19 | } 20 | 21 | async fetchContents(src) { 22 | const url = new URL(src, this.origin).toString() 23 | const response = await fetch(url, { 24 | method: 'GET', 25 | headers: { 26 | 'user-agent': 'cloudflare' 27 | } 28 | }) 29 | const content = await response.text() 30 | return content 31 | } 32 | } 33 | 34 | export default { 35 | async fetch(request, env) { 36 | const response = await env.ASSETS.fetch(request) 37 | const contentType = response.headers.get('Content-Type') 38 | 39 | if (!contentType || !contentType.startsWith('text/html')) { 40 | return response 41 | } 42 | 43 | const origin = new URL(request.url).origin 44 | const rewriter = new HTMLRewriter().on( 45 | 'html-include', 46 | new HTMLIncludeElementHandler(origin) 47 | ) 48 | 49 | return rewriter.transform(response) 50 | } 51 | } 52 | --------------------------------------------------------------------------------