├── .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 |
--------------------------------------------------------------------------------
/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 |
12 | ${todos.map((todo) => html` - ${todo}
`)}
13 |
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 |
--------------------------------------------------------------------------------