Lúmina W | Software & Technology
The digital ecosystem of Lúmina W | Software & Technology, a software company in Medellín. It brings together the bilingual site, with its two service paths (custom development and its own subscription product), and the bilingual blog with accounts, moderation, and a newsletter.
By Valentina Ramírez
Published: June 16, 2026 · Updated: October 5, 2026
- Problem
- A software company that sells custom projects and its own product needs to explain each path without mixing them up and to receive requests with useful data.
- Solution
- A bilingual site with two clear service paths from the first screen and a form that collects what is needed to reply.
- Result
- Site and blog in production, at luminaw.co and blog.luminaw.co.
01 Landing
Bilingual Astro site with two service paths and a Supabase-backed form. View detailsHide
- Astro
- TypeScript
- Tailwind CSS
- Vercel Functions
- Supabase
- Brevo
- GA4
- SEO
- A11y
The problem
A software company that sells two different things, custom projects and its own product by subscription, needs to explain them without mixing them up. And it needs to receive contact requests with enough data to answer, without building a full backend for a presentation page.
Two offers with different buyers
Custom development and a subscription product have different timelines, budgets and questions. Mixed without structure, they confuse.
Contact with friction
A form with a single message field forces a reply with questions before you can quote. One that is too long scares people off.
Two languages without duplicating the work
Maintaining an English copy by hand leaves stale strings and forgotten keys.
Contact data without a backend of your own
Storing requests needs a database and a safe write point, and standing up a server for a presentation page is out of proportion.
Results
- Lúmina W site in production at luminaw.co, in Spanish and English, with the two service paths differentiated.
- A contact form that stores each request with its need, its stage and its consent in a shared table, and notifies by email.
- TerraCore and OKroot presented on the landing and on a page of their own.
- Technical SEO with structured data, hreflang, a sitemap, robots for AI crawlers and llms.txt.
View technical details OpenHide technical details
What I built
- 01
Astro with static output and Tailwind CSS. Twelve components assemble the landing in a fixed order: hero, a fork between the two paths, problem, agitation, a phrase ribbon, solution, process, manifesto, why us, product, FAQ and contact. Each section has an anchor that matches the navigation bar and the footer.
- 02
Spanish by default at the root and English under /en, with both copies in typed dictionaries: the English dictionary requires the same keys as the Spanish one, so a missing key breaks the build. The products and contact pages have a translated route, and the legal pages keep the same name.
- 03
Contact form: the browser sends JSON to a Vercel Function that validates the fields (name, company, email, phone, need, stage and message), requires consent and silently drops whatever arrives through a hidden trap field. The need and stage values are fixed English codes, so an option is stored the same way in both languages.
- 04
The Function saves each request in Supabase and fills in the consent date, the origin, and the user agent on its own. It does not store the IP. If the email already exists, it returns a success response without creating another row, and still notifies the internal mailbox.
- 05
The internal notice goes out through Brevo and is best effort: if the email fails, the request is already stored. Storage only runs in Vercel production, so on previews the form returns 503 by design.
- 06
The requests table is a single one, shared with the TerraCore and OKroot landings, with a product column and a uniqueness constraint per product and email. It has row-level security on and no permissions for the public roles: only the Function can write.
- 07
Technical SEO: a JSON-LD graph with ProfessionalService, WebSite and WebPage, plus FAQPage on the FAQ, a self-referencing canonical, hreflang between es, en and x-default, a sitemap with alternates and a robots.txt that explicitly allows 21 AI crawlers, together with an llms.txt.
- 08
Analytics with consent: Google Analytics 4 loads only if the person accepts the cookie notice, and the decision is stored in local storage, with no first-party cookies. The notice does not block the page and appears after a little scrolling.
- 09
Performance and delivery: WebP images with explicit dimensions, fonts with swap, long-lived caching for hashed resources and images, and security headers. Deployment on Vercel is continuous.
- 10
Accessibility worked into the code: a skip link, visible focus, a label per field with announced errors and a success message with a status role, and respect for prefers-reduced-motion. A no-JavaScript fallback keeps the content that appears on scroll visible.
- 11
Quality: formatting, build, and secret scanning on every PR, with a branch flow across environments.
Two paths
A fork on the first screen separates the in-house SaaS product from custom development, and each has its own process.
Process per path
Four phases for custom development and four for the product, with the working rules of each.
Products
TerraCore and OKroot on the landing and on a page of their own, with their status and link.
FAQ
Ten questions with answers, also published as structured data.
Contact form
It collects need, stage and consent, stores the request and notifies by email.
Spanish and English
Two complete copies, with translated routes, hreflang and a language switch.
Design
- 01
An editorial, sober system: straight edges (radii of 0, 2 and 4 pixels), one-pixel lines instead of shadows and a single stylesheet with the tokens.
- 02
Blue as the main color for links, tags and focus, and amber as a fill-only accent. When amber is text on a light background, a darker version is used to keep the contrast.
- 03
Cabinet Grotesk for headings and Switzer for body text, loaded from Fontshare with swap.
- 04
A light page with fixed dark surfaces (contact, footer, products and the navigation bar). There is no theme switch.
- 05
Calls to action carry text only, with no arrows or icons, and the project rule is not to use dashes as a connector in texts.
Architecture decisions
01 Two explicit paths instead of a single service ViewClose
- Context
- Lúmina W offers custom development and its own SaaS product, and whoever buys each one arrives with different questions.
- Trade-off
- A generic software development pitch is less persuasive, but separating the paths adds a decision on the first screen.
- Decision
- A fork section, a solution in two services and a process with four phases for each path, each with its own call to action.
02 In-house products as proof ViewClose
- Context
- Someone evaluating a software company wants to see a product working, not only custom development promises.
- Trade-off
- Giving space to the products can take attention from the service, and their status changes over time.
- Decision
- TerraCore and OKroot appear on the landing and on a products page, with each one's status and link defined in the text dictionary.
03 A Vercel Function with Supabase, not a server of its own ViewClose
- Context
- The site needs to store contact requests from day one, and building a backend for a landing is out of proportion.
- Trade-off
- There is a small server piece to maintain, and the form does not work on previews, where there is no database.
- Decision
- A Vercel Function validates, applies the honeypot and the consent check, and writes to Supabase from the server. The browser never talks to Supabase.
04 One requests table shared across the landings ViewClose
- Context
- Lúmina W, TerraCore and OKroot receive requests with almost the same data, and three separate databases would have tripled the work.
- Trade-off
- If someone writes twice with the same email, the second is not stored: the internal notice mentions it, but the table keeps only the first.
- Decision
- A single table with a product column and uniqueness per product and email, with the same schema copied into the three repositories.
05 Typed dictionaries for two languages ViewClose
- Context
- Every new text must exist in Spanish and English, and a forgotten string shows only when someone switches language.
- Trade-off
- It forces translating every change at the same time, even a small one.
- Decision
- The English dictionary is typed with the Spanish one, so a missing key breaks the build.
06 Analytics only with consent ViewClose
- Context
- The site measures visits with Google Analytics 4, and that measurement requires the visitor's permission.
- Trade-off
- Without acceptance there is no data for that visitor, and the notice appears after scrolling so it does not cover the first screen.
- Decision
- The Analytics script is injected only after accepting and the decision is kept in local storage, with no first-party cookies.
Learnings
Asking for need and stage in the form, with fixed values, means the first reply can already be a proposal and the data can be compared across languages.
The promises in the copy must be something the code does: saying a confirmation email is received requires the Function to send that email.
A typed dictionary turns a forgotten translation into a build error instead of a half-translated page.
02 Blog
Bilingual Next.js blog platform with accounts, moderation, and a newsletter. View detailsHide
- Next.js
- React
- TypeScript
- Tailwind CSS
- Prisma
- PostgreSQL
- Supabase
- NextAuth
- Brevo
- Claude API
- Vercel
The problem
Technical knowledge that comes out of real projects needs its own channel, with control over the content, the comments and the subscriptions, without depending on an outside platform. The previous site was static, with a file-based CMS, and had no accounts or community.
An outside platform decides for you
Publishing on a third-party platform leaves the audience, the comments and the subscription in someone else's hands.
A static site has no community
The previous site published fine, but had no accounts, no comments and no way for someone else to propose an article.
Two languages are double the work
Keeping every article in Spanish and English by hand doubles the effort of publishing.
Open comments open the risk
Every account that writes or comments is an entry point for spam, dubious links and abuse of the routes that send email.
Results
- A platform in production at blog.luminaw.co, with accounts, publishing with approval, moderated comments and a newsletter.
- Six file-based articles imported, three in Spanish and three in English, and a flow to publish more from the application itself.
- Automatic translation of published articles into the other language, with a manual retry from the panel.
- A suite of 28 unit test files over pure logic, with lint, build and secret scanning on every PR.
View technical details OpenHide technical details
What I built
- 01
Next.js application (App Router, React and TypeScript) with Prisma on PostgreSQL in Supabase, and Tailwind CSS for styling. It replaced a previous Astro site with Decap CMS. The layout is dynamic on purpose: the CI build does not need a database.
- 02
Articles live in two ways: Markdown files in the repository (six today, three per language) that an importer loads into the database on every production build, and articles written by accounts inside the application. The importer creates or updates by language and slug, and never touches authorship, likes, saves or comments.
- 03
Publishing flow: an article goes through draft, pending, published or rejected. Any account can write and submit for review, only the administrator publishes directly, and editing a published article from an account other than hers sends it back to pending. The slug is frozen once published.
- 04
Bilingual: Spanish without a prefix and English under /en, with translated routes (articulos, categoria, cuenta) and a middleware that rewrites them to the internal routes. Each language is a Post row linked to the original, and likes, saves and comments are always anchored to the original.
- 05
Automatic translation: when an original is published, and only if a key is configured, a background task sends the title, description, body and tags to the Claude API as data, validates the response and stores the version in the other language. The administrator can retry it from the panel. File-based articles ship with their own translations.
- 06
Community: threaded comments of up to 2,000 characters, likes, saves, public profiles and following. Comment moderation is a local rule filter (blocked terms and a link cap) that calls no model: anything that matches is held as pending and the administrator approves or rejects it.
- 07
Newsletter with Brevo: one list per language, sign-up from the site and a test or campaign send from the admin panel, with Brevo's unsubscribe link. Reply, follow and save notifications also go out by email, in addition to inside the blog.
- 08
Accounts with email and password verified by email, plus Google, GitHub and LinkedIn when configured, and invitations that arrive with the email already verified. An external provider is accepted only if it confirms the email. An account is locked for 15 minutes after 5 failed attempts, and suspension and session invalidation are available from the panel.
- 09
Permissions are verified on the server on every request. Administration is restricted to a single account, protected from suspension and deletion.
- 10
Layered security: rate limits on credentials and sensitive operations, origin checks on routes that write, security headers, and user content sanitized before it renders.
- 11
Images: validated on the server, unsupported formats rejected, and converted to WebP with a size limit per use (cover, body, or avatar) before they are stored in object storage.
- 12
SEO and discovery: a dynamic sitemap with language alternates, robots with a separate group for AI crawlers, llms.txt, an RSS feed per language and JSON-LD (Organization, WebSite, BlogPosting, Blog, CollectionPage, ProfilePage and BreadcrumbList). Search uses case-insensitive matching over title, description, body, author and tags.
- 13
Analytics with consent: GA4 loads only if the person accepts and an identifier is configured, and the cross-domain link covers luminaw.co, terracoreapp.co and okroot.co.
- 14
Quality and delivery: 28 unit test files on pure logic, lint and build on every PR, secret scanning, and a branch flow across environments. In production, deployment applies the database changes and loads the articles.
- 15
A cover generator builds the 1200 by 630 WebP images, one per language, with a 150 KB cap, from a JSON spec and an SVG artwork.
Articles and approval
Write in Markdown, submit for review and publish when the administrator approves, with a note when it is sent back.
Community
Threaded comments, likes, saves, public profiles and following between accounts.
Spanish and English
Routes, texts, emails and notifications in both languages, with automatic translation of articles.
Newsletter
Sign-up per language and a test or campaign send from the admin panel.
Admin panel
Queue of articles and comments, featured posts, invitations and user management: search, suspend, reactivate and delete.
Search and discovery
Search with a keyboard shortcut, categories, popularity ordering, an RSS feed, a sitemap and llms.txt.
From draft to reader in two languages
- 01 An account writes an article in Markdown and submits it for review.
- 02 The administrator approves it, or sends it back with a note the author receives as a notification.
- 03 Once published, a background task sends the content to the Claude API as data and validates the translation.
- 04 The version in the other language is stored linked to the original and enters the sitemap with its alternate.
- 05 Likes, saves and comments on either version accumulate on the original.
Design
- 01
The styles of the separately designed prototype were ported as is, and the theme lives in a single stylesheet with Tailwind CSS without the base reset.
- 02
Article with a reading progress bar, a table of contents and a share card, with the device's native menu as the route for Instagram.
- 03
1200 by 630 covers per language, generated from a spec and an SVG artwork, with a weight cap per image.
- 04
Accessibility worked into the code: a skip link, keyboard menus and a confirmation dialog with trapped focus. The contrast changes that would alter the design are pending a decision.
- 05
A language switch that remembers the choice in a cookie, and detection of the browser language on the first visit.
Architecture decisions
01 An application with a database instead of a static site ViewClose
- Context
- The blog needed accounts, comments, article approval, subscriptions and two languages.
- Trade-off
- A dynamic application depends on a database and needs more security care than a file-based site.
- Decision
- Next.js with Prisma and PostgreSQL, and a dynamic layout so the build does not need the database. File-based articles stay versioned in the repository and are imported on deploy.
02 One row per language, linked to the original ViewClose
- Context
- An article exists in two languages with different slugs, but the conversation around it is one.
- Trade-off
- Every interaction query has to resolve which one is the original first.
- Decision
- Each version is a Post row with a reference to the original. Likes, saves and comments are always anchored to the original.
03 Claude for translation, published content only ViewClose
- Context
- Translating every article by hand doubles the work of publishing.
- Trade-off
- A model translation can be wrong and depends on an outside key, so it is only active when a key exists.
- Decision
- A background task sends only the already published article, as data and not as instructions, validates the response before storing it, and the administrator can retry it. No account or comment data goes to the API.
04 Moderation by local rules ViewClose
- Context
- Open comments receive spam and dubious links, and sending them to a model adds cost and ships users' text to a third party.
- Trade-off
- A filter by terms and link count is crude: it lets through what does not match and holds what does even if it is legitimate.
- Decision
- A local filter puts matching comments in pending and the administrator decides. Her own comments are exempt.
05 Rate limits with no new infrastructure ViewClose
- Context
- Every route that writes, sends email, or receives credentials needs a cap, and the goal was no new infrastructure.
- Trade-off
- It uses the application's database on every call and is a fixed window, less precise than a dedicated queue.
- Decision
- The limits live in the application's own database, with no new infrastructure. They are strict on credentials and sensitive operations, and more flexible elsewhere.
06 Permissions verified on the server ViewClose
- Context
- A session token holds what was true when it was issued, but an account can be suspended or change its password later.
- Trade-off
- Every authenticated request makes a database read.
- Decision
- The server checks the account's current state on every request and discards the session if the account is suspended or changed its password. The session alone never decides a permission.
07 A single administrator account ViewClose
- Context
- Someone has to approve content, without building a roles system for one person.
- Trade-off
- There are no delegable roles and no second administrator without changing the configuration.
- Decision
- There is a single administrator account. Every panel action checks that condition on the server, and that account cannot be suspended or deleted.
Learnings
Opening accounts and comments changes the kind of project: almost every new route needs a rate limit, an origin check and a decision about what happens when it fails.
Verifying permissions on the server on every request costs a query, but keeps a suspension or a password change from arriving late.
A local moderation filter is predictable and cheap, but it only orders the queue: the decision is still a person's.
Roadmap
Now
- Bilingual blog in production with community, article approval, a newsletter and automatic translation.
- Security, SEO and accessibility audits applied to the code.
Next
- Related articles by tags.
- Delete your own account and change your email from the profile.
- Report a comment.
- Self-hosted fonts, error monitoring and end-to-end tests in CI.
Later
- Draft generation with the Claude API and n8n, and automatic scheduling of posts.
- Sharing to social networks from n8n and an analytics panel.