Skip to content

Instantly share code, notes, and snippets.

@myriaglot
Created May 28, 2026 16:29
Show Gist options
  • Select an option

  • Save myriaglot/f08d2173c24f81d66e35860386460765 to your computer and use it in GitHub Desktop.

Select an option

Save myriaglot/f08d2173c24f81d66e35860386460765 to your computer and use it in GitHub Desktop.
vibe coded laravel to astro files
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\File;
use Symfony\Component\DomCrawler\Crawler;
class ExportToAstro extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'export:astro {--output=static : The target directory for Astro files}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Renders Laravel /pages routes and exports them as clean Astro components';
/**
* Execute the console command.
*/
public function handle()
{
$outputDir = base_path($this->option('output'));
// 1. Find all routes starting with /pages
$routes = collect(Route::getRoutes()->getRoutes())
->filter(function ($route) {
return in_array('GET', $route->methods()) && str_starts_with($route->uri(), 'pages');
});
if ($routes->isEmpty()) {
$this->error('No routes found starting with "/pages".');
return Command::FAILURE;
}
$this->info("Found {$routes->count()} routes to export...");
// Ensure output directory exists
if (!File::exists($outputDir)) {
File::makeDirectory($outputDir, 0755, true);
}
foreach ($routes as $route) {
$uri = $route->uri();
$this->comment("Processing: /$uri");
try {
// 2. Render the full Blade page internally
$html = $this->renderRoute($uri);
// 3. Parse the HTML using Symfony DomCrawler
$crawler = new Crawler($html);
// Extract metadata (fallback to defaults if not found)
$title = $crawler->filter('title')->count() ? $crawler->filter('title')->text() : 'Default Title';
$description = $crawler->filter('meta[name="description"]')->count()
? $crawler->filter('meta[name="description"]')->attr('content')
: 'Default description';
// 4. Extract Main Content
// Adjust selectors based on your blade layouts (e.g., 'main', '#content', 'body')
$contentHtml = '';
if ($crawler->filter('main')->count() > 0) {
$contentHtml = $crawler->filter('main')->html();
} elseif ($crawler->filter('body')->count() > 0) {
$contentHtml = $crawler->filter('body')->html();
} else {
$contentHtml = $html; // Fallback to full HTML if structural tags are missing
}
// Clean up trailing whitespace/indentation cleanly
$contentHtml = trim($contentHtml);
// 5. Generate Astro File Content
$astroContent = $this->buildAstroTemplate($title, $description, $contentHtml);
// 6. Determine target filename and save
$slug = str_replace('pages/', '', $uri);
$slug = $slug === 'pages' || $slug === '' ? 'index' : $slug;
$filePath = $outputDir . '/' . $slug . '.astro';
// Ensure subdirectories exist if routes are nested (e.g., pages/about/team)
File::ensureDirectoryExists(dirname($filePath));
File::put($filePath, $astroContent);
$this->info("Successfully exported to: {$filePath}");
} catch (\Exception $e) {
$this->error("Failed to export /$uri: " . $e->getMessage());
}
}
$this->info('Export complete!');
return Command::SUCCESS;
}
/**
* Simulates an internal HTTP request to render the HTML.
*/
protected function renderRoute(string $uri): string
{
$request = Request::create($uri, 'GET');
$response = app()->handle($request);
if ($response->getStatusCode() !== 200) {
throw new \Exception("HTTP Status Code " . $response->getStatusCode());
}
return $response->getContent();
}
/**
* Compiles the metadata and body HTML into Astro's file format.
*/
protected function buildAstroTemplate(string $title, string $description, string $bodyContent): string
{
// Escaping single quotes for Frontmatter string safety
$title = addslashes($title);
$description = addslashes($description);
return <<<ASTRO
---
import Layout from "../layouts/Layout.astro";
const title = "{$title}";
const description = "{$description}";
---
<Layout title={title} description={description}>
{$bodyContent}
</Layout>
ASTRO;
}
}

How It Works

Route Sniffing: It looks directly into your Laravel application's Router container, tracking down any registered GET routes that prefix with pages.

True Kernel Rendering: Instead of trying to parse raw text files via regex (which breaks layouts/includes), app()->handle($request) forces Laravel to process the page completely through its HTTP kernel. Your view composers, providers, translations, and asset links render identically to production.

Smart Element Stripping: Using Symfony’s DOM Crawler (pre-bundled with Laravel), it hunts for the structural <main> or <body> tag. It discards your global Blade layout wrapper elements (headers, footers, head scripts), isolating only the inner content body.

Clean Astro Pipeline: It formats the document matching your desired Frontmatter structure.

Running the Command

To execute the script and dump your files into a folder named /static in your project root, run:

php artisan export:astro

If you want to map it directly to a local Astro project repository path instead, pass the --output flag:

php artisan export:astro --output=/path/to/astro-project/src/pages

Pro-Tips for Post-Migration Integration

  1. Asset Mapping: Because this copies CSS/Image paths exactly as they were rendered in Laravel, make sure your Laravel public assets (e.g., /css/app.css, /images/logo.png) are dropped directly into your Astro project's /public folder.
  2. Dynamic IDs/Classes: Because we grabbed the fully rendered HTML state, all blade syntax (@class, {{ $id }}) resolves smoothly into vanilla HTML strings natively understood by Astro.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment