Technical Case Study: The Digital Craft Portfolio

Digital Craft Tbilisi

We approached the development of digital-craft-tbilisi.site as a product where every technical decision is justified, every byte of code is earned, and the resulting architecture scales without scaling costs.

In this case study, we break down the system from the inside: from the concept and stack selection to the specific algorithms running in production right now.


Why We Rejected Standard CMS Platforms

The first question we asked ourselves at the start: why write anything custom at all? WordPress has been around for twenty years, Tilda solves the problem in a few hours, Webflow gives visual control over layout. The arguments against a custom solution are obvious.

The arguments in favour turned out to be stronger.

Problem One: Speed and Bloated Code

WordPress with a standard installation generates each page dynamically on every request: a database query, PHP execution, HTML assembly, delivery to the client. Even with aggressive caching, Time To First Byte rarely drops below 200–400 ms. Tilda and similar site builders generate static output, but alongside it — dozens of kilobytes of unused CSS and JS, embedded trackers, calls to external services.

For an agency that sells technical expertise, having a website with mediocre Core Web Vitals is a contradiction in terms.

Problem Two: Multilingual Support Without Plugins

We operate across four markets: Georgian-speaking audiences in Georgia, the Russian-speaking diaspora, the Ukrainian market, and English-speaking clients. Each segment requires not just translated text, but technically correct SEO markup: proper hreflang tags, regional variants (ru-GE, ka-GE), a correct x-default, and corresponding entries in sitemap.xml.

Plugins like WPML solve this partially and at a cost. We needed a system where multilingual support is not a bolt-on, but part of the architecture from day one.

Problem Three: Limited SEO and Platform Dependency

Any SaaS platform imposes constraints: URL structure, the way meta tags are generated, sitemap format. These constraints are non-critical for most projects, but unacceptable for an agency whose core competency is precisely technical SEO.

The Goals We Set

  • TTFB as close to zero as possible — the page should be served as a static file from a CDN.
  • Full control over SEO markup without platform restrictions.
  • Native multilingual support at the architecture level, not the plugin level.
  • Independence from paid CMS platforms and subscriptions.
  • Readiness for AI search — a new reality in which websites are indexed not only by search bots, but by language models.

The Technical Stack and Architecture Concept

The site's architecture is built on three components, each strictly responsible for its own domain.

Firebase as a Headless CMS

Firebase Firestore serves as the database and single source of truth for all content. Editors work with data through a custom admin panel that writes directly to Firestore. Firebase plays no role in serving content to the end user — it exists only at the build and management stage. This is classic Headless architecture: the backend is fully decoupled from the frontend.

Python as a Static Generator

The Python script generate_site.py is the site's compiler. It pulls all data from Firestore, runs it through the Jinja2 templating engine, and writes finished HTML files to disk. Once the build is complete, Python is no longer needed: the site exists as a set of static files.

Vanilla JS as the SPA Engine

On the client side, a JavaScript engine built in pure Vanilla JS — no React, Vue, or Angular — handles seamless page-to-page navigation via the History API, manages animations through the Intersection Observer, and re-initialises interactive components after each transition.

The Concept: Compute Once, Serve Statically

The key architectural principle: all computational work happens once — at build time. When a content manager clicks the deploy button, the Python script generates the complete site in seconds: all pages in all languages, the sitemap, llms-full.txt, the 404 page. The result is a folder of static files that can be served from any CDN with zero server-side load.

Comparison with WordPress: with 1,000 simultaneous visitors, WordPress generates 1,000 requests to PHP and the database. Our architecture with those same 1,000 visitors generates 0 server-side computations — the CDN serves the file from cache.


Key Decisions: A Layer-by-Layer Breakdown

Hybrid Architecture: generate_site.py and Jinja2

The build script runs through several sequential stages.

Stage one — initialisation. The script connects to Firebase via a service account passed as the FIREBASE_SERVICE_ACCOUNT environment variable. Keys are never stored in the repository — this is a mandatory security requirement when working with CI/CD pipelines.

Stage two — data loading. The script traverses all Firestore collections: home, services, portfolio, blog, contact, carouselItems. Data is loaded in a single batch at the start of the run to minimise the number of API requests.

Stage three — rendering. For each document in each collection, the script identifies the corresponding Jinja2 template and renders the page with data substitution. The result is written to the public/ directory at the path corresponding to the page's future URL.

Stage four — generating service files: sitemap.xml, llms-full.txt.

Intelligent Parsing: the [TOC] Marker

One non-trivial task is the automatic generation of a table of contents for long articles. The editor doesn't mark up a table of contents manually — they simply add the line [TOC] to the beginning of the text.

The script detects this marker and launches parsing via the lxml library. The algorithm: the article's HTML content is parsed into an element tree, the script traverses the tree and collects all h2 and h3 tags, each heading is assigned an id attribute based on its text content (transliterated and sanitised), then from the collected headings a nested list of links is built and substituted in place of the marker.

An important detail: id attributes are generated through the same slugify() function used for URL generation during the build — this guarantees no conflicts and correct anchor link behaviour for Georgian and Russian headings.

Intelligent Parsing: the [CAROUSEL:key] Marker

The [CAROUSEL:key] marker allows an interactive carousel to be embedded at any point within an article. The key corresponds to the identifier of a slide collection in Firestore.

During processing, the script locates the marker in the article body, loads the corresponding carousel collection from pre-fetched data, renders the carousel HTML through a dedicated template, and substitutes it in place of the marker. The final HTML contains a fully ready component — the carousel JavaScript engine initialises it when the page loads.

Custom Transliteration: the slugify() Logic

The slugify() function is implemented in parallel in two places: in the Python script for URL generation during the build, and in Vanilla JS for client-side routing. Both instances operate on an identical algorithm, guaranteeing URL consistency between server and browser.

The algorithm is built on dual alphabet detection:

  1. The script checks whether the string contains characters in the Unicode range U+10D0–U+10FF (the Georgian Mkhedruli alphabet). If so, a custom transliteration table is applied: each Georgian character is replaced with its Latin equivalent according to national romanisation standards. For example: შ → sh, ჩ → ch, ხ → kh, ჟ → zh, ჯ → j.
  2. The script then checks for Cyrillic characters in the range U+0400–U+04FF. Cyrillic is transliterated via the transliterate library (Python) or a built-in table (JS), including Ukrainian characters: ї → yi, є → ye, ґ → g.
  3. After transliteration, all characters except a-z, 0-9, and hyphens are removed. Multiple hyphens are collapsed into one; leading and trailing hyphens are trimmed.

The result: URLs like /ka/blog/saiti-mcire-biznesisat/ and /ru/blog/tekhnicheskoe-seo-dlya-biznesa/ instead of percent-encoded strings that break analytics and cause problems when copying links.


SEO Engineering and the AI-Ready Approach

The translationGroupKey Logic

Multilingual support is implemented not as an interface toggle, but as a relationship at the database level. Every page in Firestore has a translationGroupKey field — an arbitrary identifier that groups the translations of a single piece of content. For example, an article about technical SEO in four languages will share the same value for this field: technical-seo-guide.

During the build, the script constructs a translation map: for each unique translationGroupKey, a list of all pages sharing that key is assembled. This list is then used for two purposes when generating each page: inserting hreflang tags into the head, and adding alternative URL entries to sitemap.xml.

Automatic hreflang Generation

For every page that has translations, the script generates a complete set of link rel="alternate" tags. The regional code (GE, RU, UA) is set in the region field in Firestore and automatically appended to the hreflang attribute value.

The x-default tag tells Google which version to show users without an explicit language preference. The script determines x-default via the isXDefault flag in the Firestore record. If no version has the flag set, the script automatically assigns x-default to the English version of the page.

The final hreflang block for a blog article looks like this:

<link rel="alternate" hreflang="ru-GE"
      href="https://digital-craft-tbilisi.site/ru/services/razrabotka-saitov-tbilisi/" />
<link rel="alternate" hreflang="en-GE"
      href="https://digital-craft-tbilisi.site/en/services/custom-web-development-tbilisi/" />
<link rel="alternate" hreflang="ka"
      href="https://digital-craft-tbilisi.site/ka/services/veb-gverdebis-damzadeba-tbilisi/" />
<link rel="alternate" hreflang="uk"
      href="https://digital-craft-tbilisi.site/uk/services/rozrobka-saitiv-tbilisi/" />
<link rel="alternate" hreflang="x-default"
      href="https://digital-craft-tbilisi.site/en/services/custom-web-development-tbilisi/" />

This entire block is generated automatically. The editor fills in one field — translationGroupKey — on linked pages. Everything else the system handles at the next build.

Dynamic Sitemap with lastModified

The sitemap.xml file is rebuilt on every deploy. The script takes the lastModified field from each Firestore document and inserts it into the tag. This means the search bot always sees the correct date of the last change — not the date of the last deploy, but the date of the actual update to that specific page. Priority (priority) and change frequency (changefreq) are configured individually per page via fields in Firestore.

AI-Ready Infrastructure: llms.txt and llms-full.txt

The search landscape is changing. A growing share of informational queries is handled by language models — ChatGPT, Claude, Perplexity — which form answers based on their own knowledge and the data available to them. Classic SEO is no longer enough to appear in those answers: AI agents need a structured description of the site in a format they can read.

For this, there is the emerging llms.txt / llms-full.txt standard. We implemented support for it.

llms-full.txt is a strict machine-readable format containing the site's full structure. Pages are grouped by translationGroupKey: an agent sees not a collection of isolated URLs, but logically connected groups of content with languages and regions indicated. A language model accessing this file gets a complete picture of what the agency does.

The file is generated by the script at every build — the current site structure is always available to AI agents without manual updates.


Custom CMS: the Firebase Admin Panel

Content management is handled through a custom Single Page Application built in Vanilla JS with Firebase as the backend. It is a fully featured CMS with no third-party platforms and no monthly subscriptions.

The panel covers the entire content lifecycle:

  • Creating and editing pages across all four language versions.
  • Managing multilingual relationships via translationGroupKey.
  • Configuring SEO fields: seoTitle, metaDescription, OG tags, isXDefault, region, sitemap priority.
  • Uploading media files to Firebase Storage with automatic public URL retrieval.
  • Managing carousel slides: order, images, titles, descriptions.
  • Inserting arbitrary HTML/CSS/JS for the animated background of each page.

The Autofill from Article Feature

Creating a carousel slide is a routine task for a content manager after publishing a new article. Without automation, it looks like this: open the article, copy the title, go back to the carousel, paste the title, open the article again, copy the description, go back again, paste it, copy the cover image URL, go back, paste it.

The Autofill from Article feature replaces this entire process with a single action. The editor selects the relevant article from a dropdown — the panel queries Firestore, retrieves the article document, and automatically populates the title, description, and cover image into the fields of the new slide. The result is reviewed and saved with one click.

This is an example of the principle we follow when building tools for clients: automation should apply not only to technical processes, but to editorial ones as well.


Frontend Engine: SPA Without Frameworks

The site behaves like a Single Page Application — transitions between pages are instant, interface state is preserved, animations are uninterrupted. Yet the build contains no React, no Vue, no Angular. The entire client-side engine is written in pure Vanilla JS.

This is a deliberate architectural decision, not a cost-cutting measure. Any JS framework means additional kilobytes in the bundle, runtime overhead during hydration, abstractions layered on top of browser APIs, and a dependency on an ecosystem that updates faster than projects live. For a site where every millisecond of Time To Interactive matters, native browser JavaScript is not a compromise — it is the right tool. The browser already knows how to do everything required: History API, Intersection Observer, fetch, CustomEvent. The point is to use those mechanisms directly.

Client-Side Routing via the History API

Page transitions are implemented without browser reloads. The client-side router intercepts clicks on internal links through event delegation at the document level — a single handler instead of a separate listener on every link. The following sequence then executes:

  1. A call to history.pushState() immediately updates the URL in the address bar without any page reload.
  2. The target HTML file is loaded via fetch() — the request goes to the CDN, which returns the ready static file.
  3. The contents of the
    <main>
    tag are extracted from the loaded HTML using DOMParser and swapped into the current page's DOM.
  4. Animation observers, event handlers, and all interactive components of the new page are re-initialised.
  5. A popstate handler intercepts browser navigation events and replays the same logic when the Back and Forward buttons are pressed.

A critically important detail that is easy to overlook: despite the SPA behaviour in the browser, every page remains a fully valid static HTML document on disk. A direct URL visit, a link shared in a messenger, indexation by a search bot — all work correctly and without server-side routing, because the file physically exists at the required path. This is a fundamental difference from classic SPAs built on React Router or Vue Router, which require server configuration to handle "non-existent" routes.

Intersection Observer: Lazy Loading Beyond Images

Standard practice is to use the Intersection Observer API for lazy loading images. We apply it far more broadly: the initialisation of every heavy component on the page is controlled through this API.

Heavy background blocks do not begin executing until they enter the user's visible area. This means that when a page loads, the browser spends no resources computing animations that are five screens below. The effect is direct: Total Blocking Time decreases, Largest Contentful Paint improves, and on mobile devices the page becomes responsive significantly faster.

The system implements three types of observers with fundamentally different behavioural logic:

  • animateOnce — the observer fires when an element first enters the viewport, adds the CSS class is-visible, and immediately disconnects via unobserve(). The class remains on the element permanently. This is not only the correct UX model for most entrance animations, but a deliberate optimisation: a disconnected observer consumes no resources during subsequent scrolling.
  • animateAlways — the class is added when the element enters the viewport and removed when it leaves. The observer does not disconnect. Used for elements that should replay their animation every time the user scrolls to them — counters or accent blocks, for example.
  • floatingObserver — extended logic that takes into account not only the fact of visibility, but also the element's position relative to the viewport. Elements that have already scrolled above the current position are assigned the class is-above. This allows separate CSS rules to be applied to "passed" sections — implementing a parallax-exit effect or changing the header style based on which block has been left behind, for example.

Custom Backgrounds: Isolated Animations in an iframe

Every page on the site can have a unique animated background — a p5.js sketch with generative graphics, a Three.js scene with 3D objects, a GLSL shader with procedural textures, or any other visual effect. The background code is stored in Firestore as an arbitrary HTML/CSS/JS string, entered into the admin panel field, and placed inside an

<iframe>
tag when the page is generated.

Using an iframe is not a simplification — it is a deliberate architectural decision that simultaneously solves two unrelated problems.

The first is security. The background code executes in a fully isolated browser context: it has no access to the main page's DOM, cannot read JavaScript variables, cannot see user data, and cannot intercept interface events. This matters because the background field is accessible via the admin panel, and isolation eliminates any accidental or intentional disruption to the main interface.

The second is performance. Heavy computations — WebGL rendering, particle physics simulations, per-pixel shader operations — run in a separate rendering context and do not compete with the main thread for CPU resources. The user sees a rich visual background while the interface remains responsive and does not drop frames during interaction.

Glow Carousel: Mathematics at the UI Level

The carousel on the home page is not a standard CSS slider with overflow: hidden and class toggling. It is a component with a physically grounded light-glint effect that responds to cursor position in real time.

The core of the effect is the two-argument arctangent function: atan2(dy, dx). As the cursor moves over a card, the component calculates the vector from the card's centre to the current cursor position. The atan2 function converts this vector into an angle in the range from to π. The angle is converted to degrees and used as the direction of a CSS gradient that creates the glint. The distance from the card's centre to the cursor determines the effect's intensity via the clamp() function.

The result: the glint physically and correctly "follows" the light source — the user's cursor. This is not a timer-based animation and not a pre-recorded keyframe, but a real-time calculation on every mousemove event.

Beyond the glint effect, the carousel incorporates a number of engineering solutions:

  • Background image Lazy Loading via the data-bg-src attribute. A card's image is not loaded until the card becomes active. The background-image CSS property is set programmatically only at the moment the slide is shown, after which the data-bg-src attribute is removed — no repeated loading occurs.
  • Animation interruption protection via a boolean flag isAnimating. If the user clicks an arrow faster than the transition between slides completes, the new transition does not start until the current one finishes. This prevents visual artefacts and an incorrect component state.
  • Dynamic navigation generation. Indicator dots and arrows are created programmatically based on the actual number of slides retrieved from Firestore. The template contains no hardcoded navigation elements.
  • Theme support via the isLightTheme flag. The component adapts the glint colour scheme to the page's light or dark theme without duplicating logic.

Results: What We Ended Up With

The architecture described in this case study is not an experimental build or an academic exercise. It is a production system that serves the agency's website every day and is reproducible for client projects wherever the task demands maximum technical quality.

TTFB Near Zero: the Server Doesn't Think — It Delivers

Time To First Byte — the time from sending a request to receiving the first byte of the response — is practically 117 ms. The server performs zero computations per request: no database queries, no PHP or Node.js, no server-side rendering. The CDN receives the request and immediately serves the ready HTML file from the cache of the nearest point of presence.

For comparison: WordPress with a paid aggressive-caching plugin under favourable conditions delivers a TTFB of 80–200 ms.

No Server-Side Rendering: Zero Live Backend

The production server runs no Node.js, no PHP, no live database. The infrastructure is reduced to a minimum: Firebase Firestore stores data and is only accessible during the build; the CDN serves static files. This means an entire class of problems simply does not exist — server-code vulnerabilities, memory leaks, database crashes under load, the need to update the server environment.

SEO Out of the Box: Markup as a Build Output, Not Manual Labour

Every page automatically receives a complete technical SEO markup package during generation: correct hreflang tags for all language versions with regional suffixes, a properly assigned x-default, a canonical URL, a full set of Open Graph tags for social networks, and a current entry in sitemap.xml with the real date of last modification from the database.

None of these properties require manual oversight after the initial system setup. An editor adds a new article — at the next build it automatically appears in the sitemap with correct meta tags and hreflang relationships to all translations.

Scaling Without Cost: Traffic Grows, the Bill Doesn't

Static files are served by the CDN. Hosting costs do not depend on visitor volume: one thousand requests per day and one million requests per day cost the same — close to zero. This is a fundamental difference from any server-rendered architecture, where traffic growth translates directly into rising operational expenses.

Horizontal scaling is free by default: the CDN provider distributes load across points of presence worldwide on its own.

The llms-full.txt file is updated on every deploy and gives language models — ChatGPT, Claude, Perplexity, and others — a complete and structured picture of the site: what the agency does, what services it offers, what content is published and in which languages. This is an investment in the search visibility of the future, where a significant share of traffic will be generated by AI agents rather than classical search engines.


The best way to demonstrate technical competence is to show it in action. Every decision on this site is motivated by a specific problem we defined before development began.

If your project demands the same degree of engineering precision — we are ready to discuss its architecture.