Skip to main content

Keratine Professional

Keratine Professional sells keratin shampoos, hair masks, serums and hair spa treatments in India.

Keratine Professional

Challenge

Keratine Professional sells keratin shampoos, hair masks, serums and hair spa treatments in India. In April 2026 Dcrayon moved its keratine.in store from Shopify to Dcrayon's multi-tenant Laravel commerce platform.

The range on keratine.in is built around smoothing and repair: repair and smoothening shampoos, sulphate-free shampoos with rosemary or Moroccan argan oil, keratin and nano-smoothing hair masks, Brazilian hair spa, a protein repair treatment, shine serums and a long list of shampoo, mask and serum combos. Until April 2026 the store ran on Shopify, with a blog of hair care guides published since at least December 2024. The brand also sells on Amazon, which is covered in the Keratine Professional Amazon case study; the website is the subject here.

Dcrayon's scope was the whole move: bringing the catalogue and the articles across, keeping the search titles Google already showed, building a Keratine home page on the shared platform, and getting the domain serving the new store. Laravel is a PHP framework for building web applications. Dcrayon's platform uses it to run several brand stores from one codebase, each brand with its own database. In that setup each brand is a tenant, and Keratine became one.

Why keratine.in could not simply be copied across

Shopify's product export does not match the platform's catalogue model.

Shopify exports a product as several CSV rows: the first carries the title, description, vendor and price, and the rows below carry extra images and variants. Categories arrive as a single text path, and the vendor is just a name. The Laravel platform stores products, categories, brands and images in separate related tables, and every product belongs to a seller. Pasting the export in by hand would have meant re-keying the range and losing the image order and SEO fields that came with it.

The blog and its search titles existed only on Shopify.

The guides at keratine.in/blogs/keratine-blogs covered keratin treatments, argan oil hair spa, red onion hair masks and serums for frizz. Each had its own meta title and description that Google was already showing. A platform move that left those behind would have cut the store off from the searches its content already answered.

Every section of the first Keratine home page was hard-coded.

The first version of the new home page assembled brand-specific blocks straight into the template: a serum feature, a timeless hair care panel, promotional tiles, a botox and keratin block, a hair mask block and a combos banner. Changing an offer, a banner or a product row meant editing code. For a brand that sells mostly through combos and seasonal offers, that tied everyday marketing to developer time.

The home page artwork weighed about 19 MB.

The section images arrived as PNG files, a format that keeps every pixel and produces large files. The commit that later converted them records their combined weight at about 19 MB. A shopper on mobile data would have downloaded that on a first visit to the home page.

What stood in the way of moving keratine.in

The four problems the move had to solve, and who each one affected.

IssueWho it affectedBusiness impact
Shopify export did not match the catalogue modelBrand team and Dcrayon developersRe-keying products would lose image order and SEO fields
Blog and search titles only on ShopifyReaders arriving from searchGuides and their titles would drop out of search
Home page sections hard-codedBrand marketing teamEvery offer or banner change needed a developer
About 19 MB of PNG artworkShoppers on mobile dataHeavy first visit to the home page
Source: Dcrayon platform repository history, 22 to 28 April 2026, and archived copies of the Shopify blog.

Solution

Choosing a tenant over a separate single-brand application

The first decision was where Keratine would live. A standalone Laravel application for one brand would have meant building checkout, accounts, order handling and an admin again, and maintaining them separately afterwards. Adding Keratine as a tenant on the existing platform gave the brand those features on day one and limited the brand-specific code to what made Keratine look like Keratine.

That limit was written into the build. Only the home page template was created for the brand. Collections, product pages, the cart, checkout, account and policy pages all come from the platform's shared default views. The Keratine look uses a teal accent (#5ac6c0) and the Lato typeface, set in the tenant's own stylesheet. The store was staged on a Dcrayon subdomain before keratine.in was pointed at the new platform.

How tenant lookup and view fallback serve each page

The platform uses the stancl/tenancy package with one database per tenant. When a request arrives on keratine.in, the package identifies the tenant from the domain and its bootstrappers switch the database connection, the file storage location and the queue context to Keratine's. Products, orders, customers and blog posts therefore sit in Keratine's own database, apart from every other brand on the platform.

Views are resolved by a middleware class called ResolveTenantFrontend. It looks for a folder named after the tenant and builds the view search order as frontends/keratine/views, then frontends/default/views, then the shared resources/views. Keratine's home page therefore comes from its own folder, while every other page falls back to the shared templates. The same middleware tells the layout which stylesheet and script bundle to load, and Vite builds Keratine's three entry files: frontends/keratine/css/app.css, frontends/keratine/css/home.css and frontends/keratine/js/app.js.

The rest of the stack is Laravel 12 on PHP 8.2 or later, Alpine.js for small interactive pieces in the browser, Tailwind CSS 4 for styling, Vite 6 to build the assets, spatie/laravel-permission for roles, Laravel Sanctum for API tokens, and Laravel Scout with Meilisearch for product search. The live store runs on AWS behind nginx.

Mapping Shopify's export onto the Laravel catalogue

Dcrayon wrote an import script for the move, scripts/import-keratine.php. It initialises the Keratine tenant first, so every record it writes goes into Keratine's database. It then reads the Shopify product export column by column: Handle, Title, Body (HTML), Vendor, Product Category, Type, Tags, Variant Price, Variant Compare At Price, Variant SKU, Variant Grams, Status, Published, SEO Title and SEO Description. Rows that carry only an Image Src are attached to the product above them, with their alt text and position kept, so each product arrives with its gallery in the original order.

Shopify's single category path is split into a parent and child category tree, and each product is linked to the deepest matching category. Anything Shopify left uncategorised goes into a hair care category rather than nowhere. Each vendor name becomes a Brand record, created once and reused by slug. Because the platform's products belong to a seller, the script also creates a seller record for Keratine Professional with a commission rate of zero, which keeps the brand's own products clear of any marketplace commission logic.

On the platform side, the products table stores mrp and price as decimal values with two places, a unique sku, a stock status of instock, outofstock or backorder, a tax rate defaulting to 18.00 percent, an hsncode for GST, and a seodata field for search titles and descriptions. Shopify's compare-at price maps to mrp, its price to price, and its SEO fields to seodata.

Bringing the blog across with its metadata

For the articles, Dcrayon added an Artisan command, import:keratine-blogs. Shopify's JSON feed for the blog was blocked, so the command reads the public HTML pages instead. It works through the blog's listing pages, opens each article, and saves the title, body, featured image, author, publish date, meta title, meta description and tags into the blog_posts table. It downloads each featured image into storage, skips any article whose slug already exists, and waits 500 milliseconds between requests. Options include --dry-run to preview what it would save, --skip-images, --pages to limit how many listing pages it reads, and --tenant, which defaults to keratine.

Because the platform's other brands did not need a blog table at the time, a tenant migration creates blogposts only in a tenant database where it is missing, guarded by a table check. The blogposts table holds title, a unique slug, excerpt, content, featuredimage, category, tags as JSON, authorid, ispublished, publishedat, seodata as JSON and viewcount. The imported articles are served at keratine.in/blog with the same slugs they had on Shopify.

Matching the Shopify-era titles and share cards

The search work was done as part of the move. The new home page title was set to match the existing keratine.in title word for word, and the meta description was expanded to 131 characters. Open Graph tags were added, the title, image and site name that WhatsApp, Facebook and other apps show when a link is shared, with a 1200 by 630 pixel share image, along with Twitter card tags. Section headings kept the wording shoppers already knew, such as the "by Keratine Professionals" line.

On 13 October 2026 the store's structured data, the code that tells search engines what a page describes, included an Organization record with the legal name, address, area served, contact point, logo and social profiles; a WebSite record with a site search action; and an FAQPage record on the home page. Product pages carry Product, BreadcrumbList and FAQPage records. Every page sets a canonical URL. The sitemap is an index split into products, categories, blog posts and pages, and the robots file keeps account, cart and checkout paths out of search results. This is the technical SEO side of the project.

Home page sections the brand team controls

On 27 April the hard-coded blocks were replaced with the platform's configurable sections. Each section is a row in the homepagesections table, read through the HomepageSection model, with a key, title, subtitle, type, content stored as JSON, background and text colours, an image, button text and link, a position and an isactive switch. The Keratine template checks each key before rendering it: categoriesgrid, featured, videotestimonials, bestsellers, productbanner1, benefits, deals, productbanner2, promobanner, testimonials and newarrivals. Turning a section off in the admin removes it from the page; changing its title, image or button changes the page without a deployment.

The page is assembled from shared Blade components, Laravel's reusable template pieces: x-layouts.app for the frame, x-product-card for every product tile, x-instagram-reels, x-faq-section, x-trust-badges and x-recently-viewed. Interactive behaviour comes from Alpine stores shared by every tenant, kept in frontends/_core/js/stores: cart.js, wishlist.js, pdp-pack.js for pack choices on product pages, auth-modal.js and toast.js. A flash-sale pop-up with a product count and a "No thanks" option, and a contained hero banner with rounded corners, followed on 27 and 28 April. The platform uses Blade components and shared stores rather than a formal design-system library.

The section artwork was converted from PNG to WebP, a newer format that compresses far better, and resized to 672 by 672 pixels. The page's viewport setting was changed to allow zooming up to five times, so a shopper with low vision can pinch to enlarge text. This is the custom web application work Dcrayon does when a store needs more than a theme.

Pull requests, CI checks and checksum deploys

Each piece of work went in as a small, named commit on its own branch and was merged through a numbered pull request: 28 for the tenant set-up, 74 for the home page rebuild and 80 for the banner fix. The commit records credit an AI coding assistant as co-author, so the code was AI-assisted and then merged through those pull requests. The repository's instructions for AI sessions forbid direct pushes to the main branch, hand edits on the production server and brand-specific defaults hard-coded into shared code, and a pre-push hook in the repository blocks pushes to protected branches.

Every pull request runs a continuous integration workflow with three jobs. The first checks the syntax of every PHP file, compiles every Blade template, validates the routes and the configuration cache, checks that Blade directives are balanced, and fails the build if a brand-specific default is hard-coded into shared code, which is what stops one tenant's phone number from appearing in another tenant's footer. The second runs the Vite production build. The third checks that no environment file or hard-coded secret has been committed. All three jobs passed on the pull requests for the home page rebuild and the banner fix, and again on their merges to the main branch on 27 and 28 April 2026. Deployments then run through a script that compares a checksum of every uploaded file with the copy on the server and stops if they differ. A checksum is a short fingerprint of a file's contents, so a mismatch means a file did not arrive intact.

Access, payments and headers on the store

Accounts are separated by role through spatie/laravel-permission: customer, admin, staff, seller, super admin, affiliate and delivery partner. The admin area sits behind middleware that confirms an admin session and then checks section by section whether that user may open the page, returning a 403 response when they may not. Admins can switch on two-factor sign-in, after which the admin area asks for a fresh code at set intervals; login attempts are throttled, and admin actions are written to an audit log. Every tenant's data stays in its own database.

On the payment side, by the 28 April release that carried Keratine's last home page change, the platform handled money through a Money value object that stores amounts as whole paise, never as floating-point rupees, and rejects negative values. Payment providers plug in behind a PaymentGatewayInterface with four operations: create a payment attempt, verify a payment, handle a webhook and refund. Checkout details travel in a CheckoutSessionData object, a DTO (data transfer object) that carries the cart, totals and contact details as typed, read-only values. Incoming Razorpay webhooks must pass a signature check before they are processed. Request validation sets firm limits, for example a cart quantity must be a whole number from 1 to 99, a phone number is required at checkout, and an email must be a valid address.

A check of keratine.in's public response headers on 13 October 2026 showed HTTPS with HSTS (Strict-Transport-Security) set for a year and including subdomains, X-Content-Type-Options set to nosniff, X-Frame-Options set to SAMEORIGIN, a Referrer-Policy of strict-origin-when-cross-origin, a Permissions-Policy that switches off camera and microphone access, and a Cross-Origin-Opener-Policy.

Lighter assets and AWS hosting

Vite produces content-hashed CSS and JavaScript files, and the page preloads the files it needs first. On the home page checked on 13 October 2026, 36 images were set to load lazily, only when a shopper scrolls near them. The store also ships a web app manifest, so shoppers can add Keratine to a phone's home screen and open it full screen like an app. The site is hosted on AWS.

Keratine data as the platform stores it

Real table, model and file names from the April 2026 release that holds Keratine's records.

EntityTable or fileWhat it holds
ProductproductsName, slug, sku, mrp and price (decimal), stock status, tax rate, hsn_code, seo_data
Category and Brandcategories, brandsParent and child categories from the Shopify path; one brand per vendor
SellersellersKeratine Professional as the selling business, commission rate zero
Blog postblog_postsTitle, unique slug, content, featured image, tags, published_at, seo_data
Home page sectionhomepage_sections (HomepageSection)Key, title, content JSON, image, button, position, is_active
Catalogue importscripts/import-keratine.phpMaps Shopify CSV columns into the tables above
Blog importimport:keratine-blogs commandReads Shopify article pages into blog_posts
Source: migrations, models and scripts in the Dcrayon platform repository, release of 28 April 2026.

Keratine page parts from small to large

How the Keratine pages are put together, from shared settings up to full page templates.

Component levelExamples from this buildUsed on
Brand settingsTeal accent #5ac6c0, Lato typeface in the tenant stylesheetEvery Keratine page
Browser storescart.js, wishlist.js, pdp-pack.js, auth-modal.js, toast.jsCart drawer, wishlist, product pages, sign-in
Blade componentsx-product-card, x-faq-section, x-trust-badges, x-instagram-reels, x-recently-viewedHome, collection and product pages
Configurable sectionscategories_grid, bestsellers, product_banner_1, benefits, deals, promo_banner, new_arrivalsKeratine home page
Page templatesfrontends/keratine/views/home.blade.php; shared frontends/default viewsHome page; all other pages
Source: frontends and resources folders in the Dcrayon platform repository, April 2026.

Controls protecting keratine.in orders and admin

The security controls in the platform release of 28 April 2026, and how each was confirmed for this case study.

ControlWhere it appliesHow it was checked
Database per tenantAll Keratine recordsstancl/tenancy configuration in source
Role and section permissionsAdmin, staff, seller and other accountsPermission middleware in source
Optional two-factor sign-in, login throttling, audit logAdmin accountsMiddleware in source
Signed Razorpay webhooks; amounts in paiseCheckout and payment callbacksWebhook middleware and Money class in source
HSTS, nosniff, frame and referrer policiesEvery public pageResponse headers, 13 October 2026
Source: platform source code (release of 28 April 2026) and a public header check of keratine.in on 13 October 2026.

Results

What keratine.in does now

keratine.in runs on the Laravel platform with its own Keratine home page and the shared store features behind it. Shoppers browse collections for shampoo, pre-shampoo, conditioner, hair mask, hair serum, hair spa, hair treatment and combos; open a side cart that suggests other products and shows how close the order is to a free gift; keep a wishlist; see recently viewed items; pay by UPI or cash on delivery; and reach the brand on WhatsApp.

The Shopify-era articles are readable at their original slugs under /blog, with their titles, descriptions and featured images, alongside newer guides on hair fall shampoo, frizz serums and hair spa creams. For the brand team, offers, banners and product rows are now changed in the admin rather than in code, and each section can be switched on or off.

The image conversion cut the home page section artwork from about 19 MB of PNG to about 300 KB of WebP, as recorded in the commit notes. Keratine's sales and traffic figures are not published.

The recommended next step is to point each old article address under /blogs/keratine-blogs/ at its new article, so readers and search engines land on the exact page they asked for rather than the blog index.

keratine.in before and after the move

The main changes the move made, with the record behind each.

AreaBeforeAfterEvidence
PlatformShopify storeTenant on Dcrayon's Laravel platform, hosted on AWSLive site and response headers
BlogShopify blogSame articles and slugs on the new blogImport command; live check of three articles
Home page contentHard-coded brand blocksAdmin-managed sections with on and off switchesPull request 74, 27 April 2026
Section artworkAbout 19 MB of PNGAbout 300 KB of WebP at 672 by 672 pixelsCommit notes, 23 April 2026
Release checksNot applicableSyntax, template, route, build and secrets checks on every pull requestCI runs, 27 and 28 April 2026
Source: Dcrayon platform repository history (April 2026) and live checks of keratine.in on 13 October 2026.

Key takeaways

What the Keratine move shows about changing platforms

A platform migration is also a content migration. The products were the easy part. The articles, page titles and share tags carried the store's history in search, and they needed their own import, their own metadata mapping and their own checks. Writing the blog import as a repeatable command, with a dry run and a skip for duplicates, made it safe to run more than once.

The tenant model kept the brand-specific code to one home page, three asset files and two import tools. Everything else came from shared views that other stores already used, which is also why a fix to the shared cart or checkout reaches Keratine without a separate release. Moving the home page to configurable sections took one more round of work and gave the brand team control of its own campaigns.

If you are weighing a similar move, our guide to building a high-converting e-commerce website with Laravel covers the store features worth planning for, and technical SEO architecture for D2C brands explains what to protect when URLs change. For the marketplace side of the same brand, read the Keratine Professional Amazon case study.

Planning to move your store off Shopify, or onto a platform you control? Talk to Dcrayon about your catalogue, content and URLs before you start.

CategoryWeb Development

Subscribe to Our Newsletter

Get the latest insights on digital marketing, web development, and business growth delivered straight to your inbox.