One template, both sides

Render the first paint on the server and every update in the browser, from one template.

The usual cost of server-side rendering is keeping two implementations in step — one that builds the first page and another that redraws it afterwards. Here both are the same string compiled by the same library.

<style>
    body { font: 14px/1.5 system-ui, sans-serif; margin: 0; color: #24292f; }
    table { border-collapse: collapse; width: 100%; }
    th, td { padding: 6px 10px; border-bottom: 1px solid #eaeef2; text-align: left; }
    th { font-weight: 600; background: #f6f8fa; font-size: 12px;
         text-transform: uppercase; letter-spacing: .03em; color: #57606a; }
    td.num { text-align: right; font-variant-numeric: tabular-nums; }
    .up { color: #1a7f37; } .down { color: #cf222e; }
    button { font: inherit; padding: 6px 14px; margin-top: 14px; cursor: pointer;
             border: 1px solid #d8dee4; border-radius: 6px; background: #f6f8fa; }
    .stamp { font-size: 12px; color: #8c959f; margin-top: 8px; }
</style>

<div id="app"></div>
<button onclick="tick()">New prices from the server</button>
<div class="stamp" id="stamp"></div>

<script src="https://cdn.jsdelivr.net/gh/richdafunk/hashJS@v1.3.6/hashJS.js"></script>
<script>
    // One template. In a real project this is a file that the server reads
    // from disk and the browser fetches over HTTP — the same bytes both times.
    const TEMPLATE = [
        '<table>',
        '    <thead><tr><th>Ticker</th><th>Name</th><th class="num">Price</th><th class="num">Change</th></tr></thead>',
        '    <tbody>',
        '    #for(let r of rows) {#',
        '        <tr>',
        '            <td><a href="/quote/#r.ticker#">#r.ticker#</a></td>',
        '            <td>#htmlEncode(r.name)#</td>',
        '            <td class="num">#r.price.toFixed(2)#</td>',
        '            <td class="num #r.change >= 0 ? \'up\' : \'down\'#">#r.change > 0 ? "+" : ""##r.change.toFixed(2)#%</td>',
        '        </tr>',
        '    #}#',
        '    </tbody>',
        '</table>'
    ].join('\n');

    const rows = [
        { ticker: 'EQNR', name: 'Equinor ASA',     price: 271.40, change:  1.24 },
        { ticker: 'DNB',  name: 'DNB Bank ASA',    price: 224.90, change: -0.38 },
        { ticker: 'TEL',  name: 'Telenor ASA',     price: 138.15, change:  0.62 },
        { ticker: 'MOWI', name: 'Mowi ASA',        price: 196.05, change: -1.10 }
    ];

    // Compile once. The server does this at startup; the browser does it on
    // load. Either way it is the same template producing the same function.
    const render = hashJS.compile(TEMPLATE);
    const app = document.getElementById('app');

    // 1. First paint. On a server this string is already in the response body
    //    before the browser has run a line of JavaScript.
    app.innerHTML = render({ rows: rows });
    stampIt('first paint — the string a server would have sent');

    // 2. Every update afterwards. Same compiled template, new data, and no
    //    second implementation to keep in step with the first.
    function tick() {
        for (const r of rows) {
            const move = (Math.random() - 0.48) * 4;
            r.price = Math.max(1, r.price + move);
            r.change = move;
        }
        app.innerHTML = render({ rows: rows });
        stampIt('re-rendered in the browser from that same template');
    }

    function stampIt(text) {
        document.getElementById('stamp').textContent = text;
    }
</script>
Output result:

hashJS.compile returns a function. The server calls it to fill the response body; the browser calls it again whenever the data changes. Neither side knows or cares which one ran first.

That is the practical argument for one language across both tiers. A template written for the browser already runs on a server, and a template written for a server already runs in the browser — there is no port, no second dialect and no build step between them.

The change column shows an expression inside an attribute and two expressions side by side: #r.change > 0 ? "+" : ""##r.change.toFixed(2)#. Substitution happens on the template text before any of it is markup, so position places no restrictions on where an expression may go.