DEV Community

Connie Leung
Connie Leung

Posted on

Automating DevRel: How I Use Gemini CLI and Gemini 3 to Catch Bugs in My Blog Posts

In this technical deep dive, I explore how to use the Gemini CLI and the latest Gemini 3 Pro Preview model to build a highly efficient, automated "Technical Editor" directly in your terminal. By creating a custom command, we can streamline the process of proofreading, ensuring technical accuracy, and polishing content for publication.

Later on, I used the Gemini feedback to resolve the "white screen of death" and streamline the processing of server-side debugging by returning all the missing environment variables.


The Goal: An AI-Powered Technical Editor

The objective was to create a custom command, critique, that analyzes a Markdown blog post for clarity, structure, impact, and readiness for platforms like Dev.to. The command should be versatile enough to handle both piped input strings and direct file paths.

Step 1: Generating the Custom Command

To start, I used the Gemini CLI with the gemini-3-pro-preview model to generate the initial configuration for the command using a natural language prompt.

The Initial Prompt:

Please generate a Gemini CLI custom command (critique.toml) that does the following:

  1. Critique a draft blog post for clarity, structure, and impact.
  2. If it is a file path, load the blog post from the file and critique.
  3. If the input is a string, treat the input string itself as the blog post content and critique.
  4. Provide the critique in markdown format in the following sections:
    • Look for typos and grammar errors.
    • Clarity: Does the tutorial convey the ideas to the readers clearly?
    • Structure: Does it have a good introduction, good conclusion and use the proper heading to separate sections?
    • Impacts
    • How to improve the draft so that it is ready to be published to dev.to blog site.

Step 2: Defining critique.toml

After some manual refinement to ensure the logic handled arguments correctly, the resulting critique.toml file was saved in the .gemini/commands directory.

description = "Comprehensive technical review for blog posts (Clarity, Code, SEO, and Dev.to readiness)."
prompt = """
You are a Senior Developer Relations (DevRel) Engineer and a professional technical editor for major publications like CSS-Tricks or Smashing Magazine. 

Please provide a rigorous critique of the following blog post draft.

### CONTENT TO REVIEW:
<content_to_review>
    {{args}}
</content_to_review>

### Instructions
If {{args}} is a file path that ends in .md or .txt, load the blog post from the file and critique. 
If {{args}} is a string, treat the input string itself as the blog post content, and critique.

### YOUR MISSION:
Analyze the post based on the following criteria and provide feedback in Markdown format:

### 1. 🐞 Typos, Grammar & Syntax
- Identify specific grammatical errors or awkward phrasing.
- Ensure technical terms are capitalized correctly (e.g., 'JavaScript' not 'javascript', 'API' not 'api', 'GitHub' not 'github', 'Node.js' not 'NodeJS').

### 2. 💡 Technical Accuracy & Code Quality
- Review all code blocks. Are there obvious syntax errors? 
- Is the logic sound?
- **Best Practices:** Are any patterns outdated (e.g., `var` vs `const/let`)?
- Suggest improvements for code readability (e.g., naming conventions, adding comments).

### 3. 🎯 Clarity & Narrative Flow
- Does the 'hook' in the introduction grab the reader?
- Is the difficulty level appropriate for the target audience?
- Identify any "knowledge gaps" where a concept needs more explanation.

### 4. 🏗️ Structure & Scannability
- Evaluate the information hierarchy (H1 -> H2 -> H3).
- Are the paragraphs too long? Suggest where to use bullet points or call-out boxes.
- Does the conclusion offer a clear 'Next Step' or 'Key Takeaway'?

### 5. 🚀 Dev.to & SEO Optimization
- Suggest 4 relevant tags (e.g., #webdev, #tutorial).
- Critically evaluate the Title: Provide 3 alternative "high-click-through" headline options.
- Check for the presence of a YAML frontmatter block.

### 6. 🎭 Voice & Tone
- Is the tone consistent (e.g., helpful, professional, conversational)?
- Is there too much 'fluff' or 'jargon'?

### FINAL VERDICT:
Provide a score from 1-10 on "Readiness for Publication" and list the top 3 high-priority changes required.
"""
Enter fullscreen mode Exit fullscreen mode

Step 3: The Iterative Feedback Loop

The power of this tool lies in the iterative process. By running the command against a draft, I could immediately see technical "gotchas" and areas for improvement.

Running the command from the terminal:

cd <path>/markdown
cat draft.md | gemini -m gemini-3-pro-preview critique
Enter fullscreen mode Exit fullscreen mode

I used the cat Unix command to output the content and piped it to the critique command for review.
The optional -m flag manually chooses to use the Gemini 3 Pro Preview model to critique the draft. If this flag is removed, Gemini CLI uses the auto mode to choose the models for the task.

gemini -m gemini-3-flash-preview critique "draft.md"
Enter fullscreen mode Exit fullscreen mode

Gemini CLI automatically read the content in draft.md and passed it to critique to find areas for improvement.

Alternatively, using the slash command internally in the CLI:

cd <path>/markdown
gemini -m gemini-3-pro-preview
/critique @draft.md
Enter fullscreen mode Exit fullscreen mode

Example Feedback & Fixes

To demonstrate the power of this critique command, here are three real-world issues it identified in my project and the resulting code refactoring.

Handling of App Initialization Error: The Gemini 3 Pro Preview model identified the application showed a white screen when the bootstrapFirebase function threw an error during app initialization. It was not user-friendly. bootstrapFirebase is a function that is called in the callback of the provideAppInitializer function. Then, provideAppInitializer is imported into the providers array of appConfig in app.config.ts. provideAppInitializer ensures Firebase is ready before the application starts.

Initialization of fetch interval based on build mode: The fetch interval of Remote Config is 1 hour, which is appropriate in production. During development mode, the interval should be 0 or 5 minutes, so that new configuration values can take effect sooner rather than later.

Handling Environment Variable Errors: When an environment variable was missing in the .env file, the validate function threw an error. It made debugging inefficient because the HTTP request halted at the first error. Therefore, the model suggested returning the missing environment variable names to the client side.

The Fix to Display a Message when App Initialization throws an error due to missing environment variables:

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
    provideAppInitializer(async () => bootstrapFirebase()),
    provideFirebase(),
  ]
};
Enter fullscreen mode Exit fullscreen mode
// Before:
async function loadFirebaseConfig() {
  const httpService = inject(HttpClient);
  const firebaseConfig$ = httpService.get<FirebaseConfigResponse>(`${config.appUrl}/getFirebaseConfig`);
  return lastValueFrom(firebaseConfig$);
}

export async function bootstrapFirebase() {
    try {
      const configService = inject(ConfigService);
      const { app, recaptchaSiteKey } = await loadFirebaseConfig();
      const firebaseApp = initializeApp(app);

      initializeAppCheck(firebaseApp, {
        provider: new ReCaptchaEnterpriseProvider(recaptchaSiteKey),
        isTokenAutoRefreshEnabled: true,
      });

      ... omitted due to brevity ...

      configService.loadConfig(firebaseApp, remoteConfig, functions);
    } catch (err) {
      console.error(err);
      throw err;
    }
}

// After:
async function loadFirebaseConfig() {
  const httpService = inject(HttpClient);
  const firebaseConfig$ =
    httpService.get<FirebaseConfigResponse>(`${config.appUrl}/getFirebaseConfig`)
      .pipe(catchError((e) => throwError(() => e)));
  return lastValueFrom(firebaseConfig$);
}

export async function bootstrapFirebase() {
    try {
      const configService = inject(ConfigService);
      const firebaseConfig = await loadFirebaseConfig();
      const { app, recaptchaSiteKey } = firebaseConfig;
      const firebaseApp = initializeApp(app);

      initializeAppCheck(firebaseApp, {
        provider: new ReCaptchaEnterpriseProvider(recaptchaSiteKey),
        isTokenAutoRefreshEnabled: true,
      });

      ... omitted due to brevity ...

      configService.loadConfig(firebaseApp, remoteConfig, functions);
    } catch (err) {
      // The error is caught and swallowed in this try-catch block
      console.error(err);
    }
}

@Component({
  selector: 'app-root',
  imports: [RouterOutlet],
  template: `
<div>

  @if (hasNoFirebase()) {
    <p>Firebase initialization failed.</p>
  } @else {
    <main>
      <router-outlet />
    </main>
  }
</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
  private readonly configService = inject(ConfigService);

  hasNoFirebase = computed(() =>
    !this.configService.firebaseApp || !this.configService.remoteConfig || !this.configService.functions
  );
}
Enter fullscreen mode Exit fullscreen mode

loadFirebaseConfig uses the catchError RxJS operator to catch the error and rethrow it. When the HTTP request fails, Firebase App and App Check are not initialized. The try-catch block logs the error message and swallows the error. In AppComponent, the hasNoFirebase computed signal is true when any of the Firebase app, remote config, or cloud functions is undefined. The HTML template displays Firebase initialization failed. when the signal is true. Otherwise, the Photo Edit app is rendered.

The Fix to Keep Short Fetch Interval During Development:

// Before:
import { inject } from '@angular/core';

async function fetchRemoteConfig(firebaseApp: FirebaseApp) {
  const remoteConfig = getRemoteConfig(firebaseApp);
  remoteConfig.settings.minimumFetchIntervalMillis = 3600000;
  ... omitted due to brevity ...
  return remoteConfig;
}

// After:
import { inject, isDevMode } from '@angular/core';

async function fetchRemoteConfig(firebaseApp: FirebaseApp) {
  const remoteConfig = getRemoteConfig(firebaseApp);
  remoteConfig.settings.minimumFetchIntervalMillis = isDevMode() ? 0 : 3600000;
  ... omitted due to brevity ...
  return remoteConfig;
}
Enter fullscreen mode Exit fullscreen mode

When the Angular application is running in development mode, isDevMode returns true and remoteConfig.settings.minimumFetchIntervalMillis is set to 0. In production, remoteConfig.settings.minimumFetchIntervalMillis is set to 1 hour.

The Fix to Handle Missing Environment Variables:

// Before:

export function validate(value: string | undefined, fieldName: string) {
  const err = `${fieldName} is missing.`;
  if (!value) {
    logger.error(err);
    throw new Error(err);
  }

  return value;
}

import { validate } from "./validate";

function validateFirebaseConfigFields(env: NodeJS.ProcessEnv) {
  const apiKey = validate(env.APP_API_KEY, "API Key");
  const appId = validate(env.APP_ID, "App Id");
  const messagingSenderId = validate(env.APP_MESSAGING_SENDER_ID, "Messaging Sender ID");
  const recaptchaSiteKey = validate(env.RECAPTCHA_ENTERPRISE_SITE_KEY, "Recaptcha site key");
  const strFirebaseConfig = validate(env.FIREBASE_CONFIG, "Firebase config");
  const firebaseConfig = JSON.parse(strFirebaseConfig);
  const projectId = validate(firebaseConfig?.projectId, "Project ID");
  const storageBucket = validate(firebaseConfig?.storageBucket, "Storage Bucket");

  return {
    apiKey,
    appId,
    recaptchaSiteKey,
    projectId,
    storageBucket,
    messagingSenderId,
  };
}

// After:
export function validate(value: string | undefined, fieldName: string, missingKeys: string[]) {
  const err = `${fieldName} is missing.`;
  if (!value) {
    logger.error(err);
    missingKeys.push(fieldName);
    return "";
  }

  return value;
}

function validateFirebaseConfigFields(env: NodeJS.ProcessEnv) {
  const missingKeys: string[] = [];
  const apiKey = validate(env.APP_API_KEY, "API Key", missingKeys);
  const appId = validate(env.APP_ID, "App Id", missingKeys);
  const messagingSenderId = validate(env.APP_MESSAGING_SENDER_ID, "Messaging Sender ID", missingKeys);
  const recaptchaSiteKey = validate(env.RECAPTCHA_ENTERPRISE_SITE_KEY, "Recaptcha site key", missingKeys);
  const strFirebaseConfig = validate(env.FIREBASE_CONFIG, "Firebase config", missingKeys);

  let projectId = "";
  let storageBucket = "";
  if (strFirebaseConfig) {
    const firebaseConfig = JSON.parse(strFirebaseConfig);

    projectId = validate(firebaseConfig?.projectId, "Project ID", missingKeys);
    storageBucket = validate(firebaseConfig?.storageBucket, "Storage Bucket", missingKeys);
  }

  if (missingKeys.length > 0) {
    throw new Error(`Missing environment variables: ${missingKeys.join(", ")}`);
  }

  return {
    apiKey,
    appId,
    recaptchaSiteKey,
    projectId,
    storageBucket,
    messagingSenderId,
  };
}
Enter fullscreen mode Exit fullscreen mode

I declared a missingKeys string array in validateFirebaseConfigFields. missingKeys is an argument of the validate function to collect the names of the missing environment variables.

When missingKeys is non-empty, the function throws an error. When all the environment variables are accounted for, their values are returned.


Conclusion

By leveraging the custom command capabilities of the Gemini CLI, I successfully built an expert editor that analyzes the code blocks for errors and code smell, and reviews the content to list the bad practices and risks. This workflow not only catches typos and grammar errors but acts as a proactive technical reviewer, significantly improving the quality of the content and codebase before the blog post ever hits the public eye.

Some other custom commands that I can think of:

  • A writer command that rewrites a draft in a different tone, format and length
  • A prompt suggestion command that understands the draft and suggests a prompt to generate a cover image for it using Gemini 3 Pro Image Preview model

Thank you for reading! Don't forget to explore the Gemini CLI documentation to build your own custom productivity tools.

Resources

Note: Google Cloud credits are provided for this project.

Top comments (0)